From c210315e16a3d9f88c989b96241dd46b79fc8e3c Mon Sep 17 00:00:00 2001 From: Arkm20 Date: Sun, 22 Mar 2026 19:53:38 -0500 Subject: [PATCH] Initial commit --- .gitignore | 81 + FtcRobotController/build.gradle | 30 + .../src/main/AndroidManifest.xml | 79 + .../samples/BasicOmniOpMode_Linear.java | 167 ++ .../samples/BasicOpMode_Iterative.java | 140 ++ .../external/samples/BasicOpMode_Linear.java | 115 ++ .../external/samples/ConceptAprilTag.java | 215 +++ .../external/samples/ConceptAprilTagEasy.java | 161 ++ .../samples/ConceptAprilTagLocalization.java | 250 +++ .../samples/ConceptAprilTagMultiPortal.java | 104 ++ .../ConceptAprilTagOptimizeExposure.java | 246 +++ .../ConceptAprilTagSwitchableCameras.java | 194 ++ .../external/samples/ConceptBlackboard.java | 87 + .../ConceptExploringIMUOrientation.java | 184 ++ .../samples/ConceptGamepadEdgeDetection.java | 108 ++ .../samples/ConceptGamepadRumble.java | 200 +++ .../samples/ConceptGamepadTouchpad.java | 77 + .../external/samples/ConceptLEDStick.java | 123 ++ .../samples/ConceptMotorBulkRead.java | 227 +++ .../external/samples/ConceptNullOp.java | 89 + .../samples/ConceptRampMotorSpeed.java | 114 ++ .../external/samples/ConceptRevLED.java | 78 + .../external/samples/ConceptRevSPARKMini.java | 111 ++ .../external/samples/ConceptScanServo.java | 115 ++ .../external/samples/ConceptSoundsASJava.java | 133 ++ .../samples/ConceptSoundsOnBotJava.java | 120 ++ .../samples/ConceptSoundsSKYSTONE.java | 122 ++ .../external/samples/ConceptTelemetry.java | 177 ++ .../ConceptVisionColorLocator_Circle.java | 245 +++ .../ConceptVisionColorLocator_Rectangle.java | 218 +++ .../samples/ConceptVisionColorSensor.java | 158 ++ .../RobotAutoDriveByEncoder_Linear.java | 187 ++ .../samples/RobotAutoDriveByGyro_Linear.java | 429 +++++ .../samples/RobotAutoDriveByTime_Linear.java | 128 ++ .../samples/RobotAutoDriveToAprilTagOmni.java | 321 ++++ .../samples/RobotAutoDriveToAprilTagTank.java | 298 ++++ .../samples/RobotAutoDriveToLine_Linear.java | 142 ++ .../RobotTeleopMecanumFieldRelativeDrive.java | 163 ++ .../samples/RobotTeleopPOV_Linear.java | 159 ++ .../samples/RobotTeleopTank_Iterative.java | 160 ++ .../samples/SampleRevBlinkinLedDriver.java | 163 ++ .../SensorAndyMarkIMUNonOrthogonal.java | 193 ++ .../samples/SensorAndyMarkIMUOrthogonal.java | 144 ++ .../external/samples/SensorAndyMarkTOF.java | 85 + .../external/samples/SensorBNO055IMU.java | 186 ++ .../samples/SensorBNO055IMUCalibration.java | 230 +++ .../external/samples/SensorColor.java | 225 +++ .../external/samples/SensorDigitalTouch.java | 78 + .../samples/SensorGoBildaPinpoint.java | 116 ++ .../external/samples/SensorHuskyLens.java | 160 ++ .../samples/SensorIMUNonOrthogonal.java | 184 ++ .../external/samples/SensorIMUOrthogonal.java | 146 ++ .../external/samples/SensorKLNavxMicro.java | 128 ++ .../external/samples/SensorLimelight3A.java | 157 ++ .../external/samples/SensorMRColor.java | 138 ++ .../external/samples/SensorMRGyro.java | 160 ++ .../samples/SensorMROpticalDistance.java | 70 + .../external/samples/SensorMRRangeSensor.java | 70 + .../external/samples/SensorOctoQuad.java | 156 ++ .../external/samples/SensorOctoQuadAdv.java | 288 +++ .../samples/SensorOctoQuadLocalization.java | 255 +++ .../external/samples/SensorREV2mDistance.java | 87 + .../external/samples/SensorSparkFunOTOS.java | 156 ++ .../external/samples/SensorTouch.java | 78 + .../samples/UtilityCameraFrameCapture.java | 127 ++ .../samples/UtilityOctoQuadConfigMenu.java | 821 +++++++++ .../ConceptExternalHardwareClass.java | 142 ++ .../externalhardware/RobotHardware.java | 167 ++ .../external/samples/readme.md | 45 + .../external/samples/sample_conventions.md | 113 ++ .../internal/FtcOpModeRegister.java | 68 + .../internal/FtcRobotControllerActivity.java | 845 +++++++++ .../internal/PermissionValidatorWrapper.java | 91 + .../src/main/res/drawable-xhdpi/icon_menu.png | Bin 0 -> 975 bytes .../drawable-xhdpi/icon_robotcontroller.png | Bin 0 -> 4777 bytes .../res/layout/activity_ftc_controller.xml | 184 ++ .../main/res/menu/ftc_robot_controller.xml | 78 + FtcRobotController/src/main/res/raw/gold.wav | Bin 0 -> 104016 bytes .../src/main/res/raw/silver.wav | Bin 0 -> 99068 bytes .../src/main/res/values/dimens.xml | 40 + .../src/main/res/values/strings.xml | 72 + .../src/main/res/values/styles.xml | 23 + .../src/main/res/xml/app_settings.xml | 93 + .../src/main/res/xml/device_filter.xml | 49 + LICENSE | 29 + README.md | 117 ++ TeamCode/build.gradle | 28 + TeamCode/lib/OpModeAnnotationProcessor.jar | Bin 0 -> 6032 bytes TeamCode/src/main/AndroidManifest.xml | 11 + .../ftc/teamcode/lib/BaseOpMode.java | 83 + .../ftc/teamcode/lib/PIDController.java | 51 + .../ftc/teamcode/lib/SubsysManager.java | 34 + .../ftc/teamcode/lib/Subsystem.java | 12 + .../ftc/teamcode/pedroPathing/Constants.java | 19 + .../ftc/teamcode/pedroPathing/Tuning.java | 1585 +++++++++++++++++ .../org/firstinspires/ftc/teamcode/readme.md | 131 ++ .../ftc/teamcode/subsys/Drivetrain.java | 94 + .../ftc/teamcode/teleOp/Constants.java | 27 + .../ftc/teamcode/teleOp/teleOp.java | 56 + .../ftc/teamcode/util/AutoTransfer.java | 55 + .../ftc/teamcode/util/FPSCounter.java | 62 + .../ftc/teamcode/util/OATDrawing.java | 58 + TeamCode/src/main/res/raw/readme.md | 1 + TeamCode/src/main/res/values/strings.xml | 4 + .../main/res/xml/teamwebcamcalibrations.xml | 161 ++ build.common.gradle | 117 ++ build.dependencies.gradle | 22 + build.gradle | 29 + doc/legal/AudioBlocksSounds.txt | 21 + ...A - LEGO Open Source License Agreement.txt | 15 + doc/legal/LEGO Open Source License.pdf | Bin 0 -> 34100 bytes doc/media/PullRequest.PNG | Bin 0 -> 19655 bytes gradle.properties | 12 + gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 58695 bytes gradle/wrapper/gradle-wrapper.properties | 5 + gradlew | 164 ++ gradlew.bat | 90 + libs/README.txt | 1 + libs/ftc.debug.keystore | Bin 0 -> 2146 bytes settings.gradle | 2 + 120 files changed, 16162 insertions(+) create mode 100644 .gitignore create mode 100644 FtcRobotController/build.gradle create mode 100644 FtcRobotController/src/main/AndroidManifest.xml create mode 100644 FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/BasicOmniOpMode_Linear.java create mode 100644 FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/BasicOpMode_Iterative.java create mode 100644 FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/BasicOpMode_Linear.java create mode 100644 FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/ConceptAprilTag.java create mode 100644 FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/ConceptAprilTagEasy.java create mode 100644 FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/ConceptAprilTagLocalization.java create mode 100644 FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/ConceptAprilTagMultiPortal.java create mode 100644 FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/ConceptAprilTagOptimizeExposure.java create mode 100644 FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/ConceptAprilTagSwitchableCameras.java create mode 100644 FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/ConceptBlackboard.java create mode 100644 FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/ConceptExploringIMUOrientation.java create mode 100644 FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/ConceptGamepadEdgeDetection.java create mode 100644 FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/ConceptGamepadRumble.java create mode 100644 FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/ConceptGamepadTouchpad.java create mode 100644 FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/ConceptLEDStick.java create mode 100644 FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/ConceptMotorBulkRead.java create mode 100644 FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/ConceptNullOp.java create mode 100644 FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/ConceptRampMotorSpeed.java create mode 100644 FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/ConceptRevLED.java create mode 100644 FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/ConceptRevSPARKMini.java create mode 100644 FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/ConceptScanServo.java create mode 100644 FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/ConceptSoundsASJava.java create mode 100644 FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/ConceptSoundsOnBotJava.java create mode 100644 FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/ConceptSoundsSKYSTONE.java create mode 100644 FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/ConceptTelemetry.java create mode 100644 FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/ConceptVisionColorLocator_Circle.java create mode 100644 FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/ConceptVisionColorLocator_Rectangle.java create mode 100644 FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/ConceptVisionColorSensor.java create mode 100644 FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/RobotAutoDriveByEncoder_Linear.java create mode 100644 FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/RobotAutoDriveByGyro_Linear.java create mode 100644 FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/RobotAutoDriveByTime_Linear.java create mode 100644 FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/RobotAutoDriveToAprilTagOmni.java create mode 100644 FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/RobotAutoDriveToAprilTagTank.java 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/RobotTeleopMecanumFieldRelativeDrive.java create mode 100644 FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/RobotTeleopPOV_Linear.java create mode 100644 FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/RobotTeleopTank_Iterative.java create mode 100644 FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/SampleRevBlinkinLedDriver.java create mode 100644 FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/SensorAndyMarkIMUNonOrthogonal.java create mode 100644 FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/SensorAndyMarkIMUOrthogonal.java create mode 100644 FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/SensorAndyMarkTOF.java create mode 100644 FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/SensorBNO055IMU.java create mode 100644 FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/SensorBNO055IMUCalibration.java create mode 100644 FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/SensorColor.java create mode 100644 FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/SensorDigitalTouch.java create mode 100644 FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/SensorGoBildaPinpoint.java create mode 100644 FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/SensorHuskyLens.java create mode 100644 FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/SensorIMUNonOrthogonal.java create mode 100644 FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/SensorIMUOrthogonal.java create mode 100644 FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/SensorKLNavxMicro.java create mode 100644 FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/SensorLimelight3A.java create mode 100644 FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/SensorMRColor.java create mode 100644 FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/SensorMRGyro.java create mode 100644 FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/SensorMROpticalDistance.java create mode 100644 FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/SensorMRRangeSensor.java create mode 100644 FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/SensorOctoQuad.java create mode 100644 FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/SensorOctoQuadAdv.java create mode 100644 FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/SensorOctoQuadLocalization.java create mode 100644 FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/SensorREV2mDistance.java create mode 100644 FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/SensorSparkFunOTOS.java create mode 100644 FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/SensorTouch.java create mode 100644 FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/UtilityCameraFrameCapture.java create mode 100644 FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/UtilityOctoQuadConfigMenu.java create mode 100644 FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/externalhardware/ConceptExternalHardwareClass.java create mode 100644 FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/externalhardware/RobotHardware.java create mode 100644 FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/readme.md create mode 100644 FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/sample_conventions.md create mode 100644 FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/internal/FtcOpModeRegister.java create mode 100644 FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/internal/FtcRobotControllerActivity.java create mode 100644 FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/internal/PermissionValidatorWrapper.java create mode 100644 FtcRobotController/src/main/res/drawable-xhdpi/icon_menu.png create mode 100644 FtcRobotController/src/main/res/drawable-xhdpi/icon_robotcontroller.png create mode 100644 FtcRobotController/src/main/res/layout/activity_ftc_controller.xml create mode 100644 FtcRobotController/src/main/res/menu/ftc_robot_controller.xml create mode 100644 FtcRobotController/src/main/res/raw/gold.wav create mode 100644 FtcRobotController/src/main/res/raw/silver.wav create mode 100644 FtcRobotController/src/main/res/values/dimens.xml create mode 100644 FtcRobotController/src/main/res/values/strings.xml create mode 100644 FtcRobotController/src/main/res/values/styles.xml create mode 100644 FtcRobotController/src/main/res/xml/app_settings.xml create mode 100644 FtcRobotController/src/main/res/xml/device_filter.xml create mode 100644 LICENSE create mode 100644 README.md create mode 100644 TeamCode/build.gradle create mode 100644 TeamCode/lib/OpModeAnnotationProcessor.jar create mode 100644 TeamCode/src/main/AndroidManifest.xml create mode 100644 TeamCode/src/main/java/org/firstinspires/ftc/teamcode/lib/BaseOpMode.java create mode 100644 TeamCode/src/main/java/org/firstinspires/ftc/teamcode/lib/PIDController.java create mode 100644 TeamCode/src/main/java/org/firstinspires/ftc/teamcode/lib/SubsysManager.java create mode 100644 TeamCode/src/main/java/org/firstinspires/ftc/teamcode/lib/Subsystem.java create mode 100644 TeamCode/src/main/java/org/firstinspires/ftc/teamcode/pedroPathing/Constants.java create mode 100644 TeamCode/src/main/java/org/firstinspires/ftc/teamcode/pedroPathing/Tuning.java create mode 100644 TeamCode/src/main/java/org/firstinspires/ftc/teamcode/readme.md create mode 100644 TeamCode/src/main/java/org/firstinspires/ftc/teamcode/subsys/Drivetrain.java create mode 100644 TeamCode/src/main/java/org/firstinspires/ftc/teamcode/teleOp/Constants.java create mode 100644 TeamCode/src/main/java/org/firstinspires/ftc/teamcode/teleOp/teleOp.java create mode 100755 TeamCode/src/main/java/org/firstinspires/ftc/teamcode/util/AutoTransfer.java create mode 100755 TeamCode/src/main/java/org/firstinspires/ftc/teamcode/util/FPSCounter.java create mode 100755 TeamCode/src/main/java/org/firstinspires/ftc/teamcode/util/OATDrawing.java create mode 100644 TeamCode/src/main/res/raw/readme.md create mode 100644 TeamCode/src/main/res/values/strings.xml create mode 100644 TeamCode/src/main/res/xml/teamwebcamcalibrations.xml create mode 100644 build.common.gradle create mode 100644 build.dependencies.gradle create mode 100644 build.gradle create mode 100644 doc/legal/AudioBlocksSounds.txt create mode 100644 doc/legal/Exhibit A - LEGO Open Source License Agreement.txt create mode 100644 doc/legal/LEGO Open Source License.pdf create mode 100644 doc/media/PullRequest.PNG create mode 100644 gradle.properties create mode 100644 gradle/wrapper/gradle-wrapper.jar create mode 100644 gradle/wrapper/gradle-wrapper.properties create mode 100755 gradlew create mode 100644 gradlew.bat create mode 100644 libs/README.txt create mode 100644 libs/ftc.debug.keystore create mode 100644 settings.gradle diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b85aa2f --- /dev/null +++ b/.gitignore @@ -0,0 +1,81 @@ +# Built application files +*.apk +*.aar +*.ap_ +*.aab + +!libs/*.aar + +# Files for the ART/Dalvik VM +*.dex + +# Java/JDK files +*.class +*.hprof + +# Generated files +bin/ +gen/ +out/ +# Uncomment the following line in case you need and you don't have the release build type files in your app +# release/ + +# Gradle files +.gradle/ +build/ + +# Local configuration file (sdk path, etc) +local.properties + +# Proguard folder generated by Eclipse +proguard/ + +# Log Files +*.log + +# Android Studio Navigation editor temp files +.navigation/ + +# Android Studio captures folder +captures/ + +# IntelliJ +*.iml +.idea/ + +# For Mac users +.DS_Store + +# Keystore files +# Uncomment the following lines if you do not want to check your keystore files in. +#*.jks +#*.keystore + +# External native build folder generated in Android Studio 2.2 and later +.externalNativeBuild +.cxx/ + +# Google Services (e.g. APIs or Firebase) +# google-services.json + +# Freeline +freeline.py +freeline/ +freeline_project_description.json + +# fastlane +fastlane/report.xml +fastlane/Preview.html +fastlane/screenshots +fastlane/test_output +fastlane/readme.md + +# Version control +vcs.xml + +# lint +lint/intermediates/ +lint/generated/ +lint/outputs/ +lint/tmp/ +# lint/reports/ \ No newline at end of file diff --git a/FtcRobotController/build.gradle b/FtcRobotController/build.gradle new file mode 100644 index 0000000..8c796da --- /dev/null +++ b/FtcRobotController/build.gradle @@ -0,0 +1,30 @@ +import java.text.SimpleDateFormat + +// +// build.gradle in FtcRobotController +// +apply plugin: 'com.android.library' + +android { + + defaultConfig { + minSdkVersion 24 + //noinspection ExpiredTargetSdkVersion + targetSdkVersion 28 + buildConfigField "String", "APP_BUILD_TIME", '"' + (new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSZ", Locale.ROOT).format(new Date())) + '"' + } + + buildFeatures { + buildConfig = true + } + compileSdk 34 + + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + namespace = 'com.qualcomm.ftcrobotcontroller' +} + +apply from: '../build.dependencies.gradle' diff --git a/FtcRobotController/src/main/AndroidManifest.xml b/FtcRobotController/src/main/AndroidManifest.xml new file mode 100644 index 0000000..4c58576 --- /dev/null +++ b/FtcRobotController/src/main/AndroidManifest.xml @@ -0,0 +1,79 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/BasicOmniOpMode_Linear.java b/FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/BasicOmniOpMode_Linear.java new file mode 100644 index 0000000..2644143 --- /dev/null +++ b/FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/BasicOmniOpMode_Linear.java @@ -0,0 +1,167 @@ +/* Copyright (c) 2021 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.DcMotor; +import com.qualcomm.robotcore.util.ElapsedTime; + +/* + * This file contains an 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 a selection is made from the menu, the corresponding OpMode is executed. + * + * This particular OpMode illustrates driving a 4-motor Omni-Directional (or Holonomic) robot. + * This code will work with either a Mecanum-Drive or an X-Drive train. + * Both of these drives are illustrated at https://gm0.org/en/latest/docs/robot-design/drivetrains/holonomic.html + * Note that a Mecanum drive must display an X roller-pattern when viewed from above. + * + * Also note that it is critical to set the correct rotation direction for each motor. See details below. + * + * Holonomic drives provide the ability for the robot to move in three axes (directions) simultaneously. + * Each motion axis is controlled by one Joystick axis. + * + * 1) Axial: Driving forward and backward Left-joystick Forward/Backward + * 2) Lateral: Strafing right and left Left-joystick Right and Left + * 3) Yaw: Rotating Clockwise and counter clockwise Right-joystick Right and Left + * + * This code is written assuming that the right-side motors need to be reversed for the robot to drive forward. + * When you first test your robot, if it moves backward when you push the left stick forward, then you must flip + * the direction of all 4 motors (see code below). + * + * 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="Basic: Omni Linear OpMode", group="Linear OpMode") +@Disabled +public class BasicOmniOpMode_Linear extends LinearOpMode { + + // Declare OpMode members for each of the 4 motors. + private ElapsedTime runtime = new ElapsedTime(); + private DcMotor frontLeftDrive = null; + private DcMotor backLeftDrive = null; + private DcMotor frontRightDrive = null; + private DcMotor backRightDrive = null; + + @Override + public void runOpMode() { + + // Initialize the hardware variables. Note that the strings used here must correspond + // to the names assigned during the robot configuration step on the DS or RC devices. + frontLeftDrive = hardwareMap.get(DcMotor.class, "front_left_drive"); + backLeftDrive = hardwareMap.get(DcMotor.class, "back_left_drive"); + frontRightDrive = hardwareMap.get(DcMotor.class, "front_right_drive"); + backRightDrive = hardwareMap.get(DcMotor.class, "back_right_drive"); + + // ######################################################################################## + // !!! IMPORTANT Drive Information. Test your motor directions. !!!!! + // ######################################################################################## + // Most robots need the motors on one side to be reversed to drive forward. + // 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. + frontLeftDrive.setDirection(DcMotor.Direction.REVERSE); + backLeftDrive.setDirection(DcMotor.Direction.REVERSE); + frontRightDrive.setDirection(DcMotor.Direction.FORWARD); + backRightDrive.setDirection(DcMotor.Direction.FORWARD); + + // Wait for the game to start (driver presses START) + telemetry.addData("Status", "Initialized"); + telemetry.update(); + + waitForStart(); + runtime.reset(); + + // run until the end of the match (driver presses STOP) + while (opModeIsActive()) { + double max; + + // POV Mode uses left joystick to go forward & strafe, and right joystick to rotate. + double axial = -gamepad1.left_stick_y; // Note: pushing stick forward gives negative value + double lateral = gamepad1.left_stick_x; + double yaw = gamepad1.right_stick_x; + + // Combine the joystick requests for each axis-motion to determine each wheel's power. + // Set up a variable for each drive wheel to save the power level for telemetry. + double frontLeftPower = axial + lateral + yaw; + double frontRightPower = axial - lateral - yaw; + double backLeftPower = axial - lateral + yaw; + double backRightPower = axial + lateral - yaw; + + // Normalize the values so no wheel power exceeds 100% + // This ensures that the robot maintains the desired motion. + max = Math.max(Math.abs(frontLeftPower), Math.abs(frontRightPower)); + max = Math.max(max, Math.abs(backLeftPower)); + max = Math.max(max, Math.abs(backRightPower)); + + if (max > 1.0) { + frontLeftPower /= max; + frontRightPower /= max; + backLeftPower /= max; + backRightPower /= max; + } + + // This is test code: + // + // Uncomment the following code to test your motor directions. + // Each button should make the corresponding motor run FORWARD. + // 1) First get all the motors to take to correct positions on the robot + // by adjusting your Robot Configuration if necessary. + // 2) Then make sure they run in the correct direction by modifying the + // the setDirection() calls above. + // Once the correct motors move in the correct direction re-comment this code. + + /* + frontLeftPower = gamepad1.x ? 1.0 : 0.0; // X gamepad + backLeftPower = gamepad1.a ? 1.0 : 0.0; // A gamepad + frontRightPower = gamepad1.y ? 1.0 : 0.0; // Y gamepad + backRightPower = gamepad1.b ? 1.0 : 0.0; // B gamepad + */ + + // Send calculated power to wheels + frontLeftDrive.setPower(frontLeftPower); + frontRightDrive.setPower(frontRightPower); + backLeftDrive.setPower(backLeftPower); + backRightDrive.setPower(backRightPower); + + // Show the elapsed game time and wheel power. + telemetry.addData("Status", "Run Time: " + runtime.toString()); + telemetry.addData("Front left/Right", "%4.2f, %4.2f", frontLeftPower, frontRightPower); + telemetry.addData("Back left/Right", "%4.2f, %4.2f", backLeftPower, backRightPower); + telemetry.update(); + } + }} 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 new file mode 100644 index 0000000..6504e58 --- /dev/null +++ b/FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/BasicOpMode_Iterative.java @@ -0,0 +1,140 @@ +/* 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.OpMode; +import com.qualcomm.robotcore.eventloop.opmode.TeleOp; +import com.qualcomm.robotcore.hardware.DcMotor; +import com.qualcomm.robotcore.util.ElapsedTime; +import com.qualcomm.robotcore.util.Range; + +/* + * 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 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 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="Basic: Iterative OpMode", group="Iterative OpMode") +@Disabled +public class BasicOpMode_Iterative extends OpMode +{ + // Declare OpMode members. + private ElapsedTime runtime = new ElapsedTime(); + private DcMotor leftDrive = null; + private DcMotor rightDrive = null; + + /* + * Code to run ONCE when the driver hits INIT + */ + @Override + public void init() { + telemetry.addData("Status", "Initialized"); + + // Initialize the hardware variables. Note that the strings used here as parameters + // to 'get' must correspond to the names assigned during the robot configuration + // step (using the FTC Robot Controller app on the phone). + 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); + + // Tell the driver that initialization is complete. + telemetry.addData("Status", "Initialized"); + } + + /* + * Code to run REPEATEDLY after the driver hits INIT, but before they hit START + */ + @Override + public void init_loop() { + } + + /* + * Code to run ONCE when the driver hits START + */ + @Override + public void start() { + runtime.reset(); + } + + /* + * Code to run REPEATEDLY after the driver hits START but before they hit STOP + */ + @Override + public void loop() { + // Setup a variable for each drive wheel to save power level for telemetry + double leftPower; + double rightPower; + + // Choose to drive using either Tank Mode, or POV Mode + // Comment out the method that's not used. The default below is POV. + + // POV Mode uses left stick to go forward, and right stick to turn. + // - This uses basic math to combine motions and is easier to drive straight. + double drive = -gamepad1.left_stick_y; + double turn = gamepad1.right_stick_x; + leftPower = Range.clip(drive + turn, -1.0, 1.0) ; + rightPower = Range.clip(drive - turn, -1.0, 1.0) ; + + // Tank Mode uses one stick to control each wheel. + // - This requires no math, but it is hard to drive forward slowly and keep straight. + // leftPower = -gamepad1.left_stick_y ; + // rightPower = -gamepad1.right_stick_y ; + + // Send calculated power to wheels + leftDrive.setPower(leftPower); + rightDrive.setPower(rightPower); + + // Show the elapsed game time and wheel power. + telemetry.addData("Status", "Run Time: " + runtime.toString()); + telemetry.addData("Motors", "left (%.2f), right (%.2f)", leftPower, rightPower); + } + + /* + * Code to run ONCE after the driver hits STOP + */ + @Override + public void stop() { + } + +} 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 new file mode 100644 index 0000000..ab0bb25 --- /dev/null +++ b/FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/BasicOpMode_Linear.java @@ -0,0 +1,115 @@ +/* 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.DcMotor; +import com.qualcomm.robotcore.util.ElapsedTime; +import com.qualcomm.robotcore.util.Range; + + +/* + * 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 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 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="Basic: Linear OpMode", group="Linear OpMode") +@Disabled +public class BasicOpMode_Linear extends LinearOpMode { + + // Declare OpMode members. + private ElapsedTime runtime = new ElapsedTime(); + private DcMotor leftDrive = null; + private DcMotor rightDrive = null; + + @Override + public void runOpMode() { + telemetry.addData("Status", "Initialized"); + telemetry.update(); + + // Initialize the hardware variables. Note that the strings used here as parameters + // to 'get' must correspond to the names assigned during the robot configuration + // step (using the FTC Robot Controller app on the phone). + 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); + + // Wait for the game to start (driver presses START) + waitForStart(); + runtime.reset(); + + // run until the end of the match (driver presses STOP) + while (opModeIsActive()) { + + // Setup a variable for each drive wheel to save power level for telemetry + double leftPower; + double rightPower; + + // Choose to drive using either Tank Mode, or POV Mode + // Comment out the method that's not used. The default below is POV. + + // POV Mode uses left stick to go forward, and right stick to turn. + // - This uses basic math to combine motions and is easier to drive straight. + double drive = -gamepad1.left_stick_y; + double turn = gamepad1.right_stick_x; + leftPower = Range.clip(drive + turn, -1.0, 1.0) ; + rightPower = Range.clip(drive - turn, -1.0, 1.0) ; + + // Tank Mode uses one stick to control each wheel. + // - This requires no math, but it is hard to drive forward slowly and keep straight. + // leftPower = -gamepad1.left_stick_y ; + // rightPower = -gamepad1.right_stick_y ; + + // Send calculated power to wheels + leftDrive.setPower(leftPower); + rightDrive.setPower(rightPower); + + // Show the elapsed game time and wheel power. + telemetry.addData("Status", "Run Time: " + runtime.toString()); + telemetry.addData("Motors", "left (%.2f), right (%.2f)", leftPower, rightPower); + telemetry.update(); + } + } +} diff --git a/FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/ConceptAprilTag.java b/FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/ConceptAprilTag.java new file mode 100644 index 0000000..4ee7ffe --- /dev/null +++ b/FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/ConceptAprilTag.java @@ -0,0 +1,215 @@ +/* Copyright (c) 2023 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.util.Size; +import com.qualcomm.robotcore.eventloop.opmode.Disabled; +import com.qualcomm.robotcore.eventloop.opmode.LinearOpMode; +import com.qualcomm.robotcore.eventloop.opmode.TeleOp; +import org.firstinspires.ftc.robotcore.external.hardware.camera.BuiltinCameraDirection; +import org.firstinspires.ftc.robotcore.external.hardware.camera.WebcamName; +import org.firstinspires.ftc.vision.VisionPortal; +import org.firstinspires.ftc.vision.apriltag.AprilTagDetection; +import org.firstinspires.ftc.vision.apriltag.AprilTagProcessor; + +import java.util.List; + +/* + * This OpMode illustrates the basics of AprilTag recognition and pose estimation, + * including Java Builder structures for specifying Vision parameters. + * + * For an introduction to AprilTags, see the FTC-DOCS link below: + * https://ftc-docs.firstinspires.org/en/latest/apriltag/vision_portal/apriltag_intro/apriltag-intro.html + * + * In this sample, any visible tag ID will be detected and displayed, but only tags that are included in the default + * "TagLibrary" will have their position and orientation information displayed. This default TagLibrary contains + * the current Season's AprilTags and a small set of "test Tags" in the high number range. + * + * When an AprilTag in the TagLibrary is detected, the SDK provides location and orientation of the tag, relative to the camera. + * This information is provided in the "ftcPose" member of the returned "detection", and is explained in the ftc-docs page linked below. + * https://ftc-docs.firstinspires.org/apriltag-detection-values + * + * To experiment with using AprilTags to navigate, try out these two driving samples: + * RobotAutoDriveToAprilTagOmni and RobotAutoDriveToAprilTagTank + * + * There are many "default" VisionPortal and AprilTag configuration parameters that may be overridden if desired. + * These default parameters are shown as comments in the code below. + * + * 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 = "Concept: AprilTag", group = "Concept") +@Disabled +public class ConceptAprilTag extends LinearOpMode { + + private static final boolean USE_WEBCAM = true; // true for webcam, false for phone camera + + /** + * The variable to store our instance of the AprilTag processor. + */ + private AprilTagProcessor aprilTag; + + /** + * The variable to store our instance of the vision portal. + */ + private VisionPortal visionPortal; + + @Override + public void runOpMode() { + + initAprilTag(); + + // Wait for the DS start button to be touched. + telemetry.addData("DS preview on/off", "3 dots, Camera Stream"); + telemetry.addData(">", "Touch START to start OpMode"); + telemetry.update(); + waitForStart(); + + while (opModeIsActive()) { + + telemetryAprilTag(); + + // Push telemetry to the Driver Station. + telemetry.update(); + + // Save CPU resources; can resume streaming when needed. + if (gamepad1.dpad_down) { + visionPortal.stopStreaming(); + } else if (gamepad1.dpad_up) { + visionPortal.resumeStreaming(); + } + + // Share the CPU. + sleep(20); + } + + // Save more CPU resources when camera is no longer needed. + visionPortal.close(); + + } // end method runOpMode() + + /** + * Initialize the AprilTag processor. + */ + private void initAprilTag() { + + // Create the AprilTag processor. + aprilTag = new AprilTagProcessor.Builder() + + // The following default settings are available to un-comment and edit as needed. + //.setDrawAxes(false) + //.setDrawCubeProjection(false) + //.setDrawTagOutline(true) + //.setTagFamily(AprilTagProcessor.TagFamily.TAG_36h11) + //.setTagLibrary(AprilTagGameDatabase.getCenterStageTagLibrary()) + //.setOutputUnits(DistanceUnit.INCH, AngleUnit.DEGREES) + + // == CAMERA CALIBRATION == + // If you do not manually specify calibration parameters, the SDK will attempt + // to load a predefined calibration for your camera. + //.setLensIntrinsics(578.272, 578.272, 402.145, 221.506) + // ... these parameters are fx, fy, cx, cy. + + .build(); + + // Adjust Image Decimation to trade-off detection-range for detection-rate. + // eg: Some typical detection data using a Logitech C920 WebCam + // Decimation = 1 .. Detect 2" Tag from 10 feet away at 10 Frames per second + // Decimation = 2 .. Detect 2" Tag from 6 feet away at 22 Frames per second + // Decimation = 3 .. Detect 2" Tag from 4 feet away at 30 Frames Per Second (default) + // Decimation = 3 .. Detect 5" Tag from 10 feet away at 30 Frames Per Second (default) + // Note: Decimation can be changed on-the-fly to adapt during a match. + //aprilTag.setDecimation(3); + + // Create the vision portal by using a builder. + VisionPortal.Builder builder = new VisionPortal.Builder(); + + // Set the camera (webcam vs. built-in RC phone camera). + if (USE_WEBCAM) { + builder.setCamera(hardwareMap.get(WebcamName.class, "Webcam 1")); + } else { + builder.setCamera(BuiltinCameraDirection.BACK); + } + + // Choose a camera resolution. Not all cameras support all resolutions. + //builder.setCameraResolution(new Size(640, 480)); + + // Enable the RC preview (LiveView). Set "false" to omit camera monitoring. + //builder.enableLiveView(true); + + // Set the stream format; MJPEG uses less bandwidth than default YUY2. + //builder.setStreamFormat(VisionPortal.StreamFormat.YUY2); + + // Choose whether or not LiveView stops if no processors are enabled. + // If set "true", monitor shows solid orange screen if no processors enabled. + // If set "false", monitor shows camera view without annotations. + //builder.setAutoStopLiveView(false); + + // Set and enable the processor. + builder.addProcessor(aprilTag); + + // Build the Vision Portal, using the above settings. + visionPortal = builder.build(); + + // Disable or re-enable the aprilTag processor at any time. + //visionPortal.setProcessorEnabled(aprilTag, true); + + } // end method initAprilTag() + + + /** + * Add telemetry about AprilTag detections. + */ + private void telemetryAprilTag() { + + List currentDetections = aprilTag.getDetections(); + telemetry.addData("# AprilTags Detected", currentDetections.size()); + + // Step through the list of detections and display info for each one. + for (AprilTagDetection detection : currentDetections) { + if (detection.metadata != null) { + telemetry.addLine(String.format("\n==== (ID %d) %s", detection.id, detection.metadata.name)); + telemetry.addLine(String.format("XYZ %6.1f %6.1f %6.1f (inch)", detection.ftcPose.x, detection.ftcPose.y, detection.ftcPose.z)); + telemetry.addLine(String.format("PRY %6.1f %6.1f %6.1f (deg)", detection.ftcPose.pitch, detection.ftcPose.roll, detection.ftcPose.yaw)); + telemetry.addLine(String.format("RBE %6.1f %6.1f %6.1f (inch, deg, deg)", detection.ftcPose.range, detection.ftcPose.bearing, detection.ftcPose.elevation)); + } else { + telemetry.addLine(String.format("\n==== (ID %d) Unknown", detection.id)); + telemetry.addLine(String.format("Center %6.0f %6.0f (pixels)", detection.center.x, detection.center.y)); + } + } // end for() loop + + // Add "key" information to telemetry + telemetry.addLine("\nkey:\nXYZ = X (Right), Y (Forward), Z (Up) dist."); + telemetry.addLine("PRY = Pitch, Roll & Yaw (XYZ Rotation)"); + telemetry.addLine("RBE = Range, Bearing & Elevation"); + + } // end method telemetryAprilTag() + +} // end class diff --git a/FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/ConceptAprilTagEasy.java b/FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/ConceptAprilTagEasy.java new file mode 100644 index 0000000..7bda71b --- /dev/null +++ b/FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/ConceptAprilTagEasy.java @@ -0,0 +1,161 @@ +/* Copyright (c) 2023 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 org.firstinspires.ftc.robotcore.external.hardware.camera.BuiltinCameraDirection; +import org.firstinspires.ftc.robotcore.external.hardware.camera.WebcamName; +import org.firstinspires.ftc.vision.VisionPortal; +import org.firstinspires.ftc.vision.apriltag.AprilTagDetection; +import org.firstinspires.ftc.vision.apriltag.AprilTagProcessor; + +import java.util.List; + +/* + * This OpMode illustrates the basics of AprilTag recognition and pose estimation, using + * the easy way. + * + * For an introduction to AprilTags, see the FTC-DOCS link below: + * https://ftc-docs.firstinspires.org/en/latest/apriltag/vision_portal/apriltag_intro/apriltag-intro.html + * + * In this sample, any visible tag ID will be detected and displayed, but only tags that are included in the default + * "TagLibrary" will have their position and orientation information displayed. This default TagLibrary contains + * the current Season's AprilTags and a small set of "test Tags" in the high number range. + * + * When an AprilTag in the TagLibrary is detected, the SDK provides location and orientation of the tag, relative to the camera. + * This information is provided in the "ftcPose" member of the returned "detection", and is explained in the ftc-docs page linked below. + * https://ftc-docs.firstinspires.org/apriltag-detection-values + * + * To experiment with using AprilTags to navigate, try out these two driving samples: + * RobotAutoDriveToAprilTagOmni and RobotAutoDriveToAprilTagTank + * + * 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 = "Concept: AprilTag Easy", group = "Concept") +@Disabled +public class ConceptAprilTagEasy extends LinearOpMode { + + private static final boolean USE_WEBCAM = true; // true for webcam, false for phone camera + + /** + * The variable to store our instance of the AprilTag processor. + */ + private AprilTagProcessor aprilTag; + + /** + * The variable to store our instance of the vision portal. + */ + private VisionPortal visionPortal; + + @Override + public void runOpMode() { + + initAprilTag(); + + // Wait for the DS start button to be touched. + telemetry.addData("DS preview on/off", "3 dots, Camera Stream"); + telemetry.addData(">", "Touch START to start OpMode"); + telemetry.update(); + waitForStart(); + + while (opModeIsActive()) { + + telemetryAprilTag(); + + // Push telemetry to the Driver Station. + telemetry.update(); + + // Save CPU resources; can resume streaming when needed. + if (gamepad1.dpad_down) { + visionPortal.stopStreaming(); + } else if (gamepad1.dpad_up) { + visionPortal.resumeStreaming(); + } + + // Share the CPU. + sleep(20); + } + + // Save more CPU resources when camera is no longer needed. + visionPortal.close(); + + } // end method runOpMode() + + /** + * Initialize the AprilTag processor. + */ + private void initAprilTag() { + + // Create the AprilTag processor the easy way. + aprilTag = AprilTagProcessor.easyCreateWithDefaults(); + + // Create the vision portal the easy way. + if (USE_WEBCAM) { + visionPortal = VisionPortal.easyCreateWithDefaults( + hardwareMap.get(WebcamName.class, "Webcam 1"), aprilTag); + } else { + visionPortal = VisionPortal.easyCreateWithDefaults( + BuiltinCameraDirection.BACK, aprilTag); + } + + } // end method initAprilTag() + + /** + * Add telemetry about AprilTag detections. + */ + private void telemetryAprilTag() { + + List currentDetections = aprilTag.getDetections(); + telemetry.addData("# AprilTags Detected", currentDetections.size()); + + // Step through the list of detections and display info for each one. + for (AprilTagDetection detection : currentDetections) { + if (detection.metadata != null) { + telemetry.addLine(String.format("\n==== (ID %d) %s", detection.id, detection.metadata.name)); + telemetry.addLine(String.format("XYZ %6.1f %6.1f %6.1f (inch)", detection.ftcPose.x, detection.ftcPose.y, detection.ftcPose.z)); + telemetry.addLine(String.format("PRY %6.1f %6.1f %6.1f (deg)", detection.ftcPose.pitch, detection.ftcPose.roll, detection.ftcPose.yaw)); + telemetry.addLine(String.format("RBE %6.1f %6.1f %6.1f (inch, deg, deg)", detection.ftcPose.range, detection.ftcPose.bearing, detection.ftcPose.elevation)); + } else { + telemetry.addLine(String.format("\n==== (ID %d) Unknown", detection.id)); + telemetry.addLine(String.format("Center %6.0f %6.0f (pixels)", detection.center.x, detection.center.y)); + } + } // end for() loop + + // Add "key" information to telemetry + telemetry.addLine("\nkey:\nXYZ = X (Right), Y (Forward), Z (Up) dist."); + telemetry.addLine("PRY = Pitch, Roll & Yaw (XYZ Rotation)"); + telemetry.addLine("RBE = Range, Bearing & Elevation"); + + } // end method telemetryAprilTag() + +} // end class diff --git a/FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/ConceptAprilTagLocalization.java b/FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/ConceptAprilTagLocalization.java new file mode 100644 index 0000000..3cb4d9f --- /dev/null +++ b/FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/ConceptAprilTagLocalization.java @@ -0,0 +1,250 @@ +/* Copyright (c) 2024 Dryw Wade. 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 org.firstinspires.ftc.robotcore.external.hardware.camera.BuiltinCameraDirection; +import org.firstinspires.ftc.robotcore.external.hardware.camera.WebcamName; +import org.firstinspires.ftc.robotcore.external.navigation.AngleUnit; +import org.firstinspires.ftc.robotcore.external.navigation.DistanceUnit; +import org.firstinspires.ftc.robotcore.external.navigation.Position; +import org.firstinspires.ftc.robotcore.external.navigation.YawPitchRollAngles; +import org.firstinspires.ftc.vision.VisionPortal; +import org.firstinspires.ftc.vision.apriltag.AprilTagDetection; +import org.firstinspires.ftc.vision.apriltag.AprilTagProcessor; + +import java.util.List; + +/* + * This OpMode illustrates the basics of AprilTag based localization. + * + * For an introduction to AprilTags, see the FTC-DOCS link below: + * https://ftc-docs.firstinspires.org/en/latest/apriltag/vision_portal/apriltag_intro/apriltag-intro.html + * + * In this sample, any visible tag ID will be detected and displayed, but only tags that are included in the default + * "TagLibrary" will be used to compute the robot's location and orientation. This default TagLibrary contains + * the current Season's AprilTags and a small set of "test Tags" in the high number range. + * + * When an AprilTag in the TagLibrary is detected, the SDK provides location and orientation of the robot, relative to the field origin. + * This information is provided in the "robotPose" member of the returned "detection". + * + * To learn about the Field Coordinate System that is defined for FTC (and used by this OpMode), see the FTC-DOCS link below: + * https://ftc-docs.firstinspires.org/en/latest/game_specific_resources/field_coordinate_system/field-coordinate-system.html + * + * 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 = "Concept: AprilTag Localization", group = "Concept") +@Disabled +public class ConceptAprilTagLocalization extends LinearOpMode { + + private static final boolean USE_WEBCAM = true; // true for webcam, false for phone camera + + /** + * Variables to store the position and orientation of the camera on the robot. Setting these + * values requires a definition of the axes of the camera and robot: + * + * Camera axes: + * Origin location: Center of the lens + * Axes orientation: +x right, +y down, +z forward (from camera's perspective) + * + * Robot axes (this is typical, but you can define this however you want): + * Origin location: Center of the robot at field height + * Axes orientation: +x right, +y forward, +z upward + * + * Position: + * If all values are zero (no translation), that implies the camera is at the center of the + * robot. Suppose your camera is positioned 5 inches to the left, 7 inches forward, and 12 + * inches above the ground - you would need to set the position to (-5, 7, 12). + * + * Orientation: + * If all values are zero (no rotation), that implies the camera is pointing straight up. In + * most cases, you'll need to set the pitch to -90 degrees (rotation about the x-axis), meaning + * the camera is horizontal. Use a yaw of 0 if the camera is pointing forwards, +90 degrees if + * it's pointing straight left, -90 degrees for straight right, etc. You can also set the roll + * to +/-90 degrees if it's vertical, or 180 degrees if it's upside-down. + */ + private Position cameraPosition = new Position(DistanceUnit.INCH, + 0, 0, 0, 0); + private YawPitchRollAngles cameraOrientation = new YawPitchRollAngles(AngleUnit.DEGREES, + 0, -90, 0, 0); + + /** + * The variable to store our instance of the AprilTag processor. + */ + private AprilTagProcessor aprilTag; + + /** + * The variable to store our instance of the vision portal. + */ + private VisionPortal visionPortal; + + @Override + public void runOpMode() { + + initAprilTag(); + + // Wait for the DS start button to be touched. + telemetry.addData("DS preview on/off", "3 dots, Camera Stream"); + telemetry.addData(">", "Touch START to start OpMode"); + telemetry.update(); + waitForStart(); + + while (opModeIsActive()) { + + telemetryAprilTag(); + + // Push telemetry to the Driver Station. + telemetry.update(); + + // Save CPU resources; can resume streaming when needed. + if (gamepad1.dpad_down) { + visionPortal.stopStreaming(); + } else if (gamepad1.dpad_up) { + visionPortal.resumeStreaming(); + } + + // Share the CPU. + sleep(20); + } + + // Save more CPU resources when camera is no longer needed. + visionPortal.close(); + + } // end method runOpMode() + + /** + * Initialize the AprilTag processor. + */ + private void initAprilTag() { + + // Create the AprilTag processor. + aprilTag = new AprilTagProcessor.Builder() + + // The following default settings are available to un-comment and edit as needed. + //.setDrawAxes(false) + //.setDrawCubeProjection(false) + //.setDrawTagOutline(true) + //.setTagFamily(AprilTagProcessor.TagFamily.TAG_36h11) + //.setTagLibrary(AprilTagGameDatabase.getCenterStageTagLibrary()) + //.setOutputUnits(DistanceUnit.INCH, AngleUnit.DEGREES) + .setCameraPose(cameraPosition, cameraOrientation) + + // == CAMERA CALIBRATION == + // If you do not manually specify calibration parameters, the SDK will attempt + // to load a predefined calibration for your camera. + //.setLensIntrinsics(578.272, 578.272, 402.145, 221.506) + // ... these parameters are fx, fy, cx, cy. + + .build(); + + // Adjust Image Decimation to trade-off detection-range for detection-rate. + // eg: Some typical detection data using a Logitech C920 WebCam + // Decimation = 1 .. Detect 2" Tag from 10 feet away at 10 Frames per second + // Decimation = 2 .. Detect 2" Tag from 6 feet away at 22 Frames per second + // Decimation = 3 .. Detect 2" Tag from 4 feet away at 30 Frames Per Second (default) + // Decimation = 3 .. Detect 5" Tag from 10 feet away at 30 Frames Per Second (default) + // Note: Decimation can be changed on-the-fly to adapt during a match. + //aprilTag.setDecimation(3); + + // Create the vision portal by using a builder. + VisionPortal.Builder builder = new VisionPortal.Builder(); + + // Set the camera (webcam vs. built-in RC phone camera). + if (USE_WEBCAM) { + builder.setCamera(hardwareMap.get(WebcamName.class, "Webcam 1")); + } else { + builder.setCamera(BuiltinCameraDirection.BACK); + } + + // Choose a camera resolution. Not all cameras support all resolutions. + //builder.setCameraResolution(new Size(640, 480)); + + // Enable the RC preview (LiveView). Set "false" to omit camera monitoring. + //builder.enableLiveView(true); + + // Set the stream format; MJPEG uses less bandwidth than default YUY2. + //builder.setStreamFormat(VisionPortal.StreamFormat.YUY2); + + // Choose whether or not LiveView stops if no processors are enabled. + // If set "true", monitor shows solid orange screen if no processors enabled. + // If set "false", monitor shows camera view without annotations. + //builder.setAutoStopLiveView(false); + + // Set and enable the processor. + builder.addProcessor(aprilTag); + + // Build the Vision Portal, using the above settings. + visionPortal = builder.build(); + + // Disable or re-enable the aprilTag processor at any time. + //visionPortal.setProcessorEnabled(aprilTag, true); + + } // end method initAprilTag() + + /** + * Add telemetry about AprilTag detections. + */ + private void telemetryAprilTag() { + + List currentDetections = aprilTag.getDetections(); + telemetry.addData("# AprilTags Detected", currentDetections.size()); + + // Step through the list of detections and display info for each one. + for (AprilTagDetection detection : currentDetections) { + if (detection.metadata != null) { + telemetry.addLine(String.format("\n==== (ID %d) %s", detection.id, detection.metadata.name)); + // Only use tags that don't have Obelisk in them + if (!detection.metadata.name.contains("Obelisk")) { + telemetry.addLine(String.format("XYZ %6.1f %6.1f %6.1f (inch)", + detection.robotPose.getPosition().x, + detection.robotPose.getPosition().y, + detection.robotPose.getPosition().z)); + telemetry.addLine(String.format("PRY %6.1f %6.1f %6.1f (deg)", + detection.robotPose.getOrientation().getPitch(AngleUnit.DEGREES), + detection.robotPose.getOrientation().getRoll(AngleUnit.DEGREES), + detection.robotPose.getOrientation().getYaw(AngleUnit.DEGREES))); + } + } else { + telemetry.addLine(String.format("\n==== (ID %d) Unknown", detection.id)); + telemetry.addLine(String.format("Center %6.0f %6.0f (pixels)", detection.center.x, detection.center.y)); + } + } // end for() loop + + // Add "key" information to telemetry + telemetry.addLine("\nkey:\nXYZ = X (Right), Y (Forward), Z (Up) dist."); + telemetry.addLine("PRY = Pitch, Roll & Yaw (XYZ Rotation)"); + + } // end method telemetryAprilTag() + +} // end class diff --git a/FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/ConceptAprilTagMultiPortal.java b/FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/ConceptAprilTagMultiPortal.java new file mode 100644 index 0000000..da5cc3e --- /dev/null +++ b/FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/ConceptAprilTagMultiPortal.java @@ -0,0 +1,104 @@ +/* Copyright (c) 2024 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 org.firstinspires.ftc.robotcore.external.hardware.camera.BuiltinCameraDirection; +import org.firstinspires.ftc.robotcore.external.hardware.camera.WebcamName; +import org.firstinspires.ftc.vision.VisionPortal; +import org.firstinspires.ftc.vision.apriltag.AprilTagProcessor; + +/** + * This OpMode demonstrates the basics of using multiple vision portals simultaneously + */ +@TeleOp(name = "Concept: AprilTagMultiPortal", group = "Concept") +@Disabled +public class ConceptAprilTagMultiPortal extends LinearOpMode +{ + VisionPortal portal1; + VisionPortal portal2; + + AprilTagProcessor aprilTagProcessor1; + AprilTagProcessor aprilTagProcessor2; + + @Override + public void runOpMode() throws InterruptedException + { + // Because we want to show two camera feeds simultaneously, we need to inform + // the SDK that we want it to split the camera monitor area into two smaller + // areas for us. It will then give us View IDs which we can pass to the individual + // vision portals to allow them to properly hook into the UI in tandem. + int[] viewIds = VisionPortal.makeMultiPortalView(2, VisionPortal.MultiPortalLayout.VERTICAL); + + // We extract the two view IDs from the array to make our lives a little easier later. + // NB: the array is 2 long because we asked for 2 portals up above. + int portal1ViewId = viewIds[0]; + int portal2ViewId = viewIds[1]; + + // If we want to run AprilTag detection on two portals simultaneously, + // we need to create two distinct instances of the AprilTag processor, + // one for each portal. If you want to see more detail about different + // options that you have when creating these processors, go check out + // the ConceptAprilTag OpMode. + aprilTagProcessor1 = AprilTagProcessor.easyCreateWithDefaults(); + aprilTagProcessor2 = AprilTagProcessor.easyCreateWithDefaults(); + + // Now we build both portals. The CRITICAL thing to notice here is the call to + // setLiveViewContainerId(), where we pass in the IDs we received earlier from + // makeMultiPortalView(). + portal1 = new VisionPortal.Builder() + .setCamera(hardwareMap.get(WebcamName.class, "Webcam 1")) + .setLiveViewContainerId(portal1ViewId) + .addProcessor(aprilTagProcessor1) + .build(); + portal2 = new VisionPortal.Builder() + .setCamera(hardwareMap.get(WebcamName.class, "Webcam 2")) + .setLiveViewContainerId(portal2ViewId) + .addProcessor(aprilTagProcessor2) + .build(); + + waitForStart(); + + // Main Loop + while (opModeIsActive()) + { + // Just show some basic telemetry to demonstrate both processors are working in parallel + // on their respective cameras. If you want to see more detail about the information you + // can get back from the processor, you should look at ConceptAprilTag. + telemetry.addData("Number of tags in Camera 1", aprilTagProcessor1.getDetections().size()); + telemetry.addData("Number of tags in Camera 2", aprilTagProcessor2.getDetections().size()); + telemetry.update(); + sleep(20); + } + } +} diff --git a/FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/ConceptAprilTagOptimizeExposure.java b/FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/ConceptAprilTagOptimizeExposure.java new file mode 100644 index 0000000..adee952 --- /dev/null +++ b/FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/ConceptAprilTagOptimizeExposure.java @@ -0,0 +1,246 @@ +/* Copyright (c) 2023 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; +import org.firstinspires.ftc.robotcore.external.hardware.camera.WebcamName; +import org.firstinspires.ftc.robotcore.external.hardware.camera.controls.ExposureControl; +import org.firstinspires.ftc.robotcore.external.hardware.camera.controls.GainControl; +import org.firstinspires.ftc.vision.VisionPortal; +import org.firstinspires.ftc.vision.apriltag.AprilTagDetection; +import org.firstinspires.ftc.vision.apriltag.AprilTagProcessor; + +import java.util.List; +import java.util.concurrent.TimeUnit; + +/* + * This OpMode determines the best Exposure for minimizing image motion-blur on a Webcam + * Note that it is not possible to control the exposure for a Phone Camera, so if you are using a Phone for the Robot Controller + * this OpMode/Feature only applies to an externally connected Webcam + * + * The goal is to determine the smallest (shortest) Exposure value that still provides reliable Tag Detection. + * Starting with the minimum Exposure and maximum Gain, the exposure is slowly increased until the Tag is + * detected reliably from the likely operational distance. + * + * + * The best way to run this optimization is to view the camera preview screen while changing the exposure and gains. + * + * To do this, you need to view the RobotController screen directly (not from Driver Station) + * This can be done directly from a RC phone screen (if you are using an external Webcam), but for a Control Hub you must either plug an + * HDMI monitor into the Control Hub HDMI port, or use an external viewer program like ScrCpy (https://scrcpy.org/) + * + * Use Android Studio to Copy this Class, and Paste it into the TeamCode/src/main/java/org/firstinspires/ftc/teamcode folder. + * Remove or comment out the @Disabled line to add this OpMode to the Driver Station OpMode list. + */ + +@TeleOp(name="Optimize AprilTag Exposure", group = "Concept") +@Disabled +public class ConceptAprilTagOptimizeExposure extends LinearOpMode +{ + private VisionPortal visionPortal = null; // Used to manage the video source. + private AprilTagProcessor aprilTag; // Used for managing the AprilTag detection process. + private int myExposure ; + private int minExposure ; + private int maxExposure ; + private int myGain ; + private int minGain ; + private int maxGain ; + + boolean thisExpUp = false; + boolean thisExpDn = false; + boolean thisGainUp = false; + boolean thisGainDn = false; + + boolean lastExpUp = false; + boolean lastExpDn = false; + boolean lastGainUp = false; + boolean lastGainDn = false; + @Override public void runOpMode() + { + // Initialize the Apriltag Detection process + initAprilTag(); + + // Establish Min and Max Gains and Exposure. Then set a low exposure with high gain + getCameraSetting(); + myExposure = Math.min(5, minExposure); + myGain = maxGain; + setManualExposure(myExposure, myGain); + + // Wait for the match to begin. + telemetry.addData("Camera preview on/off", "3 dots, Camera Stream"); + telemetry.addData(">", "Touch START to start OpMode"); + telemetry.update(); + waitForStart(); + + while (opModeIsActive()) + { + telemetry.addLine("Find lowest Exposure that gives reliable detection."); + telemetry.addLine("Use Left bump/trig to adjust Exposure."); + telemetry.addLine("Use Right bump/trig to adjust Gain.\n"); + + // Display how many Tags Detected + List currentDetections = aprilTag.getDetections(); + int numTags = currentDetections.size(); + if (numTags > 0 ) + telemetry.addData("Tag", "####### %d Detected ######", currentDetections.size()); + else + telemetry.addData("Tag", "----------- none - ----------"); + + telemetry.addData("Exposure","%d (%d - %d)", myExposure, minExposure, maxExposure); + telemetry.addData("Gain","%d (%d - %d)", myGain, minGain, maxGain); + telemetry.update(); + + // check to see if we need to change exposure or gain. + thisExpUp = gamepad1.left_bumper; + thisExpDn = gamepad1.left_trigger > 0.25; + thisGainUp = gamepad1.right_bumper; + thisGainDn = gamepad1.right_trigger > 0.25; + + // look for clicks to change exposure + if (thisExpUp && !lastExpUp) { + myExposure = Range.clip(myExposure + 1, minExposure, maxExposure); + setManualExposure(myExposure, myGain); + } else if (thisExpDn && !lastExpDn) { + myExposure = Range.clip(myExposure - 1, minExposure, maxExposure); + setManualExposure(myExposure, myGain); + } + + // look for clicks to change the gain + if (thisGainUp && !lastGainUp) { + myGain = Range.clip(myGain + 1, minGain, maxGain ); + setManualExposure(myExposure, myGain); + } else if (thisGainDn && !lastGainDn) { + myGain = Range.clip(myGain - 1, minGain, maxGain ); + setManualExposure(myExposure, myGain); + } + + lastExpUp = thisExpUp; + lastExpDn = thisExpDn; + lastGainUp = thisGainUp; + lastGainDn = thisGainDn; + + sleep(20); + } + } + + /** + * Initialize the AprilTag processor. + */ + private void initAprilTag() { + // Create the AprilTag processor by using a builder. + aprilTag = new AprilTagProcessor.Builder().build(); + + // Create the WEBCAM vision portal by using a builder. + visionPortal = new VisionPortal.Builder() + .setCamera(hardwareMap.get(WebcamName.class, "Webcam 1")) + .addProcessor(aprilTag) + .build(); + } + + /* + Manually set the camera gain and exposure. + Can only be called AFTER calling initAprilTag(); + Returns true if controls are set. + */ + private boolean setManualExposure(int exposureMS, int gain) { + // Ensure Vision Portal has been setup. + if (visionPortal == null) { + return false; + } + + // Wait for the camera to be open + if (visionPortal.getCameraState() != VisionPortal.CameraState.STREAMING) { + telemetry.addData("Camera", "Waiting"); + telemetry.update(); + while (!isStopRequested() && (visionPortal.getCameraState() != VisionPortal.CameraState.STREAMING)) { + sleep(20); + } + telemetry.addData("Camera", "Ready"); + telemetry.update(); + } + + // Set camera controls unless we are stopping. + if (!isStopRequested()) + { + // Set exposure. Make sure we are in Manual Mode for these values to take effect. + ExposureControl exposureControl = visionPortal.getCameraControl(ExposureControl.class); + if (exposureControl.getMode() != ExposureControl.Mode.Manual) { + exposureControl.setMode(ExposureControl.Mode.Manual); + sleep(50); + } + exposureControl.setExposure((long)exposureMS, TimeUnit.MILLISECONDS); + sleep(20); + + // Set Gain. + GainControl gainControl = visionPortal.getCameraControl(GainControl.class); + gainControl.setGain(gain); + sleep(20); + return (true); + } else { + return (false); + } + } + + /* + Read this camera's minimum and maximum Exposure and Gain settings. + Can only be called AFTER calling initAprilTag(); + */ + private void getCameraSetting() { + // Ensure Vision Portal has been setup. + if (visionPortal == null) { + return; + } + + // Wait for the camera to be open + if (visionPortal.getCameraState() != VisionPortal.CameraState.STREAMING) { + telemetry.addData("Camera", "Waiting"); + telemetry.update(); + while (!isStopRequested() && (visionPortal.getCameraState() != VisionPortal.CameraState.STREAMING)) { + sleep(20); + } + telemetry.addData("Camera", "Ready"); + telemetry.update(); + } + + // Get camera control values unless we are stopping. + if (!isStopRequested()) { + ExposureControl exposureControl = visionPortal.getCameraControl(ExposureControl.class); + minExposure = (int)exposureControl.getMinExposure(TimeUnit.MILLISECONDS) + 1; + maxExposure = (int)exposureControl.getMaxExposure(TimeUnit.MILLISECONDS); + + GainControl gainControl = visionPortal.getCameraControl(GainControl.class); + minGain = gainControl.getMinGain(); + maxGain = gainControl.getMaxGain(); + } + } +} diff --git a/FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/ConceptAprilTagSwitchableCameras.java b/FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/ConceptAprilTagSwitchableCameras.java new file mode 100644 index 0000000..02e83d3 --- /dev/null +++ b/FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/ConceptAprilTagSwitchableCameras.java @@ -0,0 +1,194 @@ +/* Copyright (c) 2023 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 org.firstinspires.ftc.robotcore.external.ClassFactory; +import org.firstinspires.ftc.robotcore.external.hardware.camera.CameraName; +import org.firstinspires.ftc.robotcore.external.hardware.camera.WebcamName; +import org.firstinspires.ftc.vision.VisionPortal; +import org.firstinspires.ftc.vision.VisionPortal.CameraState; +import org.firstinspires.ftc.vision.apriltag.AprilTagDetection; +import org.firstinspires.ftc.vision.apriltag.AprilTagProcessor; + +import java.util.List; + +/* + * This OpMode illustrates the basics of AprilTag recognition and pose estimation, using + * two webcams. + * + * 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 = "Concept: AprilTag Switchable Cameras", group = "Concept") +@Disabled +public class ConceptAprilTagSwitchableCameras extends LinearOpMode { + + /* + * Variables used for switching cameras. + */ + private WebcamName webcam1, webcam2; + private boolean oldLeftBumper; + private boolean oldRightBumper; + + /** + * The variable to store our instance of the AprilTag processor. + */ + private AprilTagProcessor aprilTag; + + /** + * The variable to store our instance of the vision portal. + */ + private VisionPortal visionPortal; + + @Override + public void runOpMode() { + + initAprilTag(); + + // Wait for the DS start button to be touched. + telemetry.addData("DS preview on/off", "3 dots, Camera Stream"); + telemetry.addData(">", "Touch START to start OpMode"); + telemetry.update(); + waitForStart(); + + while (opModeIsActive()) { + + telemetryCameraSwitching(); + telemetryAprilTag(); + + // Push telemetry to the Driver Station. + telemetry.update(); + + // Save CPU resources; can resume streaming when needed. + if (gamepad1.dpad_down) { + visionPortal.stopStreaming(); + } else if (gamepad1.dpad_up) { + visionPortal.resumeStreaming(); + } + + doCameraSwitching(); + + // Share the CPU. + sleep(20); + } + + // Save more CPU resources when camera is no longer needed. + visionPortal.close(); + + } // end runOpMode() + + /** + * Initialize the AprilTag processor. + */ + private void initAprilTag() { + + // Create the AprilTag processor by using a builder. + aprilTag = new AprilTagProcessor.Builder().build(); + + webcam1 = hardwareMap.get(WebcamName.class, "Webcam 1"); + webcam2 = hardwareMap.get(WebcamName.class, "Webcam 2"); + CameraName switchableCamera = ClassFactory.getInstance() + .getCameraManager().nameForSwitchableCamera(webcam1, webcam2); + + // Create the vision portal by using a builder. + visionPortal = new VisionPortal.Builder() + .setCamera(switchableCamera) + .addProcessor(aprilTag) + .build(); + + } // end method initAprilTag() + + /** + * Add telemetry about camera switching. + */ + private void telemetryCameraSwitching() { + + if (visionPortal.getActiveCamera().equals(webcam1)) { + telemetry.addData("activeCamera", "Webcam 1"); + telemetry.addData("Press RightBumper", "to switch to Webcam 2"); + } else { + telemetry.addData("activeCamera", "Webcam 2"); + telemetry.addData("Press LeftBumper", "to switch to Webcam 1"); + } + + } // end method telemetryCameraSwitching() + + /** + * Add telemetry about AprilTag detections. + */ + private void telemetryAprilTag() { + + List currentDetections = aprilTag.getDetections(); + telemetry.addData("# AprilTags Detected", currentDetections.size()); + + // Step through the list of detections and display info for each one. + for (AprilTagDetection detection : currentDetections) { + if (detection.metadata != null) { + telemetry.addLine(String.format("\n==== (ID %d) %s", detection.id, detection.metadata.name)); + telemetry.addLine(String.format("XYZ %6.1f %6.1f %6.1f (inch)", detection.ftcPose.x, detection.ftcPose.y, detection.ftcPose.z)); + telemetry.addLine(String.format("PRY %6.1f %6.1f %6.1f (deg)", detection.ftcPose.pitch, detection.ftcPose.roll, detection.ftcPose.yaw)); + telemetry.addLine(String.format("RBE %6.1f %6.1f %6.1f (inch, deg, deg)", detection.ftcPose.range, detection.ftcPose.bearing, detection.ftcPose.elevation)); + } else { + telemetry.addLine(String.format("\n==== (ID %d) Unknown", detection.id)); + telemetry.addLine(String.format("Center %6.0f %6.0f (pixels)", detection.center.x, detection.center.y)); + } + } // end for() loop + + // Add "key" information to telemetry + telemetry.addLine("\nkey:\nXYZ = X (Right), Y (Forward), Z (Up) dist."); + telemetry.addLine("PRY = Pitch, Roll & Yaw (XYZ Rotation)"); + telemetry.addLine("RBE = Range, Bearing & Elevation"); + + } // end method telemetryAprilTag() + + /** + * Set the active camera according to input from the gamepad. + */ + private void doCameraSwitching() { + if (visionPortal.getCameraState() == CameraState.STREAMING) { + // If the left bumper is pressed, use Webcam 1. + // If the right bumper is pressed, use Webcam 2. + boolean newLeftBumper = gamepad1.left_bumper; + boolean newRightBumper = gamepad1.right_bumper; + if (newLeftBumper && !oldLeftBumper) { + visionPortal.setActiveCamera(webcam1); + } else if (newRightBumper && !oldRightBumper) { + visionPortal.setActiveCamera(webcam2); + } + oldLeftBumper = newLeftBumper; + oldRightBumper = newRightBumper; + } + + } // end method doCameraSwitching() + +} // end class diff --git a/FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/ConceptBlackboard.java b/FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/ConceptBlackboard.java new file mode 100644 index 0000000..2c93abb --- /dev/null +++ b/FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/ConceptBlackboard.java @@ -0,0 +1,87 @@ +/* Copyright (c) 2025 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.OpMode; +import com.qualcomm.robotcore.eventloop.opmode.TeleOp; + +/* + * Demonstrates how to use an OpMode to store data in the blackboard and retrieve it. + * This is useful for storing information in Auto and then retrieving it in your TeleOp. + * + * Be aware that this is NOT saved across power reboots or downloads or robot resets. + * + * You also need to make sure that both the storing and retrieving are done using the same + * type. For example, storing a Double in the blackboard and retrieving it as an Integer + * will fail when you typecast it. + * + * The blackboard is a standard hashmap so you can use methods like: + * put, get, containsKey, remove, etc. + */ +@TeleOp(name = "Concept: Blackboard", group = "Concept") +@Disabled +public class ConceptBlackboard extends OpMode { + // We use constants here so we won't have the problem of typos in different places when we + // meant to refer to the same thing. + public static final String TIMES_STARTED_KEY = "Times started"; + public static final String ALLIANCE_KEY = "Alliance"; + + /** + * This method will be called once, when the INIT button is pressed. + */ + @Override + public void init() { + // This gets us what is in the blackboard or the default if it isn't in there. + Object timesStarted = blackboard.getOrDefault(TIMES_STARTED_KEY, 0); + blackboard.put(TIMES_STARTED_KEY, (int) timesStarted + 1); + + telemetry.addData("OpMode started times", blackboard.get(TIMES_STARTED_KEY)); + } + + /** + * This method will be called repeatedly during the period between when + * the START button is pressed and when the OpMode is stopped. + *

+ * If the left bumper is pressed it will store the value "RED" in the blackboard for alliance. + * If the right bumper is pressed it will store the value "BLUE" in the blackboard for alliance. + *

+ * You'll notice that if you quit the OpMode and come back in, the value will persist. + */ + @Override + public void loop() { + if (gamepad1.left_bumper) { + blackboard.put(ALLIANCE_KEY, "RED"); + } else if (gamepad1.right_bumper) { + blackboard.put(ALLIANCE_KEY, "BLUE"); + } + telemetry.addData("Alliance", blackboard.get(ALLIANCE_KEY)); + } +} diff --git a/FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/ConceptExploringIMUOrientation.java b/FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/ConceptExploringIMUOrientation.java new file mode 100644 index 0000000..751d54f --- /dev/null +++ b/FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/ConceptExploringIMUOrientation.java @@ -0,0 +1,184 @@ +/* +Copyright (c) 2022 REV Robotics, 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 REV Robotics 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.rev.RevHubOrientationOnRobot; +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.IMU; +import org.firstinspires.ftc.robotcore.external.navigation.AngleUnit; +import org.firstinspires.ftc.robotcore.external.navigation.AngularVelocity; +import org.firstinspires.ftc.robotcore.external.navigation.YawPitchRollAngles; + +/* + * This OpMode demonstrates the impact of setting the IMU orientation correctly or incorrectly. This + * code assumes there is an IMU configured with the name "imu". + * + * Note: This OpMode is more of a tool than a code sample. The User Interface portion of this code + * goes beyond simply showing how to interface to the IMU.
+ * For a minimal example of interfacing to an IMU, please see the SensorIMUOrthogonal or SensorIMUNonOrthogonal sample OpModes. + * + * This OpMode enables you to re-specify the Hub Mounting orientation dynamically by using gamepad controls. + * While doing so, the sample will display how Pitch, Roll and Yaw angles change as the hub is moved. + * + * The gamepad controls let you change the two parameters that specify how the Control/Expansion Hub is mounted.
+ * The first parameter specifies which direction the printed logo on the Hub is pointing.
+ * The second parameter specifies which direction the USB connector on the Hub is pointing.
+ * All directions are relative to the robot, and left/right is as viewed from behind the robot. + * + * How will you know if you have chosen the correct Orientation? With the correct orientation + * parameters selected, pitch/roll/yaw should act as follows: + * + * Pitch value should INCREASE as the robot is tipped UP at the front. (Rotation about X)
+ * Roll value should INCREASE as the robot is tipped UP at the left side. (Rotation about Y)
+ * Yaw value should INCREASE as the robot is rotated Counter Clockwise. (Rotation about Z)
+ * + * The Yaw can be reset (to zero) by pressing the Y button on the gamepad (Triangle on a PS4 controller) + * + * The rotational velocities should follow the change in corresponding axes. + */ + +@TeleOp(name="Concept: IMU Orientation", group="Concept") +@Disabled +public class ConceptExploringIMUOrientation extends LinearOpMode { + static RevHubOrientationOnRobot.LogoFacingDirection[] logoFacingDirections + = RevHubOrientationOnRobot.LogoFacingDirection.values(); + static RevHubOrientationOnRobot.UsbFacingDirection[] usbFacingDirections + = RevHubOrientationOnRobot.UsbFacingDirection.values(); + static int LAST_DIRECTION = logoFacingDirections.length - 1; + static float TRIGGER_THRESHOLD = 0.2f; + + IMU imu; + int logoFacingDirectionPosition; + int usbFacingDirectionPosition; + boolean orientationIsValid = true; + + @Override public void runOpMode() throws InterruptedException { + imu = hardwareMap.get(IMU.class, "imu"); + logoFacingDirectionPosition = 0; // Up + usbFacingDirectionPosition = 2; // Forward + + updateOrientation(); + + boolean justChangedLogoDirection = false; + boolean justChangedUsbDirection = false; + + // Loop until stop requested + while (!isStopRequested()) { + + // Check to see if Yaw reset is requested (Y button) + if (gamepad1.y) { + telemetry.addData("Yaw", "Resetting\n"); + imu.resetYaw(); + } else { + telemetry.addData("Yaw", "Press Y (triangle) on Gamepad to reset.\n"); + } + + // Check to see if new Logo Direction is requested + if (gamepad1.left_bumper || gamepad1.right_bumper) { + if (!justChangedLogoDirection) { + justChangedLogoDirection = true; + if (gamepad1.left_bumper) { + logoFacingDirectionPosition--; + if (logoFacingDirectionPosition < 0) { + logoFacingDirectionPosition = LAST_DIRECTION; + } + } else { + logoFacingDirectionPosition++; + if (logoFacingDirectionPosition > LAST_DIRECTION) { + logoFacingDirectionPosition = 0; + } + } + updateOrientation(); + } + } else { + justChangedLogoDirection = false; + } + + // Check to see if new USB Direction is requested + if (gamepad1.left_trigger > TRIGGER_THRESHOLD || gamepad1.right_trigger > TRIGGER_THRESHOLD) { + if (!justChangedUsbDirection) { + justChangedUsbDirection = true; + if (gamepad1.left_trigger > TRIGGER_THRESHOLD) { + usbFacingDirectionPosition--; + if (usbFacingDirectionPosition < 0) { + usbFacingDirectionPosition = LAST_DIRECTION; + } + } else { + usbFacingDirectionPosition++; + if (usbFacingDirectionPosition > LAST_DIRECTION) { + usbFacingDirectionPosition = 0; + } + } + updateOrientation(); + } + } else { + justChangedUsbDirection = false; + } + + // Display User instructions and IMU data + telemetry.addData("logo Direction (set with bumpers)", logoFacingDirections[logoFacingDirectionPosition]); + telemetry.addData("usb Direction (set with triggers)", usbFacingDirections[usbFacingDirectionPosition] + "\n"); + + if (orientationIsValid) { + YawPitchRollAngles orientation = imu.getRobotYawPitchRollAngles(); + AngularVelocity angularVelocity = imu.getRobotAngularVelocity(AngleUnit.DEGREES); + + telemetry.addData("Yaw (Z)", "%.2f Deg. (Heading)", orientation.getYaw(AngleUnit.DEGREES)); + telemetry.addData("Pitch (X)", "%.2f Deg.", orientation.getPitch(AngleUnit.DEGREES)); + telemetry.addData("Roll (Y)", "%.2f Deg.\n", orientation.getRoll(AngleUnit.DEGREES)); + telemetry.addData("Yaw (Z) velocity", "%.2f Deg/Sec", angularVelocity.zRotationRate); + telemetry.addData("Pitch (X) velocity", "%.2f Deg/Sec", angularVelocity.xRotationRate); + telemetry.addData("Roll (Y) velocity", "%.2f Deg/Sec", angularVelocity.yRotationRate); + } else { + telemetry.addData("Error", "Selected orientation on robot is invalid"); + } + + telemetry.update(); + } + } + + // apply any requested orientation changes. + void updateOrientation() { + RevHubOrientationOnRobot.LogoFacingDirection logo = logoFacingDirections[logoFacingDirectionPosition]; + RevHubOrientationOnRobot.UsbFacingDirection usb = usbFacingDirections[usbFacingDirectionPosition]; + try { + RevHubOrientationOnRobot orientationOnRobot = new RevHubOrientationOnRobot(logo, usb); + imu.initialize(new IMU.Parameters(orientationOnRobot)); + orientationIsValid = true; + } catch (IllegalArgumentException e) { + orientationIsValid = false; + } + } +} diff --git a/FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/ConceptGamepadEdgeDetection.java b/FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/ConceptGamepadEdgeDetection.java new file mode 100644 index 0000000..d3c4417 --- /dev/null +++ b/FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/ConceptGamepadEdgeDetection.java @@ -0,0 +1,108 @@ +/* +Copyright (c) 2024 Miriam Sinton-Remes + +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 FITNESSFOR 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; + +/* + * This OpMode illustrates using edge detection on a gamepad. + * + * Simply checking the state of a gamepad button each time could result in triggering an effect + * multiple times. Edge detection ensures that you only detect one button press, regardless of how + * long the button is held. + * + * There are two main types of edge detection. Rising edge detection will trigger when a button is + * first pressed. Falling edge detection will trigger when the button is released. + * + * 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. + */ + +@Disabled +@TeleOp(name="Concept: Gamepad Edge Detection", group ="Concept") +public class ConceptGamepadEdgeDetection extends LinearOpMode { + + @Override + public void runOpMode() { + // Wait for the DS start button to be pressed + waitForStart(); + + while (opModeIsActive()) { + // Update the telemetry + telemetryButtonData(); + + // Wait 2 seconds before doing another check + sleep(2000); + } + } + + public void telemetryButtonData() { + // Add the status of the Gamepad 1 Left Bumper + telemetry.addData("Gamepad 1 Left Bumper Pressed", gamepad1.leftBumperWasPressed()); + telemetry.addData("Gamepad 1 Left Bumper Released", gamepad1.leftBumperWasReleased()); + telemetry.addData("Gamepad 1 Left Bumper Status", gamepad1.left_bumper); + + // Add an empty line to separate the buttons in telemetry + telemetry.addLine(); + + // Add the status of the Gamepad 1 Right Bumper + telemetry.addData("Gamepad 1 Right Bumper Pressed", gamepad1.rightBumperWasPressed()); + telemetry.addData("Gamepad 1 Right Bumper Released", gamepad1.rightBumperWasReleased()); + telemetry.addData("Gamepad 1 Right Bumper Status", gamepad1.right_bumper); + + // Add an empty line to separate the buttons in telemetry + telemetry.addLine(); + + // Add the status of the Gamepad 1 Left trigger + telemetry.addData("Gamepad 1 Left Trigger Pressed", gamepad1.leftTriggerWasPressed()); + telemetry.addData("Gamepad 1 Left Trigger Released", gamepad1.leftTriggerWasReleased()); + telemetry.addData("Gamepad 1 Left Trigger Status", gamepad1.left_trigger_pressed); + + // Add an empty line to separate the buttons in telemetry + telemetry.addLine(); + + // Add the status of the Gamepad 1 Right trigger + telemetry.addData("Gamepad 1 Right Trigger Pressed", gamepad1.rightTriggerWasPressed()); + telemetry.addData("Gamepad 1 Right Trigger Released", gamepad1.rightTriggerWasReleased()); + telemetry.addData("Gamepad 1 Right Trigger Status", gamepad1.right_trigger_pressed); + + // Add a note that the telemetry is only updated every 2 seconds + telemetry.addLine("\nTelemetry is updated every 2 seconds."); + + // Update the telemetry on the DS screen + telemetry.update(); + } +} diff --git a/FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/ConceptGamepadRumble.java b/FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/ConceptGamepadRumble.java new file mode 100644 index 0000000..cf846e1 --- /dev/null +++ b/FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/ConceptGamepadRumble.java @@ -0,0 +1,200 @@ +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.Gamepad; +import com.qualcomm.robotcore.util.ElapsedTime; + +/* + * This OpMode illustrates using the rumble feature of many gamepads. + * + * Note: Some gamepads "rumble" better than others. + * The Xbox & PS4 have a left (rumble1) and right (rumble2) rumble motor. + * These two gamepads have two distinct rumble modes: Large on the left, and small on the right + * The Etpark gamepad may only respond to rumble1, and may only run at full power. + * The Logitech F310 gamepad does not have *any* rumble ability. + * + * Moral: You should use this sample to experiment with your specific gamepads to explore their rumble features. + * + * The rumble motors are accessed through the standard gamepad1 and gamepad2 objects. + * Several new methods were added to the Gamepad class in FTC SDK Rev 7 + * The key methods are as follows: + * + * .rumble(double rumble1, double rumble2, int durationMs) + * This method sets the rumble power of both motors for a specific time duration. + * Both rumble arguments are motor-power levels in the 0.0 to 1.0 range. + * durationMs is the desired length of the rumble action in milliseconds. + * This method returns immediately. + * Note: + * Use a durationMs of Gamepad.RUMBLE_DURATION_CONTINUOUS to provide a continuous rumble + * Use a power of 0, or duration of 0 to stop a rumble. + * + * .rumbleBlips(int count) allows an easy way to signal the driver with a series of rumble blips. + * Just specify how many blips you want. + * This method returns immediately. + * + * .runRumbleEffect(customRumbleEffect) allows you to run a custom rumble sequence that you have + * built using the Gamepad.RumbleEffect.Builder() + * A "custom effect" is a sequence of steps, where each step can rumble any of the + * rumble motors for a specific period at a specific power level. + * The Custom Effect will play as the (un-blocked) OpMode continues to run + * + * .isRumbling() returns true if there is a rumble effect in progress. + * Use this call to prevent stepping on a current rumble. + * + * .stopRumble() Stop any ongoing rumble or custom rumble effect. + * + * .rumble(int durationMs) Full power rumble for fixed duration. + * + * Note: Whenever a new Rumble command is issued, any currently executing rumble action will + * be truncated, and the new action started immediately. Take these precautions: + * 1) Do Not SPAM the rumble motors by issuing rapid fire commands + * 2) Multiple sources for rumble commands must coordinate to avoid tromping on each other. + * + * This can be achieved several possible ways: + * 1) Only having one source for rumble actions + * 2) Issuing rumble commands on transitions, rather than states. + * e.g. The moment a touch sensor is pressed, rather than the entire time it is being pressed. + * 3) Scheduling rumble commands based on timed events. e.g. 10 seconds prior to endgame + * 4) Rumble on non-overlapping mechanical actions. e.g. arm fully-extended or fully-retracted. + * 5) Use isRumbling() to hold off on a new rumble if one is already in progress. + * + * The examples shown here are representstive of how to invoke a gamepad rumble. + * It is assumed that these will be modified to suit the specific robot and team strategy needs. + * + * ######## Read the telemetry display on the Driver Station Screen for instructions. ###### + * + * Ex 1) This example shows a) how to create a custom rumble effect, and then b) how to trigger it based + * on game time. One use for this might be to alert the driver that half-time or End-game is approaching. + * + * Ex 2) This example shows tying the rumble power to a changing sensor value. + * In this case it is the Gamepad trigger, but it could be any sensor output scaled to the 0-1 range. + * Since it takes over the rumble motors, it is only performed when the Left Bumper is pressed. + * Note that this approach MUST include a way to turn OFF the rumble when the button is released. + * + * Ex 3) This example shows a simple way to trigger a 3-blip sequence. In this case it is + * triggered by the gamepad A (Cross) button, but it could be any sensor, like a touch or light sensor. + * Note that this code ensures that it only rumbles once when the input goes true. + * + * Ex 4) This example shows how to trigger a single rumble when an input value gets over a certain value. + * In this case it is reading the Right Trigger, but it could be any variable sensor, like a + * range sensor, or position sensor. The code needs to ensure that it is only triggered once, so + * it waits till the sensor drops back below the threshold before it can trigger again. + * + * 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. + */ + +@Disabled +@TeleOp(name="Concept: Gamepad Rumble", group ="Concept") +public class ConceptGamepadRumble extends LinearOpMode +{ + boolean lastA = false; // Use to track the prior button state. + boolean lastLB = false; // Use to track the prior button state. + boolean highLevel = false; // used to prevent multiple level-based rumbles. + boolean secondHalf = false; // Use to prevent multiple half-time warning rumbles. + + Gamepad.RumbleEffect customRumbleEffect; // Use to build a custom rumble sequence. + ElapsedTime runtime = new ElapsedTime(); // Use to determine when end game is starting. + + final double HALF_TIME = 60.0; // Wait this many seconds before rumble-alert for half-time. + final double TRIGGER_THRESHOLD = 0.75; // Squeeze more than 3/4 to get rumble. + + @Override + public void runOpMode() + { + // Example 1. a) start by creating a three-pulse rumble sequence: right, LEFT, LEFT + customRumbleEffect = new Gamepad.RumbleEffect.Builder() + .addStep(0.0, 1.0, 500) // Rumble right motor 100% for 500 mSec + .addStep(0.0, 0.0, 300) // Pause for 300 mSec + .addStep(1.0, 0.0, 250) // Rumble left motor 100% for 250 mSec + .addStep(0.0, 0.0, 250) // Pause for 250 mSec + .addStep(1.0, 0.0, 250) // Rumble left motor 100% for 250 mSec + .build(); + + telemetry.addData(">", "Press Start"); + telemetry.update(); + + waitForStart(); + runtime.reset(); // Start game timer. + + // Loop while monitoring buttons for rumble triggers + while (opModeIsActive()) + { + // Read and save the current gamepad button states. + boolean currentA = gamepad1.a ; + boolean currentLB = gamepad1.left_bumper ; + + // Display the current Rumble status. Just for interest. + telemetry.addData(">", "Are we RUMBLING? %s\n", gamepad1.isRumbling() ? "YES" : "no" ); + + // ---------------------------------------------------------------------------------------- + // Example 1. b) Watch the runtime timer, and run the custom rumble when we hit half-time. + // Make sure we only signal once by setting "secondHalf" flag to prevent further rumbles. + // ---------------------------------------------------------------------------------------- + if ((runtime.seconds() > HALF_TIME) && !secondHalf) { + gamepad1.runRumbleEffect(customRumbleEffect); + secondHalf =true; + } + + // Display the time remaining while we are still counting down. + if (!secondHalf) { + telemetry.addData(">", "Halftime Alert Countdown: %3.0f Sec \n", (HALF_TIME - runtime.seconds()) ); + } + + + // ---------------------------------------------------------------------------------------- + // Example 2. If Left Bumper is being pressed, power the rumble motors based on the two trigger depressions. + // This is useful to see how the rumble feels at various power levels. + // ---------------------------------------------------------------------------------------- + if (currentLB) { + // Left Bumper is being pressed, so send left and right "trigger" values to left and right rumble motors. + gamepad1.rumble(gamepad1.left_trigger, gamepad1.right_trigger, Gamepad.RUMBLE_DURATION_CONTINUOUS); + + // Show what is being sent to rumbles + telemetry.addData(">", "Squeeze triggers to control rumbles"); + telemetry.addData("> : Rumble", "Left: %.0f%% Right: %.0f%%", gamepad1.left_trigger * 100, gamepad1.right_trigger * 100); + } else { + // Make sure rumble is turned off when Left Bumper is released (only one time each press) + if (lastLB) { + gamepad1.stopRumble(); + } + + // Prompt for manual rumble action + telemetry.addData(">", "Hold Left-Bumper to test Manual Rumble"); + telemetry.addData(">", "Press A (Cross) for three blips"); + telemetry.addData(">", "Squeeze right trigger slowly for 1 blip"); + } + lastLB = currentLB; // remember the current button state for next time around the loop + + + // ---------------------------------------------------------------------------------------- + // Example 3. Blip 3 times at the moment that A (Cross) is pressed. (look for pressed transition) + // BUT !!! Skip it altogether if the Gamepad is already rumbling. + // ---------------------------------------------------------------------------------------- + if (currentA && !lastA) { + if (!gamepad1.isRumbling()) // Check for possible overlap of rumbles. + gamepad1.rumbleBlips(3); + } + lastA = currentA; // remember the current button state for next time around the loop + + + // ---------------------------------------------------------------------------------------- + // Example 4. Rumble once when gamepad right trigger goes above the THRESHOLD. + // ---------------------------------------------------------------------------------------- + if (gamepad1.right_trigger > TRIGGER_THRESHOLD) { + if (!highLevel) { + gamepad1.rumble(0.9, 0, 200); // 200 mSec burst on left motor. + highLevel = true; // Hold off any more triggers + } + } else { + highLevel = false; // We can trigger again now. + } + + // Send the telemetry data to the Driver Station, and then pause to pace the program. + telemetry.update(); + sleep(10); + } + } +} diff --git a/FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/ConceptGamepadTouchpad.java b/FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/ConceptGamepadTouchpad.java new file mode 100644 index 0000000..84d8cec --- /dev/null +++ b/FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/ConceptGamepadTouchpad.java @@ -0,0 +1,77 @@ +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 org.firstinspires.ftc.robotcore.external.Telemetry; + +/* + * This OpMode illustrates using the touchpad feature found on some gamepads. + * + * The Sony PS4 gamepad can detect two distinct touches on the central touchpad. + * Other gamepads with different touchpads may provide mixed results. + * + * The touchpads are accessed through the standard gamepad1 and gamepad2 objects. + * Several new members were added to the Gamepad class in FTC SDK Rev 7 + * + * .touchpad_finger_1 returns true if at least one finger is detected. + * .touchpad_finger_1_x finger 1 X coordinate. Valid if touchpad_finger_1 is true + * .touchpad_finger_1_y finger 1 Y coordinate. Valid if touchpad_finger_1 is true + * + * .touchpad_finger_2 returns true if a second finger is detected + * .touchpad_finger_2_x finger 2 X coordinate. Valid if touchpad_finger_2 is true + * .touchpad_finger_2_y finger 2 Y coordinate. Valid if touchpad_finger_2 is true + * + * Finger touches are reported with an X and Y coordinate in following coordinate system. + * + * 1) X is the Horizontal axis, and Y is the vertical axis + * 2) The 0,0 origin is at the center of the touchpad. + * 3) 1.0, 1.0 is at the top right corner of the touchpad. + * 4) -1.0,-1.0 is at the bottom left corner of the touchpad. + * + * 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. + */ + +@Disabled +@TeleOp(name="Concept: Gamepad Touchpad", group ="Concept") +public class ConceptGamepadTouchpad extends LinearOpMode +{ + @Override + public void runOpMode() + { + telemetry.setDisplayFormat(Telemetry.DisplayFormat.MONOSPACE); + + telemetry.addData(">", "Press Start"); + telemetry.update(); + + waitForStart(); + + while (opModeIsActive()) + { + boolean finger = false; + + // Display finger 1 x & y position if finger detected + if(gamepad1.touchpad_finger_1) + { + finger = true; + telemetry.addLine(String.format("Finger 1: x=%5.2f y=%5.2f\n", gamepad1.touchpad_finger_1_x, gamepad1.touchpad_finger_1_y)); + } + + // Display finger 2 x & y position if finger detected + if(gamepad1.touchpad_finger_2) + { + finger = true; + telemetry.addLine(String.format("Finger 2: x=%5.2f y=%5.2f\n", gamepad1.touchpad_finger_2_x, gamepad1.touchpad_finger_2_y)); + } + + if(!finger) + { + telemetry.addLine("No fingers"); + } + + telemetry.update(); + sleep(10); + } + } +} diff --git a/FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/ConceptLEDStick.java b/FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/ConceptLEDStick.java new file mode 100644 index 0000000..01729bb --- /dev/null +++ b/FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/ConceptLEDStick.java @@ -0,0 +1,123 @@ +package org.firstinspires.ftc.robotcontroller.external.samples; +/* + Copyright (c) 2021-24 Alan Smith + + 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 Alan Smith 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 FITNESSFOR 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. +*/ + +import android.graphics.Color; + +import com.qualcomm.hardware.sparkfun.SparkFunLEDStick; +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.util.Range; + +/* + * This OpMode illustrates how to use the SparkFun QWIIC LED Strip + * + * This is a simple way to add a strip of 10 LEDs to your robot where you can set the color of each + * LED or the whole strip. This allows for driver feedback or even just fun ways to show your team + * colors. + * + * Why? + * Because more LEDs == more fun!! + * + * This OpMode assumes that the QWIIC LED Stick is attached to an I2C interface named "back_leds" in the robot configuration. + * + * 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 + * + * You can buy this product here: https://www.sparkfun.com/products/18354 + * Don't forget to also buy this to make it easy to connect to your Control or Expansion Hub: + * https://www.sparkfun.com/products/25596 + */ +@TeleOp(name = "Concept: LED Stick", group = "Concept") +@Disabled +public class ConceptLEDStick extends OpMode { + private boolean wasUp; + private boolean wasDown; + private int brightness = 5; // this needs to be between 0 and 31 + private final static double END_GAME_TIME = 120 - 30; + + private SparkFunLEDStick ledStick; + + @Override + public void init() { + ledStick = hardwareMap.get(SparkFunLEDStick.class, "back_leds"); + ledStick.setBrightness(brightness); + ledStick.setColor(Color.GREEN); + } + + @Override + public void start() { + resetRuntime(); + } + + @Override + public void loop() { + telemetry.addLine("Hold the A button to turn blue"); + telemetry.addLine("Hold the B button to turn red"); + telemetry.addLine("Hold the left bumper to turn off"); + telemetry.addLine("Use DPAD Up/Down to change brightness"); + + if (getRuntime() > END_GAME_TIME) { + int[] ledColors = {Color.RED, Color.YELLOW, Color.RED, Color.YELLOW, Color.RED, + Color.YELLOW, Color.RED, Color.YELLOW, Color.RED, Color.YELLOW}; + ledStick.setColors(ledColors); + } else if (gamepad1.a) { + ledStick.setColor(Color.BLUE); + } else if (gamepad1.b) { + ledStick.setColor(Color.RED); + } else if (gamepad1.left_bumper) { + ledStick.turnAllOff(); + } else { + ledStick.setColor(Color.GREEN); + } + + /* + * Use DPAD up and down to change brightness + */ + int newBrightness = brightness; + if (gamepad1.dpad_up && !wasUp) { + newBrightness = brightness + 1; + } else if (gamepad1.dpad_down && !wasDown) { + newBrightness = brightness - 1; + } + if (newBrightness != brightness) { + brightness = Range.clip(newBrightness, 0, 31); + ledStick.setBrightness(brightness); + } + telemetry.addData("Brightness", brightness); + + wasDown = gamepad1.dpad_down; + wasUp = gamepad1.dpad_up; + } +} 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 new file mode 100644 index 0000000..5439f78 --- /dev/null +++ b/FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/ConceptMotorBulkRead.java @@ -0,0 +1,227 @@ +/* Copyright (c) 2019 Phil Malone. 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.lynx.LynxModule; +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.DcMotorEx; +import com.qualcomm.robotcore.util.ElapsedTime; + +import java.util.List; + +/* + * This OpMode illustrates how to use the Expansion Hub's Bulk-Read feature to speed up control cycle times. + * In this example there are 4 motors that need their encoder positions, and velocities read. + * The sample is written to work with one or two expansion hubs, with no assumption as to where the motors are located. + * + * 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, 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 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. + * 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 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. + * + * eg: if you are sending encoder positions to your telemetry display, putting a getCurrentPosition() + * call in the telemetry statement will force the code to go and get another copy which will take time. + * It's much better read the position into a variable once, and use that variable for control AND display. + * Reading saved variables takes no time at all. + * + * Once you put all your sensor reads at the beginning of the control cycle, it's very easy to use + * the bulk-read AUTO mode to streamline your cycle timing. + */ +@TeleOp (name = "Motor Bulk Reads", group = "Tests") +@Disabled +public class ConceptMotorBulkRead extends LinearOpMode { + + final int TEST_CYCLES = 500; // Number of control cycles to run to determine cycle times. + + private DcMotorEx m1, m2, m3, m4; // Motor Objects + private long e1, e2, e3, e4; // Encoder Values + private double v1, v2, v3, v4; // Velocities + + // Cycle Times + double t1 = 0; + double t2 = 0; + double t3 = 0; + + @Override + public void runOpMode() { + + int cycles; + + // Important Step 1: Make sure you use DcMotorEx when you instantiate your motors. + m1 = hardwareMap.get(DcMotorEx.class, "m1"); // Configure the robot to use these 4 motor names, + m2 = hardwareMap.get(DcMotorEx.class, "m2"); // or change these strings to match your existing Robot Configuration. + m3 = hardwareMap.get(DcMotorEx.class, "m3"); + m4 = hardwareMap.get(DcMotorEx.class, "m4"); + + // Important Step 2: Get access to a list of Expansion Hub Modules to enable changing caching methods. + List allHubs = hardwareMap.getAll(LynxModule.class); + + ElapsedTime timer = new ElapsedTime(); + + telemetry.addData(">", "Press START to start tests"); + telemetry.addData(">", "Test results will update for each access method."); + telemetry.update(); + waitForStart(); + + // -------------------------------------------------------------------------------------- + // Run control loop using legacy encoder reads + // In this mode, a single read is done for each encoder position, and a bulk read is done for each velocity read. + // This is the worst case scenario. + // This is the same as using LynxModule.BulkCachingMode.OFF + // -------------------------------------------------------------------------------------- + + displayCycleTimes("Test 1 of 3 (Wait for completion)"); + + timer.reset(); + cycles = 0; + while (opModeIsActive() && (cycles++ < TEST_CYCLES)) { + e1 = m1.getCurrentPosition(); + e2 = m2.getCurrentPosition(); + e3 = m3.getCurrentPosition(); + e4 = m4.getCurrentPosition(); + + v1 = m1.getVelocity(); + v2 = m2.getVelocity(); + v3 = m3.getVelocity(); + v4 = m4.getVelocity(); + + // Put Control loop action code here. + + } + // calculate the average cycle time. + t1 = timer.milliseconds() / cycles; + displayCycleTimes("Test 2 of 3 (Wait for completion)"); + + // -------------------------------------------------------------------------------------- + // Run test cycles using AUTO cache mode + // In this mode, only one bulk read is done per cycle, UNLESS you read a specific encoder/velocity item AGAIN in that cycle. + // -------------------------------------------------------------------------------------- + + // Important Step 3: Option A. Set all Expansion hubs to use the AUTO Bulk Caching mode + for (LynxModule module : allHubs) { + module.setBulkCachingMode(LynxModule.BulkCachingMode.AUTO); + } + + timer.reset(); + cycles = 0; + while (opModeIsActive() && (cycles++ < TEST_CYCLES)) { + e1 = m1.getCurrentPosition(); // Uses 1 bulk-read for all 4 encoder/velocity reads, + e2 = m2.getCurrentPosition(); // but don't do any `get` operations more than once per cycle. + e3 = m3.getCurrentPosition(); + e4 = m4.getCurrentPosition(); + + v1 = m1.getVelocity(); + v2 = m2.getVelocity(); + v3 = m3.getVelocity(); + v4 = m4.getVelocity(); + + // Put Control loop action code here. + + } + // calculate the average cycle time. + t2 = timer.milliseconds() / cycles; + displayCycleTimes("Test 3 of 3 (Wait for completion)"); + + // -------------------------------------------------------------------------------------- + // Run test cycles using MANUAL cache mode + // In this mode, only one block read is done each control cycle. + // This is the MOST efficient method, but it does require that the cache is cleared manually each control cycle. + // -------------------------------------------------------------------------------------- + + // Important Step 3: Option B. Set all Expansion hubs to use the MANUAL Bulk Caching mode + for (LynxModule module : allHubs) { + module.setBulkCachingMode(LynxModule.BulkCachingMode.MANUAL); + } + + timer.reset(); + cycles = 0; + while (opModeIsActive() && (cycles++ < TEST_CYCLES)) { + + // Important Step 4: If you are using MANUAL mode, you must clear the BulkCache once per control cycle + for (LynxModule module : allHubs) { + module.clearBulkCache(); + } + + e1 = m1.getCurrentPosition(); // Uses 1 bulk-read to obtain ALL the motor data + e2 = m2.getCurrentPosition(); // There is no penalty for doing more `get` operations in this cycle, + e3 = m3.getCurrentPosition(); // but they will return the same data. + e4 = m4.getCurrentPosition(); + + v1 = m1.getVelocity(); + v2 = m2.getVelocity(); + v3 = m3.getVelocity(); + v4 = m4.getVelocity(); + + // Put Control loop action code here. + + } + // calculate the average cycle time. + t3 = timer.milliseconds() / cycles; + displayCycleTimes("Complete"); + + // wait until op-mode is stopped by user, before clearing display. + while (opModeIsActive()) ; + } + + // Display three comparison times. + void displayCycleTimes(String status) { + telemetry.addData("Testing", status); + telemetry.addData("Cache = OFF", "%5.1f mS/cycle", t1); + telemetry.addData("Cache = AUTO", "%5.1f mS/cycle", t2); + telemetry.addData("Cache = MANUAL", "%5.1f mS/cycle", t3); + telemetry.update(); + } +} + diff --git a/FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/ConceptNullOp.java b/FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/ConceptNullOp.java new file mode 100644 index 0000000..4a887bd --- /dev/null +++ b/FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/ConceptNullOp.java @@ -0,0 +1,89 @@ +/* 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.OpMode; +import com.qualcomm.robotcore.eventloop.opmode.TeleOp; +import com.qualcomm.robotcore.util.ElapsedTime; + +/* + * Demonstrates an empty iterative OpMode + */ +@TeleOp(name = "Concept: NullOp", group = "Concept") +@Disabled +public class ConceptNullOp extends OpMode { + + private ElapsedTime runtime = new ElapsedTime(); + + /** + * This method will be called once, when the INIT button is pressed. + */ + @Override + public void init() { + telemetry.addData("Status", "Initialized"); + } + + /** + * This method will be called repeatedly during the period between when + * the INIT button is pressed and when the START button is pressed (or the + * OpMode is stopped). + */ + @Override + public void init_loop() { + } + + /** + * This method will be called once, when the START button is pressed. + */ + @Override + public void start() { + runtime.reset(); + } + + /** + * This method will be called repeatedly during the period between when + * the START button is pressed and when the OpMode is stopped. + */ + @Override + public void loop() { + telemetry.addData("Status", "Run Time: " + runtime.toString()); + } + + /** + * This method will be called once, when this OpMode is stopped. + *

+ * Your ability to control hardware from this method will be limited. + */ + @Override + public void stop() { + + } +} 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 new file mode 100644 index 0000000..6e0be37 --- /dev/null +++ b/FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/ConceptRampMotorSpeed.java @@ -0,0 +1,114 @@ +/* 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.DcMotor; + +/* + * 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 Robot. + * + * INCREMENT sets how much to increase/decrease the power each cycle + * CYCLE_MS sets the update period. + * + * 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 = "Concept: Ramp Motor Speed", group = "Concept") +@Disabled +public class ConceptRampMotorSpeed extends LinearOpMode { + + static final double INCREMENT = 0.01; // amount to ramp motor each CYCLE_MS cycle + static final int CYCLE_MS = 50; // period of each cycle + static final double MAX_FWD = 1.0; // Maximum FWD power applied to motor + static final double MAX_REV = -1.0; // Maximum REV power applied to motor + + // Define class members + DcMotor motor; + double power = 0; + boolean rampUp = true; + + + @Override + public void runOpMode() { + + // Connect to motor (Assume standard left wheel) + // Change the text in quotes to match any motor name on your robot. + motor = hardwareMap.get(DcMotor.class, "left_drive"); + + // Wait for the start button + telemetry.addData(">", "Press Start to run Motors." ); + telemetry.update(); + waitForStart(); + + // Ramp motor speeds till stop pressed. + while(opModeIsActive()) { + + // Ramp the motors, according to the rampUp variable. + if (rampUp) { + // Keep stepping up until we hit the max value. + power += INCREMENT ; + if (power >= MAX_FWD ) { + power = MAX_FWD; + rampUp = !rampUp; // Switch ramp direction + } + } + else { + // Keep stepping down until we hit the min value. + power -= INCREMENT ; + if (power <= MAX_REV ) { + power = MAX_REV; + rampUp = !rampUp; // Switch ramp direction + } + } + + // Display the current value + telemetry.addData("Motor Power", "%5.2f", power); + telemetry.addData(">", "Press Stop to end test." ); + telemetry.update(); + + // Set the motor to the new power and pause; + motor.setPower(power); + sleep(CYCLE_MS); + idle(); + } + + // Turn off motor and signal done; + motor.setPower(0); + telemetry.addData(">", "Done"); + telemetry.update(); + + } +} diff --git a/FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/ConceptRevLED.java b/FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/ConceptRevLED.java new file mode 100644 index 0000000..9c168d5 --- /dev/null +++ b/FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/ConceptRevLED.java @@ -0,0 +1,78 @@ +/* + Copyright (c) 2024 Alan Smith + 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 Alan Smith 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 FITNESSFOR 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; + +/* + * This OpMode illustrates how to use the REV Digital Indicator + * + * This is a simple way to add the REV Digital Indicator which has a red and green LED. + * (and as you may remember, red + green = yellow so when they are both on you can get yellow) + * + * Why? + * You can use this to show information to the driver. For example, green might be that you can + * pick up more game elements and red would be that you already have the possession limit. + * + * This OpMode assumes that the REV Digital Indicator is setup as 2 Digital Channels named + * front_led_green and front_led_red. (the green should be the lower of the 2 channels it is plugged + * into and the red should be the higher) + * + * 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 + * + * You can buy this product here: https://www.revrobotics.com/rev-31-2010/ + */ +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.LED; + +@TeleOp(name = "Concept: RevLED", group = "Concept") +@Disabled +public class ConceptRevLED extends OpMode { + LED frontLED_red; + LED frontLED_green; + + @Override + public void init() { + frontLED_green = hardwareMap.get(LED.class, "front_led_green"); + frontLED_red = hardwareMap.get(LED.class, "front_led_red"); + } + + @Override + public void loop() { + if (gamepad1.a) { + frontLED_red.on(); + } else { + frontLED_red.off(); + } + if (gamepad1.b) { + frontLED_green.on(); + } else { + frontLED_green.off(); + } + } +} 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 new file mode 100644 index 0000000..798d689 --- /dev/null +++ b/FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/ConceptRevSPARKMini.java @@ -0,0 +1,111 @@ +/* Copyright (c) 2018 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.DcMotorSimple; +import com.qualcomm.robotcore.util.ElapsedTime; +import com.qualcomm.robotcore.util.Range; + + +/* + * This OpMode demonstrates a POV Drive system, with commented-out code for a Tank Drive system, + * for a two wheeled robot using two REV SPARKminis. + * To use this example, connect two REV SPARKminis into servo ports on the Control Hub. On the + * robot configuration, use the drop down list under 'Servos' to select 'REV SPARKmini Controller' + * and name them 'left_drive' and 'right_drive'. + * + * 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") +@Disabled +public class ConceptRevSPARKMini extends LinearOpMode { + + // Declare OpMode members. + private ElapsedTime runtime = new ElapsedTime(); + private DcMotorSimple leftDrive = null; + private DcMotorSimple rightDrive = null; + + @Override + public void runOpMode() { + telemetry.addData("Status", "Initialized"); + telemetry.update(); + + // Initialize the hardware variables. Note that the strings used here as parameters + // to 'get' must correspond to the names assigned during robot configuration. + leftDrive = hardwareMap.get(DcMotorSimple.class, "left_drive"); + 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 backward when connected directly to the battery + leftDrive.setDirection(DcMotorSimple.Direction.FORWARD); + rightDrive.setDirection(DcMotorSimple.Direction.REVERSE); + + // Wait for the game to start (driver presses START) + waitForStart(); + runtime.reset(); + + // run until the end of the match (driver presses STOP) + while (opModeIsActive()) { + + // Setup a variable for each drive wheel to save power level for telemetry + double leftPower; + double rightPower; + + // Choose to drive using either Tank Mode, or POV Mode + // Comment out the method that's not used. The default below is POV. + + // POV Mode uses left stick to go forward, and right stick to turn. + // - This uses basic math to combine motions and is easier to drive straight. + double drive = -gamepad1.left_stick_y; + double turn = gamepad1.right_stick_x; + leftPower = Range.clip(drive + turn, -1.0, 1.0) ; + rightPower = Range.clip(drive - turn, -1.0, 1.0) ; + + // Tank Mode uses one stick to control each wheel. + // - This requires no math, but it is hard to drive forward slowly and keep straight. + // leftPower = -gamepad1.left_stick_y ; + // rightPower = -gamepad1.right_stick_y ; + + // Send calculated power to wheels + leftDrive.setPower(leftPower); + rightDrive.setPower(rightPower); + + // Show the elapsed game time and wheel power. + telemetry.addData("Status", "Run Time: " + runtime.toString()); + telemetry.addData("Motors", "left (%.2f), right (%.2f)", leftPower, rightPower); + telemetry.update(); + } + } +} 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 new file mode 100644 index 0000000..2b8ad33 --- /dev/null +++ b/FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/ConceptScanServo.java @@ -0,0 +1,115 @@ +/* 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.Servo; + +/* + * 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 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. + * + * 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 = "Concept: Scan Servo", group = "Concept") +@Disabled +public class ConceptScanServo extends LinearOpMode { + + static final double INCREMENT = 0.01; // amount to slew servo each CYCLE_MS cycle + static final int CYCLE_MS = 50; // period of each cycle + static final double MAX_POS = 1.0; // Maximum rotational position + static final double MIN_POS = 0.0; // Minimum rotational position + + // Define class members + Servo servo; + double position = (MAX_POS - MIN_POS) / 2; // Start at halfway position + boolean rampUp = true; + + + @Override + public void runOpMode() { + + // 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"); + + // Wait for the start button + telemetry.addData(">", "Press Start to scan Servo." ); + telemetry.update(); + waitForStart(); + + + // Scan servo till stop pressed. + while(opModeIsActive()){ + + // slew the servo, according to the rampUp (direction) variable. + if (rampUp) { + // Keep stepping up until we hit the max value. + position += INCREMENT ; + if (position >= MAX_POS ) { + position = MAX_POS; + rampUp = !rampUp; // Switch ramp direction + } + } + else { + // Keep stepping down until we hit the min value. + position -= INCREMENT ; + if (position <= MIN_POS ) { + position = MIN_POS; + rampUp = !rampUp; // Switch ramp direction + } + } + + // Display the current value + telemetry.addData("Servo Position", "%5.2f", position); + telemetry.addData(">", "Press Stop to end test." ); + telemetry.update(); + + // Set the servo to the new position and pause; + servo.setPosition(position); + sleep(CYCLE_MS); + idle(); + } + + // Signal done; + telemetry.addData(">", "Done"); + telemetry.update(); + } +} 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 new file mode 100644 index 0000000..1ceaa17 --- /dev/null +++ b/FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/ConceptSoundsASJava.java @@ -0,0 +1,133 @@ +/* Copyright (c) 2018 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.ftccommon.SoundPlayer; +import com.qualcomm.robotcore.eventloop.opmode.Disabled; +import com.qualcomm.robotcore.eventloop.opmode.LinearOpMode; +import com.qualcomm.robotcore.eventloop.opmode.TeleOp; + +/* + * This OpMode demonstrates how to play simple sounds on both the RC and DS phones. + * It illustrates how to build sounds into your application as a resource. + * This technique is best suited for use with Android Studio since it assumes you will be creating a new application + * + * If you are using OnBotJava, please see the ConceptSoundsOnBotJava sample + * + * 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: + * + * Gamepad X & B buttons are used to trigger sounds in this example, but any event can be used. + * Note: Time should be allowed for sounds to complete before playing other sounds. + * + * For sound files to be used as a compiled-in resource, they need to be located in a folder called "raw" under your "res" (resources) folder. + * You can create your own "raw" folder from scratch, or you can copy the one from the FtcRobotController module. + * + * Android Studio coders will ultimately need a folder in your path as follows: + * /TeamCode/src/main/res/raw + * + * Copy any .wav files you want to play into this folder. + * Make sure that your files ONLY use lower-case characters, and have no spaces or special characters other than underscore. + * + * The name you give your .wav files will become the resource ID for these sounds. + * eg: gold.wav becomes R.raw.gold + * + * If you wish to use the sounds provided for this sample, they are located in: + * /FtcRobotController/src/main/res/raw + * You can copy and paste the entire 'raw' folder using Android Studio. + * + */ + +@TeleOp(name="Concept: Sound Resources", group="Concept") +@Disabled +public class ConceptSoundsASJava extends LinearOpMode { + + // Declare OpMode members. + private boolean goldFound; // Sound file present flags + private boolean silverFound; + + private boolean isX = false; // Gamepad button state variables + private boolean isB = false; + + private boolean wasX = false; // Gamepad button history variables + private boolean WasB = false; + + @Override + public void runOpMode() { + + // Determine Resource IDs for sounds built into the RC application. + int silverSoundID = hardwareMap.appContext.getResources().getIdentifier("silver", "raw", hardwareMap.appContext.getPackageName()); + int goldSoundID = hardwareMap.appContext.getResources().getIdentifier("gold", "raw", hardwareMap.appContext.getPackageName()); + + // Determine if sound resources are found. + // Note: Preloading is NOT required, but it's a good way to verify all your sounds are available before you run. + if (goldSoundID != 0) + goldFound = SoundPlayer.getInstance().preload(hardwareMap.appContext, goldSoundID); + + if (silverSoundID != 0) + silverFound = SoundPlayer.getInstance().preload(hardwareMap.appContext, silverSoundID); + + // Display sound status + telemetry.addData("gold resource", goldFound ? "Found" : "NOT found\n Add gold.wav to /src/main/res/raw" ); + telemetry.addData("silver resource", silverFound ? "Found" : "Not found\n Add silver.wav to /src/main/res/raw" ); + + // Wait for the game to start (driver presses START) + telemetry.addData(">", "Press Start to continue"); + telemetry.update(); + waitForStart(); + + telemetry.addData(">", "Press X, B to play sounds."); + telemetry.update(); + + // run until the end of the match (driver presses STOP) + while (opModeIsActive()) { + + // say Silver each time gamepad X is pressed (This sound is a resource) + if (silverFound && (isX = gamepad1.x) && !wasX) { + SoundPlayer.getInstance().startPlaying(hardwareMap.appContext, silverSoundID); + telemetry.addData("Playing", "Resource Silver"); + telemetry.update(); + } + + // say Gold each time gamepad B is pressed (This sound is a resource) + if (goldFound && (isB = gamepad1.b) && !WasB) { + SoundPlayer.getInstance().startPlaying(hardwareMap.appContext, goldSoundID); + telemetry.addData("Playing", "Resource Gold"); + telemetry.update(); + } + + // Save last button states + wasX = isX; + WasB = isB; + } + } +} diff --git a/FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/ConceptSoundsOnBotJava.java b/FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/ConceptSoundsOnBotJava.java new file mode 100644 index 0000000..fbb7416 --- /dev/null +++ b/FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/ConceptSoundsOnBotJava.java @@ -0,0 +1,120 @@ +/* Copyright (c) 2018 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.ftccommon.SoundPlayer; +import com.qualcomm.robotcore.eventloop.opmode.Disabled; +import com.qualcomm.robotcore.eventloop.opmode.LinearOpMode; +import com.qualcomm.robotcore.eventloop.opmode.TeleOp; + +import java.io.File; + +/* + * This OpMode demonstrates how to play simple sounds on both the RC and DS phones. + * It illustrates how to play sound files that have been copied to the RC Phone + * This technique is best suited for use with OnBotJava since it does not require the app to be modified. + * + * Operation: + * + * Gamepad X & B buttons are used to trigger sounds in this example, but any event can be used. + * Note: Time should be allowed for sounds to complete before playing other sounds. + * + * To play a new sound, you will need to copy the .wav files to the phone, and then provide the full path to them as part of your OpMode. + * This is done in this sample for the two sound files. silver.wav and gold.wav + * + * You can put the files in a variety of soundPaths, but we recommend you put them in the /FIRST/blocks/sounds folder. + * Your OpModes will have guaranteed access to this folder, and you can transfer files into this folder using the BLOCKS web page. + * -- There is a link called "sounds" on the right hand side of the color bar on the BLOCKS page that can be used to send sound files to this folder by default. + * Or you can use Windows File Manager, or ADB to transfer the sound files + * + * To get full use of THIS sample, you will need to copy two sound file called silver.wav and gold.wav to /FIRST/blocks/sounds on the RC phone. + * They can be located here: + * https://github.com/ftctechnh/ftc_app/tree/master/FtcRobotController/src/main/res/raw/gold.wav + * https://github.com/ftctechnh/ftc_app/tree/master/FtcRobotController/src/main/res/raw/silver.wav + */ + +@TeleOp(name="Concept: Sound Files", group="Concept") +@Disabled +public class ConceptSoundsOnBotJava extends LinearOpMode { + + // Point to sound files on the phone's drive + private String soundPath = "/FIRST/blocks/sounds"; + private File goldFile = new File("/sdcard" + soundPath + "/gold.wav"); + private File silverFile = new File("/sdcard" + soundPath + "/silver.wav"); + + // Declare OpMode members. + private boolean isX = false; // Gamepad button state variables + private boolean isB = false; + + private boolean wasX = false; // Gamepad button history variables + private boolean WasB = false; + + @Override + public void runOpMode() { + + // Make sure that the sound files exist on the phone + boolean goldFound = goldFile.exists(); + boolean silverFound = silverFile.exists(); + + // Display sound status + telemetry.addData("gold sound", goldFound ? "Found" : "NOT Found \nCopy gold.wav to " + soundPath ); + telemetry.addData("silver sound", silverFound ? "Found" : "NOT Found \nCopy silver.wav to " + soundPath ); + + // Wait for the game to start (driver presses PLAY) + telemetry.addData(">", "Press Start to continue"); + telemetry.update(); + waitForStart(); + + telemetry.addData(">", "Press X or B to play sounds."); + telemetry.update(); + + // run until the end of the match (driver presses STOP) + while (opModeIsActive()) { + + // say Silver each time gamepad X is pressed (This sound is a resource) + if (silverFound && (isX = gamepad1.x) && !wasX) { + SoundPlayer.getInstance().startPlaying(hardwareMap.appContext, silverFile); + telemetry.addData("Playing", "Silver File"); + telemetry.update(); + } + + // say Gold each time gamepad B is pressed (This sound is a resource) + if (goldFound && (isB = gamepad1.b) && !WasB) { + SoundPlayer.getInstance().startPlaying(hardwareMap.appContext, goldFile); + telemetry.addData("Playing", "Gold File"); + telemetry.update(); + } + + // Save last button states + wasX = isX; + WasB = isB; + } + } +} 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 new file mode 100644 index 0000000..983e434 --- /dev/null +++ b/FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/ConceptSoundsSKYSTONE.java @@ -0,0 +1,122 @@ +/* Copyright (c) 2018 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.content.Context; +import com.qualcomm.ftccommon.SoundPlayer; +import com.qualcomm.robotcore.eventloop.opmode.Disabled; +import com.qualcomm.robotcore.eventloop.opmode.LinearOpMode; +import com.qualcomm.robotcore.eventloop.opmode.TeleOp; + +/* + * This OpMode demonstrates how to play one of the several SKYSTONE/Star Wars sounds loaded into the SDK. + * 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 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. + */ + +@TeleOp(name="SKYSTONE Sounds", group="Concept") +@Disabled +public class ConceptSoundsSKYSTONE extends LinearOpMode { + + // List of available sound resources + String sounds[] = {"ss_alarm", "ss_bb8_down", "ss_bb8_up", "ss_darth_vader", "ss_fly_by", + "ss_mf_fail", "ss_laser", "ss_laser_burst", "ss_light_saber", "ss_light_saber_long", "ss_light_saber_short", + "ss_light_speed", "ss_mine", "ss_power_up", "ss_r2d2_up", "ss_roger_roger", "ss_siren", "ss_wookie" }; + boolean soundPlaying = false; + + @Override + public void runOpMode() { + + // Variables for choosing from the available sounds + int soundIndex = 0; + int soundID = -1; + boolean was_dpad_up = false; + boolean was_dpad_down = false; + + Context myApp = hardwareMap.appContext; + + // create a sound parameter that holds the desired player parameters. + SoundPlayer.PlaySoundParams params = new SoundPlayer.PlaySoundParams(); + params.loopControl = 0; + params.waitForNonLoopingSoundsToFinish = true; + + // In this sample, we will skip waiting for the user to press play, and start displaying sound choices right away + while (!isStopRequested()) { + + // Look for DPAD presses to change the selection + if (gamepad1.dpad_down && !was_dpad_down) { + // Go to next sound (with list wrap) and display it + soundIndex = (soundIndex + 1) % sounds.length; + } + + if (gamepad1.dpad_up && !was_dpad_up) { + // Go to previous sound (with list wrap) and display it + soundIndex = (soundIndex + sounds.length - 1) % sounds.length; + } + + // Look for trigger to see if we should play sound + // Only start a new sound if we are currently not playing one. + if (gamepad1.right_bumper && !soundPlaying) { + + // Determine Resource IDs for the sounds you want to play, and make sure it's valid. + if ((soundID = myApp.getResources().getIdentifier(sounds[soundIndex], "raw", myApp.getPackageName())) != 0){ + + // Signal that the sound is now playing. + soundPlaying = true; + + // Start playing, and also Create a callback that will clear the playing flag when the sound is complete. + SoundPlayer.getInstance().startPlaying(myApp, soundID, params, null, + new Runnable() { + public void run() { + soundPlaying = false; + }} ); + } + } + + // Remember the last state of the dpad to detect changes. + was_dpad_up = gamepad1.dpad_up; + was_dpad_down = gamepad1.dpad_down; + + // Display the current sound choice, and the playing status. + telemetry.addData("", "Use DPAD up/down to choose sound."); + telemetry.addData("", "Press Right Bumper to play sound."); + telemetry.addData("", ""); + telemetry.addData("Sound >", sounds[soundIndex]); + telemetry.addData("Status >", soundPlaying ? "Playing" : "Stopped"); + telemetry.update(); + } + } +} diff --git a/FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/ConceptTelemetry.java b/FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/ConceptTelemetry.java new file mode 100644 index 0000000..f2c6097 --- /dev/null +++ b/FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/ConceptTelemetry.java @@ -0,0 +1,177 @@ +/* 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.VoltageSensor; +import com.qualcomm.robotcore.util.ElapsedTime; +import org.firstinspires.ftc.robotcore.external.Func; +import org.firstinspires.ftc.robotcore.external.Telemetry; + +/* + * This OpMode illustrates various ways in which telemetry can be + * transmitted from the robot controller to the driver station. The sample illustrates + * numeric and text data, formatted output, and optimized evaluation of expensive-to-acquire + * information. The telemetry log is illustrated by scrolling a poem + * to the driver station. + * + * Also see the Telemetry javadocs. + */ +@TeleOp(name = "Concept: Telemetry", group = "Concept") +@Disabled +public class ConceptTelemetry extends LinearOpMode { + /** Keeps track of the line of the poem which is to be emitted next */ + int poemLine = 0; + + /** Keeps track of how long it's been since we last emitted a line of poetry */ + ElapsedTime poemElapsed = new ElapsedTime(); + + static final String[] poem = new String[] { + + "Mary had a little lamb,", + "His fleece was white as snow,", + "And everywhere that Mary went,", + "The lamb was sure to go.", + "", + "He followed her to school one day,", + "Which was against the rule,", + "It made the children laugh and play", + "To see a lamb at school.", + "", + "And so the teacher turned it out,", + "But still it lingered near,", + "And waited patiently about,", + "Till Mary did appear.", + "", + "\"Why does the lamb love Mary so?\"", + "The eager children cry.", + "\"Why, Mary loves the lamb, you know,\"", + "The teacher did reply.", + "", + "" + }; + + @Override public void runOpMode() { + + /* we keep track of how long it's been since the OpMode was started, just + * to have some interesting data to show */ + ElapsedTime opmodeRunTime = new ElapsedTime(); + + // We show the log in oldest-to-newest order, as that's better for poetry + telemetry.log().setDisplayOrder(Telemetry.Log.DisplayOrder.OLDEST_FIRST); + // We can control the number of lines shown in the log + telemetry.log().setCapacity(6); + // The interval between lines of poetry, in seconds + double sPoemInterval = 0.6; + + /* + * Wait until we've been given the ok to go. For something to do, we emit the + * elapsed time as we sit here and wait. If we didn't want to do anything while + * we waited, we would just call waitForStart(). + */ + while (!isStarted()) { + telemetry.addData("time", "%.1f seconds", opmodeRunTime.seconds()); + telemetry.update(); + idle(); + } + + // Ok, we've been given the ok to go + + /* + * As an illustration, the first line on our telemetry display will display the battery voltage. + * The idea here is that it's expensive to compute the voltage (at least for purposes of illustration) + * so you don't want to do it unless the data is _actually_ going to make it to the + * driver station (recall that telemetry transmission is throttled to reduce bandwidth use. + * Note that getBatteryVoltage() below returns 'Infinity' if there's no voltage sensor attached. + * + * @see Telemetry#getMsTransmissionInterval() + */ + telemetry.addData("voltage", "%.1f volts", new Func() { + @Override public Double value() { + return getBatteryVoltage(); + } + }); + + // Reset to keep some timing stats for the post-'start' part of the OpMode + opmodeRunTime.reset(); + int loopCount = 1; + + // Go go gadget robot! + while (opModeIsActive()) { + + // Emit poetry if it's been a while + if (poemElapsed.seconds() > sPoemInterval) { + emitPoemLine(); + } + + // As an illustration, show some loop timing information + telemetry.addData("loop count", loopCount); + telemetry.addData("ms/loop", "%.3f ms", opmodeRunTime.milliseconds() / loopCount); + + // Show joystick information as some other illustrative data + telemetry.addLine("left joystick | ") + .addData("x", gamepad1.left_stick_x) + .addData("y", gamepad1.left_stick_y); + telemetry.addLine("right joystick | ") + .addData("x", gamepad1.right_stick_x) + .addData("y", gamepad1.right_stick_y); + + /* + * Transmit the telemetry to the driver station, subject to throttling. + * See the documentation for Telemetry.getMsTransmissionInterval() for more information. + */ + telemetry.update(); + + // Update loop info + loopCount++; + } + } + + // emits a line of poetry to the telemetry log + void emitPoemLine() { + telemetry.log().add(poem[poemLine]); + poemLine = (poemLine+1) % poem.length; + poemElapsed.reset(); + } + + // Computes the current battery voltage + double getBatteryVoltage() { + double result = Double.POSITIVE_INFINITY; + for (VoltageSensor sensor : hardwareMap.voltageSensor) { + double voltage = sensor.getVoltage(); + if (voltage > 0) { + result = Math.min(result, voltage); + } + } + return result; + } +} diff --git a/FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/ConceptVisionColorLocator_Circle.java b/FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/ConceptVisionColorLocator_Circle.java new file mode 100644 index 0000000..4ec17aa --- /dev/null +++ b/FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/ConceptVisionColorLocator_Circle.java @@ -0,0 +1,245 @@ +/* + * Copyright (c) 2024 Phil Malone + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package org.firstinspires.ftc.robotcontroller.external.samples; + +import android.graphics.Color; +import android.util.Size; + +import com.qualcomm.robotcore.eventloop.opmode.Disabled; +import com.qualcomm.robotcore.eventloop.opmode.LinearOpMode; +import com.qualcomm.robotcore.eventloop.opmode.TeleOp; + +import org.firstinspires.ftc.robotcore.external.Telemetry; +import org.firstinspires.ftc.robotcore.external.hardware.camera.WebcamName; +import org.firstinspires.ftc.vision.VisionPortal; +import org.firstinspires.ftc.vision.opencv.Circle; +import org.firstinspires.ftc.vision.opencv.ColorBlobLocatorProcessor; +import org.firstinspires.ftc.vision.opencv.ColorRange; +import org.firstinspires.ftc.vision.opencv.ImageRegion; + +import java.util.List; + +/* + * This OpMode illustrates how to use a video source (camera) to locate specifically colored regions. + * This sample is targeted towards circular blobs. To see rectangles, look at ConceptVisionColorLocator_Rectangle + * + * Unlike a "color sensor" which determines the color of nearby object, this "color locator" + * will search the Region Of Interest (ROI) in a camera image, and find any "blobs" of color that + * match the requested color range. These blobs can be further filtered and sorted to find the one + * most likely to be the item the user is looking for. + * + * To perform this function, a VisionPortal runs a ColorBlobLocatorProcessor process. + * The ColorBlobLocatorProcessor (CBLP) process is created first, and then the VisionPortal is built. + * The (CBLP) analyses the ROI and locates pixels that match the ColorRange to form a "mask". + * The matching pixels are then collected into contiguous "blobs" of pixels. + * The outer boundaries of these blobs are called its "contour". For each blob, the process then + * creates the smallest possible circle that will fully encase the contour. The user can then call + * getBlobs() to retrieve the list of Blobs, where each contains the contour and the circle. + * Note: The default sort order for Blobs is ContourArea, in descending order, so the biggest + * contours are listed first. + * + * A colored enclosing circle is drawn on the camera preview to show the location of each Blob + * The original Blob contour can also be added to the preview. + * This is helpful when configuring the ColorBlobLocatorProcessor parameters. + * + * Tip: Connect an HDMI monitor to the Control Hub to view the Color Location process in real-time. + * Or use a screen copy utility like ScrCpy.exe to view the video remotely. + * + * 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 + */ + +@Disabled +@TeleOp(name = "Concept: Vision Color-Locator (Circle)", group = "Concept") +public class ConceptVisionColorLocator_Circle extends LinearOpMode { + @Override + public void runOpMode() { + /* Build a "Color Locator" vision processor based on the ColorBlobLocatorProcessor class. + * - Specify the color range you are looking for. Use a predefined color, or create your own + * + * .setTargetColorRange(ColorRange.BLUE) // use a predefined color match + * Available colors are: RED, BLUE, YELLOW, GREEN, ARTIFACT_GREEN, ARTIFACT_PURPLE + * .setTargetColorRange(new ColorRange(ColorSpace.YCrCb, // or define your own color match + * new Scalar( 32, 176, 0), + * new Scalar(255, 255, 132))) + * + * - Focus the color locator by defining a RegionOfInterest (ROI) which you want to search. + * This can be the entire frame, or a sub-region defined using: + * 1) standard image coordinates or 2) a normalized +/- 1.0 coordinate system. + * Use one form of the ImageRegion class to define the ROI. + * ImageRegion.entireFrame() + * ImageRegion.asImageCoordinates(50, 50, 150, 150) 100x100 pixels at upper left corner + * ImageRegion.asUnityCenterCoordinates(-0.5, 0.5, 0.5, -0.5) 50% width/height in center + * + * - Define which contours are included. + * You can get ALL the contours, ignore contours that are completely inside another contour. + * .setContourMode(ColorBlobLocatorProcessor.ContourMode.ALL_FLATTENED_HIERARCHY) + * .setContourMode(ColorBlobLocatorProcessor.ContourMode.EXTERNAL_ONLY) + * EXTERNAL_ONLY helps to avoid bright reflection spots from breaking up solid colors. + * + * - Turn the displays of contours ON or OFF. + * Turning these on helps debugging but takes up valuable CPU time. + * .setDrawContours(true) Draws an outline of each contour. + * .setEnclosingCircleColor(int color) Draws a circle around each contour. 0 to disable. + * .setBoxFitColor(int color) Draws a rectangle around each contour. 0 to disable. ON by default. + * + * + * - include any pre-processing of the image or mask before looking for Blobs. + * There are some extra processing you can include to improve the formation of blobs. + * Using these features requires an understanding of how they may effect the final + * blobs. The "pixels" argument sets the NxN kernel size. + * .setBlurSize(int pixels) + * Blurring an image helps to provide a smooth color transition between objects, + * and smoother contours. The higher the number, the more blurred the image becomes. + * Note: Even "pixels" values will be incremented to satisfy the "odd number" requirement. + * Blurring too much may hide smaller features. A size of 5 is good for a 320x240 image. + * + * .setErodeSize(int pixels) + * Erosion removes floating pixels and thin lines so that only substantive objects remain. + * Erosion can grow holes inside regions, and also shrink objects. + * "pixels" in the range of 2-4 are suitable for low res images. + * + * .setDilateSize(int pixels) + * Dilation makes objects and lines more visible by filling in small holes, and making + * filled shapes appear larger. Dilation is useful for joining broken parts of an + * object, such as when removing noise from an image. + * "pixels" in the range of 2-4 are suitable for low res images. + * + * .setMorphOperationType(MorphOperationType morphOperationType) + * This defines the order in which the Erode/Dilate actions are performed. + * OPENING: Will Erode and then Dilate which will make small noise blobs go away + * CLOSING: Will Dilate and then Erode which will tend to fill in any small holes in blob edges. + */ + ColorBlobLocatorProcessor colorLocator = new ColorBlobLocatorProcessor.Builder() + .setTargetColorRange(ColorRange.ARTIFACT_PURPLE) // Use a predefined color match + .setContourMode(ColorBlobLocatorProcessor.ContourMode.EXTERNAL_ONLY) + .setRoi(ImageRegion.asUnityCenterCoordinates(-0.75, 0.75, 0.75, -0.75)) + .setDrawContours(true) // Show contours on the Stream Preview + .setBoxFitColor(0) // Disable the drawing of rectangles + .setCircleFitColor(Color.rgb(255, 255, 0)) // Draw a circle + .setBlurSize(5) // Smooth the transitions between different colors in image + + // the following options have been added to fill in perimeter holes. + .setDilateSize(15) // Expand blobs to fill any divots on the edges + .setErodeSize(15) // Shrink blobs back to original size + .setMorphOperationType(ColorBlobLocatorProcessor.MorphOperationType.CLOSING) + + .build(); + /* + * Build a vision portal to run the Color Locator process. + * + * - Add the colorLocator process created above. + * - Set the desired video resolution. + * Since a high resolution will not improve this process, choose a lower resolution + * that is supported by your camera. This will improve overall performance and reduce + * latency. + * - Choose your video source. This may be + * .setCamera(hardwareMap.get(WebcamName.class, "Webcam 1")) ..... for a webcam + * or + * .setCamera(BuiltinCameraDirection.BACK) ... for a Phone Camera + */ + VisionPortal portal = new VisionPortal.Builder() + .addProcessor(colorLocator) + .setCameraResolution(new Size(320, 240)) + .setCamera(hardwareMap.get(WebcamName.class, "Webcam 1")) + .build(); + + telemetry.setMsTransmissionInterval(100); // Speed up telemetry updates for debugging. + telemetry.setDisplayFormat(Telemetry.DisplayFormat.MONOSPACE); + + // WARNING: To view the stream preview on the Driver Station, this code runs in INIT mode. + while (opModeIsActive() || opModeInInit()) { + telemetry.addData("preview on/off", "... Camera Stream\n"); + + // Read the current list + List blobs = colorLocator.getBlobs(); + + /* + * The list of Blobs can be filtered to remove unwanted Blobs. + * Note: All contours will be still displayed on the Stream Preview, but only those + * that satisfy the filter conditions will remain in the current list of + * "blobs". Multiple filters may be used. + * + * To perform a filter + * ColorBlobLocatorProcessor.Util.filterByCriteria(criteria, minValue, maxValue, blobs); + * + * The following criteria are currently supported. + * + * ColorBlobLocatorProcessor.BlobCriteria.BY_CONTOUR_AREA + * A Blob's area is the number of pixels contained within the Contour. Filter out any + * that are too big or small. Start with a large range and then refine the range based + * on the likely size of the desired object in the viewfinder. + * + * ColorBlobLocatorProcessor.BlobCriteria.BY_DENSITY + * A blob's density is an indication of how "full" the contour is. + * If you put a rubber band around the contour you would get the "Convex Hull" of the + * contour. The density is the ratio of Contour-area to Convex Hull-area. + * + * ColorBlobLocatorProcessor.BlobCriteria.BY_ASPECT_RATIO + * A blob's Aspect ratio is the ratio of boxFit long side to short side. + * A perfect Square has an aspect ratio of 1. All others are > 1 + * + * ColorBlobLocatorProcessor.BlobCriteria.BY_ARC_LENGTH + * A blob's arc length is the perimeter of the blob. + * This can be used in conjunction with an area filter to detect oddly shaped blobs. + * + * ColorBlobLocatorProcessor.BlobCriteria.BY_CIRCULARITY + * A blob's circularity is how circular it is based on the known area and arc length. + * A perfect circle has a circularity of 1. All others are < 1 + */ + ColorBlobLocatorProcessor.Util.filterByCriteria( + ColorBlobLocatorProcessor.BlobCriteria.BY_CONTOUR_AREA, + 50, 20000, blobs); // filter out very small blobs. + + ColorBlobLocatorProcessor.Util.filterByCriteria( + ColorBlobLocatorProcessor.BlobCriteria.BY_CIRCULARITY, + 0.6, 1, blobs); /* filter out non-circular blobs. + * NOTE: You may want to adjust the minimum value depending on your use case. + * Circularity values will be affected by shadows, and will therefore vary based + * on the location of the camera on your robot and venue lighting. It is strongly + * encouraged to test your vision on the competition field if your event allows + * sensor calibration time. + */ + + /* + * The list of Blobs can be sorted using the same Blob attributes as listed above. + * No more than one sort call should be made. Sorting can use ascending or descending order. + * Here is an example.: + * ColorBlobLocatorProcessor.Util.sortByCriteria( + * ColorBlobLocatorProcessor.BlobCriteria.BY_CONTOUR_AREA, SortOrder.DESCENDING, blobs); + */ + + telemetry.addLine("Circularity Radius Center"); + + // Display the Blob's circularity, and the size (radius) and center location of its circleFit. + for (ColorBlobLocatorProcessor.Blob b : blobs) { + + Circle circleFit = b.getCircle(); + telemetry.addLine(String.format("%5.3f %3d (%3d,%3d)", + b.getCircularity(), (int) circleFit.getRadius(), (int) circleFit.getX(), (int) circleFit.getY())); + } + + telemetry.update(); + sleep(100); // Match the telemetry update interval. + } + } +} diff --git a/FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/ConceptVisionColorLocator_Rectangle.java b/FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/ConceptVisionColorLocator_Rectangle.java new file mode 100644 index 0000000..5681f71 --- /dev/null +++ b/FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/ConceptVisionColorLocator_Rectangle.java @@ -0,0 +1,218 @@ +/* + * Copyright (c) 2024 Phil Malone + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package org.firstinspires.ftc.robotcontroller.external.samples; + +import android.util.Size; + +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.SortOrder; + +import org.firstinspires.ftc.robotcore.external.Telemetry; +import org.firstinspires.ftc.robotcore.external.hardware.camera.WebcamName; +import org.firstinspires.ftc.vision.VisionPortal; +import org.firstinspires.ftc.vision.opencv.ColorBlobLocatorProcessor; +import org.firstinspires.ftc.vision.opencv.ColorRange; +import org.firstinspires.ftc.vision.opencv.ImageRegion; +import org.opencv.core.RotatedRect; + +import java.util.List; + +/* + * This OpMode illustrates how to use a video source (camera) to locate specifically colored regions + * + * Unlike a "color sensor" which determines the color of nearby object, this "color locator" + * will search the Region Of Interest (ROI) in a camera image, and find any "blobs" of color that + * match the requested color range. These blobs can be further filtered and sorted to find the one + * most likely to be the item the user is looking for. + * + * To perform this function, a VisionPortal runs a ColorBlobLocatorProcessor process. + * The ColorBlobLocatorProcessor (CBLP) process is created first, and then the VisionPortal is built. + * The (CBLP) analyses the ROI and locates pixels that match the ColorRange to form a "mask". + * The matching pixels are then collected into contiguous "blobs" of pixels. + * The outer boundaries of these blobs are called its "contour". For each blob, the process then + * creates the smallest possible rectangle "boxFit" that will fully encase the contour. The user can + * then call getBlobs() to retrieve the list of Blobs, where each contains the contour and the boxFit. + * Note: The default sort order for Blobs is ContourArea, in descending order, so the biggest + * contours are listed first. + * + * A colored boxFit rectangle is drawn on the camera preview to show the location of each Blob + * The original Blob contour can also be added to the preview. + * This is helpful when configuring the ColorBlobLocatorProcessor parameters. + * + * 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 + */ + +@Disabled +@TeleOp(name = "Concept: Color-Locator (Rectangle)", group = "Concept") +public class ConceptVisionColorLocator_Rectangle extends LinearOpMode +{ + @Override + public void runOpMode() + { + /* Build a "Color Locator" vision processor based on the ColorBlobLocatorProcessor class. + * - Specify the color range you are looking for. Use a predefined color, or create your own + * + * .setTargetColorRange(ColorRange.BLUE) // use a predefined color match + * Available colors are: RED, BLUE YELLOW GREEN ARTIFACT_GREEN and ARTIFACT_PURPLE + * .setTargetColorRange(new ColorRange(ColorSpace.YCrCb, // or define your own color match + * new Scalar( 32, 176, 0), + * new Scalar(255, 255, 132))) + * + * - Focus the color locator by defining a RegionOfInterest (ROI) which you want to search. + * This can be the entire frame, or a sub-region defined using: + * 1) standard image coordinates or 2) a normalized +/- 1.0 coordinate system. + * Use one form of the ImageRegion class to define the ROI. + * ImageRegion.entireFrame() + * ImageRegion.asImageCoordinates(50, 50, 150, 150) 100x100 pixels at upper left corner + * ImageRegion.asUnityCenterCoordinates(-0.5, 0.5, 0.5, -0.5) 50% width/height in center + * + * - Define which contours are included. + * You can get ALL the contours, ignore contours that are completely inside another contour. + * .setContourMode(ColorBlobLocatorProcessor.ContourMode.ALL_FLATTENED_HIERARCHY) + * .setContourMode(ColorBlobLocatorProcessor.ContourMode.EXTERNAL_ONLY) + * EXTERNAL_ONLY helps to avoid bright reflection spots from breaking up solid colors. + * + * - turn the display of contours ON or OFF. Helps debugging but takes up valuable CPU time. + * .setDrawContours(true) + * + * - include any pre-processing of the image or mask before looking for Blobs. + * There are some extra processing you can include to improve the formation of blobs. + * Using these features requires an understanding of how they may effect the final blobs. + * The "pixels" argument sets the NxN kernel size. + * .setBlurSize(int pixels) + * Blurring an image helps to provide a smooth color transition between objects, + * and smoother contours. The higher the number, the more blurred the image becomes. + * Note: Even "pixels" values will be incremented to satisfy the "odd number" requirement. + * Blurring too much may hide smaller features. A size of 5 is good for a 320x240 image. + * + * .setErodeSize(int pixels) + * Erosion removes floating pixels and thin lines so that only substantive objects remain. + * Erosion can grow holes inside regions, and also shrink objects. + * "pixels" in the range of 2-4 are suitable for low res images. + * + * .setDilateSize(int pixels) + * Dilation makes objects and lines more visible by filling in small holes, and making + * filled shapes appear larger. Dilation is useful for joining broken parts of an + * object, such as when removing noise from an image. + * "pixels" in the range of 2-4 are suitable for low res images. + */ + ColorBlobLocatorProcessor colorLocator = new ColorBlobLocatorProcessor.Builder() + .setTargetColorRange(ColorRange.ARTIFACT_PURPLE) // use a predefined color match + .setContourMode(ColorBlobLocatorProcessor.ContourMode.EXTERNAL_ONLY) + .setRoi(ImageRegion.asUnityCenterCoordinates(-0.75, 0.75, 0.75, -0.75)) + .setDrawContours(true) // Show contours on the Stream Preview + .setBlurSize(5) // Smooth the transitions between different colors in image + .build(); + + /* + * Build a vision portal to run the Color Locator process. + * + * - Add the colorLocator process created above. + * - Set the desired video resolution. + * A high resolution will not improve this process, so choose a lower resolution that is + * supported by your camera. This will improve overall performance and reduce latency. + * - Choose your video source. This may be + * .setCamera(hardwareMap.get(WebcamName.class, "Webcam 1")) ..... for a webcam + * or + * .setCamera(BuiltinCameraDirection.BACK) ... for a Phone Camera + */ + VisionPortal portal = new VisionPortal.Builder() + .addProcessor(colorLocator) + .setCameraResolution(new Size(320, 240)) + .setCamera(hardwareMap.get(WebcamName.class, "Webcam 1")) + .build(); + + telemetry.setMsTransmissionInterval(100); // Speed up telemetry updates for debugging. + telemetry.setDisplayFormat(Telemetry.DisplayFormat.MONOSPACE); + + // WARNING: To view the stream preview on the Driver Station, this code runs in INIT mode. + while (opModeIsActive() || opModeInInit()) + { + telemetry.addData("preview on/off", "... Camera Stream\n"); + + // Read the current list + List blobs = colorLocator.getBlobs(); + + /* + * The list of Blobs can be filtered to remove unwanted Blobs. + * Note: All contours will be still displayed on the Stream Preview, but only those + * that satisfy the filter conditions will remain in the current list of "blobs". + * Multiple filters may be used. + * + * To perform a filter + * ColorBlobLocatorProcessor.Util.filterByCriteria(criteria, minValue, maxValue, blobs); + * + * The following criteria are currently supported. + * + * ColorBlobLocatorProcessor.BlobCriteria.BY_CONTOUR_AREA + * A Blob's area is the number of pixels contained within the Contour. Filter out any + * that are too big or small. Start with a large range and then refine the range + * based on the likely size of the desired object in the viewfinder. + * + * ColorBlobLocatorProcessor.BlobCriteria.BY_DENSITY + * A blob's density is an indication of how "full" the contour is. + * If you put a rubber band around the contour you get the "Convex Hull" of the contour. + * The density is the ratio of Contour-area to Convex Hull-area. + * + * ColorBlobLocatorProcessor.BlobCriteria.BY_ASPECT_RATIO + * A blob's Aspect ratio is the ratio of boxFit long side to short side. + * A perfect Square has an aspect ratio of 1. All others are > 1 + * + * ColorBlobLocatorProcessor.BlobCriteria.BY_ARC_LENGTH + * A blob's arc length is the perimeter of the blob. + * This can be used in conjunction with an area filter to detect oddly shaped blobs. + * + * ColorBlobLocatorProcessor.BlobCriteria.BY_CIRCULARITY + * A blob's circularity is how circular it is based on the known area and arc length. + * A perfect circle has a circularity of 1. All others are < 1 + */ + ColorBlobLocatorProcessor.Util.filterByCriteria( + ColorBlobLocatorProcessor.BlobCriteria.BY_CONTOUR_AREA, + 50, 20000, blobs); // filter out very small blobs. + + /* + * The list of Blobs can be sorted using the same Blob attributes as listed above. + * No more than one sort call should be made. Sorting can be ASCENDING or DESCENDING. + * Here is an example.: + * ColorBlobLocatorProcessor.Util.sortByCriteria( + * ColorBlobLocatorProcessor.BlobCriteria.BY_CONTOUR_AREA, SortOrder.DESCENDING, blobs); + */ + + telemetry.addLine("Ctr:(X,Y) Area Dens Aspect Arc Circ"); + + // Display the size (area) and center location for each Blob. + for(ColorBlobLocatorProcessor.Blob b : blobs) + { + RotatedRect boxFit = b.getBoxFit(); + telemetry.addLine(String.format("(%3d,%3d) %5d %4.2f %5.2f %3d %5.3f ", + (int) boxFit.center.x, (int) boxFit.center.y, b.getContourArea(), b.getDensity(), + b.getAspectRatio(), (int) b.getArcLength(), b.getCircularity())); + } + + telemetry.update(); + sleep(100); // Match the telemetry update interval. + } + } +} \ No newline at end of file diff --git a/FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/ConceptVisionColorSensor.java b/FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/ConceptVisionColorSensor.java new file mode 100644 index 0000000..5cce468 --- /dev/null +++ b/FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/ConceptVisionColorSensor.java @@ -0,0 +1,158 @@ +/* + * Copyright (c) 2024 Phil Malone + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package org.firstinspires.ftc.robotcontroller.external.samples; + +import android.util.Size; + +import com.qualcomm.robotcore.eventloop.opmode.Disabled; +import com.qualcomm.robotcore.eventloop.opmode.LinearOpMode; +import com.qualcomm.robotcore.eventloop.opmode.TeleOp; + +import org.firstinspires.ftc.robotcore.external.Telemetry; +import org.firstinspires.ftc.robotcore.external.hardware.camera.WebcamName; +import org.firstinspires.ftc.vision.VisionPortal; +import org.firstinspires.ftc.vision.opencv.ImageRegion; +import org.firstinspires.ftc.vision.opencv.PredominantColorProcessor; + +/* + * This OpMode illustrates how to use a video source (camera) as a color sensor + * + * A "color sensor" will typically determine the color of the object that it is pointed at. + * + * This sample performs the same function, except it uses a video camera to inspect an object or scene. + * The user may choose to inspect all, or just a Region of Interest (ROI), of the active camera view. + * The user must also provide a list of "acceptable colors" (Swatches) from which the closest matching + * color will be selected. + * + * To perform this function, a VisionPortal runs a PredominantColorProcessor process. + * The PredominantColorProcessor (PCP) process is created first, and then the VisionPortal is built. + * The (PCP) analyses the ROI and splits the colored pixels into several color-clusters. + * The largest of these clusters is then considered to be the "Predominant Color" + * The process then matches the Predominant Color with the closest Swatch and returns that match. + * + * To aid the user, a colored rectangle is drawn on the camera preview to show the RegionOfInterest, + * The Predominant Color is used to paint the rectangle border, so the user can visualize the color. + * + * Tip: Connect an HDMI monitor to the Control Hub to view the Color Sensor process in real-time. + * Or use a screen copy utility like ScrCpy.exe to view the video remotely. + * + * 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 + */ + +@Disabled +@TeleOp(name = "Concept: Vision Color-Sensor", group = "Concept") +public class ConceptVisionColorSensor extends LinearOpMode +{ + @Override + public void runOpMode() + { + /* Build a "Color Sensor" vision processor based on the PredominantColorProcessor class. + * + * - Focus the color sensor by defining a RegionOfInterest (ROI) which you want to inspect. + * This can be the entire frame, or a sub-region defined using: + * 1) standard image coordinates or 2) a normalized +/- 1.0 coordinate system. + * Use one form of the ImageRegion class to define the ROI. + * ImageRegion.entireFrame() + * ImageRegion.asImageCoordinates(50, 50, 150, 150) 100x100 square at the top left corner + * ImageRegion.asUnityCenterCoordinates(-0.1, 0.1, 0.1, -0.1) 10% W * H centered square + * + * - Set the list of "acceptable" color swatches (matches). + * Only colors that you assign here will be returned. + * If you know the sensor will be pointing to one of a few specific colors, enter them here. + * Or, if the sensor may be pointed randomly, provide some additional colors that may match. + * Possible choices are: + * RED, ORANGE, YELLOW, GREEN, CYAN, BLUE, PURPLE, MAGENTA, BLACK, WHITE + * Note: For the 2026 season ARTIFACT_PURPLE and ARTIFACT_GREEN have been added. + * + * Note that in the example shown below, only some of the available colors are included. + * This will force any other colored region into one of these colors. + * eg: Green may be reported as YELLOW, as this may be the "closest" match. + */ + PredominantColorProcessor colorSensor = new PredominantColorProcessor.Builder() + .setRoi(ImageRegion.asUnityCenterCoordinates(-0.1, 0.1, 0.1, -0.1)) + .setSwatches( + PredominantColorProcessor.Swatch.ARTIFACT_GREEN, + PredominantColorProcessor.Swatch.ARTIFACT_PURPLE, + PredominantColorProcessor.Swatch.RED, + PredominantColorProcessor.Swatch.BLUE, + PredominantColorProcessor.Swatch.YELLOW, + PredominantColorProcessor.Swatch.BLACK, + PredominantColorProcessor.Swatch.WHITE) + .build(); + + /* + * Build a vision portal to run the Color Sensor process. + * + * - Add the colorSensor process created above. + * - Set the desired video resolution. + * Since a high resolution will not improve this process, choose a lower resolution + * supported by your camera. This will improve overall performance and reduce latency. + * - Choose your video source. This may be + * .setCamera(hardwareMap.get(WebcamName.class, "Webcam 1")) ..... for a webcam + * or + * .setCamera(BuiltinCameraDirection.BACK) ... for a Phone Camera + */ + VisionPortal portal = new VisionPortal.Builder() + .addProcessor(colorSensor) + .setCameraResolution(new Size(320, 240)) + .setCamera(hardwareMap.get(WebcamName.class, "Webcam 1")) + .build(); + + telemetry.setMsTransmissionInterval(100); // Speed up telemetry updates, for debugging. + telemetry.setDisplayFormat(Telemetry.DisplayFormat.MONOSPACE); + + // WARNING: To view the stream preview on the Driver Station, this code runs in INIT mode. + while (opModeIsActive() || opModeInInit()) + { + telemetry.addLine("Preview on/off: 3 dots, Camera Stream\n"); + + // Request the most recent color analysis. This will return the closest matching + // colorSwatch and the predominant color in the RGB, HSV and YCrCb color spaces. + // The color space values are returned as three-element int[] arrays as follows: + // RGB Red 0-255, Green 0-255, Blue 0-255 + // HSV Hue 0-180, Saturation 0-255, Value 0-255 + // YCrCb Luminance(Y) 0-255, Cr 0-255 (center 128), Cb 0-255 (center 128) + // + // Note: to take actions based on the detected color, simply use the colorSwatch or + // color space value in a comparison or switch. eg: + + // if (result.closestSwatch == PredominantColorProcessor.Swatch.RED) {.. some code ..} + // or: + // if (result.RGB[0] > 128) {... some code ...} + + PredominantColorProcessor.Result result = colorSensor.getAnalysis(); + + // Display the Color Sensor result. + telemetry.addData("Best Match", result.closestSwatch); + telemetry.addLine(String.format("RGB (%3d, %3d, %3d)", + result.RGB[0], result.RGB[1], result.RGB[2])); + telemetry.addLine(String.format("HSV (%3d, %3d, %3d)", + result.HSV[0], result.HSV[1], result.HSV[2])); + telemetry.addLine(String.format("YCrCb (%3d, %3d, %3d)", + result.YCrCb[0], result.YCrCb[1], result.YCrCb[2])); + telemetry.update(); + + sleep(20); + } + } +} diff --git a/FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/RobotAutoDriveByEncoder_Linear.java b/FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/RobotAutoDriveByEncoder_Linear.java new file mode 100644 index 0000000..63293d0 --- /dev/null +++ b/FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/RobotAutoDriveByEncoder_Linear.java @@ -0,0 +1,187 @@ +/* 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.DcMotor; +import com.qualcomm.robotcore.util.ElapsedTime; + +/* + * This OpMode illustrates the concept of driving a path based on encoder counts. + * The code is structured as a LinearOpMode + * + * The code REQUIRES that you DO have encoders on the wheels, + * otherwise you would use: RobotAutoDriveByTime; + * + * This code ALSO requires that the drive Motors have been configured such that a positive + * 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 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 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 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 By Encoder", group="Robot") +@Disabled +public class RobotAutoDriveByEncoder_Linear extends LinearOpMode { + + /* Declare OpMode members. */ + 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 = 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); + static final double DRIVE_SPEED = 0.6; + static final double TURN_SPEED = 0.5; + + @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); + + leftDrive.setMode(DcMotor.RunMode.STOP_AND_RESET_ENCODER); + rightDrive.setMode(DcMotor.RunMode.STOP_AND_RESET_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("Starting at", "%7d :%7d", + leftDrive.getCurrentPosition(), + rightDrive.getCurrentPosition()); + telemetry.update(); + + // Wait for the game to start (driver presses START) + waitForStart(); + + // Step through each leg of the path, + // Note: Reverse movement is obtained by setting a negative distance (not speed) + encoderDrive(DRIVE_SPEED, 48, 48, 5.0); // S1: Forward 47 Inches with 5 Sec timeout + 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 + + telemetry.addData("Path", "Complete"); + telemetry.update(); + sleep(1000); // pause to display final telemetry message. + } + + /* + * Method to perform a relative move, based on encoder counts. + * Encoders are not reset as the move is based on the current position. + * Move will stop if any of three conditions occur: + * 1) Move gets to the desired position + * 2) Move runs out of time + * 3) Driver stops the OpMode running. + */ + public void encoderDrive(double speed, + double leftInches, double rightInches, + double timeoutS) { + int newLeftTarget; + int newRightTarget; + + // Ensure that the OpMode is still active + if (opModeIsActive()) { + + // Determine new target position, and pass to motor controller + 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 + leftDrive.setMode(DcMotor.RunMode.RUN_TO_POSITION); + rightDrive.setMode(DcMotor.RunMode.RUN_TO_POSITION); + + // reset the timeout time and start motion. + runtime.reset(); + 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 + // its target position, the motion will stop. This is "safer" in the event that the robot will + // always end the motion as soon as possible. + // However, if you require that BOTH motors have finished their moves before the robot continues + // onto the next step, use (isBusy() || isBusy()) in the loop test. + while (opModeIsActive() && + (runtime.seconds() < timeoutS) && + (leftDrive.isBusy() && rightDrive.isBusy())) { + + // Display it for the driver. + telemetry.addData("Running to", " %7d :%7d", newLeftTarget, newRightTarget); + telemetry.addData("Currently at", " at %7d :%7d", + leftDrive.getCurrentPosition(), rightDrive.getCurrentPosition()); + telemetry.update(); + } + + // Stop all motion; + leftDrive.setPower(0); + rightDrive.setPower(0); + + // Turn off RUN_TO_POSITION + leftDrive.setMode(DcMotor.RunMode.RUN_USING_ENCODER); + rightDrive.setMode(DcMotor.RunMode.RUN_USING_ENCODER); + + 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 0000000..ab70934 --- /dev/null +++ b/FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/RobotAutoDriveByGyro_Linear.java @@ -0,0 +1,429 @@ +/* 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.rev.RevHubOrientationOnRobot; +import com.qualcomm.robotcore.eventloop.opmode.Autonomous; +import com.qualcomm.robotcore.eventloop.opmode.Disabled; +import com.qualcomm.robotcore.eventloop.opmode.LinearOpMode; +import com.qualcomm.robotcore.hardware.DcMotor; +import com.qualcomm.robotcore.hardware.IMU; +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.YawPitchRollAngles; + +/* + * This OpMode illustrates the concept of driving an autonomous path based on Gyro (IMU) 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 uses the Universal IMU interface so it will work with either the BNO055, or BHI260 IMU. + * To run as written, the Control/Expansion hub should be mounted horizontally on a flat part of the robot chassis. + * The REV Logo should be facing UP, and the USB port should be facing forward. + * If this is not the configuration of your REV Control Hub, then the code should be modified to reflect the correct orientation. + * + * 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: This code implements the requirement of calling 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: + * https://ftc-docs.firstinspires.org/field-coordinate-system + * + * 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 IMU imu = null; // Control/Expansion Hub IMU + + 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 correct 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); + + /* The next two lines define Hub orientation. + * The Default Orientation (shown) is when a hub is mounted horizontally with the printed logo pointing UP and the USB port pointing FORWARD. + * + * To Do: EDIT these two lines to match YOUR mounting configuration. + */ + RevHubOrientationOnRobot.LogoFacingDirection logoDirection = RevHubOrientationOnRobot.LogoFacingDirection.UP; + RevHubOrientationOnRobot.UsbFacingDirection usbDirection = RevHubOrientationOnRobot.UsbFacingDirection.FORWARD; + RevHubOrientationOnRobot orientationOnRobot = new RevHubOrientationOnRobot(logoDirection, usbDirection); + + // Now initialize the IMU with this mounting orientation + // This sample expects the IMU to be in a REV Hub and named "imu". + imu = hardwareMap.get(IMU.class, "imu"); + imu.initialize(new IMU.Parameters(orientationOnRobot)); + + // 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", getHeading()); + 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); + imu.resetYaw(); + + // 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. ******************** + + /** + * 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); + } + } + + /** + * Spin on the 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); + } + + /** + * 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 its 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. ******************** + + /** + * Use 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 + + // Determine the heading current error + headingError = targetHeading - getHeading(); + + // 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); + } + + /** + * Take 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("Heading- Target : Current", "%5.2f : %5.0f", targetHeading, getHeading()); + telemetry.addData("Error : Steer Pwr", "%5.1f : %5.1f", headingError, turnSpeed); + telemetry.addData("Wheel Speeds L : R", "%5.2f : %5.2f", leftSpeed, rightSpeed); + telemetry.update(); + } + + /** + * read the Robot heading directly from the IMU (in degrees) + */ + public double getHeading() { + YawPitchRollAngles orientation = imu.getRobotYawPitchRollAngles(); + return orientation.getYaw(AngleUnit.DEGREES); + } +} diff --git a/FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/RobotAutoDriveByTime_Linear.java b/FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/RobotAutoDriveByTime_Linear.java new file mode 100644 index 0000000..a714748 --- /dev/null +++ b/FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/RobotAutoDriveByTime_Linear.java @@ -0,0 +1,128 @@ +/* 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.DcMotor; +import com.qualcomm.robotcore.util.ElapsedTime; + +/* + * This OpMode illustrates the concept of driving a path based on time. + * The code is structured as a LinearOpMode + * + * The code assumes that you do NOT have encoders on the wheels, + * otherwise you would use: RobotAutoDriveByEncoder; + * + * The desired path in this example is: + * - Drive forward for 3 seconds + * - Spin right for 1.3 seconds + * - 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 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 By Time", group="Robot") +@Disabled +public class RobotAutoDriveByTime_Linear extends LinearOpMode { + + /* Declare OpMode members. */ + private DcMotor leftDrive = null; + private DcMotor rightDrive = null; + + private ElapsedTime runtime = new ElapsedTime(); + + + static final double FORWARD_SPEED = 0.6; + static final double TURN_SPEED = 0.5; + + @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); + + // Send telemetry message to signify robot waiting; + telemetry.addData("Status", "Ready to run"); // + telemetry.update(); + + // Wait for the game to start (driver presses START) + waitForStart(); + + // Step through each leg of the path, ensuring that the OpMode has not been stopped along the way. + + // Step 1: Drive forward for 3 seconds + leftDrive.setPower(FORWARD_SPEED); + rightDrive.setPower(FORWARD_SPEED); + runtime.reset(); + while (opModeIsActive() && (runtime.seconds() < 3.0)) { + telemetry.addData("Path", "Leg 1: %4.1f S Elapsed", runtime.seconds()); + telemetry.update(); + } + + // Step 2: Spin right for 1.3 seconds + leftDrive.setPower(TURN_SPEED); + rightDrive.setPower(-TURN_SPEED); + runtime.reset(); + while (opModeIsActive() && (runtime.seconds() < 1.3)) { + telemetry.addData("Path", "Leg 2: %4.1f S Elapsed", runtime.seconds()); + telemetry.update(); + } + + // 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: %4.1f S Elapsed", runtime.seconds()); + telemetry.update(); + } + + // Step 4: Stop + leftDrive.setPower(0); + rightDrive.setPower(0); + + telemetry.addData("Path", "Complete"); + telemetry.update(); + sleep(1000); + } +} diff --git a/FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/RobotAutoDriveToAprilTagOmni.java b/FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/RobotAutoDriveToAprilTagOmni.java new file mode 100644 index 0000000..4b777e2 --- /dev/null +++ b/FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/RobotAutoDriveToAprilTagOmni.java @@ -0,0 +1,321 @@ +/* Copyright (c) 2023 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.DcMotor; +import com.qualcomm.robotcore.util.Range; +import org.firstinspires.ftc.robotcore.external.hardware.camera.BuiltinCameraDirection; +import org.firstinspires.ftc.robotcore.external.hardware.camera.WebcamName; +import org.firstinspires.ftc.robotcore.external.hardware.camera.controls.ExposureControl; +import org.firstinspires.ftc.robotcore.external.hardware.camera.controls.GainControl; +import org.firstinspires.ftc.vision.VisionPortal; +import org.firstinspires.ftc.vision.apriltag.AprilTagDetection; +import org.firstinspires.ftc.vision.apriltag.AprilTagProcessor; + +import java.util.List; +import java.util.concurrent.TimeUnit; + +/* + * This OpMode illustrates using a camera to locate and drive towards a specific AprilTag. + * The code assumes a Holonomic (Mecanum or X Drive) Robot. + * + * For an introduction to AprilTags, see the ftc-docs link below: + * https://ftc-docs.firstinspires.org/en/latest/apriltag/vision_portal/apriltag_intro/apriltag-intro.html + * + * When an AprilTag in the TagLibrary is detected, the SDK provides location and orientation of the tag, relative to the camera. + * This information is provided in the "ftcPose" member of the returned "detection", and is explained in the ftc-docs page linked below. + * https://ftc-docs.firstinspires.org/apriltag-detection-values + * + * The drive goal is to rotate to keep the Tag centered in the camera, while strafing to be directly in front of the tag, and + * driving towards the tag to achieve the desired distance. + * To reduce any motion blur (which will interrupt the detection process) the Camera exposure is reduced to a very low value (5mS) + * You can determine the best Exposure and Gain values by using the ConceptAprilTagOptimizeExposure OpMode in this Samples folder. + * + * The code assumes a Robot Configuration with motors named: front_left_drive and front_right_drive, back_left_drive and back_right_drive. + * The motor directions must be set so a positive power goes forward on all wheels. + * This sample assumes that the current game AprilTag Library (usually for the current season) is being loaded by default, + * so you should choose to approach a valid tag ID. + * + * Under manual control, the left stick will move forward/back & left/right. The right stick will rotate the robot. + * Manually drive the robot until it displays Target data on the Driver Station. + * + * Press and hold the *Left Bumper* to enable the automatic "Drive to target" mode. + * Release the Left Bumper to return to manual driving mode. + * + * Under "Drive To Target" mode, the robot has three goals: + * 1) Turn the robot to always keep the Tag centered on the camera frame. (Use the Target Bearing to turn the robot.) + * 2) Strafe the robot towards the centerline of the Tag, so it approaches directly in front of the tag. (Use the Target Yaw to strafe the robot) + * 3) Drive towards the Tag to get to the desired distance. (Use Tag Range to drive the robot forward/backward) + * + * Use DESIRED_DISTANCE to set how close you want the robot to get to the target. + * Speed and Turn sensitivity can be adjusted using the SPEED_GAIN, STRAFE_GAIN and TURN_GAIN constants. + * + * Use Android Studio to Copy this Class, and Paste it into the TeamCode/src/main/java/org/firstinspires/ftc/teamcode folder. + * Remove or comment out the @Disabled line to add this OpMode to the Driver Station OpMode list. + * + */ + +@TeleOp(name="Omni Drive To AprilTag", group = "Concept") +@Disabled +public class RobotAutoDriveToAprilTagOmni extends LinearOpMode +{ + // Adjust these numbers to suit your robot. + final double DESIRED_DISTANCE = 12.0; // this is how close the camera should get to the target (inches) + + // Set the GAIN constants to control the relationship between the measured position error, and how much power is + // applied to the drive motors to correct the error. + // Drive = Error * Gain Make these values smaller for smoother control, or larger for a more aggressive response. + final double SPEED_GAIN = 0.02 ; // Forward Speed Control "Gain". e.g. Ramp up to 50% power at a 25 inch error. (0.50 / 25.0) + final double STRAFE_GAIN = 0.015 ; // Strafe Speed Control "Gain". e.g. Ramp up to 37% power at a 25 degree Yaw error. (0.375 / 25.0) + final double TURN_GAIN = 0.01 ; // Turn Control "Gain". e.g. Ramp up to 25% power at a 25 degree error. (0.25 / 25.0) + + final double MAX_AUTO_SPEED = 0.5; // Clip the approach speed to this max value (adjust for your robot) + final double MAX_AUTO_STRAFE= 0.5; // Clip the strafing speed to this max value (adjust for your robot) + final double MAX_AUTO_TURN = 0.3; // Clip the turn speed to this max value (adjust for your robot) + + private DcMotor frontLeftDrive = null; // Used to control the left front drive wheel + private DcMotor frontRightDrive = null; // Used to control the right front drive wheel + private DcMotor backLeftDrive = null; // Used to control the left back drive wheel + private DcMotor backRightDrive = null; // Used to control the right back drive wheel + + private static final boolean USE_WEBCAM = true; // Set true to use a webcam, or false for a phone camera + private static final int DESIRED_TAG_ID = -1; // Choose the tag you want to approach or set to -1 for ANY tag. + private VisionPortal visionPortal; // Used to manage the video source. + private AprilTagProcessor aprilTag; // Used for managing the AprilTag detection process. + private AprilTagDetection desiredTag = null; // Used to hold the data for a detected AprilTag + + @Override public void runOpMode() + { + boolean targetFound = false; // Set to true when an AprilTag target is detected + double drive = 0; // Desired forward power/speed (-1 to +1) + double strafe = 0; // Desired strafe power/speed (-1 to +1) + double turn = 0; // Desired turning power/speed (-1 to +1) + + // Initialize the Apriltag Detection process + initAprilTag(); + + // Initialize the hardware variables. Note that the strings used here as parameters + // to 'get' must match the names assigned during the robot configuration. + // step (using the FTC Robot Controller app on the phone). + frontLeftDrive = hardwareMap.get(DcMotor.class, "front_left_drive"); + frontRightDrive = hardwareMap.get(DcMotor.class, "front_right_drive"); + backLeftDrive = hardwareMap.get(DcMotor.class, "back_left_drive"); + backRightDrive = hardwareMap.get(DcMotor.class, "back_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 + frontLeftDrive.setDirection(DcMotor.Direction.REVERSE); + backLeftDrive.setDirection(DcMotor.Direction.REVERSE); + frontRightDrive.setDirection(DcMotor.Direction.FORWARD); + backRightDrive.setDirection(DcMotor.Direction.FORWARD); + + if (USE_WEBCAM) + setManualExposure(6, 250); // Use low exposure time to reduce motion blur + + // Wait for driver to press start + telemetry.addData("Camera preview on/off", "3 dots, Camera Stream"); + telemetry.addData(">", "Touch START to start OpMode"); + telemetry.update(); + waitForStart(); + + while (opModeIsActive()) + { + targetFound = false; + desiredTag = null; + + // Step through the list of detected tags and look for a matching tag + List currentDetections = aprilTag.getDetections(); + for (AprilTagDetection detection : currentDetections) { + // Look to see if we have size info on this tag. + if (detection.metadata != null) { + // Check to see if we want to track towards this tag. + if ((DESIRED_TAG_ID < 0) || (detection.id == DESIRED_TAG_ID)) { + // Yes, we want to use this tag. + targetFound = true; + desiredTag = detection; + break; // don't look any further. + } else { + // This tag is in the library, but we do not want to track it right now. + telemetry.addData("Skipping", "Tag ID %d is not desired", detection.id); + } + } else { + // This tag is NOT in the library, so we don't have enough information to track to it. + telemetry.addData("Unknown", "Tag ID %d is not in TagLibrary", detection.id); + } + } + + // Tell the driver what we see, and what to do. + if (targetFound) { + telemetry.addData("\n>","HOLD Left-Bumper to Drive to Target\n"); + telemetry.addData("Found", "ID %d (%s)", desiredTag.id, desiredTag.metadata.name); + telemetry.addData("Range", "%5.1f inches", desiredTag.ftcPose.range); + telemetry.addData("Bearing","%3.0f degrees", desiredTag.ftcPose.bearing); + telemetry.addData("Yaw","%3.0f degrees", desiredTag.ftcPose.yaw); + } else { + telemetry.addData("\n>","Drive using joysticks to find valid target\n"); + } + + // If Left Bumper is being pressed, AND we have found the desired target, Drive to target Automatically . + if (gamepad1.left_bumper && targetFound) { + + // Determine heading, range and Yaw (tag image rotation) error so we can use them to control the robot automatically. + double rangeError = (desiredTag.ftcPose.range - DESIRED_DISTANCE); + double headingError = desiredTag.ftcPose.bearing; + double yawError = desiredTag.ftcPose.yaw; + + // Use the speed and turn "gains" to calculate how we want the robot to move. + drive = Range.clip(rangeError * SPEED_GAIN, -MAX_AUTO_SPEED, MAX_AUTO_SPEED); + turn = Range.clip(headingError * TURN_GAIN, -MAX_AUTO_TURN, MAX_AUTO_TURN) ; + strafe = Range.clip(-yawError * STRAFE_GAIN, -MAX_AUTO_STRAFE, MAX_AUTO_STRAFE); + + telemetry.addData("Auto","Drive %5.2f, Strafe %5.2f, Turn %5.2f ", drive, strafe, turn); + } else { + + // drive using manual POV Joystick mode. Slow things down to make the robot more controlable. + drive = -gamepad1.left_stick_y / 2.0; // Reduce drive rate to 50%. + strafe = -gamepad1.left_stick_x / 2.0; // Reduce strafe rate to 50%. + turn = -gamepad1.right_stick_x / 3.0; // Reduce turn rate to 33%. + telemetry.addData("Manual","Drive %5.2f, Strafe %5.2f, Turn %5.2f ", drive, strafe, turn); + } + telemetry.update(); + + // Apply desired axes motions to the drivetrain. + moveRobot(drive, strafe, turn); + sleep(10); + } + } + + /** + * Move robot according to desired axes motions + *

+ * Positive X is forward + *

+ * Positive Y is strafe left + *

+ * Positive Yaw is counter-clockwise + */ + public void moveRobot(double x, double y, double yaw) { + // Calculate wheel powers. + double frontLeftPower = x - y - yaw; + double frontRightPower = x + y + yaw; + double backLeftPower = x + y - yaw; + double backRightPower = x - y + yaw; + + // Normalize wheel powers to be less than 1.0 + double max = Math.max(Math.abs(frontLeftPower), Math.abs(frontRightPower)); + max = Math.max(max, Math.abs(backLeftPower)); + max = Math.max(max, Math.abs(backRightPower)); + + if (max > 1.0) { + frontLeftPower /= max; + frontRightPower /= max; + backLeftPower /= max; + backRightPower /= max; + } + + // Send powers to the wheels. + frontLeftDrive.setPower(frontLeftPower); + frontRightDrive.setPower(frontRightPower); + backLeftDrive.setPower(backLeftPower); + backRightDrive.setPower(backRightPower); + } + + /** + * Initialize the AprilTag processor. + */ + private void initAprilTag() { + // Create the AprilTag processor by using a builder. + aprilTag = new AprilTagProcessor.Builder().build(); + + // Adjust Image Decimation to trade-off detection-range for detection-rate. + // e.g. Some typical detection data using a Logitech C920 WebCam + // Decimation = 1 .. Detect 2" Tag from 10 feet away at 10 Frames per second + // Decimation = 2 .. Detect 2" Tag from 6 feet away at 22 Frames per second + // Decimation = 3 .. Detect 2" Tag from 4 feet away at 30 Frames Per Second + // Decimation = 3 .. Detect 5" Tag from 10 feet away at 30 Frames Per Second + // Note: Decimation can be changed on-the-fly to adapt during a match. + aprilTag.setDecimation(2); + + // Create the vision portal by using a builder. + if (USE_WEBCAM) { + visionPortal = new VisionPortal.Builder() + .setCamera(hardwareMap.get(WebcamName.class, "Webcam 1")) + .addProcessor(aprilTag) + .build(); + } else { + visionPortal = new VisionPortal.Builder() + .setCamera(BuiltinCameraDirection.BACK) + .addProcessor(aprilTag) + .build(); + } + } + + /* + Manually set the camera gain and exposure. + This can only be called AFTER calling initAprilTag(), and only works for Webcams; + */ + private void setManualExposure(int exposureMS, int gain) { + // Wait for the camera to be open, then use the controls + + if (visionPortal == null) { + return; + } + + // Make sure camera is streaming before we try to set the exposure controls + if (visionPortal.getCameraState() != VisionPortal.CameraState.STREAMING) { + telemetry.addData("Camera", "Waiting"); + telemetry.update(); + while (!isStopRequested() && (visionPortal.getCameraState() != VisionPortal.CameraState.STREAMING)) { + sleep(20); + } + telemetry.addData("Camera", "Ready"); + telemetry.update(); + } + + // Set camera controls unless we are stopping. + if (!isStopRequested()) + { + ExposureControl exposureControl = visionPortal.getCameraControl(ExposureControl.class); + if (exposureControl.getMode() != ExposureControl.Mode.Manual) { + exposureControl.setMode(ExposureControl.Mode.Manual); + sleep(50); + } + exposureControl.setExposure((long)exposureMS, TimeUnit.MILLISECONDS); + sleep(20); + GainControl gainControl = visionPortal.getCameraControl(GainControl.class); + gainControl.setGain(gain); + sleep(20); + } + } +} diff --git a/FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/RobotAutoDriveToAprilTagTank.java b/FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/RobotAutoDriveToAprilTagTank.java new file mode 100644 index 0000000..ba3eb4f --- /dev/null +++ b/FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/RobotAutoDriveToAprilTagTank.java @@ -0,0 +1,298 @@ +/* Copyright (c) 2023 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.DcMotor; +import com.qualcomm.robotcore.util.Range; +import org.firstinspires.ftc.robotcore.external.hardware.camera.BuiltinCameraDirection; +import org.firstinspires.ftc.robotcore.external.hardware.camera.WebcamName; +import org.firstinspires.ftc.robotcore.external.hardware.camera.controls.ExposureControl; +import org.firstinspires.ftc.robotcore.external.hardware.camera.controls.GainControl; +import org.firstinspires.ftc.vision.VisionPortal; +import org.firstinspires.ftc.vision.apriltag.AprilTagDetection; +import org.firstinspires.ftc.vision.apriltag.AprilTagProcessor; + +import java.util.List; +import java.util.concurrent.TimeUnit; + +/* + * This OpMode illustrates using a camera to locate and drive towards a specific AprilTag. + * The code assumes a basic two-wheel (Tank) Robot Drivetrain + * + * For an introduction to AprilTags, see the ftc-docs link below: + * https://ftc-docs.firstinspires.org/en/latest/apriltag/vision_portal/apriltag_intro/apriltag-intro.html + * + * When an AprilTag in the TagLibrary is detected, the SDK provides location and orientation of the tag, relative to the camera. + * This information is provided in the "ftcPose" member of the returned "detection", and is explained in the ftc-docs page linked below. + * https://ftc-docs.firstinspires.org/apriltag-detection-values + * + * The driving goal is to rotate to keep the tag centered in the camera, while driving towards the tag to achieve the desired distance. + * To reduce any motion blur (which will interrupt the detection process) the Camera exposure is reduced to a very low value (5mS) + * You can determine the best exposure and gain values by using the ConceptAprilTagOptimizeExposure OpMode in this Samples folder. + * + * The code assumes a Robot Configuration with motors named left_drive and right_drive. + * The motor directions must be set so a positive power goes forward on both wheels; + * This sample assumes that the default AprilTag Library (usually for the current season) is being loaded by default + * so you should choose to approach a valid tag ID. + * + * Under manual control, the left stick will move forward/back, and the right stick will rotate the robot. + * This is called POV Joystick mode, different than Tank Drive (where each joystick controls a wheel). + * + * Manually drive the robot until it displays Target data on the Driver Station. + * Press and hold the *Left Bumper* to enable the automatic "Drive to target" mode. + * Release the Left Bumper to return to manual driving mode. + * + * Under "Drive To Target" mode, the robot has two goals: + * 1) Turn the robot to always keep the Tag centered on the camera frame. (Use the Target Bearing to turn the robot.) + * 2) Drive towards the Tag to get to the desired distance. (Use Tag Range to drive the robot forward/backward) + * + * Use DESIRED_DISTANCE to set how close you want the robot to get to the target. + * Speed and Turn sensitivity can be adjusted using the SPEED_GAIN and TURN_GAIN constants. + * + * Use Android Studio to Copy this Class, and Paste it into the TeamCode/src/main/java/org/firstinspires/ftc/teamcode folder. + * Remove or comment out the @Disabled line to add this OpMode to the Driver Station OpMode list. + * + */ + +@TeleOp(name="Tank Drive To AprilTag", group = "Concept") +@Disabled +public class RobotAutoDriveToAprilTagTank extends LinearOpMode +{ + // Adjust these numbers to suit your robot. + final double DESIRED_DISTANCE = 12.0; // this is how close the camera should get to the target (inches) + + // Set the GAIN constants to control the relationship between the measured position error, and how much power is + // applied to the drive motors to correct the error. + // Drive = Error * Gain Make these values smaller for smoother control, or larger for a more aggressive response. + final double SPEED_GAIN = 0.02 ; // Speed Control "Gain". e.g. Ramp up to 50% power at a 25 inch error. (0.50 / 25.0) + final double TURN_GAIN = 0.01 ; // Turn Control "Gain". e.g. Ramp up to 25% power at a 25 degree error. (0.25 / 25.0) + + final double MAX_AUTO_SPEED = 0.5; // Clip the approach speed to this max value (adjust for your robot) + final double MAX_AUTO_TURN = 0.25; // Clip the turn speed to this max value (adjust for your robot) + + private DcMotor leftDrive = null; // Used to control the left drive wheel + private DcMotor rightDrive = null; // Used to control the right drive wheel + + private static final boolean USE_WEBCAM = true; // Set true to use a webcam, or false for a phone camera + private static final int DESIRED_TAG_ID = -1; // Choose the tag you want to approach or set to -1 for ANY tag. + private VisionPortal visionPortal; // Used to manage the video source. + private AprilTagProcessor aprilTag; // Used for managing the AprilTag detection process. + private AprilTagDetection desiredTag = null; // Used to hold the data for a detected AprilTag + + @Override public void runOpMode() + { + boolean targetFound = false; // Set to true when an AprilTag target is detected + double drive = 0; // Desired forward power/speed (-1 to +1) +ve is forward + double turn = 0; // Desired turning power/speed (-1 to +1) +ve is CounterClockwise + + // Initialize the Apriltag Detection process + initAprilTag(); + + // Initialize the hardware variables. Note that the strings used here as parameters + // to 'get' must match the names assigned during the robot configuration. + // step (using the FTC Robot Controller app on the phone). + 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. Single Gear Reduction or 90 Deg drives may require direction flips + leftDrive.setDirection(DcMotor.Direction.REVERSE); + rightDrive.setDirection(DcMotor.Direction.FORWARD); + + if (USE_WEBCAM) + setManualExposure(6, 250); // Use low exposure time to reduce motion blur + + // Wait for the driver to press Start + telemetry.addData("Camera preview on/off", "3 dots, Camera Stream"); + telemetry.addData(">", "Touch START to start OpMode"); + telemetry.update(); + waitForStart(); + + while (opModeIsActive()) + { + targetFound = false; + desiredTag = null; + + // Step through the list of detected tags and look for a matching tag + List currentDetections = aprilTag.getDetections(); + for (AprilTagDetection detection : currentDetections) { + // Look to see if we have size info on this tag. + if (detection.metadata != null) { + // Check to see if we want to track towards this tag. + if ((DESIRED_TAG_ID < 0) || (detection.id == DESIRED_TAG_ID)) { + // Yes, we want to use this tag. + targetFound = true; + desiredTag = detection; + break; // don't look any further. + } else { + // This tag is in the library, but we do not want to track it right now. + telemetry.addData("Skipping", "Tag ID %d is not desired", detection.id); + } + } else { + // This tag is NOT in the library, so we don't have enough information to track to it. + telemetry.addData("Unknown", "Tag ID %d is not in TagLibrary", detection.id); + } + } + + // Tell the driver what we see, and what to do. + if (targetFound) { + telemetry.addData("\n>","HOLD Left-Bumper to Drive to Target\n"); + telemetry.addData("Found", "ID %d (%s)", desiredTag.id, desiredTag.metadata.name); + telemetry.addData("Range", "%5.1f inches", desiredTag.ftcPose.range); + telemetry.addData("Bearing","%3.0f degrees", desiredTag.ftcPose.bearing); + } else { + telemetry.addData("\n>","Drive using joysticks to find valid target\n"); + } + + // If Left Bumper is being pressed, AND we have found the desired target, Drive to target Automatically . + if (gamepad1.left_bumper && targetFound) { + + // Determine heading and range error so we can use them to control the robot automatically. + double rangeError = (desiredTag.ftcPose.range - DESIRED_DISTANCE); + double headingError = desiredTag.ftcPose.bearing; + + // Use the speed and turn "gains" to calculate how we want the robot to move. Clip it to the maximum + drive = Range.clip(rangeError * SPEED_GAIN, -MAX_AUTO_SPEED, MAX_AUTO_SPEED); + turn = Range.clip(headingError * TURN_GAIN, -MAX_AUTO_TURN, MAX_AUTO_TURN) ; + + telemetry.addData("Auto","Drive %5.2f, Turn %5.2f", drive, turn); + } else { + + // drive using manual POV Joystick mode. + drive = -gamepad1.left_stick_y / 2.0; // Reduce drive rate to 50%. + turn = -gamepad1.right_stick_x / 4.0; // Reduce turn rate to 25%. + telemetry.addData("Manual","Drive %5.2f, Turn %5.2f", drive, turn); + } + telemetry.update(); + + // Apply desired axes motions to the drivetrain. + moveRobot(drive, turn); + sleep(10); + } + } + + /** + * Move robot according to desired axes motions + *

+ * Positive X is forward + *

+ * Positive Yaw is counter-clockwise + */ + public void moveRobot(double x, double yaw) { + // Calculate left and right wheel powers. + double leftPower = x - yaw; + double rightPower = x + yaw; + + // Normalize wheel powers to be less than 1.0 + double max = Math.max(Math.abs(leftPower), Math.abs(rightPower)); + if (max >1.0) { + leftPower /= max; + rightPower /= max; + } + + // Send powers to the wheels. + leftDrive.setPower(leftPower); + rightDrive.setPower(rightPower); + } + + /** + * Initialize the AprilTag processor. + */ + private void initAprilTag() { + // Create the AprilTag processor by using a builder. + aprilTag = new AprilTagProcessor.Builder().build(); + + // Adjust Image Decimation to trade-off detection-range for detection-rate. + // e.g. Some typical detection data using a Logitech C920 WebCam + // Decimation = 1 .. Detect 2" Tag from 10 feet away at 10 Frames per second + // Decimation = 2 .. Detect 2" Tag from 6 feet away at 22 Frames per second + // Decimation = 3 .. Detect 2" Tag from 4 feet away at 30 Frames Per Second + // Decimation = 3 .. Detect 5" Tag from 10 feet away at 30 Frames Per Second + // Note: Decimation can be changed on-the-fly to adapt during a match. + aprilTag.setDecimation(2); + + // Create the vision portal by using a builder. + if (USE_WEBCAM) { + visionPortal = new VisionPortal.Builder() + .setCamera(hardwareMap.get(WebcamName.class, "Webcam 1")) + .addProcessor(aprilTag) + .build(); + } else { + visionPortal = new VisionPortal.Builder() + .setCamera(BuiltinCameraDirection.BACK) + .addProcessor(aprilTag) + .build(); + } + } + + /* + Manually set the camera gain and exposure. + This can only be called AFTER calling initAprilTag(), and only works for Webcams; + */ + private void setManualExposure(int exposureMS, int gain) { + // Wait for the camera to be open, then use the controls + + if (visionPortal == null) { + return; + } + + // Make sure camera is streaming before we try to set the exposure controls + if (visionPortal.getCameraState() != VisionPortal.CameraState.STREAMING) { + telemetry.addData("Camera", "Waiting"); + telemetry.update(); + while (!isStopRequested() && (visionPortal.getCameraState() != VisionPortal.CameraState.STREAMING)) { + sleep(20); + } + telemetry.addData("Camera", "Ready"); + telemetry.update(); + } + + // Set camera controls unless we are stopping. + if (!isStopRequested()) + { + ExposureControl exposureControl = visionPortal.getCameraControl(ExposureControl.class); + if (exposureControl.getMode() != ExposureControl.Mode.Manual) { + exposureControl.setMode(ExposureControl.Mode.Manual); + sleep(50); + } + exposureControl.setExposure((long)exposureMS, TimeUnit.MILLISECONDS); + sleep(20); + GainControl gainControl = visionPortal.getCameraControl(GainControl.class); + gainControl.setGain(gain); + sleep(20); + telemetry.addData("Camera", "Ready"); + 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 0000000..780f260 --- /dev/null +++ b/FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/RobotAutoDriveToLine_Linear.java @@ -0,0 +1,142 @@ +/* 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.DcMotor; +import com.qualcomm.robotcore.hardware.NormalizedColorSensor; +import com.qualcomm.robotcore.hardware.NormalizedRGBA; +import com.qualcomm.robotcore.hardware.SwitchableLight; + +/* + * This OpMode 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 variable to store 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 START) + // 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/RobotTeleopMecanumFieldRelativeDrive.java b/FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/RobotTeleopMecanumFieldRelativeDrive.java new file mode 100644 index 0000000..c5c64d0 --- /dev/null +++ b/FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/RobotTeleopMecanumFieldRelativeDrive.java @@ -0,0 +1,163 @@ +/* Copyright (c) 2025 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.rev.RevHubOrientationOnRobot; +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.IMU; + +import org.firstinspires.ftc.robotcore.external.navigation.AngleUnit; +/* + * This OpMode illustrates how to program your robot to drive field relative. This means + * that the robot drives the direction you push the joystick regardless of the current orientation + * of the robot. + * + * This OpMode assumes that you have four mecanum wheels each on its own motor named: + * front_left_motor, front_right_motor, back_left_motor, back_right_motor + * + * and that the left motors are flipped such that when they turn clockwise the wheel moves backwards + * + * 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 = "Robot: Field Relative Mecanum Drive", group = "Robot") +@Disabled +public class RobotTeleopMecanumFieldRelativeDrive extends OpMode { + // This declares the four motors needed + DcMotor frontLeftDrive; + DcMotor frontRightDrive; + DcMotor backLeftDrive; + DcMotor backRightDrive; + + // This declares the IMU needed to get the current direction the robot is facing + IMU imu; + + @Override + public void init() { + frontLeftDrive = hardwareMap.get(DcMotor.class, "front_left_drive"); + frontRightDrive = hardwareMap.get(DcMotor.class, "front_right_drive"); + backLeftDrive = hardwareMap.get(DcMotor.class, "back_left_drive"); + backRightDrive = hardwareMap.get(DcMotor.class, "back_right_drive"); + + // We set the left motors in reverse which is needed for drive trains where the left + // motors are opposite to the right ones. + backLeftDrive.setDirection(DcMotor.Direction.REVERSE); + frontLeftDrive.setDirection(DcMotor.Direction.REVERSE); + + // This uses RUN_USING_ENCODER to be more accurate. If you don't have the encoder + // wires, you should remove these + frontLeftDrive.setMode(DcMotor.RunMode.RUN_USING_ENCODER); + frontRightDrive.setMode(DcMotor.RunMode.RUN_USING_ENCODER); + backLeftDrive.setMode(DcMotor.RunMode.RUN_USING_ENCODER); + backRightDrive.setMode(DcMotor.RunMode.RUN_USING_ENCODER); + + imu = hardwareMap.get(IMU.class, "imu"); + // This needs to be changed to match the orientation on your robot + RevHubOrientationOnRobot.LogoFacingDirection logoDirection = + RevHubOrientationOnRobot.LogoFacingDirection.UP; + RevHubOrientationOnRobot.UsbFacingDirection usbDirection = + RevHubOrientationOnRobot.UsbFacingDirection.FORWARD; + + RevHubOrientationOnRobot orientationOnRobot = new + RevHubOrientationOnRobot(logoDirection, usbDirection); + imu.initialize(new IMU.Parameters(orientationOnRobot)); + } + + @Override + public void loop() { + telemetry.addLine("Press A to reset Yaw"); + telemetry.addLine("Hold left bumper to drive in robot relative"); + telemetry.addLine("The left joystick sets the robot direction"); + telemetry.addLine("Moving the right joystick left and right turns the robot"); + + // If you press the A button, then you reset the Yaw to be zero from the way + // the robot is currently pointing + if (gamepad1.a) { + imu.resetYaw(); + } + // If you press the left bumper, you get a drive from the point of view of the robot + // (much like driving an RC vehicle) + if (gamepad1.left_bumper) { + drive(-gamepad1.left_stick_y, gamepad1.left_stick_x, gamepad1.right_stick_x); + } else { + driveFieldRelative(-gamepad1.left_stick_y, gamepad1.left_stick_x, gamepad1.right_stick_x); + } + } + + // This routine drives the robot field relative + private void driveFieldRelative(double forward, double right, double rotate) { + // First, convert direction being asked to drive to polar coordinates + double theta = Math.atan2(forward, right); + double r = Math.hypot(right, forward); + + // Second, rotate angle by the angle the robot is pointing + theta = AngleUnit.normalizeRadians(theta - + imu.getRobotYawPitchRollAngles().getYaw(AngleUnit.RADIANS)); + + // Third, convert back to cartesian + double newForward = r * Math.sin(theta); + double newRight = r * Math.cos(theta); + + // Finally, call the drive method with robot relative forward and right amounts + drive(newForward, newRight, rotate); + } + + // Thanks to FTC16072 for sharing this code!! + public void drive(double forward, double right, double rotate) { + // This calculates the power needed for each wheel based on the amount of forward, + // strafe right, and rotate + double frontLeftPower = forward + right + rotate; + double frontRightPower = forward - right - rotate; + double backRightPower = forward + right - rotate; + double backLeftPower = forward - right + rotate; + + double maxPower = 1.0; + double maxSpeed = 1.0; // make this slower for outreaches + + // This is needed to make sure we don't pass > 1.0 to any wheel + // It allows us to keep all of the motors in proportion to what they should + // be and not get clipped + maxPower = Math.max(maxPower, Math.abs(frontLeftPower)); + maxPower = Math.max(maxPower, Math.abs(frontRightPower)); + maxPower = Math.max(maxPower, Math.abs(backRightPower)); + maxPower = Math.max(maxPower, Math.abs(backLeftPower)); + + // We multiply by maxSpeed so that it can be set lower for outreaches + // When a young child is driving the robot, we may not want to allow full + // speed. + frontLeftDrive.setPower(maxSpeed * (frontLeftPower / maxPower)); + frontRightDrive.setPower(maxSpeed * (frontRightPower / maxPower)); + backLeftDrive.setPower(maxSpeed * (backLeftPower / maxPower)); + backRightDrive.setPower(maxSpeed * (backRightPower / maxPower)); + } +} diff --git a/FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/RobotTeleopPOV_Linear.java b/FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/RobotTeleopPOV_Linear.java new file mode 100644 index 0000000..af3840d --- /dev/null +++ b/FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/RobotTeleopPOV_Linear.java @@ -0,0 +1,159 @@ +/* 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.DcMotor; +import com.qualcomm.robotcore.hardware.Servo; +import com.qualcomm.robotcore.util.Range; + +/* + * This OpMode executes a POV Game style Teleop for a direct drive robot + * The code is structured as a LinearOpMode + * + * In this mode the left stick moves the robot FWD and back, the Right stick turns left and right. + * 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 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="Robot: Teleop POV", group="Robot") +@Disabled +public class RobotTeleopPOV_Linear extends LinearOpMode { + + /* Declare OpMode members. */ + 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() { + double left; + double right; + double drive; + double turn; + double max; + + // 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(">", "Robot Ready. Press START."); // + telemetry.update(); + + // Wait for the game to start (driver presses START) + 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. + left = drive + turn; + right = drive - turn; + + // Normalize the values so neither exceed +/- 1.0 + max = Math.max(Math.abs(left), Math.abs(right)); + if (max > 1.0) + { + left /= max; + right /= max; + } + + // Output the safe vales to the motor drives. + leftDrive.setPower(left); + rightDrive.setPower(right); + + // Use gamepad left & right Bumpers to open and close the claw + if (gamepad1.right_bumper) + clawOffset += CLAW_SPEED; + else if (gamepad1.left_bumper) + clawOffset -= CLAW_SPEED; + + // Move both servos to new position. Assume servos are mirror image of each other. + clawOffset = Range.clip(clawOffset, -0.5, 0.5); + leftClaw.setPosition(MID_SERVO + clawOffset); + rightClaw.setPosition(MID_SERVO - clawOffset); + + // Use gamepad buttons to move arm up (Y) and down (A) + if (gamepad1.y) + leftArm.setPower(ARM_UP_POWER); + else if (gamepad1.a) + leftArm.setPower(ARM_DOWN_POWER); + else + leftArm.setPower(0.0); + + // Send telemetry message to signify robot running; + telemetry.addData("claw", "Offset = %.2f", clawOffset); + telemetry.addData("left", "%.2f", left); + telemetry.addData("right", "%.2f", right); + telemetry.update(); + + // Pace this loop so jaw action is reasonable speed. + sleep(50); + } + } +} diff --git a/FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/RobotTeleopTank_Iterative.java b/FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/RobotTeleopTank_Iterative.java new file mode 100644 index 0000000..a622f27 --- /dev/null +++ b/FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/RobotTeleopTank_Iterative.java @@ -0,0 +1,160 @@ +/* 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.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 OpMode executes a Tank Drive control TeleOp a direct drive robot + * The code is structured as an Iterative OpMode + * + * 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 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="Robot: Teleop Tank", group="Robot") +@Disabled +public class RobotTeleopTank_Iterative extends OpMode{ + + /* Declare OpMode members. */ + 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() { + // 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(">", "Robot Ready. Press START."); // + } + + /* + * Code to run REPEATEDLY after the driver hits INIT, but before they hit START + */ + @Override + public void init_loop() { + } + + /* + * Code to run ONCE when the driver hits START + */ + @Override + public void start() { + } + + /* + * Code to run REPEATEDLY after the driver hits START but before they hit STOP + */ + @Override + public void loop() { + double left; + double right; + + // 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; + + leftDrive.setPower(left); + rightDrive.setPower(right); + + // Use gamepad left & right Bumpers to open and close the claw + if (gamepad1.right_bumper) + clawOffset += CLAW_SPEED; + else if (gamepad1.left_bumper) + clawOffset -= CLAW_SPEED; + + // Move both servos to new position. Assume servos are mirror image of each other. + clawOffset = Range.clip(clawOffset, -0.5, 0.5); + 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) + leftArm.setPower(ARM_UP_POWER); + else if (gamepad1.a) + leftArm.setPower(ARM_DOWN_POWER); + else + leftArm.setPower(0.0); + + // Send telemetry message to signify robot running; + telemetry.addData("claw", "Offset = %.2f", clawOffset); + telemetry.addData("left", "%.2f", left); + telemetry.addData("right", "%.2f", right); + } + + /* + * Code to run ONCE after the driver hits STOP + */ + @Override + public void stop() { + } +} diff --git a/FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/SampleRevBlinkinLedDriver.java b/FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/SampleRevBlinkinLedDriver.java new file mode 100644 index 0000000..bcf5b80 --- /dev/null +++ b/FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/SampleRevBlinkinLedDriver.java @@ -0,0 +1,163 @@ +/* + * Copyright (c) 2018 Craig MacFarlane + * + * 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 Craig MacFarlane 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.rev.RevBlinkinLedDriver; +import com.qualcomm.robotcore.eventloop.opmode.Disabled; +import com.qualcomm.robotcore.eventloop.opmode.OpMode; +import com.qualcomm.robotcore.eventloop.opmode.TeleOp; +import org.firstinspires.ftc.robotcore.external.Telemetry; +import org.firstinspires.ftc.robotcore.internal.system.Deadline; + +import java.util.concurrent.TimeUnit; + +/* + * This OpMode demonstrates use of the REV Robotics Blinkin LED Driver. + * AUTO mode cycles through all of the patterns. + * MANUAL mode allows the user to manually change patterns using the + * left and right bumpers of a gamepad. + * + * Configure the driver on a servo port, and name it "blinkin". + * + * Displays the first pattern upon init. + */ +@TeleOp(name="BlinkinExample") +@Disabled +public class SampleRevBlinkinLedDriver extends OpMode { + + /* + * Change the pattern every 10 seconds in AUTO mode. + */ + private final static int LED_PERIOD = 10; + + /* + * Rate limit gamepad button presses to every 500ms. + */ + private final static int GAMEPAD_LOCKOUT = 500; + + RevBlinkinLedDriver blinkinLedDriver; + RevBlinkinLedDriver.BlinkinPattern pattern; + + Telemetry.Item patternName; + Telemetry.Item display; + DisplayKind displayKind; + Deadline ledCycleDeadline; + Deadline gamepadRateLimit; + + protected enum DisplayKind { + MANUAL, + AUTO + } + + @Override + public void init() + { + displayKind = DisplayKind.AUTO; + + blinkinLedDriver = hardwareMap.get(RevBlinkinLedDriver.class, "blinkin"); + pattern = RevBlinkinLedDriver.BlinkinPattern.RAINBOW_RAINBOW_PALETTE; + blinkinLedDriver.setPattern(pattern); + + display = telemetry.addData("Display Kind: ", displayKind.toString()); + patternName = telemetry.addData("Pattern: ", pattern.toString()); + + ledCycleDeadline = new Deadline(LED_PERIOD, TimeUnit.SECONDS); + gamepadRateLimit = new Deadline(GAMEPAD_LOCKOUT, TimeUnit.MILLISECONDS); + } + + @Override + public void loop() + { + handleGamepad(); + + if (displayKind == DisplayKind.AUTO) { + doAutoDisplay(); + } else { + /* + * MANUAL mode: Nothing to do, setting the pattern as a result of a gamepad event. + */ + } + } + + /* + * handleGamepad + * + * Responds to a gamepad button press. Demonstrates rate limiting for + * button presses. If loop() is called every 10ms and and you don't rate + * limit, then any given button press may register as multiple button presses, + * which in this application is problematic. + * + * A: Manual mode, Right bumper displays the next pattern, left bumper displays the previous pattern. + * B: Auto mode, pattern cycles, changing every LED_PERIOD seconds. + */ + protected void handleGamepad() + { + if (!gamepadRateLimit.hasExpired()) { + return; + } + + if (gamepad1.a) { + setDisplayKind(DisplayKind.MANUAL); + gamepadRateLimit.reset(); + } else if (gamepad1.b) { + setDisplayKind(DisplayKind.AUTO); + gamepadRateLimit.reset(); + } else if ((displayKind == DisplayKind.MANUAL) && (gamepad1.left_bumper)) { + pattern = pattern.previous(); + displayPattern(); + gamepadRateLimit.reset(); + } else if ((displayKind == DisplayKind.MANUAL) && (gamepad1.right_bumper)) { + pattern = pattern.next(); + displayPattern(); + gamepadRateLimit.reset(); + } + } + + protected void setDisplayKind(DisplayKind displayKind) + { + this.displayKind = displayKind; + display.setValue(displayKind.toString()); + } + + protected void doAutoDisplay() + { + if (ledCycleDeadline.hasExpired()) { + pattern = pattern.next(); + displayPattern(); + ledCycleDeadline.reset(); + } + } + + protected void displayPattern() + { + blinkinLedDriver.setPattern(pattern); + patternName.setValue(pattern.toString()); + } +} diff --git a/FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/SensorAndyMarkIMUNonOrthogonal.java b/FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/SensorAndyMarkIMUNonOrthogonal.java new file mode 100644 index 0000000..cd678ef --- /dev/null +++ b/FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/SensorAndyMarkIMUNonOrthogonal.java @@ -0,0 +1,193 @@ +/* Copyright (c) 2025 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.andymark.AndyMarkIMUOrientationOnRobot; +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.IMU; +import org.firstinspires.ftc.robotcore.external.navigation.AngleUnit; +import org.firstinspires.ftc.robotcore.external.navigation.AngularVelocity; +import org.firstinspires.ftc.robotcore.external.navigation.Orientation; +import org.firstinspires.ftc.robotcore.external.navigation.YawPitchRollAngles; + +import static com.qualcomm.hardware.andymark.AndyMarkIMUOrientationOnRobot.xyzOrientation; + +/* + * This OpMode shows how to use the AndyMark IMU sensor. It assumes that the AndyMark IMU is + * configured with the name "imu". + * + * The sample will display the current Yaw, Pitch and Roll of the robot. + * + * With the correct orientation parameters selected, pitch/roll/yaw should act as follows: + * Pitch value should INCREASE as the robot is tipped UP at the front. (Rotation about X) + * Roll value should INCREASE as the robot is tipped UP at the left side. (Rotation about Y) + * Yaw value should INCREASE as the robot is rotated Counter Clockwise. (Rotation about Z) + * + * The yaw can be reset (to zero) by pressing the Y button on the gamepad (Triangle on a PS4 controller). + * + * This specific sample DOES NOT assume that the AndyMark IMU is mounted on one of the three + * orthogonal planes (X/Y, X/Z or Y/Z) OR that the AndyMark IMU has only been rotated in a range of + * 90 degree increments. + * + * Note: if your AndyMark IMU is mounted Orthogonally (on a orthogonal surface, angled at some + * multiple of 90 Degrees), then you should use the simpler SensorAndyMarkIMUOrthogonal sample in + * this folder. + * + * But... If your AndyMark IMU is mounted Non-Orthogonally, you must specify one or more rotational + * angles that transform a "Default" IMU orientation into your desired orientation. That is what is + * illustrated here. + * + * 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. + * + * Finally, edit this OpMode to use at least one angle around an axis to orient your AndyMark IMU. + */ +@TeleOp(name = "Sensor: AndyMark IMU Non-Orthogonal", group = "Sensor") +@Disabled // Comment this out to add to the OpMode list +public class SensorAndyMarkIMUNonOrthogonal extends LinearOpMode +{ + // The AndyMark IMU sensor object + private IMU imu; + + //---------------------------------------------------------------------------------------------- + // Main logic + //---------------------------------------------------------------------------------------------- + + @Override public void runOpMode() throws InterruptedException { + + // Retrieve and initialize the AndyMark IMU. + // This sample expects the AndyMark IMU to be named "imu". + imu = hardwareMap.get(IMU.class, "imu"); + + /* Define how the AndyMark IMU is mounted to the robot to get the correct Yaw, Pitch, and + * Roll values. + * + * You can apply up to three axis rotations to orient your AndyMark IMU according to how + * it's mounted on the robot. + * + * The starting point for these rotations is the "Default" IMU orientation, which is: + * 1) IMU laying flat on a horizontal surface, with the AndyMark logo facing up. + * 2) Rotated such that the I2C port is facing forward on the robot. + * + * The order that the rotations are performed matters, so this sample shows doing them in + * the order X, Y, then Z. + * For specifying non-orthogonal IMU mounting orientations, we must temporarily use axes + * defined relative to the AndyMark IMU itself, instead of the usual Robot Coordinate System + * axes used for the results the AndyMark IMU gives us. In the starting orientation, the + * AndyMark IMU axes are aligned with the Robot Coordinate System: + * + * X Axis: Starting at center of IMU, pointing out towards the side. + * Y Axis: Starting at center of IMU, pointing out towards I2C port. + * Z Axis: Starting at center of IMU, pointing up through the AndyMark logo. + * + * Positive rotation is defined by right-hand rule with thumb pointing in +ve direction on + * axis. + * + * Some examples. + * + * ---------------------------------------------------------------------------------------------------------------------------------- + * Example A) Assume that the AndyMark IMU is mounted on a sloped plate at the back of the + * robot, with the I2C port coming out the top of the AndyMark IMU. The plate is tilted UP + * 60 degrees from horizontal. + * + * To get the "Default" IMU into this configuration you would just need a single rotation. + * 1) Rotate the AndyMark IMU +60 degrees around the X axis to tilt up the front edge. + * 2) No rotation around the Y or Z axes. + * + * So the X,Y,Z rotations would be 60,0,0 + * + * ---------------------------------------------------------------------------------------------------------------------------------- + * Example B) Assume that the AndyMark IMU is laying flat on the chassis, but it has been + * twisted 30 degrees towards the right front wheel. + * + * To get the "Default" IMU into this configuration you would just need a single rotation, + * but around a different axis. + * 1) No rotation around the X or Y axes. + * 2) Rotate the AndyMark IMU -30 degrees (Clockwise) around the Z axis, since a positive + * angle would be Counter Clockwise. + * + * So the X,Y,Z rotations would be 0,0,-30 + * + * ---------------------------------------------------------------------------------------------------------------------------------- + * Example C) Assume that the AndyMark IMU is mounted on a vertical plate on the right side + * of the robot, with the AndyMark logo facing out, and the AndyMark IMU rotated so that the + * I2C port is facing down 30 degrees towards the back wheels of the robot. + * + * To get the "Default" IMU into this configuration will require several rotations. + * 1) Rotate the AndyMark IMU +90 degrees around the X axis to get it standing upright with + * the logo pointing backwards on the robot + * 2) Next, rotate the AndyMark IMU +90 around the Y axis to get the logo facing to the + * right. + * 3) Finally rotate the AndyMark IMU +120 degrees around the Z axis to take the I2C port + * from vertical to sloping down 30 degrees and facing towards the back of the robot. + * + * So the X,Y,Z rotations would be 90,90,120 + */ + + // The next three lines define the desired axis rotations. + // To Do: EDIT these values to match YOUR mounting configuration. + double xRotation = 0; // enter the desired X rotation angle here. + double yRotation = 0; // enter the desired Y rotation angle here. + double zRotation = 0; // enter the desired Z rotation angle here. + + Orientation imuRotation = xyzOrientation(xRotation, yRotation, zRotation); + + // Now initialize the AndyMark IMU with this mounting orientation. + AndyMarkIMUOrientationOnRobot orientationOnRobot = new AndyMarkIMUOrientationOnRobot(imuRotation); + imu.initialize(new IMU.Parameters(orientationOnRobot)); + + // Loop and update the dashboard. + while (!isStopRequested()) { + telemetry.addData("IMU orientation", "X=%.1f, Y=%.1f, Z=%.1f \n", xRotation, yRotation, zRotation); + + // Check to see if heading reset is requested. + if (gamepad1.y) { + telemetry.addData("Yaw", "Resetting\n"); + imu.resetYaw(); + } else { + telemetry.addData("Yaw", "Press Y (triangle) on Gamepad to reset\n"); + } + + // Retrieve rotational angles and velocities. + YawPitchRollAngles orientation = imu.getRobotYawPitchRollAngles(); + AngularVelocity angularVelocity = imu.getRobotAngularVelocity(AngleUnit.DEGREES); + + telemetry.addData("Yaw (Z)", "%.2f Deg. (Heading)", orientation.getYaw(AngleUnit.DEGREES)); + telemetry.addData("Pitch (X)", "%.2f Deg.", orientation.getPitch(AngleUnit.DEGREES)); + telemetry.addData("Roll (Y)", "%.2f Deg.\n", orientation.getRoll(AngleUnit.DEGREES)); + telemetry.addData("Yaw (Z) velocity", "%.2f Deg/Sec", angularVelocity.zRotationRate); + telemetry.addData("Pitch (X) velocity", "%.2f Deg/Sec", angularVelocity.xRotationRate); + telemetry.addData("Roll (Y) velocity", "%.2f Deg/Sec", angularVelocity.yRotationRate); + telemetry.update(); + } + } +} diff --git a/FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/SensorAndyMarkIMUOrthogonal.java b/FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/SensorAndyMarkIMUOrthogonal.java new file mode 100644 index 0000000..58cee6d --- /dev/null +++ b/FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/SensorAndyMarkIMUOrthogonal.java @@ -0,0 +1,144 @@ +/* Copyright (c) 2025 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.andymark.AndyMarkIMUOrientationOnRobot; +import com.qualcomm.hardware.andymark.AndyMarkIMUOrientationOnRobot.I2cPortFacingDirection; +import com.qualcomm.hardware.andymark.AndyMarkIMUOrientationOnRobot.LogoFacingDirection; +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.IMU; +import org.firstinspires.ftc.robotcore.external.navigation.AngleUnit; +import org.firstinspires.ftc.robotcore.external.navigation.AngularVelocity; +import org.firstinspires.ftc.robotcore.external.navigation.YawPitchRollAngles; + +/* + * This OpMode shows how to use the AndyMark IMU sensor. It assumes that the AndyMark IMU is + * configured with the name "imu". + * + * The sample will display the current Yaw, Pitch and Roll of the robot. + * + * With the correct orientation parameters selected, pitch/roll/yaw should act as follows: + * Pitch value should INCREASE as the robot is tipped UP at the front. (Rotation about X) + * Roll value should INCREASE as the robot is tipped UP at the left side. (Rotation about Y) + * Yaw value should INCREASE as the robot is rotated Counter Clockwise. (Rotation about Z) + * + * The yaw can be reset (to zero) by pressing the Y button on the gamepad (Triangle on a PS4 controller). + * + * This specific sample assumes that the AndyMark IMU is mounted on one of the three orthogonal + * planes (X/Y, X/Z or Y/Z) and that the AndyMark IMU has only been rotated in a range of 90 degree + * increments. + * + * Note: if your AndyMark IMU is mounted on a surface angled at some non-90 Degree multiple (like + * 30), then you should use the SensorAndyMarkIMUNonOrthogonal sample in this folder. + * + * This "Orthogonal" requirement means that: + * + * 1) The AndyMark logo printed on the top of the AndyMark IMU can ONLY be pointing in one of six directions: + * FORWARD, BACKWARD, UP, DOWN, LEFT and RIGHT. + * + * 2) The I2C port can only be pointing in one of the same six directions: + * FORWARD, BACKWARD, UP, DOWN, LEFT and RIGHT. + * + * So, to fully define how your AndyMark IMU is mounted to the robot, you must simply specify: + * LogoFacingDirection + * I2cPortFacingDirection + * + * 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. + * + * Finally, choose the two correct parameters to define how your AndyMark IMU is mounted and edit + * this OpMode to use those parameters. + */ +@TeleOp(name = "Sensor: AndyMark IMU Orthogonal", group = "Sensor") +@Disabled // Comment this out to add to the OpMode list +public class SensorAndyMarkIMUOrthogonal extends LinearOpMode +{ + // The AndyMark IMU sensor object + private IMU imu; + + //---------------------------------------------------------------------------------------------- + // Main logic + //---------------------------------------------------------------------------------------------- + + @Override public void runOpMode() throws InterruptedException { + + // Retrieve and initialize the AndyMark IMU. + // This sample expects the AndyMark IMU to be named "imu". + imu = hardwareMap.get(IMU.class, "imu"); + + /* Define how the AndyMark IMU is mounted to the robot to get the correct Yaw, Pitch, and + * Roll values. + * + * Two input parameters are required to fully specify the Orientation. + * The first parameter specifies the direction the AndyMark logo on the IMU is pointing. + * The second parameter specifies the direction the I2C port on the IMU is pointing. + * All directions are relative to the robot, and left/right is as-viewed from behind the robot. + */ + + /* The next two lines define the IMU orientation. + * To Do: EDIT these two lines to match YOUR mounting configuration. + */ + LogoFacingDirection logoDirection = LogoFacingDirection.UP; + I2cPortFacingDirection i2cDirection = I2cPortFacingDirection.FORWARD; + + AndyMarkIMUOrientationOnRobot orientationOnRobot = new AndyMarkIMUOrientationOnRobot(logoDirection, i2cDirection); + + // Now initialize the AndyMark IMU with this mounting orientation. + // Note: if you choose two conflicting directions, this initialization will cause a code exception. + imu.initialize(new IMU.Parameters(orientationOnRobot)); + + // Loop and update the dashboard. + while (!isStopRequested()) { + telemetry.addData("IMU orientation", "Logo=%s I2C=%s\n ", logoDirection, i2cDirection); + + // Check to see if heading reset is requested. + if (gamepad1.y) { + telemetry.addData("Yaw", "Resetting\n"); + imu.resetYaw(); + } else { + telemetry.addData("Yaw", "Press Y (triangle) on Gamepad to reset\n"); + } + + // Retrieve rotational angles and velocities. + YawPitchRollAngles orientation = imu.getRobotYawPitchRollAngles(); + AngularVelocity angularVelocity = imu.getRobotAngularVelocity(AngleUnit.DEGREES); + + telemetry.addData("Yaw (Z)", "%.2f Deg. (Heading)", orientation.getYaw(AngleUnit.DEGREES)); + telemetry.addData("Pitch (X)", "%.2f Deg.", orientation.getPitch(AngleUnit.DEGREES)); + telemetry.addData("Roll (Y)", "%.2f Deg.\n", orientation.getRoll(AngleUnit.DEGREES)); + telemetry.addData("Yaw (Z) velocity", "%.2f Deg/Sec", angularVelocity.zRotationRate); + telemetry.addData("Pitch (X) velocity", "%.2f Deg/Sec", angularVelocity.xRotationRate); + telemetry.addData("Roll (Y) velocity", "%.2f Deg/Sec", angularVelocity.yRotationRate); + telemetry.update(); + } + } +} diff --git a/FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/SensorAndyMarkTOF.java b/FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/SensorAndyMarkTOF.java new file mode 100644 index 0000000..ce6e943 --- /dev/null +++ b/FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/SensorAndyMarkTOF.java @@ -0,0 +1,85 @@ +/* +Copyright (c) 2025 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.andymark.AndyMarkTOF; +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.DistanceSensor; +import org.firstinspires.ftc.robotcore.external.navigation.DistanceUnit; + +/* + * This OpMode illustrates how to use the AndyMark 2m TOF Lidar Sensor. + * + * The OpMode assumes that the sensor is configured with a name of "sensor_distance". + * + * 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: AndyMarkTOF", group = "Sensor") +@Disabled +public class SensorAndyMarkTOF extends LinearOpMode { + + private DistanceSensor sensorDistance; + + @Override + public void runOpMode() { + // you can use this as a regular DistanceSensor. + sensorDistance = hardwareMap.get(DistanceSensor.class, "sensor_distance"); + + // you can also cast this to a AndyMarkTOF if you want to use added + // methods associated with the AndyMarkTOF class. + AndyMarkTOF sensorTimeOfFlight = (AndyMarkTOF) sensorDistance; + + telemetry.addData(">>", "Press start to continue"); + telemetry.update(); + + waitForStart(); + while(opModeIsActive()) { + // generic DistanceSensor methods. + telemetry.addData("deviceName", sensorDistance.getDeviceName() ); + telemetry.addData("range", String.format("%.01f mm", sensorDistance.getDistance(DistanceUnit.MM))); + telemetry.addData("range", String.format("%.01f cm", sensorDistance.getDistance(DistanceUnit.CM))); + telemetry.addData("range", String.format("%.01f m", sensorDistance.getDistance(DistanceUnit.METER))); + telemetry.addData("range", String.format("%.01f in", sensorDistance.getDistance(DistanceUnit.INCH))); + + // AndyMarkTOF specific methods. + telemetry.addData("ID", String.format("%x", sensorTimeOfFlight.getModelID())); + telemetry.addData("did time out", Boolean.toString(sensorTimeOfFlight.didTimeoutOccur())); + + telemetry.update(); + } + } + +} diff --git a/FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/SensorBNO055IMU.java b/FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/SensorBNO055IMU.java new file mode 100644 index 0000000..405da1e --- /dev/null +++ b/FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/SensorBNO055IMU.java @@ -0,0 +1,186 @@ +/* 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.bosch.BNO055IMU; +import com.qualcomm.hardware.bosch.JustLoggingAccelerationIntegrator; +import com.qualcomm.robotcore.eventloop.opmode.Disabled; +import com.qualcomm.robotcore.eventloop.opmode.LinearOpMode; +import com.qualcomm.robotcore.eventloop.opmode.TeleOp; +import org.firstinspires.ftc.robotcore.external.Func; +import org.firstinspires.ftc.robotcore.external.navigation.Acceleration; +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; +import org.firstinspires.ftc.robotcore.external.navigation.Position; +import org.firstinspires.ftc.robotcore.external.navigation.Velocity; + +import java.util.Locale; + +/* + * This OpMode gives a short demo on how to use the BNO055 Inertial Motion Unit (IMU) from AdaFruit. + * + * Note: this is a Legacy example that will not work with newer Control/Expansion Hubs that use a different IMU + * Please use the new SensorIMUOrthogonal or SensorIMUNonOrthogonal samples for a more universal IMU interface. + * + * 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 + * + * @see Adafruit IMU + */ +@TeleOp(name = "Sensor: BNO055 IMU", group = "Sensor") +@Disabled // Comment this out to add to the OpMode list +public class SensorBNO055IMU extends LinearOpMode + { + //---------------------------------------------------------------------------------------------- + // State + //---------------------------------------------------------------------------------------------- + + // The IMU sensor object + BNO055IMU imu; + + // State used for updating telemetry + Orientation angles; + Acceleration gravity; + + //---------------------------------------------------------------------------------------------- + // Main logic + //---------------------------------------------------------------------------------------------- + + @Override public void runOpMode() { + + // Set up the parameters with which we will use our IMU. Note that integration + // algorithm here just reports accelerations to the logcat log; it doesn't actually + // provide positional information. + BNO055IMU.Parameters parameters = new BNO055IMU.Parameters(); + parameters.angleUnit = BNO055IMU.AngleUnit.DEGREES; + parameters.accelUnit = BNO055IMU.AccelUnit.METERS_PERSEC_PERSEC; + parameters.calibrationDataFile = "BNO055IMUCalibration.json"; // see the calibration sample OpMode + parameters.loggingEnabled = true; + parameters.loggingTag = "IMU"; + parameters.accelerationIntegrationAlgorithm = new JustLoggingAccelerationIntegrator(); + + // Retrieve and initialize the IMU. We expect the IMU to be attached to an I2C port + // on a Core Device Interface Module, configured to be a sensor of type "AdaFruit IMU", + // and named "imu". + imu = hardwareMap.get(BNO055IMU.class, "imu"); + imu.initialize(parameters); + + // Set up our telemetry dashboard + composeTelemetry(); + + // Wait until we're told to go + waitForStart(); + + // Start the logging of measured acceleration + imu.startAccelerationIntegration(new Position(), new Velocity(), 1000); + + // Loop and update the dashboard + while (opModeIsActive()) { + telemetry.update(); + } + } + + //---------------------------------------------------------------------------------------------- + // Telemetry Configuration + //---------------------------------------------------------------------------------------------- + + void composeTelemetry() { + + // At the beginning of each telemetry update, grab a bunch of data + // from the IMU that we will then display in separate lines. + telemetry.addAction(new Runnable() { @Override public void run() + { + // Acquiring the angles is relatively expensive; we don't want + // to do that in each of the three items that need that info, as that's + // three times the necessary expense. + angles = imu.getAngularOrientation(AxesReference.INTRINSIC, AxesOrder.ZYX, AngleUnit.DEGREES); + gravity = imu.getGravity(); + } + }); + + telemetry.addLine() + .addData("status", new Func() { + @Override public String value() { + return imu.getSystemStatus().toShortString(); + } + }) + .addData("calib", new Func() { + @Override public String value() { + return imu.getCalibrationStatus().toString(); + } + }); + + telemetry.addLine() + .addData("heading", new Func() { + @Override public String value() { + return formatAngle(angles.angleUnit, angles.firstAngle); + } + }) + .addData("roll", new Func() { + @Override public String value() { + return formatAngle(angles.angleUnit, angles.secondAngle); + } + }) + .addData("pitch", new Func() { + @Override public String value() { + return formatAngle(angles.angleUnit, angles.thirdAngle); + } + }); + + telemetry.addLine() + .addData("grvty", new Func() { + @Override public String value() { + return gravity.toString(); + } + }) + .addData("mag", new Func() { + @Override public String value() { + return String.format(Locale.getDefault(), "%.3f", + Math.sqrt(gravity.xAccel*gravity.xAccel + + gravity.yAccel*gravity.yAccel + + gravity.zAccel*gravity.zAccel)); + } + }); + } + + //---------------------------------------------------------------------------------------------- + // Formatting + //---------------------------------------------------------------------------------------------- + + String formatAngle(AngleUnit angleUnit, double angle) { + return formatDegrees(AngleUnit.DEGREES.fromUnit(angleUnit, angle)); + } + + String formatDegrees(double degrees){ + return String.format(Locale.getDefault(), "%.1f", AngleUnit.DEGREES.normalize(degrees)); + } +} diff --git a/FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/SensorBNO055IMUCalibration.java b/FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/SensorBNO055IMUCalibration.java new file mode 100644 index 0000000..93f1789 --- /dev/null +++ b/FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/SensorBNO055IMUCalibration.java @@ -0,0 +1,230 @@ +/* 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.bosch.BNO055IMU; +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.ReadWriteFile; +import org.firstinspires.ftc.robotcore.external.Func; +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; +import org.firstinspires.ftc.robotcore.internal.system.AppUtil; + +import java.io.File; +import java.util.Locale; + +/* + * This OpMode calibrates a BNO055 IMU per + * "Section 3.11 Calibration" of the BNO055 specification. + * + * Note: this is a Legacy example that will not work with newer Control/Expansion Hubs that use a different IMU + * Please use the new SensorIMUOrthogonal or SensorIMUNonOrthogonal samples for a more universal IMU interface. + * + * Manual calibration of the IMU is definitely NOT necessary: except for the magnetometer (which is not used by the + * default "IMU" SensorMode), the BNO055 is internally self-calibrating and thus can be very successfully used without + * manual intervention. That said, performing a one-time calibration, saving the results persistently, then loading them + * again at each run can help reduce the time that automatic calibration requires. + * + * This summary of the calibration process from Intel is informative: + * http://iotdk.intel.com/docs/master/upm/classupm_1_1_b_n_o055.html + * + * "This device requires calibration in order to operate accurately. [...] Calibration data is + * lost on a power cycle. See one of the examples for a description of how to calibrate the device, + * but in essence: + * + * There is a calibration status register available [...] that returns the calibration status + * of the accelerometer (ACC), magnetometer (MAG), gyroscope (GYR), and overall system (SYS). + * Each of these values range from 0 (uncalibrated) to 3 (fully calibrated). Calibration [ideally] + * involves certain motions to get all 4 values at 3. The motions are as follows (though see the + * datasheet for more information): + * + * 1. GYR: Simply let the sensor sit flat for a few seconds. + * 2. ACC: Move the sensor in various positions. Start flat, then rotate slowly by 45 + * degrees, hold for a few seconds, then continue rotating another 45 degrees and + * hold, etc. 6 or more movements of this type may be required. You can move through + * any axis you desire, but make sure that the device is lying at least once + * perpendicular to the x, y, and z axis. + * 3. MAG: Move slowly in a figure 8 pattern in the air, until the calibration values reaches 3. + * 4. SYS: This will usually reach 3 when the other items have also reached 3. If not, continue + * slowly moving the device though various axes until it does." + * + * To calibrate the IMU, run this sample OpMode with a gamepad attached to the driver station. + * Once the IMU has reached sufficient calibration as reported on telemetry, press the 'A' + * button on the gamepad to write the calibration to a file. That file can then be indicated + * later when running an OpMode which uses the IMU. + * + * Note: if your intended uses of the IMU do not include use of all its sensors (for example, + * you might not use the magnetometer), then it makes little sense for you to wait for full + * calibration of the sensors you are not using before saving the calibration data. Indeed, + * it appears that in a SensorMode that doesn't use the magnetometer (for example), the + * magnetometer cannot actually be calibrated. + * + * References: + * The AdafruitBNO055IMU Javadoc + * The BNO055IMU.Parameters.calibrationDataFile Javadoc + * The BNO055 product page: https://www.bosch-sensortec.com/bst/products/all_products/bno055 + * The BNO055 datasheet: https://www.bosch-sensortec.com/media/boschsensortec/downloads/datasheets/bst-bno055-ds000.pdf + */ +@TeleOp(name = "Sensor: BNO055 IMU Calibration", group = "Sensor") +@Disabled // Uncomment this to add to the OpMode list +public class SensorBNO055IMUCalibration extends LinearOpMode + { + //---------------------------------------------------------------------------------------------- + // State + //---------------------------------------------------------------------------------------------- + + // Our sensors, motors, and other devices go here, along with other long term state + BNO055IMU imu; + + // State used for updating telemetry + Orientation angles; + + //---------------------------------------------------------------------------------------------- + // Main logic + //---------------------------------------------------------------------------------------------- + + @Override public void runOpMode() { + + telemetry.log().setCapacity(12); + telemetry.log().add(""); + telemetry.log().add("Please refer to the calibration instructions"); + telemetry.log().add("contained in the Adafruit IMU calibration"); + telemetry.log().add("sample OpMode."); + telemetry.log().add(""); + telemetry.log().add("When sufficient calibration has been reached,"); + telemetry.log().add("press the 'A' button to write the current"); + telemetry.log().add("calibration data to a file."); + telemetry.log().add(""); + + // We are expecting the IMU to be attached to an I2C port on a Core Device Interface Module and named "imu". + BNO055IMU.Parameters parameters = new BNO055IMU.Parameters(); + parameters.loggingEnabled = true; + parameters.loggingTag = "IMU"; + imu = hardwareMap.get(BNO055IMU.class, "imu"); + imu.initialize(parameters); + + composeTelemetry(); + telemetry.log().add("Waiting for start..."); + + // Wait until we're told to go + while (!isStarted()) { + telemetry.update(); + idle(); + } + + telemetry.log().add("...started..."); + + while (opModeIsActive()) { + + if (gamepad1.a) { + + // Get the calibration data + BNO055IMU.CalibrationData calibrationData = imu.readCalibrationData(); + + // Save the calibration data to a file. You can choose whatever file + // name you wish here, but you'll want to indicate the same file name + // when you initialize the IMU in an OpMode in which it is used. If you + // have more than one IMU on your robot, you'll of course want to use + // different configuration file names for each. + String filename = "AdafruitIMUCalibration.json"; + File file = AppUtil.getInstance().getSettingsFile(filename); + ReadWriteFile.writeFile(file, calibrationData.serialize()); + telemetry.log().add("saved to '%s'", filename); + + // Wait for the button to be released + while (gamepad1.a) { + telemetry.update(); + idle(); + } + } + + telemetry.update(); + } + } + + void composeTelemetry() { + + // At the beginning of each telemetry update, grab a bunch of data + // from the IMU that we will then display in separate lines. + telemetry.addAction(new Runnable() { @Override public void run() + { + // Acquiring the angles is relatively expensive; we don't want + // to do that in each of the three items that need that info, as that's + // three times the necessary expense. + angles = imu.getAngularOrientation(AxesReference.INTRINSIC, AxesOrder.ZYX, AngleUnit.DEGREES); + } + }); + + telemetry.addLine() + .addData("status", new Func() { + @Override public String value() { + return imu.getSystemStatus().toShortString(); + } + }) + .addData("calib", new Func() { + @Override public String value() { + return imu.getCalibrationStatus().toString(); + } + }); + + telemetry.addLine() + .addData("heading", new Func() { + @Override public String value() { + return formatAngle(angles.angleUnit, angles.firstAngle); + } + }) + .addData("roll", new Func() { + @Override public String value() { + return formatAngle(angles.angleUnit, angles.secondAngle); + } + }) + .addData("pitch", new Func() { + @Override public String value() { + return formatAngle(angles.angleUnit, angles.thirdAngle); + } + }); + } + + //---------------------------------------------------------------------------------------------- + // Formatting + //---------------------------------------------------------------------------------------------- + + String formatAngle(AngleUnit angleUnit, double angle) { + return formatDegrees(AngleUnit.DEGREES.fromUnit(angleUnit, angle)); + } + + String formatDegrees(double degrees){ + return String.format(Locale.getDefault(), "%.1f", AngleUnit.DEGREES.normalize(degrees)); + } +} diff --git a/FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/SensorColor.java b/FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/SensorColor.java new file mode 100644 index 0000000..d0411af --- /dev/null +++ b/FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/SensorColor.java @@ -0,0 +1,225 @@ +/* Copyright (c) 2017-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.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.DistanceSensor; +import com.qualcomm.robotcore.hardware.NormalizedColorSensor; +import com.qualcomm.robotcore.hardware.NormalizedRGBA; +import com.qualcomm.robotcore.hardware.SwitchableLight; +import org.firstinspires.ftc.robotcore.external.navigation.DistanceUnit; + +/* + * This OpMode shows how to use a color sensor in a generic + * way, regardless of which particular make or model of color sensor is used. The OpMode + * assumes that the color sensor is configured with a name of "sensor_color". + * + * There will be some variation in the values measured depending on the specific sensor you are using. + * + * If the color sensor supports adjusting the gain, you can increase the gain (a multiplier to make + * the sensor report higher values) by holding down the A button on the gamepad, and decrease the + * gain by holding down the B button on the gamepad. The AndyMark Proximity & Color Sensor does not + * support this. + * + * If the color sensor has a light which is controllable from software, you can use the X button on + * the gamepad to toggle the light on and off. The REV sensors don't support this, but instead have + * a physical switch on them to turn the light on and off, beginning with REV Color Sensor V2. The + * AndyMark Proximity & Color Sensor does not support this. + * + * If the color sensor also supports short-range distance measurements (usually via an infrared + * proximity sensor), the reported distance will be written to telemetry. As of September 2025, + * the only color sensors that support this are the ones from REV Robotics and the AndyMark + * Proximity & Color Sensor. These infrared proximity sensor measurements are only useful at very + * small distances, and are sensitive to ambient light and surface reflectivity. You should use a + * different sensor if you need precise distance measurements. + * + * 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: Color", group = "Sensor") +@Disabled +public class SensorColor extends LinearOpMode { + + /** The colorSensor field will contain a reference to our color sensor hardware object */ + NormalizedColorSensor colorSensor; + + /** The relativeLayout field is used to aid in providing interesting visual feedback + * in this sample application; you probably *don't* need this when you use a color sensor on your + * robot. Note that you won't see anything change on the Driver Station, only on the Robot Controller. */ + View relativeLayout; + + /* + * The runOpMode() method is the root of this OpMode, as it is in all LinearOpModes. + * Our implementation here, though is a bit unusual: we've decided to put all the actual work + * in the runSample() method rather than directly in runOpMode() itself. The reason we do that is + * that in this sample we're changing the background color of the robot controller screen as the + * OpMode runs, and we want to be able to *guarantee* that we restore it to something reasonable + * and palatable when the OpMode ends. The simplest way to do that is to use a try...finally + * block around the main, core logic, and an easy way to make that all clear was to separate + * the former from the latter in separate methods. + */ + @Override public void runOpMode() { + + // Get a reference to the RelativeLayout so we can later 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()); + relativeLayout = ((Activity) hardwareMap.appContext).findViewById(relativeLayoutId); + + try { + runSample(); // actually execute the sample + } finally { + // On the way out, *guarantee* that the background is reasonable. It doesn't actually start off + // as pure white, but it's too much work to dig out what actually was used, and this is good + // enough to at least make the screen reasonable again. + // Set the panel back to the default color + relativeLayout.post(new Runnable() { + public void run() { + relativeLayout.setBackgroundColor(Color.WHITE); + } + }); + } + } + + protected void runSample() { + // You can give the sensor a gain value, will be multiplied by the sensor's raw value before the + // normalized color values are calculated. Color sensors (especially the REV Color Sensor V3) + // can give very low values (depending on the lighting conditions), which only use a small part + // of the 0-1 range that is available for the red, green, and blue values. In brighter conditions, + // you should use a smaller gain than in dark conditions. If your gain is too high, all of the + // colors will report at or near 1, and you won't be able to determine what color you are + // actually looking at. For this reason, it's better to err on the side of a lower gain + // (but always greater than or equal to 1). + float gain = 2; + + // Once per loop, we will update this hsvValues array. The first element (0) will contain the + // hue, the second element (1) will contain the saturation, and the third element (2) will + // contain the value. See http://web.archive.org/web/20190311170843/https://infohost.nmt.edu/tcc/help/pubs/colortheory/web/hsv.html + // for an explanation of HSV color. + final float[] hsvValues = new float[3]; + + // xButtonPreviouslyPressed and xButtonCurrentlyPressed keep track of the previous and current + // state of the X button on the gamepad + boolean xButtonPreviouslyPressed = false; + boolean xButtonCurrentlyPressed = false; + + // 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 possible, turn the light on in the beginning (it might already be on anyway, + // we just make sure it is if we can). + if (colorSensor instanceof SwitchableLight) { + ((SwitchableLight)colorSensor).enableLight(true); + } + + // Wait for the start button to be pressed. + waitForStart(); + + // Loop until we are asked to stop + while (opModeIsActive()) { + // Explain basic gain information via telemetry + telemetry.addLine("Hold the A button on gamepad 1 to increase gain, or B to decrease it.\n"); + telemetry.addLine("Higher gain values mean that the sensor will report larger numbers for Red, Green, and Blue, and Value\n"); + + // Update the gain value if either of the A or B gamepad buttons is being held + if (gamepad1.a) { + // Only increase the gain by a small amount, since this loop will occur multiple times per second. + gain += 0.005; + } else if (gamepad1.b && gain > 1) { // A gain of less than 1 will make the values smaller, which is not helpful. + gain -= 0.005; + } + + // Show the gain value via telemetry + telemetry.addData("Gain", gain); + + // Tell the sensor our desired gain value (normally you would do this during initialization, + // not during the loop) + colorSensor.setGain(gain); + + // Check the status of the X button on the gamepad + xButtonCurrentlyPressed = gamepad1.x; + + // If the button state is different than what it was, then act + if (xButtonCurrentlyPressed != xButtonPreviouslyPressed) { + // If the button is (now) down, then toggle the light + if (xButtonCurrentlyPressed) { + if (colorSensor instanceof SwitchableLight) { + SwitchableLight light = (SwitchableLight)colorSensor; + light.enableLight(!light.isLightOn()); + } + } + } + xButtonPreviouslyPressed = xButtonCurrentlyPressed; + + // Get the normalized colors from the sensor + NormalizedRGBA colors = colorSensor.getNormalizedColors(); + + /* Use telemetry to display feedback on the driver station. We show the red, green, and blue + * normalized values from the sensor (in the range of 0 to 1), as well as the equivalent + * HSV (hue, saturation and value) values. See http://web.archive.org/web/20190311170843/https://infohost.nmt.edu/tcc/help/pubs/colortheory/web/hsv.html + * for an explanation of HSV color. */ + + // Update the hsvValues array by passing it to Color.colorToHSV() + Color.colorToHSV(colors.toColor(), hsvValues); + + telemetry.addLine() + .addData("Red", "%.3f", colors.red) + .addData("Green", "%.3f", colors.green) + .addData("Blue", "%.3f", colors.blue); + telemetry.addLine() + .addData("Hue", "%.3f", hsvValues[0]) + .addData("Saturation", "%.3f", hsvValues[1]) + .addData("Value", "%.3f", hsvValues[2]); + telemetry.addData("Alpha", "%.3f", colors.alpha); + + /* If this color sensor also has a distance sensor, display the measured distance. + * Note that the reported distance is only useful at very close range, and is impacted by + * ambient light and surface reflectivity. */ + if (colorSensor instanceof DistanceSensor) { + telemetry.addData("Distance (cm)", "%.3f", ((DistanceSensor) colorSensor).getDistance(DistanceUnit.CM)); + } + + telemetry.update(); + + // Change the Robot Controller's background color to match the color detected by the color sensor. + relativeLayout.post(new Runnable() { + public void run() { + relativeLayout.setBackgroundColor(Color.HSVToColor(hsvValues)); + } + }); + } + } +} diff --git a/FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/SensorDigitalTouch.java b/FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/SensorDigitalTouch.java new file mode 100644 index 0000000..44c3ca9 --- /dev/null +++ b/FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/SensorDigitalTouch.java @@ -0,0 +1,78 @@ +/* Copyright (c) 2024 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.DigitalChannel; + +/* + * This OpMode demonstrates how to use a digital channel. + * + * The OpMode assumes that the digital channel is configured with a name of "digitalTouch". + * + * 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: digital channel", group = "Sensor") +@Disabled +public class SensorDigitalTouch extends LinearOpMode { + DigitalChannel digitalTouch; // Digital channel Object + + @Override + public void runOpMode() { + + // get a reference to our touchSensor object. + digitalTouch = hardwareMap.get(DigitalChannel.class, "digitalTouch"); + + digitalTouch.setMode(DigitalChannel.Mode.INPUT); + telemetry.addData("DigitalTouchSensorExample", "Press start to continue..."); + telemetry.update(); + + // wait for the start button to be pressed. + waitForStart(); + + // while the OpMode is active, loop and read the digital channel. + // Note we use opModeIsActive() as our loop condition because it is an interruptible method. + while (opModeIsActive()) { + + // button is pressed if value returned is LOW or false. + // send the info back to driver station using telemetry function. + if (digitalTouch.getState() == false) { + telemetry.addData("Button", "PRESSED"); + } else { + telemetry.addData("Button", "NOT PRESSED"); + } + + telemetry.update(); + } + } +} diff --git a/FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/SensorGoBildaPinpoint.java b/FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/SensorGoBildaPinpoint.java new file mode 100644 index 0000000..5b0f6e9 --- /dev/null +++ b/FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/SensorGoBildaPinpoint.java @@ -0,0 +1,116 @@ +/* +* Copyright (c) 2025 Alan Smith +* +* Permission is hereby granted, free of charge, to any person obtaining a copy +* of this software and associated documentation files (the "Software"), to deal +* in the Software without restriction, including without limitation the rights +* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +* copies of the Software, and to permit persons to whom the Software is +* furnished to do so, subject to the following conditions: + +* The above copyright notice and this permission notice shall be included in all +* copies or substantial portions of the Software. + +* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +* SOFTWARE. +*/ +package org.firstinspires.ftc.robotcontroller.external.samples; + +import com.qualcomm.hardware.gobilda.GoBildaPinpointDriver; +import com.qualcomm.robotcore.eventloop.opmode.Disabled; +import com.qualcomm.robotcore.eventloop.opmode.OpMode; +import com.qualcomm.robotcore.eventloop.opmode.TeleOp; + +import org.firstinspires.ftc.robotcore.external.navigation.AngleUnit; +import org.firstinspires.ftc.robotcore.external.navigation.DistanceUnit; +import org.firstinspires.ftc.robotcore.external.navigation.Pose2D; + +/* + * This OpMode illustrates how to use the GoBildaPinpoint + * + * The OpMode assumes that the sensor is configured with a name of "pinpoint". + * + * 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 + * + * See the sensor's product page: https://www.gobilda.com/pinpoint-odometry-computer-imu-sensor-fusion-for-2-wheel-odometry/ + */ +@TeleOp(name = "Sensor: GoBilda Pinpoint", group = "Sensor") +@Disabled +public class SensorGoBildaPinpoint extends OpMode { + // Create an instance of the sensor + GoBildaPinpointDriver pinpoint; + + @Override + public void init() { + // Get a reference to the sensor + pinpoint = hardwareMap.get(GoBildaPinpointDriver.class, "pinpoint"); + + // Configure the sensor + configurePinpoint(); + + // Set the location of the robot - this should be the place you are starting the robot from + pinpoint.setPosition(new Pose2D(DistanceUnit.INCH, 0, 0, AngleUnit.DEGREES, 0)); + } + + @Override + public void loop() { + telemetry.addLine("Push your robot around to see it track"); + telemetry.addLine("Press A to reset the position"); + if(gamepad1.a){ + // You could use readings from April Tags here to give a new known position to the pinpoint + pinpoint.setPosition(new Pose2D(DistanceUnit.INCH, 0, 0, AngleUnit.DEGREES, 0)); + } + pinpoint.update(); + Pose2D pose2D = pinpoint.getPosition(); + + telemetry.addData("X coordinate (IN)", pose2D.getX(DistanceUnit.INCH)); + telemetry.addData("Y coordinate (IN)", pose2D.getY(DistanceUnit.INCH)); + telemetry.addData("Heading angle (DEGREES)", pose2D.getHeading(AngleUnit.DEGREES)); + } + + public void configurePinpoint(){ + /* + * Set the odometry pod positions relative to the point that you want the position to be measured from. + * + * The X pod offset refers to how far sideways from the tracking point the X (forward) odometry pod is. + * Left of the center is a positive number, right of center is a negative number. + * + * The Y pod offset refers to how far forwards from the tracking point the Y (strafe) odometry pod is. + * Forward of center is a positive number, backwards is a negative number. + */ + pinpoint.setOffsets(-84.0, -168.0, DistanceUnit.MM); //these are tuned for 3110-0002-0001 Product Insight #1 + + /* + * Set the kind of pods used by your robot. If you're using goBILDA odometry pods, select either + * the goBILDA_SWINGARM_POD, or the goBILDA_4_BAR_POD. + * If you're using another kind of odometry pod, uncomment setEncoderResolution and input the + * number of ticks per unit of your odometry pod. For example: + * pinpoint.setEncoderResolution(13.26291192, DistanceUnit.MM); + */ + pinpoint.setEncoderResolution(GoBildaPinpointDriver.GoBildaOdometryPods.goBILDA_4_BAR_POD); + + /* + * Set the direction that each of the two odometry pods count. The X (forward) pod should + * increase when you move the robot forward. And the Y (strafe) pod should increase when + * you move the robot to the left. + */ + pinpoint.setEncoderDirections(GoBildaPinpointDriver.EncoderDirection.FORWARD, + GoBildaPinpointDriver.EncoderDirection.FORWARD); + + /* + * Before running the robot, recalibrate the IMU. This needs to happen when the robot is stationary + * The IMU will automatically calibrate when first powered on, but recalibrating before running + * the robot is a good idea to ensure that the calibration is "good". + * resetPosAndIMU will reset the position to 0,0,0 and also recalibrate the IMU. + * This is recommended before you run your autonomous, as a bad initial calibration can cause + * an incorrect starting value for x, y, and heading. + */ + pinpoint.resetPosAndIMU(); + } +} diff --git a/FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/SensorHuskyLens.java b/FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/SensorHuskyLens.java new file mode 100644 index 0000000..af7ca55 --- /dev/null +++ b/FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/SensorHuskyLens.java @@ -0,0 +1,160 @@ +/* +Copyright (c) 2023 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.dfrobot.HuskyLens; +import com.qualcomm.robotcore.eventloop.opmode.Disabled; +import com.qualcomm.robotcore.eventloop.opmode.LinearOpMode; +import com.qualcomm.robotcore.eventloop.opmode.TeleOp; + +import org.firstinspires.ftc.robotcore.internal.system.Deadline; + +import java.util.concurrent.TimeUnit; + +/* + * This OpMode illustrates how to use the DFRobot HuskyLens. + * + * The HuskyLens is a Vision Sensor with a built-in object detection model. It can + * detect a number of predefined objects and AprilTags in the 36h11 family, can + * recognize colors, and can be trained to detect custom objects. See this website for + * documentation: https://wiki.dfrobot.com/HUSKYLENS_V1.0_SKU_SEN0305_SEN0336 + * + * For detailed instructions on how a HuskyLens is used in FTC, please see this tutorial: + * https://ftc-docs.firstinspires.org/en/latest/devices/huskylens/huskylens.html + * + * This sample illustrates how to detect AprilTags, but can be used to detect other types + * of objects by changing the algorithm. It assumes that the HuskyLens is configured with + * a name of "huskylens". + * + * 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: HuskyLens", group = "Sensor") +@Disabled +public class SensorHuskyLens extends LinearOpMode { + + private final int READ_PERIOD = 1; + + private HuskyLens huskyLens; + + @Override + public void runOpMode() + { + huskyLens = hardwareMap.get(HuskyLens.class, "huskylens"); + + /* + * This sample rate limits the reads solely to allow a user time to observe + * what is happening on the Driver Station telemetry. Typical applications + * would not likely rate limit. + */ + Deadline rateLimit = new Deadline(READ_PERIOD, TimeUnit.SECONDS); + + /* + * Immediately expire so that the first time through we'll do the read. + */ + rateLimit.expire(); + + /* + * Basic check to see if the device is alive and communicating. This is not + * technically necessary here as the HuskyLens class does this in its + * doInitialization() method which is called when the device is pulled out of + * the hardware map. However, sometimes it's unclear why a device reports as + * failing on initialization. In the case of this device, it's because the + * call to knock() failed. + */ + if (!huskyLens.knock()) { + telemetry.addData(">>", "Problem communicating with " + huskyLens.getDeviceName()); + } else { + telemetry.addData(">>", "Press start to continue"); + } + + /* + * The device uses the concept of an algorithm to determine what types of + * objects it will look for and/or what mode it is in. The algorithm may be + * selected using the scroll wheel on the device, or via software as shown in + * the call to selectAlgorithm(). + * + * The SDK itself does not assume that the user wants a particular algorithm on + * startup, and hence does not set an algorithm. + * + * Users, should, in general, explicitly choose the algorithm they want to use + * within the OpMode by calling selectAlgorithm() and passing it one of the values + * found in the enumeration HuskyLens.Algorithm. + * + * Other algorithm choices for FTC might be: OBJECT_RECOGNITION, COLOR_RECOGNITION or OBJECT_CLASSIFICATION. + */ + huskyLens.selectAlgorithm(HuskyLens.Algorithm.TAG_RECOGNITION); + + telemetry.update(); + waitForStart(); + + /* + * Looking for AprilTags per the call to selectAlgorithm() above. A handy grid + * for testing may be found at https://wiki.dfrobot.com/HUSKYLENS_V1.0_SKU_SEN0305_SEN0336#target_20. + * + * Note again that the device only recognizes the 36h11 family of tags out of the box. + */ + while(opModeIsActive()) { + if (!rateLimit.hasExpired()) { + continue; + } + rateLimit.reset(); + + /* + * All algorithms, except for LINE_TRACKING, return a list of Blocks where a + * Block represents the outline of a recognized object along with its ID number. + * ID numbers allow you to identify what the device saw. See the HuskyLens documentation + * referenced in the header comment above for more information on IDs and how to + * assign them to objects. + * + * Returns an empty array if no objects are seen. + */ + HuskyLens.Block[] blocks = huskyLens.blocks(); + telemetry.addData("Block count", blocks.length); + for (int i = 0; i < blocks.length; i++) { + telemetry.addData("Block", blocks[i].toString()); + /* + * Here inside the FOR loop, you could save or evaluate specific info for the currently recognized Bounding Box: + * - blocks[i].width and blocks[i].height (size of box, in pixels) + * - blocks[i].left and blocks[i].top (edges of box) + * - blocks[i].x and blocks[i].y (center location) + * - blocks[i].id (Color ID) + * + * These values have Java type int (integer). + */ + } + + telemetry.update(); + } + } +} \ No newline at end of file diff --git a/FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/SensorIMUNonOrthogonal.java b/FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/SensorIMUNonOrthogonal.java new file mode 100644 index 0000000..70bc8d4 --- /dev/null +++ b/FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/SensorIMUNonOrthogonal.java @@ -0,0 +1,184 @@ +/* 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.rev.RevHubOrientationOnRobot; +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.IMU; +import org.firstinspires.ftc.robotcore.external.navigation.AngleUnit; +import org.firstinspires.ftc.robotcore.external.navigation.AngularVelocity; +import org.firstinspires.ftc.robotcore.external.navigation.Orientation; +import org.firstinspires.ftc.robotcore.external.navigation.YawPitchRollAngles; + +import static com.qualcomm.hardware.rev.RevHubOrientationOnRobot.xyzOrientation; + +/* + * This OpMode shows how to use the new universal IMU interface. This + * interface may be used with the BNO055 IMU or the BHI260 IMU. It assumes that an IMU is configured + * on the robot with the name "imu". + * + * The sample will display the current Yaw, Pitch and Roll of the robot.
+ * With the correct orientation parameters selected, pitch/roll/yaw should act as follows: + * Pitch value should INCREASE as the robot is tipped UP at the front. (Rotation about X) + * Roll value should INCREASE as the robot is tipped UP at the left side. (Rotation about Y) + * Yaw value should INCREASE as the robot is rotated Counter Clockwise. (Rotation about Z) + * + * The yaw can be reset (to zero) by pressing the Y button on the gamepad (Triangle on a PS4 controller) + * + * This specific sample DOES NOT assume that the Hub is mounted on one of the three orthogonal + * planes (X/Y, X/Z or Y/Z) OR that the Hub has only been rotated in a range of 90 degree increments. + * + * Note: if your Hub is mounted Orthogonally (on a orthogonal surface, angled at some multiple of + * 90 Degrees) then you should use the simpler SensorIMUOrthogonal sample in this folder. + * + * But... If your Hub is mounted Non-Orthogonally, you must specify one or more rotational angles + * that transform a "Default" Hub orientation into your desired orientation. That is what is + * illustrated here. + * + * 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. + * + * Finally, edit this OpMode to use at least one angle around an axis to orient your Hub. + */ +@TeleOp(name = "Sensor: IMU Non-Orthogonal", group = "Sensor") +@Disabled // Comment this out to add to the OpMode list +public class SensorIMUNonOrthogonal extends LinearOpMode +{ + // The IMU sensor object + IMU imu; + + //---------------------------------------------------------------------------------------------- + // Main logic + //---------------------------------------------------------------------------------------------- + + @Override public void runOpMode() throws InterruptedException { + + // Retrieve and initialize the IMU. + // This sample expects the IMU to be in a REV Hub and named "imu". + imu = hardwareMap.get(IMU.class, "imu"); + + /* Define how the hub is mounted to the robot to get the correct Yaw, Pitch and Roll values. + * + * You can apply up to three axis rotations to orient your Hub according to how it's mounted on the robot. + * + * The starting point for these rotations is the "Default" Hub orientation, which is: + * 1) Hub laying flat on a horizontal surface, with the Printed Logo facing UP + * 2) Rotated such that the USB ports are facing forward on the robot. + * + * If you are using a REV External IMU, the "Default" orientation is the same as for a REV Hub, but instead of + * the USB ports facing forward, the I2C port faces forward. + * + * The order that the rotations are performed matters, so this sample shows doing them in the order X, Y, then Z. + * For specifying non-orthogonal hub mounting orientations, we must temporarily use axes + * defined relative to the Hub itself, instead of the usual Robot Coordinate System axes + * used for the results the IMU gives us. In the starting orientation, the Hub axes are + * aligned with the Robot Coordinate System: + * + * X Axis: Starting at Center of Hub, pointing out towards I2C connectors + * Y Axis: Starting at Center of Hub, pointing out towards USB connectors + * Z Axis: Starting at Center of Hub, pointing Up through LOGO + * + * Positive rotation is defined by right-hand rule with thumb pointing in +ve direction on axis. + * + * Some examples. + * + * ---------------------------------------------------------------------------------------------------------------------------------- + * Example A) Assume that the hub is mounted on a sloped plate at the back of the robot, with the USB ports coming out the top of the hub. + * The plate is tilted UP 60 degrees from horizontal. + * + * To get the "Default" hub into this configuration you would just need a single rotation. + * 1) Rotate the Hub +60 degrees around the X axis to tilt up the front edge. + * 2) No rotation around the Y or Z axes. + * + * So the X,Y,Z rotations would be 60,0,0 + * + * ---------------------------------------------------------------------------------------------------------------------------------- + * Example B) Assume that the hub is laying flat on the chassis, but it has been twisted 30 degrees towards the right front wheel to make + * the USB cable accessible. + * + * To get the "Default" hub into this configuration you would just need a single rotation, but around a different axis. + * 1) No rotation around the X or Y axes. + * 2) Rotate the Hub -30 degrees (Clockwise) around the Z axis, since a positive angle would be Counter Clockwise. + * + * So the X,Y,Z rotations would be 0,0,-30 + * + * ---------------------------------------------------------------------------------------------------------------------------------- + * Example C) Assume that the hub is mounted on a vertical plate on the right side of the robot, with the Logo facing out, and the + * Hub rotated so that the USB ports are facing down 30 degrees towards the back wheels of the robot. + * + * To get the "Default" hub into this configuration will require several rotations. + * 1) Rotate the hub +90 degrees around the X axis to get it standing upright with the logo pointing backwards on the robot + * 2) Next, rotate the hub +90 around the Y axis to get it facing to the right. + * 3) Finally rotate the hub +120 degrees around the Z axis to take the USB ports from vertical to sloping down 30 degrees and + * facing towards the back of the robot. + * + * So the X,Y,Z rotations would be 90,90,120 + */ + + // The next three lines define the desired axis rotations. + // To Do: EDIT these values to match YOUR mounting configuration. + double xRotation = 0; // enter the desired X rotation angle here. + double yRotation = 0; // enter the desired Y rotation angle here. + double zRotation = 0; // enter the desired Z rotation angle here. + + Orientation hubRotation = xyzOrientation(xRotation, yRotation, zRotation); + + // Now initialize the IMU with this mounting orientation + RevHubOrientationOnRobot orientationOnRobot = new RevHubOrientationOnRobot(hubRotation); + imu.initialize(new IMU.Parameters(orientationOnRobot)); + + // Loop and update the dashboard + while (!isStopRequested()) { + telemetry.addData("Hub orientation", "X=%.1f, Y=%.1f, Z=%.1f \n", xRotation, yRotation, zRotation); + + // Check to see if heading reset is requested + if (gamepad1.y) { + telemetry.addData("Yaw", "Resetting\n"); + imu.resetYaw(); + } else { + telemetry.addData("Yaw", "Press Y (triangle) on Gamepad to reset\n"); + } + + // Retrieve Rotational Angles and Velocities + YawPitchRollAngles orientation = imu.getRobotYawPitchRollAngles(); + AngularVelocity angularVelocity = imu.getRobotAngularVelocity(AngleUnit.DEGREES); + + telemetry.addData("Yaw (Z)", "%.2f Deg. (Heading)", orientation.getYaw(AngleUnit.DEGREES)); + telemetry.addData("Pitch (X)", "%.2f Deg.", orientation.getPitch(AngleUnit.DEGREES)); + telemetry.addData("Roll (Y)", "%.2f Deg.\n", orientation.getRoll(AngleUnit.DEGREES)); + telemetry.addData("Yaw (Z) velocity", "%.2f Deg/Sec", angularVelocity.zRotationRate); + telemetry.addData("Pitch (X) velocity", "%.2f Deg/Sec", angularVelocity.xRotationRate); + telemetry.addData("Roll (Y) velocity", "%.2f Deg/Sec", angularVelocity.yRotationRate); + telemetry.update(); + } + } +} diff --git a/FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/SensorIMUOrthogonal.java b/FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/SensorIMUOrthogonal.java new file mode 100644 index 0000000..af4c202 --- /dev/null +++ b/FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/SensorIMUOrthogonal.java @@ -0,0 +1,146 @@ +/* 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.rev.RevHubOrientationOnRobot; +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.IMU; +import org.firstinspires.ftc.robotcore.external.navigation.AngleUnit; +import org.firstinspires.ftc.robotcore.external.navigation.AngularVelocity; +import org.firstinspires.ftc.robotcore.external.navigation.YawPitchRollAngles; + +/* + * This OpMode shows how to use the new universal IMU interface. This + * interface may be used with the BNO055 IMU or the BHI260 IMU. It assumes that an IMU is configured + * on the robot with the name "imu". + * + * The sample will display the current Yaw, Pitch and Roll of the robot.
+ * With the correct orientation parameters selected, pitch/roll/yaw should act as follows: + * Pitch value should INCREASE as the robot is tipped UP at the front. (Rotation about X)
+ * Roll value should INCREASE as the robot is tipped UP at the left side. (Rotation about Y)
+ * Yaw value should INCREASE as the robot is rotated Counter Clockwise. (Rotation about Z)
+ * + * The yaw can be reset (to zero) by pressing the Y button on the gamepad (Triangle on a PS4 controller) + * + * This specific sample assumes that the Hub is mounted on one of the three orthogonal planes + * (X/Y, X/Z or Y/Z) and that the Hub has only been rotated in a range of 90 degree increments. + * + * Note: if your Hub is mounted on a surface angled at some non-90 Degree multiple (like 30) look at + * the alternative SensorIMUNonOrthogonal sample in this folder. + * + * This "Orthogonal" requirement means that: + * + * 1) The Logo printed on the top of the Hub can ONLY be pointing in one of six directions: + * FORWARD, BACKWARD, UP, DOWN, LEFT and RIGHT. + * + * 2) The USB ports can only be pointing in one of the same six directions:
+ * FORWARD, BACKWARD, UP, DOWN, LEFT and RIGHT. + * + * So, To fully define how your Hub is mounted to the robot, you must simply specify:
+ * logoFacingDirection
+ * usbFacingDirection + * + * 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. + * + * Finally, choose the two correct parameters to define how your Hub is mounted and edit this OpMode + * to use those parameters. + */ +@TeleOp(name = "Sensor: IMU Orthogonal", group = "Sensor") +@Disabled // Comment this out to add to the OpMode list +public class SensorIMUOrthogonal extends LinearOpMode +{ + // The IMU sensor object + IMU imu; + + //---------------------------------------------------------------------------------------------- + // Main logic + //---------------------------------------------------------------------------------------------- + + @Override public void runOpMode() throws InterruptedException { + + // Retrieve and initialize the IMU. + // This sample expects the IMU to be in a REV Hub and named "imu". + imu = hardwareMap.get(IMU.class, "imu"); + + /* Define how the hub is mounted on the robot to get the correct Yaw, Pitch and Roll values. + * + * Two input parameters are required to fully specify the Orientation. + * The first parameter specifies the direction the printed logo on the Hub is pointing. + * The second parameter specifies the direction the USB connector on the Hub is pointing. + * All directions are relative to the robot, and left/right is as-viewed from behind the robot. + * + * If you are using a REV 9-Axis IMU, you can use the Rev9AxisImuOrientationOnRobot class instead of the + * RevHubOrientationOnRobot class, which has an I2cPortFacingDirection instead of a UsbFacingDirection. + */ + + /* The next two lines define Hub orientation. + * The Default Orientation (shown) is when a hub is mounted horizontally with the printed logo pointing UP and the USB port pointing FORWARD. + * + * To Do: EDIT these two lines to match YOUR mounting configuration. + */ + RevHubOrientationOnRobot.LogoFacingDirection logoDirection = RevHubOrientationOnRobot.LogoFacingDirection.UP; + RevHubOrientationOnRobot.UsbFacingDirection usbDirection = RevHubOrientationOnRobot.UsbFacingDirection.FORWARD; + + RevHubOrientationOnRobot orientationOnRobot = new RevHubOrientationOnRobot(logoDirection, usbDirection); + + // Now initialize the IMU with this mounting orientation + // Note: if you choose two conflicting directions, this initialization will cause a code exception. + imu.initialize(new IMU.Parameters(orientationOnRobot)); + + // Loop and update the dashboard + while (!isStopRequested()) { + + telemetry.addData("Hub orientation", "Logo=%s USB=%s\n ", logoDirection, usbDirection); + + // Check to see if heading reset is requested + if (gamepad1.y) { + telemetry.addData("Yaw", "Resetting\n"); + imu.resetYaw(); + } else { + telemetry.addData("Yaw", "Press Y (triangle) on Gamepad to reset\n"); + } + + // Retrieve Rotational Angles and Velocities + YawPitchRollAngles orientation = imu.getRobotYawPitchRollAngles(); + AngularVelocity angularVelocity = imu.getRobotAngularVelocity(AngleUnit.DEGREES); + + telemetry.addData("Yaw (Z)", "%.2f Deg. (Heading)", orientation.getYaw(AngleUnit.DEGREES)); + telemetry.addData("Pitch (X)", "%.2f Deg.", orientation.getPitch(AngleUnit.DEGREES)); + telemetry.addData("Roll (Y)", "%.2f Deg.\n", orientation.getRoll(AngleUnit.DEGREES)); + telemetry.addData("Yaw (Z) velocity", "%.2f Deg/Sec", angularVelocity.zRotationRate); + telemetry.addData("Pitch (X) velocity", "%.2f Deg/Sec", angularVelocity.xRotationRate); + telemetry.addData("Roll (Y) velocity", "%.2f Deg/Sec", angularVelocity.yRotationRate); + telemetry.update(); + } + } +} diff --git a/FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/SensorKLNavxMicro.java b/FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/SensorKLNavxMicro.java new file mode 100644 index 0000000..ccc945f --- /dev/null +++ b/FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/SensorKLNavxMicro.java @@ -0,0 +1,128 @@ +/* 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.kauailabs.NavxMicroNavigationSensor; +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.Gyroscope; +import com.qualcomm.robotcore.hardware.IntegratingGyroscope; +import com.qualcomm.robotcore.util.ElapsedTime; +import org.firstinspires.ftc.robotcore.external.navigation.AngleUnit; +import org.firstinspires.ftc.robotcore.external.navigation.AngularVelocity; +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 OpMode shows how to use Kauai Labs navX Micro Robotics Navigation + * Sensor. It assumes that the sensor is configured with a name of "navx". + * + * 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: KL navX Micro", group = "Sensor") +@Disabled +public class SensorKLNavxMicro extends LinearOpMode { + + /** In this sample, for illustration purposes we use two interfaces on the one gyro object. + * That's likely atypical: you'll probably use one or the other in any given situation, + * depending on what you're trying to do. {@link IntegratingGyroscope} (and it's base interface, + * {@link Gyroscope}) are common interfaces supported by possibly several different gyro + * implementations. {@link NavxMicroNavigationSensor}, by contrast, provides functionality that + * is unique to the navX Micro sensor. + */ + IntegratingGyroscope gyro; + NavxMicroNavigationSensor navxMicro; + + // A timer helps provide feedback while calibration is taking place + ElapsedTime timer = new ElapsedTime(); + + @Override public void runOpMode() throws InterruptedException { + // Get a reference to a Modern Robotics GyroSensor object. We use several interfaces + // on this object to illustrate which interfaces support which functionality. + navxMicro = hardwareMap.get(NavxMicroNavigationSensor.class, "navx"); + gyro = (IntegratingGyroscope)navxMicro; + // If you're only interested int the IntegratingGyroscope interface, the following will suffice. + // gyro = hardwareMap.get(IntegratingGyroscope.class, "navx"); + + // The gyro automatically starts calibrating. This takes a few seconds. + telemetry.log().add("Gyro Calibrating. Do Not Move!"); + + // Wait until the gyro calibration is complete + timer.reset(); + while (navxMicro.isCalibrating()) { + telemetry.addData("calibrating", "%s", Math.round(timer.seconds())%2==0 ? "|.." : "..|"); + telemetry.update(); + Thread.sleep(50); + } + telemetry.log().clear(); telemetry.log().add("Gyro Calibrated. Press Start."); + telemetry.clear(); telemetry.update(); + + // Wait for the start button to be pressed + waitForStart(); + telemetry.log().clear(); + + while (opModeIsActive()) { + + // Read dimensionalized data from the gyro. This gyro can report angular velocities + // about all three axes. Additionally, it internally integrates the Z axis to + // be able to report an absolute angular Z orientation. + AngularVelocity rates = gyro.getAngularVelocity(AngleUnit.DEGREES); + Orientation angles = gyro.getAngularOrientation(AxesReference.INTRINSIC, AxesOrder.ZYX, AngleUnit.DEGREES); + + telemetry.addLine() + .addData("dx", formatRate(rates.xRotationRate)) + .addData("dy", formatRate(rates.yRotationRate)) + .addData("dz", "%s deg/s", formatRate(rates.zRotationRate)); + + telemetry.addLine() + .addData("heading", formatAngle(angles.angleUnit, angles.firstAngle)) + .addData("roll", formatAngle(angles.angleUnit, angles.secondAngle)) + .addData("pitch", "%s deg", formatAngle(angles.angleUnit, angles.thirdAngle)); + telemetry.update(); + + idle(); // Always call idle() at the bottom of your while(opModeIsActive()) loop + } + } + + String formatRate(float rate) { + return String.format("%.3f", rate); + } + + String formatAngle(AngleUnit angleUnit, double angle) { + return formatDegrees(AngleUnit.DEGREES.fromUnit(angleUnit, angle)); + } + + String formatDegrees(double degrees){ + return String.format("%.1f", AngleUnit.DEGREES.normalize(degrees)); + } +} diff --git a/FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/SensorLimelight3A.java b/FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/SensorLimelight3A.java new file mode 100644 index 0000000..2579cd0 --- /dev/null +++ b/FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/SensorLimelight3A.java @@ -0,0 +1,157 @@ +/* +Copyright (c) 2024 Limelight Vision + +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.limelightvision.LLResult; +import com.qualcomm.hardware.limelightvision.LLResultTypes; +import com.qualcomm.hardware.limelightvision.LLStatus; +import com.qualcomm.hardware.limelightvision.Limelight3A; +import com.qualcomm.robotcore.eventloop.opmode.Disabled; +import com.qualcomm.robotcore.eventloop.opmode.LinearOpMode; +import com.qualcomm.robotcore.eventloop.opmode.TeleOp; + +import org.firstinspires.ftc.robotcore.external.navigation.Pose3D; + +import java.util.List; + +/* + * This OpMode illustrates how to use the Limelight3A Vision Sensor. + * + * @see Limelight + * + * Notes on configuration: + * + * The device presents itself, when plugged into a USB port on a Control Hub as an ethernet + * interface. A DHCP server running on the Limelight automatically assigns the Control Hub an + * ip address for the new ethernet interface. + * + * Since the Limelight is plugged into a USB port, it will be listed on the top level configuration + * activity along with the Control Hub Portal and other USB devices such as webcams. Typically + * serial numbers are displayed below the device's names. In the case of the Limelight device, the + * Control Hub's assigned ip address for that ethernet interface is used as the "serial number". + * + * Tapping the Limelight's name, transitions to a new screen where the user can rename the Limelight + * and specify the Limelight's ip address. Users should take care not to confuse the ip address of + * the Limelight itself, which can be configured through the Limelight settings page via a web browser, + * and the ip address the Limelight device assigned the Control Hub and which is displayed in small text + * below the name of the Limelight on the top level configuration screen. + */ +@TeleOp(name = "Sensor: Limelight3A", group = "Sensor") +@Disabled +public class SensorLimelight3A extends LinearOpMode { + + private Limelight3A limelight; + + @Override + public void runOpMode() throws InterruptedException + { + limelight = hardwareMap.get(Limelight3A.class, "limelight"); + + telemetry.setMsTransmissionInterval(11); + + limelight.pipelineSwitch(0); + + /* + * Starts polling for data. If you neglect to call start(), getLatestResult() will return null. + */ + limelight.start(); + + telemetry.addData(">", "Robot Ready. Press Play."); + telemetry.update(); + waitForStart(); + + while (opModeIsActive()) { + LLStatus status = limelight.getStatus(); + telemetry.addData("Name", "%s", + status.getName()); + telemetry.addData("LL", "Temp: %.1fC, CPU: %.1f%%, FPS: %d", + status.getTemp(), status.getCpu(),(int)status.getFps()); + telemetry.addData("Pipeline", "Index: %d, Type: %s", + status.getPipelineIndex(), status.getPipelineType()); + + LLResult result = limelight.getLatestResult(); + if (result.isValid()) { + // Access general information + Pose3D botpose = result.getBotpose(); + double captureLatency = result.getCaptureLatency(); + double targetingLatency = result.getTargetingLatency(); + double parseLatency = result.getParseLatency(); + telemetry.addData("LL Latency", captureLatency + targetingLatency); + telemetry.addData("Parse Latency", parseLatency); + telemetry.addData("PythonOutput", java.util.Arrays.toString(result.getPythonOutput())); + + telemetry.addData("tx", result.getTx()); + telemetry.addData("txnc", result.getTxNC()); + telemetry.addData("ty", result.getTy()); + telemetry.addData("tync", result.getTyNC()); + + telemetry.addData("Botpose", botpose.toString()); + + // Access barcode results + List barcodeResults = result.getBarcodeResults(); + for (LLResultTypes.BarcodeResult br : barcodeResults) { + telemetry.addData("Barcode", "Data: %s", br.getData()); + } + + // Access classifier results + List classifierResults = result.getClassifierResults(); + for (LLResultTypes.ClassifierResult cr : classifierResults) { + telemetry.addData("Classifier", "Class: %s, Confidence: %.2f", cr.getClassName(), cr.getConfidence()); + } + + // Access detector results + List detectorResults = result.getDetectorResults(); + for (LLResultTypes.DetectorResult dr : detectorResults) { + telemetry.addData("Detector", "Class: %s, Area: %.2f", dr.getClassName(), dr.getTargetArea()); + } + + // Access fiducial results + List fiducialResults = result.getFiducialResults(); + for (LLResultTypes.FiducialResult fr : fiducialResults) { + telemetry.addData("Fiducial", "ID: %d, Family: %s, X: %.2f, Y: %.2f", fr.getFiducialId(), fr.getFamily(), fr.getTargetXDegrees(), fr.getTargetYDegrees()); + } + + // Access color results + List colorResults = result.getColorResults(); + for (LLResultTypes.ColorResult cr : colorResults) { + telemetry.addData("Color", "X: %.2f, Y: %.2f", cr.getTargetXDegrees(), cr.getTargetYDegrees()); + } + } else { + telemetry.addData("Limelight", "No data available"); + } + + telemetry.update(); + } + limelight.stop(); + } +} 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 new file mode 100644 index 0000000..32b37e0 --- /dev/null +++ b/FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/SensorMRColor.java @@ -0,0 +1,138 @@ +/* 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; + +/* + * + * This OpMode that shows how to use + * a Modern Robotics Color Sensor. + * + * The OpMode assumes that the color sensor + * is configured with a name of "sensor_color". + * + * 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: MR Color", group = "Sensor") +@Disabled +public class SensorMRColor extends LinearOpMode { + + ColorSensor colorSensor; // Hardware Device Object + + + @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 ColorSensor object. + colorSensor = hardwareMap.get(ColorSensor.class, "sensor_color"); + + // Set the LED in the beginning + colorSensor.enableLed(bLedOn); + + // wait for the start button to be pressed. + waitForStart(); + + // while the OpMode is active, 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 either gamepad. + bCurrState = gamepad1.x; + + // check for button state transitions. + if (bCurrState && (bCurrState != bPrevState)) { + + // button is transitioning to a pressed state. So Toggle LED + bLedOn = !bLedOn; + colorSensor.enableLed(bLedOn); + } + + // update previous state variable. + bPrevState = bCurrState; + + // convert the RGB values to HSV values. + Color.RGBToHSV(colorSensor.red() * 8, colorSensor.green() * 8, colorSensor.blue() * 8, hsvValues); + + // send the info back to driver station using telemetry function. + telemetry.addData("LED", bLedOn ? "On" : "Off"); + telemetry.addData("Clear", colorSensor.alpha()); + telemetry.addData("Red ", colorSensor.red()); + telemetry.addData("Green", colorSensor.green()); + telemetry.addData("Blue ", colorSensor.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/SensorMRGyro.java b/FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/SensorMRGyro.java new file mode 100644 index 0000000..91c0062 --- /dev/null +++ b/FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/SensorMRGyro.java @@ -0,0 +1,160 @@ +/* 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.Disabled; +import com.qualcomm.robotcore.eventloop.opmode.LinearOpMode; +import com.qualcomm.robotcore.eventloop.opmode.TeleOp; +import com.qualcomm.robotcore.hardware.IntegratingGyroscope; +import com.qualcomm.robotcore.util.ElapsedTime; +import org.firstinspires.ftc.robotcore.external.navigation.AngleUnit; +import org.firstinspires.ftc.robotcore.external.navigation.AngularVelocity; +import org.firstinspires.ftc.robotcore.external.navigation.AxesOrder; +import org.firstinspires.ftc.robotcore.external.navigation.AxesReference; + +/* + * This OpMode shows how to use the Modern Robotics Gyro. + * + * The OpMode assumes that the gyro sensor is attached to a Device Interface Module + * 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 +*/ +@TeleOp(name = "Sensor: MR Gyro", group = "Sensor") +@Disabled +public class SensorMRGyro extends LinearOpMode { + + /* In this sample, for illustration purposes we use two interfaces on the one gyro object. + * That's likely atypical: you'll probably use one or the other in any given situation, + * depending on what you're trying to do. {@link IntegratingGyroscope} (and it's base interface, + * {@link Gyroscope}) are common interfaces supported by possibly several different gyro + * implementations. {@link ModernRoboticsI2cGyro}, by contrast, provides functionality that + * is unique to the Modern Robotics gyro sensor. + */ + IntegratingGyroscope gyro; + ModernRoboticsI2cGyro modernRoboticsI2cGyro; + + // A timer helps provide feedback while calibration is taking place + ElapsedTime timer = new ElapsedTime(); + + @Override + public void runOpMode() { + + boolean lastResetState = false; + boolean curResetState = false; + + // Get a reference to a Modern Robotics gyro object. We use several interfaces + // on this object to illustrate which interfaces support which functionality. + modernRoboticsI2cGyro = hardwareMap.get(ModernRoboticsI2cGyro.class, "gyro"); + gyro = (IntegratingGyroscope)modernRoboticsI2cGyro; + // If you're only interested int the IntegratingGyroscope interface, the following will suffice. + // gyro = hardwareMap.get(IntegratingGyroscope.class, "gyro"); + // 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. + telemetry.log().add("Gyro Calibrating. Do Not Move!"); + modernRoboticsI2cGyro.calibrate(); + + // Wait until the gyro calibration is complete + timer.reset(); + while (!isStopRequested() && modernRoboticsI2cGyro.isCalibrating()) { + telemetry.addData("calibrating", "%s", Math.round(timer.seconds())%2==0 ? "|.." : "..|"); + telemetry.update(); + sleep(50); + } + + telemetry.log().clear(); telemetry.log().add("Gyro Calibrated. Press Start."); + telemetry.clear(); telemetry.update(); + + // Wait for the start button to be pressed + waitForStart(); + telemetry.log().clear(); + telemetry.log().add("Press A & B to reset heading"); + + // Loop until we're asked to stop + while (opModeIsActive()) { + + // If the A and B buttons are pressed just now, reset Z heading. + curResetState = (gamepad1.a && gamepad1.b); + if (curResetState && !lastResetState) { + modernRoboticsI2cGyro.resetZAxisIntegrator(); + } + lastResetState = curResetState; + + // The raw() methods report the angular rate of change about each of the + // three axes directly as reported by the underlying sensor IC. + int rawX = modernRoboticsI2cGyro.rawX(); + int rawY = modernRoboticsI2cGyro.rawY(); + int rawZ = modernRoboticsI2cGyro.rawZ(); + int heading = modernRoboticsI2cGyro.getHeading(); + int integratedZ = modernRoboticsI2cGyro.getIntegratedZValue(); + + // Read dimensionalized data from the gyro. This gyro can report angular velocities + // about all three axes. Additionally, it internally integrates the Z axis to + // be able to report an absolute angular Z orientation. + AngularVelocity rates = gyro.getAngularVelocity(AngleUnit.DEGREES); + float zAngle = gyro.getAngularOrientation(AxesReference.INTRINSIC, AxesOrder.ZYX, AngleUnit.DEGREES).firstAngle; + + // Read administrative information from the gyro + int zAxisOffset = modernRoboticsI2cGyro.getZAxisOffset(); + int zAxisScalingCoefficient = modernRoboticsI2cGyro.getZAxisScalingCoefficient(); + + telemetry.addLine() + .addData("dx", formatRate(rates.xRotationRate)) + .addData("dy", formatRate(rates.yRotationRate)) + .addData("dz", "%s deg/s", formatRate(rates.zRotationRate)); + telemetry.addData("angle", "%s deg", formatFloat(zAngle)); + telemetry.addData("heading", "%3d deg", heading); + telemetry.addData("integrated Z", "%3d", integratedZ); + telemetry.addLine() + .addData("rawX", formatRaw(rawX)) + .addData("rawY", formatRaw(rawY)) + .addData("rawZ", formatRaw(rawZ)); + telemetry.addLine().addData("z offset", zAxisOffset).addData("z coeff", zAxisScalingCoefficient); + telemetry.update(); + } + } + + String formatRaw(int rawValue) { + return String.format("%d", rawValue); + } + + String formatRate(float rate) { + return String.format("%.3f", rate); + } + + String formatFloat(float rate) { + return String.format("%.3f", rate); + } + +} diff --git a/FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/SensorMROpticalDistance.java b/FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/SensorMROpticalDistance.java new file mode 100644 index 0000000..6d2dfa3 --- /dev/null +++ b/FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/SensorMROpticalDistance.java @@ -0,0 +1,70 @@ +/* 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.OpticalDistanceSensor; + +/* + * This OpMode shows how to use a Modern Robotics Optical Distance Sensor + * It assumes that the ODS sensor is configured with a name of "sensor_ods". + * + * 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: MR ODS", group = "Sensor") +@Disabled +public class SensorMROpticalDistance extends LinearOpMode { + + OpticalDistanceSensor odsSensor; // Hardware Device Object + + @Override + public void runOpMode() { + + // get a reference to our Light Sensor object. + odsSensor = hardwareMap.get(OpticalDistanceSensor.class, "sensor_ods"); + + // wait for the start button to be pressed. + waitForStart(); + + // while the OpMode is active, loop and read the light levels. + // Note we use opModeIsActive() as our loop condition because it is an interruptible method. + while (opModeIsActive()) { + + // send the info back to driver station using telemetry function. + telemetry.addData("Raw", odsSensor.getRawLightDetected()); + telemetry.addData("Normal", odsSensor.getLightDetected()); + + telemetry.update(); + } + } +} diff --git a/FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/SensorMRRangeSensor.java b/FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/SensorMRRangeSensor.java new file mode 100644 index 0000000..2039ef9 --- /dev/null +++ b/FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/SensorMRRangeSensor.java @@ -0,0 +1,70 @@ +/* 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.ModernRoboticsI2cRangeSensor; +import com.qualcomm.robotcore.eventloop.opmode.Disabled; +import com.qualcomm.robotcore.eventloop.opmode.LinearOpMode; +import com.qualcomm.robotcore.eventloop.opmode.TeleOp; +import org.firstinspires.ftc.robotcore.external.navigation.DistanceUnit; + +/* + * This OpMode illustrates how to use the Modern Robotics Range Sensor. + * + * The OpMode assumes that the range sensor is configured with a name of "sensor_range". + * + * 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 + * + * @see MR Range Sensor + */ +@TeleOp(name = "Sensor: MR range sensor", group = "Sensor") +@Disabled // comment out or remove this line to enable this OpMode +public class SensorMRRangeSensor extends LinearOpMode { + + ModernRoboticsI2cRangeSensor rangeSensor; + + @Override public void runOpMode() { + + // get a reference to our compass + rangeSensor = hardwareMap.get(ModernRoboticsI2cRangeSensor.class, "sensor_range"); + + // wait for the start button to be pressed + waitForStart(); + + while (opModeIsActive()) { + telemetry.addData("raw ultrasonic", rangeSensor.rawUltrasonic()); + telemetry.addData("raw optical", rangeSensor.rawOptical()); + telemetry.addData("cm optical", "%.2f cm", rangeSensor.cmOptical()); + telemetry.addData("cm", "%.2f cm", rangeSensor.getDistance(DistanceUnit.CM)); + telemetry.update(); + } + } +} diff --git a/FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/SensorOctoQuad.java b/FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/SensorOctoQuad.java new file mode 100644 index 0000000..312d7f5 --- /dev/null +++ b/FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/SensorOctoQuad.java @@ -0,0 +1,156 @@ +/* + * Copyright (c) 2025 DigitalChickenLabs + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package org.firstinspires.ftc.robotcontroller.external.samples; + +import com.qualcomm.hardware.digitalchickenlabs.OctoQuad; +import com.qualcomm.robotcore.eventloop.opmode.LinearOpMode; +import com.qualcomm.robotcore.eventloop.opmode.TeleOp; +import com.qualcomm.robotcore.eventloop.opmode.Disabled; +import com.qualcomm.robotcore.util.ElapsedTime; + +import org.firstinspires.ftc.robotcore.external.Telemetry; + +/* + * This OpMode illustrates how to use DigitalChickenLabs OctoQuad Quad Encoder & Pulse Width I/F Module + * + * The OctoQuad has 8 input channels that can used to read either Relative Quadrature Encoders or + * Pulse-Width Absolute Encoder inputs. Relative Quadrature encoders are found on most FTC motors, + * and some stand-alone position sensors like the REV Thru-Bore encoder. Pulse-Width encoders are + * less common. The REV Thru-Bore encoder can provide its absolute position via a variable pulse width, + * as can several sonar rangefinders such as the MaxBotix MB1000 series. + * + * Note: SDK 11.0+ requires that the OctoQuad is running firmware V3.0 or greater. + * Visit https://github.com/DigitalChickenLabs/OctoQuad/tree/master/firmware for instruction + * on how to upgrade your OctoQuad's firmware. + * + * This basic sample shows how an OctoQuad can be used to read the position of three Odometry pods + * fitted with REV Thru-Bore encoders. For a more advanced example with additional OctoQuad + * capabilities, see the SensorOctoQuadAdv sample. + * + * This OpMode assumes the OctoQuad is attached to an I2C interface named "octoquad" in the robot config. + * + * The code assumes the first three OctoQuad inputs are connected as follows + * - Chan 0: for measuring forward motion on the left side of the robot. + * - Chan 1: for measuring forward motion on the right side of the robot. + * - Chan 2: for measuring Lateral (strafing) motion. + * + * The encoder values may be reset to zero by pressing the X (left most) button on Gamepad 1. + * + * This sample does not show how to interpret these readings, just how to obtain and display them. + * + * 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 + * + * See the sensor's product page: https://www.tindie.com/products/35114/ + */ +@Disabled +@TeleOp(name = "OctoQuad Basic", group="OctoQuad") +public class SensorOctoQuad extends LinearOpMode { + + // Identify which encoder OctoQuad inputs are connected to each odometry pod. + private final int ODO_LEFT = 0; // Facing forward direction on left side of robot + private final int ODO_RIGHT = 1; // Facing forward direction on right side or robot + private final int ODO_PERP = 2; // Facing perpendicular direction at the center of the robot + + // Declare the OctoQuad object; + private OctoQuad octoquad; + + private int posLeft; + private int posRight; + private int posPerp; + private int velLeft; + private int velRight; + private int velPerp; + + /** + * This function is executed when this OpMode is selected from the Driver Station. + */ + @Override + public void runOpMode() { + + // Connect to OctoQuad by referring to its name in the Robot Configuration. + octoquad = hardwareMap.get(OctoQuad.class, "octoquad"); + + // Read the Firmware Revision number from the OctoQuad and display it as telemetry. + telemetry.addData("OctoQuad Firmware Version ", octoquad.getFirmwareVersion()); + + // Reverse the count-direction of any encoder that is not what you require. + // e.g. if you push the robot forward and the left encoder counts down, then reverse it. + octoquad.setSingleEncoderDirection(ODO_LEFT, OctoQuad.EncoderDirection.REVERSE); + octoquad.setSingleEncoderDirection(ODO_RIGHT, OctoQuad.EncoderDirection.FORWARD); + octoquad.setSingleEncoderDirection(ODO_PERP, OctoQuad.EncoderDirection.FORWARD); + + // set the interval over which pulses are counted to determine velocity. + octoquad.setAllVelocitySampleIntervals(50); // 50 mSec means 20 velocity updates per second. + + // Save any changes that are made, just in case there is a sensor power glitch. + octoquad.saveParametersToFlash(); + + telemetry.addLine("\nPress START to read encoder values"); + telemetry.update(); + + waitForStart(); + + // Configure the telemetry for optimal display of data. + telemetry.setDisplayFormat(Telemetry.DisplayFormat.MONOSPACE); + telemetry.setMsTransmissionInterval(100); + + // Set all the encoder inputs to zero. + octoquad.resetAllPositions(); + + // Loop while displaying the odometry pod positions. + while (opModeIsActive()) { + telemetry.addData(">", "Press X to Reset Encoders\n"); + + // Check for X button to reset encoders. + if (gamepad1.x) { + // Reset the position of all encoders to zero. + octoquad.resetAllPositions(); + } + + // Read all the encoder data. Load into local members. + readOdometryPods(); + + // Display the values. + telemetry.addData("Left P", "%7d V :%6d CPS ", posLeft, velLeft); + telemetry.addData("Right P", "%7d V :%6d CPS ", posRight, velRight); + telemetry.addData("Perp P", "%7d V :%6d CPS ", posPerp, velPerp); + telemetry.update(); + } + } + + private void readOdometryPods() { + // For best performance, we should only perform ONE transaction with the OctoQuad each cycle. + // This can be achieved in one of two ways: + // 1) by doing a block data read once per control cycle + // or + // 2) by doing individual caching reads, but only reading each encoder value ONCE per cycle. + // + // Since method 2 is simplest, we will use it here. + posLeft = octoquad.readSinglePosition_Caching(ODO_LEFT); + posRight = octoquad.readSinglePosition_Caching(ODO_RIGHT); + posPerp = octoquad.readSinglePosition_Caching(ODO_PERP); + velLeft = octoquad.readSingleVelocity_Caching(ODO_LEFT) * 20; // scale up to cps + velRight = octoquad.readSingleVelocity_Caching(ODO_RIGHT) * 20; // scale up to cps + velPerp = octoquad.readSingleVelocity_Caching(ODO_PERP) * 20; // scale up to cps + } +} diff --git a/FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/SensorOctoQuadAdv.java b/FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/SensorOctoQuadAdv.java new file mode 100644 index 0000000..0e412ef --- /dev/null +++ b/FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/SensorOctoQuadAdv.java @@ -0,0 +1,288 @@ +/* + * Copyright (c) 2024 DigitalChickenLabs + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package org.firstinspires.ftc.robotcontroller.external.samples; + +import com.qualcomm.hardware.digitalchickenlabs.OctoQuad; +import com.qualcomm.robotcore.eventloop.opmode.LinearOpMode; +import com.qualcomm.robotcore.eventloop.opmode.TeleOp; +import com.qualcomm.robotcore.util.ElapsedTime; +import com.qualcomm.robotcore.util.MovingStatistics; +import com.qualcomm.robotcore.eventloop.opmode.Disabled; + + +import org.firstinspires.ftc.robotcore.external.Telemetry; +import org.firstinspires.ftc.robotcore.external.navigation.AngleUnit; + +import java.util.ArrayList; +import java.util.List; + +/* + * This OpMode illustrates how to use advanced features of the DigitalChickenLabs OctoQuad Quadrature + * Encoder & Pulse Width Interface Module. The OctoQuad has 8 input channels that can used to read + * either Quadrature Encoder signals (like with most FTC motors) or Pulse-Width style Absolute Encoder + * inputs (eg: REV Through Bore encoder pulse width output). + * + * This OpMode illustrates several of the more advanced features of an OctoQuad, including Pulse Width + * measurement and velocity measurement. For a more basic OpMode just showing how to read encoder + * inputs, see the SensorOctoQuad sample. + * + * This OpMode assumes the OctoQuad is attached to an I2C interface named "octoquad" in the robot config. + * + * One system that requires a lot of encoder inputs is a Swerve Drive system. + * Such a drive requires two encoders per drive module: + * One encoder for the Drive motor/wheel position.velocity, and one for Steer motor angle. + * The Drive motor requires a quadrature encoder, and the Steering motor requires an Absolute encoder. + * + * This sample will configure an OctoQuad for a swerve drive, as follows + * - Configure 4 Relative Quadrature Encoder inputs and 4 Absolute Pulse-Width Encoder inputs + * - Configure a velocity sample interval of 25 mSec + * - Configure a pulse width input range of 1-1024 uSec for a REV Encoder's Absolute Pulse output. + * + * An OctoSwerveDrive class will be created to initialize the OctoQuad, and manage 4 swerve modules. + * An OctoSwerveModule class will be created to configure and read a single swerve module. + * + * Wiring: + * The OctoQuad will be configured to accept Quadrature encoders on the first four channels and + * Absolute (pulse width) encoders on the last four channels. + * + * The standard encoder cable will connect each Driver Motor encoder to the OctoQuad. (channels 0-3) + * + * A modified version of the REV 6-4 pin cable (shipped with the encoder) connects the steering encoder + * to the OctoQuad. (channels 4-7) + * + * To connect the Absolute position signal from a REV Thru-Bore encoder to the OctoQuad, the + * Provided 6-pin to 4-pin cable will need to be modified. + * At the 6-pin connector end, move the yellow wire from its initial pin-3 position to the + * ABS pin-5 position. This can be done easily by using a small flathead screwdriver to lift the + * small white tab holding the metal wire crimp in place and gently pulling the wire out. + * See the OctoSwerveDrive() constructor below for the correct wheel/channel assignment. + * + * 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 + * + * See the sensor's product page: https://www.tindie.com/products/35114/ + */ +@Disabled +@TeleOp(name="OctoQuad Advanced", group="OctoQuad") +public class SensorOctoQuadAdv extends LinearOpMode { + + @Override + public void runOpMode() throws InterruptedException { + // Connect to the OctoQuad by looking up its name in the hardwareMap. + OctoQuad octoquad = hardwareMap.get(OctoQuad.class, "octoquad"); + + // Create the interface for the Swerve Drive Encoders + OctoSwerveDrive octoSwerveDrive = new OctoSwerveDrive(octoquad); + + // Display the OctoQuad firmware revision + telemetry.addLine("OctoQuad Firmware v" + octoquad.getFirmwareVersion()); + telemetry.addLine("\nPress START to read encoder values"); + telemetry.update(); + + waitForStart(); + + // Configure the telemetry for optimal display of data. + telemetry.setDisplayFormat(Telemetry.DisplayFormat.MONOSPACE); + telemetry.setMsTransmissionInterval(50); + + // Run stats to determine cycle times. + MovingStatistics avgTime = new MovingStatistics(100); + ElapsedTime elapsedTime = new ElapsedTime(); + + while (opModeIsActive()) { + telemetry.addData(">", "Press X to Reset Encoders\n"); + + if(gamepad1.x) { + octoquad.resetAllPositions(); + } + + // read all the swerve drive encoders and update positions and velocities. + octoSwerveDrive.updateModules(); + octoSwerveDrive.show(telemetry); + + // Update cycle time stats + avgTime.add(elapsedTime.nanoseconds()); + elapsedTime.reset(); + + telemetry.addData("Loop time", "%.1f mS", avgTime.getMean()/1000000); + telemetry.update(); + } + } +} + +// ============================ Internal (Inner) Classes ============================= + +/*** + * OctoSwerveDrive class manages 4 Swerve Modules + * - Performs general OctoQuad initialization + * - Creates 4 module classes, one for each swerve module + * - Updates swerve drive status by reading all data from OctoQuad and Updating each module + * - Displays all swerve drive data as telemetry + */ +class OctoSwerveDrive { + + private final OctoQuad octoquad; + private final List allModules = new ArrayList<>(); + + // members to hold encoder data for each module. + public final OctoSwerveModule LeftFront; + public final OctoSwerveModule RightFront; + public final OctoSwerveModule LeftBack; + public final OctoSwerveModule RightBack; + + // Prepare an object to hold an entire OctoQuad encoder readable register bank (pos and vel) + private final OctoQuad.EncoderDataBlock encoderDataBlock = new OctoQuad.EncoderDataBlock(); + + public OctoSwerveDrive(OctoQuad octoquad) { + this.octoquad = octoquad; + + // Clear out all prior settings and encoder data before setting up desired configuration + octoquad.resetEverything(); + + // Assume first 4 channels are relative encoders and the next 4 are absolute encoders + octoquad.setChannelBankConfig(OctoQuad.ChannelBankConfig.BANK1_QUADRATURE_BANK2_PULSE_WIDTH); + + // Create the four OctoSwerveModules, and add them to a 'list' for easier reference. + + // Notes: + // The wheel/channel order is set here. Ensure your encoder cables match. + // + // The angleOffset must be set for each module so a forward facing wheel shows a steer + // angle of 0 degrees. The process for determining the correct angleOffsets is as follows: + // 1) Set all the angleValues (below) to zero then build and deploy the code. + // 2) Rotate all the swerve drives so the wheels are forward in the desired 0 degree position + // 3) Run the OpMode, view the telemetry and record the "Degrees" value for each module. + // 4) Update the code by entering the recorded Degrees value for each module as the + // angleOffset (last) parameter in the lines below. + // + // Rebuild and deploy the new code. Verify that the telemetry now indicates 0 degrees when + // the wheels are facing forward. Also verify that the correct module values change + // appropriately when you manually spin (drive) and rotate (steer) a wheel. + + allModules.add(LeftFront = new OctoSwerveModule(octoquad, "LF ",0,0));// Drive=0, Steer=4 + allModules.add(RightFront = new OctoSwerveModule(octoquad, "RF ",1,0));// Drive=1, Steer=5 + allModules.add(LeftBack = new OctoSwerveModule(octoquad, "LB ",2,0));// Drive=2, Steer=6 + allModules.add(RightBack = new OctoSwerveModule(octoquad, "RB ",3,0));// Drive=3, Steer=7 + + // now make sure the settings persist through any power glitches. + octoquad.saveParametersToFlash(); + } + + /** + * Updates all 4 swerve modules + */ + public void updateModules() { + // Read full OctoQuad data block and then pass it to each swerve module to update themselves. + octoquad.readAllEncoderData(encoderDataBlock); + for (OctoSwerveModule module : allModules) { + module.updateModule(encoderDataBlock); + } + } + + /** + * Generate telemetry data for all modules. + * @param telemetry OpMode Telemetry object + */ + public void show(Telemetry telemetry) { + // create general header block and then have each module add its own telemetry + telemetry.addData("pos", " Count CPS Degree DPS"); + for (OctoSwerveModule module : allModules) { + module.show(telemetry); + } + } +} + +/*** + * The OctoSwerveModule class manages a single swerve module + */ +class OctoSwerveModule { + + public double driveCounts; + public double driveCountsPerSec; + public double steerDegrees; + public double steerDegreesPerSec; + + private final String name; + private final int channel; + private final double angleOffset; + + private static final int VELOCITY_SAMPLE_INTERVAL_MS = 25; // To provide 40 updates/Sec. + private static final double DEGREES_PER_US = (360.0 / 1024.0); // REV Through Bore Encoder + private static final double VELOCITY_SAMPLES_PER_S = (1000.0 / VELOCITY_SAMPLE_INTERVAL_MS); + + // The correct drive and turn directions must be set for the Swerve Module. + // Forward motion must generate an increasing drive count. + // Counter Clockwise steer rotation must generate an increasing Steer Angle (degrees) + private static final boolean INVERT_DRIVE_ENCODER = false; + private static final boolean INVERT_STEER_ENCODER = false; + + /*** + * @param octoquad provide access to configure OctoQuad + * @param name name used for telemetry display + * @param quadChannel Quadrature encoder channel. Pulse Width channel is this + 4 + * @param angleOffset Angle to subtract from absolute encoder to calibrate zero position. + */ + public OctoSwerveModule (OctoQuad octoquad, String name, int quadChannel, double angleOffset) { + this.name = name; + this.channel = quadChannel; + this.angleOffset = angleOffset; + + // Set both encoder directions. + octoquad.setSingleEncoderDirection(channel, + INVERT_DRIVE_ENCODER ? OctoQuad.EncoderDirection.REVERSE : OctoQuad.EncoderDirection.FORWARD); + octoquad.setSingleEncoderDirection(channel + 4, + INVERT_STEER_ENCODER ? OctoQuad.EncoderDirection.REVERSE : OctoQuad.EncoderDirection.FORWARD); + + // Set the velocity sample interval on both encoders + octoquad.setSingleVelocitySampleInterval(channel, VELOCITY_SAMPLE_INTERVAL_MS); + octoquad.setSingleVelocitySampleInterval(channel + 4, VELOCITY_SAMPLE_INTERVAL_MS); + + // Setup Absolute encoder pulse range to match REV Through Bore encoder. + octoquad.setSingleChannelPulseWidthParams (channel + 4, + new OctoQuad.ChannelPulseWidthParams(1,1024)); + } + + /*** + * Calculate the Swerve module's position and velocity values + * @param encoderDataBlock most recent full data block read from OctoQuad. + */ + public void updateModule(OctoQuad.EncoderDataBlock encoderDataBlock) { + driveCounts = encoderDataBlock.positions[channel]; + driveCountsPerSec = encoderDataBlock.velocities[channel] * VELOCITY_SAMPLES_PER_S; + + // convert uS to degrees. Add in any possible direction flip. + steerDegrees = AngleUnit.normalizeDegrees( + (encoderDataBlock.positions[channel+ 4] * DEGREES_PER_US) - angleOffset); + // convert uS/interval to deg per sec. Add in any possible direction flip. + steerDegreesPerSec = encoderDataBlock.velocities[channel + 4] * + DEGREES_PER_US * VELOCITY_SAMPLES_PER_S; + } + + /** + * Display the Swerve module's state as telemetry + * @param telemetry OpMode Telemetry object + */ + public void show(Telemetry telemetry) { + telemetry.addData(name, "%8.0f %7.0f %7.0f %6.0f", + driveCounts, driveCountsPerSec, steerDegrees, steerDegreesPerSec); + } +} diff --git a/FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/SensorOctoQuadLocalization.java b/FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/SensorOctoQuadLocalization.java new file mode 100644 index 0000000..486c468 --- /dev/null +++ b/FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/SensorOctoQuadLocalization.java @@ -0,0 +1,255 @@ +/* + * Copyright (c) 2025 DigitalChickenLabs + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package org.firstinspires.ftc.robotcontroller.external.samples; + +import com.qualcomm.hardware.digitalchickenlabs.OctoQuad; +import com.qualcomm.robotcore.eventloop.opmode.Disabled; +import com.qualcomm.robotcore.eventloop.opmode.LinearOpMode; +import com.qualcomm.robotcore.eventloop.opmode.TeleOp; + +import org.firstinspires.ftc.robotcore.external.Telemetry; + +/* + * This OpMode demonstrates how to use the "absolute localizer" feature of the + * Digital Chicken OctoQuad FTC Edition MK2. + * + * Note: The MK2 OctoQuad has an Inertial Measurement Unit (IMU) that the MK1 does not, so this + * code will ONLY work with the MK2 version. + * + * It is STRONGLY recommended to read the Quick Start guide for the localizer feature, located here: + * https://github.com/DigitalChickenLabs/OctoQuad/blob/master/documentation/ + * + * This OpMode assumes that the OctoQuad is attached to an I2C interface named "octoquad" in the + * robot configuration. + * + * 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 + * + * See the sensor's product page: https://www.tindie.com/products/37799/ + */ + +@Disabled +@TeleOp(name="OctoQuad Localizer", group="OctoQuad") +public class SensorOctoQuadLocalization extends LinearOpMode +{ + // ##################################################################################### + // + // YOU MUST ADJUST THESE CONSTANTS FOR YOUR ROBOT! + // SEE THE QUICKSTART GUIDE: + // https://github.com/DigitalChickenLabs/OctoQuad/blob/master/documentation/ + // + // AND THE TUNING OPMODES: + // https://github.com/DigitalChickenLabs/OctoQuad/blob/master/code_examples/FTC + // + // ##################################################################################### + static final int DEADWHEEL_PORT_X = 0; // encoder port on OctoQuad + static final int DEADWHEEL_PORT_Y = 1; // encoder port on OctoQuad + static final OctoQuad.EncoderDirection DEADWHEEL_X_DIR = OctoQuad.EncoderDirection.FORWARD; + static final OctoQuad.EncoderDirection DEADWHEEL_Y_DIR = OctoQuad.EncoderDirection.REVERSE; + static final float X_TICKS_PER_MM = 12.34f; // eg. 19.89f for a goBILDA 4-Bar Odometry Pod + static final float Y_TICKS_PER_MM = 12.34f; // eg. 19.89f for a goBILDA 4-Bar Odometry Pod + static final float TCP_OFFSET_X_MM = 123.4f; // eg. 147.0f from QuickStart Guide diagram + static final float TCP_OFFSET_Y_MM = 123.4f; // eg.-158.0f from QuickStart Guide diagram + static final float IMU_SCALAR = 1.0f; // Rotational scale factor. + // ##################################################################################### + + // Conversion factor for radians --> degrees + static final double RAD2DEG = 180/Math.PI; + + // For tracking the number of CRC mismatches + int badPackets = 0; + int totalPackets = 0; + boolean warn = false; + + // Data structure which will store the localizer data read from the OctoQuad + OctoQuad.LocalizerDataBlock localizer = new OctoQuad.LocalizerDataBlock(); + + /* + * Main OpMode function + */ + public void runOpMode() + { + // Begin by retrieving a handle to the device from the hardware map. + OctoQuad oq = hardwareMap.get(OctoQuad.class, "octoquad"); + + // Increase the telemetry update frequency to make the data display a bit less laggy + telemetry.setMsTransmissionInterval(50); + telemetry.setDisplayFormat(Telemetry.DisplayFormat.HTML); + + // Configure a range of parameters for the absolute localizer + // --> Read the quick start guide for an explanation of these!! + // IMPORTANT: these parameter changes will not take effect until the localizer is reset! + oq.setSingleEncoderDirection(DEADWHEEL_PORT_X, DEADWHEEL_X_DIR); + oq.setSingleEncoderDirection(DEADWHEEL_PORT_Y, DEADWHEEL_Y_DIR); + oq.setLocalizerPortX(DEADWHEEL_PORT_X); + oq.setLocalizerPortY(DEADWHEEL_PORT_Y); + oq.setLocalizerCountsPerMM_X(X_TICKS_PER_MM); + oq.setLocalizerCountsPerMM_Y(Y_TICKS_PER_MM); + oq.setLocalizerTcpOffsetMM_X(TCP_OFFSET_X_MM); + oq.setLocalizerTcpOffsetMM_Y(TCP_OFFSET_Y_MM); + oq.setLocalizerImuHeadingScalar(IMU_SCALAR); + oq.setLocalizerVelocityIntervalMS(25); + oq.setI2cRecoveryMode(OctoQuad.I2cRecoveryMode.MODE_1_PERIPH_RST_ON_FRAME_ERR); + + // Resetting the localizer will apply the parameters configured above. + // This function will NOT block until calibration of the IMU is complete - + // for that you need to look at the status returned by getLocalizerStatus() + oq.resetLocalizerAndCalibrateIMU(); + + /* + * Init Loop + */ + while (opModeInInit()) + { + telemetry.addData("OQ Firmware Version", oq.getFirmwareVersionString()); + telemetry.addData("Localizer Status", oq.getLocalizerStatus()); + telemetry.addData("Heading Axis Detection", oq.getLocalizerHeadingAxisChoice()); + telemetry.addLine(" "); + + warnIfNotTuned(); + + telemetry.addLine("Press Play to start navigating"); + telemetry.update(); + + sleep(100); + } + + /* + * Don't proceed to the main loop until calibration is complete + */ + while (!isStopRequested() && (oq.getLocalizerStatus() != OctoQuad.LocalizerStatus.RUNNING)) + { + telemetry.addLine("Waiting for IMU Calibration to complete\n"); + telemetry.addData("Localizer Status", oq.getLocalizerStatus()); + telemetry.update(); + sleep(100); + } + + // Establish a starting position for the robot. This will be 0,0,0 by default so this line + // is rather redundant, but you could change it to be anything you want + oq.setLocalizerPose(0, 0, 0); + + // Not required for localizer, but left in here since raw counts are displayed + // on telemetry for debugging + oq.resetAllPositions(); + + /* + * MAIN LOOP + */ + while (opModeIsActive()) + { + // Use the Gamepad A/Cross button to teleport to a new location and heading + if (gamepad1.crossWasPressed()) + { + oq.setLocalizerPose(200, 200, (float)(Math.PI/2.0f)); + } + + // Read updated data from the OctoQuad into the 'localizer' data structure + oq.readLocalizerData(localizer); + + // ################################################################################# + // IMPORTANT! Check whether the received CRC for the data is correct. An incorrect + // CRC indicates that the data was corrupted in transit and cannot be + // trusted. This can be caused by internal or external ESD. + // + // If the CRC is NOT reported as OK, you should throw away the data and + // try to read again. + // + // NOTE: Raw odometry pod counts are displayed to verify correct direction of rotation + // When the robot is pushed FWD, the X pod counts must increase in value + // When the robot is pushed LEFT, the Y pod counts must increase in value + // Use the setSingleEncoderDirection() method to make any reversals. + // ################################################################################# + if (localizer.crcOk) + { + warnIfNotTuned(); + + // Display Robot position data + telemetry.addData("Localizer Status", localizer.localizerStatus); + telemetry.addData("Heading deg", "%.2f", localizer.heading_rad * RAD2DEG); + telemetry.addData("Rotation dps", "%.2f", localizer.velHeading_radS * RAD2DEG); + telemetry.addData("X Pos mm", localizer.posX_mm); + telemetry.addData("Y Pos mm", localizer.posY_mm); + telemetry.addData("X Vel mm/s", localizer.velX_mmS); + telemetry.addData("Y Vel mm/s", localizer.velY_mmS); + telemetry.addLine("\nPress Gamepad A (Cross) to teleport"); + + // ############################################################################# + // IMPORTANT!! + // + // These two encoder status lines will slow the loop down, + // so they can be removed once the encoder direction has been verified. + // ############################################################################# + telemetry.addData("\nRaw X Pod Counts", oq.readSinglePosition_Caching(DEADWHEEL_PORT_X)); + telemetry.addData("Raw Y Pod Counts", oq.readSinglePosition_Caching(DEADWHEEL_PORT_Y)); + } + else + { + badPackets++; + telemetry.addLine("Data CRC not valid"); + } + totalPackets++; + + // Print some statistics about CRC validation + telemetry.addLine(String.format("%d CRC error(s) in %d packets", badPackets, totalPackets)); + + // Send updated telemetry to the Driver Station + telemetry.update(); + } + } + + long lastWarnFlashTimeMs = 0; + boolean warnFlash = false; + + void warnIfNotTuned() + { + String warnString = ""; + if (IMU_SCALAR == 1.0f) + { + warnString += "WARNING: IMU_SCALAR not tuned.
"; + warn = true; + } + if (X_TICKS_PER_MM == 12.34f || Y_TICKS_PER_MM == 12.34f) + { + warnString += "WARNING: TICKS_PER_MM not tuned.
"; + warn = true; + } + if (TCP_OFFSET_X_MM == 123.4f || TCP_OFFSET_Y_MM == 123.4f) + { + warnString += "WARNING: TCP_OFFSET not tuned.
"; + warn = true; + } + if (warn) + { + warnString +="
 - Read the code COMMENTS for tuning help.
"; + + if (System.currentTimeMillis() - lastWarnFlashTimeMs > 1000) + { + lastWarnFlashTimeMs = System.currentTimeMillis(); + warnFlash = !warnFlash; + } + + telemetry.addLine(String.format("%s", + warnFlash ? "red" : "white", warnString)); + } + } +} diff --git a/FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/SensorREV2mDistance.java b/FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/SensorREV2mDistance.java new file mode 100644 index 0000000..13883c3 --- /dev/null +++ b/FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/SensorREV2mDistance.java @@ -0,0 +1,87 @@ +/* +Copyright (c) 2018 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.rev.Rev2mDistanceSensor; +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.DistanceSensor; +import org.firstinspires.ftc.robotcore.external.navigation.DistanceUnit; + +/* + * This OpMode illustrates how to use the REV Robotics 2M Distance Sensor. + * + * The OpMode assumes that the sensor is configured with a name of "sensor_distance". + * + * 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 + * + * See the sensor's product page: https://www.revrobotics.com/rev-31-1505/ + */ +@TeleOp(name = "Sensor: REV2mDistance", group = "Sensor") +@Disabled +public class SensorREV2mDistance extends LinearOpMode { + + private DistanceSensor sensorDistance; + + @Override + public void runOpMode() { + // you can use this as a regular DistanceSensor. + sensorDistance = hardwareMap.get(DistanceSensor.class, "sensor_distance"); + + // you can also cast this to a Rev2mDistanceSensor if you want to use added + // methods associated with the Rev2mDistanceSensor class. + Rev2mDistanceSensor sensorTimeOfFlight = (Rev2mDistanceSensor) sensorDistance; + + telemetry.addData(">>", "Press start to continue"); + telemetry.update(); + + waitForStart(); + while(opModeIsActive()) { + // generic DistanceSensor methods. + telemetry.addData("deviceName", sensorDistance.getDeviceName() ); + telemetry.addData("range", String.format("%.01f mm", sensorDistance.getDistance(DistanceUnit.MM))); + telemetry.addData("range", String.format("%.01f cm", sensorDistance.getDistance(DistanceUnit.CM))); + telemetry.addData("range", String.format("%.01f m", sensorDistance.getDistance(DistanceUnit.METER))); + telemetry.addData("range", String.format("%.01f in", sensorDistance.getDistance(DistanceUnit.INCH))); + + // Rev2mDistanceSensor specific methods. + telemetry.addData("ID", String.format("%x", sensorTimeOfFlight.getModelID())); + telemetry.addData("did time out", Boolean.toString(sensorTimeOfFlight.didTimeoutOccur())); + + telemetry.update(); + } + } + +} diff --git a/FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/SensorSparkFunOTOS.java b/FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/SensorSparkFunOTOS.java new file mode 100644 index 0000000..3a25230 --- /dev/null +++ b/FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/SensorSparkFunOTOS.java @@ -0,0 +1,156 @@ +/* + SPDX-License-Identifier: MIT + + Copyright (c) 2024 SparkFun Electronics +*/ +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.hardware.sparkfun.SparkFunOTOS; + +import org.firstinspires.ftc.robotcore.external.navigation.AngleUnit; +import org.firstinspires.ftc.robotcore.external.navigation.DistanceUnit; + +/* + * This OpMode illustrates how to use the SparkFun Qwiic Optical Tracking Odometry Sensor (OTOS) + * + * The OpMode assumes that the sensor is configured with a name of "sensor_otos". + * + * 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 + * + * See the sensor's product page: https://www.sparkfun.com/products/24904 + */ +@TeleOp(name = "Sensor: SparkFun OTOS", group = "Sensor") +@Disabled +public class SensorSparkFunOTOS extends LinearOpMode { + // Create an instance of the sensor + SparkFunOTOS myOtos; + + @Override + public void runOpMode() throws InterruptedException { + // Get a reference to the sensor + myOtos = hardwareMap.get(SparkFunOTOS.class, "sensor_otos"); + + // All the configuration for the OTOS is done in this helper method, check it out! + configureOtos(); + + // Wait for the start button to be pressed + waitForStart(); + + // Loop until the OpMode ends + while (opModeIsActive()) { + // Get the latest position, which includes the x and y coordinates, plus the + // heading angle + SparkFunOTOS.Pose2D pos = myOtos.getPosition(); + + // Reset the tracking if the user requests it + if (gamepad1.y) { + myOtos.resetTracking(); + } + + // Re-calibrate the IMU if the user requests it + if (gamepad1.x) { + myOtos.calibrateImu(); + } + + // Inform user of available controls + telemetry.addLine("Press Y (triangle) on Gamepad to reset tracking"); + telemetry.addLine("Press X (square) on Gamepad to calibrate the IMU"); + telemetry.addLine(); + + // Log the position to the telemetry + telemetry.addData("X coordinate", pos.x); + telemetry.addData("Y coordinate", pos.y); + telemetry.addData("Heading angle", pos.h); + + // Update the telemetry on the driver station + telemetry.update(); + } + } + + private void configureOtos() { + telemetry.addLine("Configuring OTOS..."); + telemetry.update(); + + // Set the desired units for linear and angular measurements. Can be either + // meters or inches for linear, and radians or degrees for angular. If not + // set, the default is inches and degrees. Note that this setting is not + // persisted in the sensor, so you need to set at the start of all your + // OpModes if using the non-default value. + // myOtos.setLinearUnit(DistanceUnit.METER); + myOtos.setLinearUnit(DistanceUnit.INCH); + // myOtos.setAngularUnit(AnguleUnit.RADIANS); + myOtos.setAngularUnit(AngleUnit.DEGREES); + + // Assuming you've mounted your sensor to a robot and it's not centered, + // you can specify the offset for the sensor relative to the center of the + // robot. The units default to inches and degrees, but if you want to use + // different units, specify them before setting the offset! Note that as of + // firmware version 1.0, these values will be lost after a power cycle, so + // you will need to set them each time you power up the sensor. For example, if + // the sensor is mounted 5 inches to the left (negative X) and 10 inches + // forward (positive Y) of the center of the robot, and mounted 90 degrees + // clockwise (negative rotation) from the robot's orientation, the offset + // would be {-5, 10, -90}. These can be any value, even the angle can be + // tweaked slightly to compensate for imperfect mounting (eg. 1.3 degrees). + SparkFunOTOS.Pose2D offset = new SparkFunOTOS.Pose2D(0, 0, 0); + myOtos.setOffset(offset); + + // Here we can set the linear and angular scalars, which can compensate for + // scaling issues with the sensor measurements. Note that as of firmware + // version 1.0, these values will be lost after a power cycle, so you will + // need to set them each time you power up the sensor. They can be any value + // from 0.872 to 1.127 in increments of 0.001 (0.1%). It is recommended to + // first set both scalars to 1.0, then calibrate the angular scalar, then + // the linear scalar. To calibrate the angular scalar, spin the robot by + // multiple rotations (eg. 10) to get a precise error, then set the scalar + // to the inverse of the error. Remember that the angle wraps from -180 to + // 180 degrees, so for example, if after 10 rotations counterclockwise + // (positive rotation), the sensor reports -15 degrees, the required scalar + // would be 3600/3585 = 1.004. To calibrate the linear scalar, move the + // robot a known distance and measure the error; do this multiple times at + // multiple speeds to get an average, then set the linear scalar to the + // inverse of the error. For example, if you move the robot 100 inches and + // the sensor reports 103 inches, set the linear scalar to 100/103 = 0.971 + myOtos.setLinearScalar(1.0); + myOtos.setAngularScalar(1.0); + + // The IMU on the OTOS includes a gyroscope and accelerometer, which could + // have an offset. Note that as of firmware version 1.0, the calibration + // will be lost after a power cycle; the OTOS performs a quick calibration + // when it powers up, but it is recommended to perform a more thorough + // calibration at the start of all your OpModes. Note that the sensor must + // be completely stationary and flat during calibration! When calling + // calibrateImu(), you can specify the number of samples to take and whether + // to wait until the calibration is complete. If no parameters are provided, + // it will take 255 samples and wait until done; each sample takes about + // 2.4ms, so about 612ms total + myOtos.calibrateImu(); + + // Reset the tracking algorithm - this resets the position to the origin, + // but can also be used to recover from some rare tracking errors + myOtos.resetTracking(); + + // After resetting the tracking, the OTOS will report that the robot is at + // the origin. If your robot does not start at the origin, or you have + // another source of location information (eg. vision odometry), you can set + // the OTOS location to match and it will continue to track from there. + SparkFunOTOS.Pose2D currentPosition = new SparkFunOTOS.Pose2D(0, 0, 0); + myOtos.setPosition(currentPosition); + + // Get the hardware and firmware version + SparkFunOTOS.Version hwVersion = new SparkFunOTOS.Version(); + SparkFunOTOS.Version fwVersion = new SparkFunOTOS.Version(); + myOtos.getVersionInfo(hwVersion, fwVersion); + + telemetry.addLine("OTOS configured! Press start to get position data!"); + telemetry.addLine(); + telemetry.addLine(String.format("OTOS Hardware Version: v%d.%d", hwVersion.major, hwVersion.minor)); + telemetry.addLine(String.format("OTOS Firmware Version: v%d.%d", fwVersion.major, fwVersion.minor)); + telemetry.update(); + } +} diff --git a/FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/SensorTouch.java b/FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/SensorTouch.java new file mode 100644 index 0000000..3d79447 --- /dev/null +++ b/FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/SensorTouch.java @@ -0,0 +1,78 @@ +/* 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.TouchSensor; + +/* + * This OpMode demonstrates how to use a REV Robotics Touch Sensor, REV Robotics Magnetic Limit Switch, or other device + * that implements the TouchSensor interface. Any touch sensor that connects its output to ground when pressed + * (known as "active low") can be configured as a "REV Touch Sensor". This includes REV's Magnetic Limit Switch. + * + * The OpMode assumes that the touch sensor is configured with a name of "sensor_touch". + * + * A REV Robotics Touch Sensor must be configured on digital port number 1, 3, 5, or 7. + * A Magnetic Limit Switch can be configured on any digital port. + * + * 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: REV touch sensor", group = "Sensor") +@Disabled +public class SensorTouch extends LinearOpMode { + TouchSensor touchSensor; // Touch sensor Object + + @Override + public void runOpMode() { + + // get a reference to our touchSensor object. + touchSensor = hardwareMap.get(TouchSensor.class, "sensor_touch"); + + // wait for the start button to be pressed. + waitForStart(); + + // while the OpMode is active, loop and read whether the sensor is being pressed. + // Note we use opModeIsActive() as our loop condition because it is an interruptible method. + while (opModeIsActive()) { + + // send the info back to driver station using telemetry function. + if (touchSensor.isPressed()) { + telemetry.addData("Touch Sensor", "Is Pressed"); + } else { + telemetry.addData("Touch Sensor", "Is Not Pressed"); + } + + telemetry.update(); + } + } +} diff --git a/FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/UtilityCameraFrameCapture.java b/FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/UtilityCameraFrameCapture.java new file mode 100644 index 0000000..69420cc --- /dev/null +++ b/FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/UtilityCameraFrameCapture.java @@ -0,0 +1,127 @@ +/* + * Copyright (c) 2023 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.util.Size; +import com.qualcomm.robotcore.eventloop.opmode.Disabled; +import com.qualcomm.robotcore.eventloop.opmode.LinearOpMode; +import com.qualcomm.robotcore.eventloop.opmode.TeleOp; +import org.firstinspires.ftc.robotcore.external.hardware.camera.BuiltinCameraDirection; +import org.firstinspires.ftc.robotcore.external.hardware.camera.WebcamName; +import org.firstinspires.ftc.vision.VisionPortal; + +import java.util.Locale; + +/* + * This OpMode helps calibrate a webcam or RC phone camera, useful for AprilTag pose estimation + * with the FTC VisionPortal. It captures a camera frame (image) and stores it on the Robot Controller + * (Control Hub or RC phone), with each press of the gamepad button X (or Square). + * Full calibration instructions are here: + * + * https://ftc-docs.firstinspires.org/camera-calibration + * + * In Android Studio, copy this class 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. + * + * In OnBot Java, use "Add File" to add this OpMode from the list of Samples. + */ + +@TeleOp(name = "Utility: Camera Frame Capture", group = "Utility") +@Disabled +public class UtilityCameraFrameCapture extends LinearOpMode +{ + /* + * EDIT THESE PARAMETERS AS NEEDED + */ + final boolean USING_WEBCAM = false; + final BuiltinCameraDirection INTERNAL_CAM_DIR = BuiltinCameraDirection.BACK; + final int RESOLUTION_WIDTH = 640; + final int RESOLUTION_HEIGHT = 480; + + // Internal state + boolean lastX; + int frameCount; + long capReqTime; + + @Override + public void runOpMode() + { + VisionPortal portal; + + if (USING_WEBCAM) + { + portal = new VisionPortal.Builder() + .setCamera(hardwareMap.get(WebcamName.class, "Webcam 1")) + .setCameraResolution(new Size(RESOLUTION_WIDTH, RESOLUTION_HEIGHT)) + .build(); + } + else + { + portal = new VisionPortal.Builder() + .setCamera(INTERNAL_CAM_DIR) + .setCameraResolution(new Size(RESOLUTION_WIDTH, RESOLUTION_HEIGHT)) + .build(); + } + + while (!isStopRequested()) + { + boolean x = gamepad1.x; + + if (x && !lastX) + { + portal.saveNextFrameRaw(String.format(Locale.US, "CameraFrameCapture-%06d", frameCount++)); + capReqTime = System.currentTimeMillis(); + } + + lastX = x; + + telemetry.addLine("######## Camera Capture Utility ########"); + telemetry.addLine(String.format(Locale.US, " > Resolution: %dx%d", RESOLUTION_WIDTH, RESOLUTION_HEIGHT)); + telemetry.addLine(" > Press X (or Square) to capture a frame"); + telemetry.addData(" > Camera Status", portal.getCameraState()); + + if (capReqTime != 0) + { + telemetry.addLine("\nCaptured Frame!"); + } + + if (capReqTime != 0 && System.currentTimeMillis() - capReqTime > 1000) + { + capReqTime = 0; + } + + telemetry.update(); + } + } +} diff --git a/FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/UtilityOctoQuadConfigMenu.java b/FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/UtilityOctoQuadConfigMenu.java new file mode 100644 index 0000000..60be6c9 --- /dev/null +++ b/FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/UtilityOctoQuadConfigMenu.java @@ -0,0 +1,821 @@ +/* + * Copyright (c) 2024 DigitalChickenLabs + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package org.firstinspires.ftc.robotcontroller.external.samples; + +import com.qualcomm.hardware.digitalchickenlabs.OctoQuad; +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.Gamepad; + +import org.firstinspires.ftc.robotcore.external.Telemetry; + +import java.util.ArrayList; +import java.util.Stack; + +/* + * This OpMode illustrates how to use the DigitalChickenLabs OctoQuad Quadrature Encoder & Pulse Width Interface Module. + * + * The OctoQuad has 8 input channels that can used to read either Relative Quadrature Encoders or Pulse-Width Absolute Encoder inputs. + * Relative Quadrature encoders are found on most FTC motors, and some stand-alone position sensors like the REV Thru-Bore encoder. + * Pulse-Width encoders are less common. The REV Thru-Bore encoder can provide its absolute position via a variable pulse width, + * as can several sonar rangefinders such as the MaxBotix MB1000 series. + * + * This OpMode assumes that the OctoQuad is attached to an I2C interface named "octoquad" in the robot configuration. + * + * Remove or comment out the @Disabled line to add this OpMode to the Driver Station OpMode list + * + * Select, Init and run the "OctoQuad Configuration Tool" OpMode + * Read the blue User-Interface tips at the top of the telemetry screen. + * Use the UI buttons to navigate the menu and make any desired changes to the OctoQuad configuration. + * Use the Program Settings To FLASH option to make any changes permanent. + * + * See the sensor's product page: https://www.tindie.com/products/digitalchickenlabs/octoquad-8ch-quadrature-pulse-width-decoder/ + */ +@TeleOp(name = "OctoQuad Configuration Tool", group="OctoQuad") +@Disabled +public class UtilityOctoQuadConfigMenu extends LinearOpMode +{ + TelemetryMenu.MenuElement rootMenu = new TelemetryMenu.MenuElement("OctoQuad Config Menu", true); + TelemetryMenu.MenuElement menuHwInfo = new TelemetryMenu.MenuElement("Hardware Information", false); + TelemetryMenu.EnumOption optionI2cResetMode; + TelemetryMenu.EnumOption optionChannelBankConfig; + + TelemetryMenu.MenuElement menuEncoderDirections = new TelemetryMenu.MenuElement("Set Encoder Directions", false); + TelemetryMenu.BooleanOption[] optionsEncoderDirections = new TelemetryMenu.BooleanOption[OctoQuad.NUM_ENCODERS]; + + TelemetryMenu.MenuElement menuVelocityIntervals = new TelemetryMenu.MenuElement("Velocity Measurement Intervals", false); + TelemetryMenu.IntegerOption[] optionsVelocityIntervals = new TelemetryMenu.IntegerOption[OctoQuad.NUM_ENCODERS]; + + TelemetryMenu.MenuElement menuAbsParams = new TelemetryMenu.MenuElement("Abs. Encoder Pulse Width Params", false); + TelemetryMenu.IntegerOption[] optionsAbsParamsMax = new TelemetryMenu.IntegerOption[OctoQuad.NUM_ENCODERS]; + TelemetryMenu.IntegerOption[] optionsAbsParamsMin = new TelemetryMenu.IntegerOption[OctoQuad.NUM_ENCODERS]; + TelemetryMenu.BooleanOption[] optionsAbsParamsWrapTracking = new TelemetryMenu.BooleanOption[OctoQuad.NUM_ENCODERS]; + + TelemetryMenu.OptionElement optionProgramToFlash; + TelemetryMenu.OptionElement optionSendToRAM; + + TelemetryMenu.StaticClickableOption optionExit; + + OctoQuad octoquad; + + boolean error = false; + + @Override + public void runOpMode() + { + octoquad = hardwareMap.getAll(OctoQuad.class).get(0); + + if(octoquad.getChipId() != OctoQuad.OCTOQUAD_CHIP_ID) + { + telemetry.addLine("Error: cannot communicate with OctoQuad. Check your wiring and configuration and try again"); + telemetry.update(); + + error = true; + } + else + { + if(octoquad.getFirmwareVersion().maj != OctoQuad.SUPPORTED_FW_VERSION_MAJ) + { + telemetry.addLine("Error: The OctoQuad is running a different major firmware version than this driver was built for. Cannot run configuration tool"); + telemetry.update(); + + error = true; + } + } + + if(error) + { + waitForStart(); + return; + } + + telemetry.addLine("Retrieving current configuration from OctoQuad"); + telemetry.update(); + + optionExit = new TelemetryMenu.StaticClickableOption("Exit configuration menu") + { + @Override + void onClick() // called on OpMode thread + { + requestOpModeStop(); + } + }; + + optionI2cResetMode = new TelemetryMenu.EnumOption("I2C Reset Mode", OctoQuad.I2cRecoveryMode.values(), octoquad.getI2cRecoveryMode()); + optionChannelBankConfig = new TelemetryMenu.EnumOption("Channel Bank Modes", OctoQuad.ChannelBankConfig.values(), octoquad.getChannelBankConfig()); + + menuHwInfo.addChild(new TelemetryMenu.StaticItem("Board Firmware: v" + octoquad.getFirmwareVersion())); + //menuHwInfo.addChild(new TelemetryMenu.StaticItem("Board unique ID: FIXME")); + + for(int i = 0; i < OctoQuad.NUM_ENCODERS; i++) + { + optionsEncoderDirections[i] = new TelemetryMenu.BooleanOption( + String.format("Encoder %d direction", i), + octoquad.getSingleEncoderDirection(i) == OctoQuad.EncoderDirection.REVERSE, + "-", + "+"); + } + menuEncoderDirections.addChildren(optionsEncoderDirections); + + for(int i = 0; i < OctoQuad.NUM_ENCODERS; i++) + { + optionsVelocityIntervals[i] = new TelemetryMenu.IntegerOption( + String.format("Chan %d velocity intvl", i), + OctoQuad.MIN_VELOCITY_MEASUREMENT_INTERVAL_MS, + OctoQuad.MAX_VELOCITY_MEASUREMENT_INTERVAL_MS, + octoquad.getSingleVelocitySampleInterval(i)); + } + menuVelocityIntervals.addChildren(optionsVelocityIntervals); + + for(int i = 0; i < OctoQuad.NUM_ENCODERS; i++) + { + OctoQuad.ChannelPulseWidthParams params = octoquad.getSingleChannelPulseWidthParams(i); + + optionsAbsParamsMax[i] = new TelemetryMenu.IntegerOption( + String.format("Chan %d max pulse length", i), + OctoQuad.MIN_PULSE_WIDTH_US, + OctoQuad.MAX_PULSE_WIDTH_US, + params.max_length_us); + + optionsAbsParamsMin[i] = new TelemetryMenu.IntegerOption( + String.format("Chan %d min pulse length", i), + OctoQuad.MIN_PULSE_WIDTH_US, + OctoQuad.MAX_PULSE_WIDTH_US, + params.min_length_us); + + optionsAbsParamsWrapTracking[i] = new TelemetryMenu.BooleanOption( + String.format("Chan %d wrap tracking enabled", i), + octoquad.getSingleChannelPulseWidthTracksWrap(i), + "yes", + "no"); + } + menuAbsParams.addChildren(optionsAbsParamsMin); + menuAbsParams.addChildren(optionsAbsParamsMax); + menuAbsParams.addChildren(optionsAbsParamsWrapTracking); + + optionProgramToFlash = new TelemetryMenu.OptionElement() + { + String name = "Program Settings to FLASH"; + long lastClickTime = 0; + + @Override + protected String getDisplayText() + { + if(lastClickTime == 0) + { + return name; + } + else + { + if(System.currentTimeMillis() - lastClickTime < 1000) + { + return name + " **OK**"; + } + else + { + lastClickTime = 0; + return name; + } + } + } + + @Override + void onClick() + { + sendSettingsToRam(); + octoquad.saveParametersToFlash(); + lastClickTime = System.currentTimeMillis(); + } + }; + + optionSendToRAM = new TelemetryMenu.OptionElement() + { + String name = "Send Settings to RAM"; + long lastClickTime = 0; + + @Override + protected String getDisplayText() + { + if(lastClickTime == 0) + { + return name; + } + else + { + if(System.currentTimeMillis() - lastClickTime < 1000) + { + return name + " **OK**"; + } + else + { + lastClickTime = 0; + return name; + } + } + } + + @Override + void onClick() + { + sendSettingsToRam(); + lastClickTime = System.currentTimeMillis(); + } + }; + + rootMenu.addChild(menuHwInfo); + rootMenu.addChild(optionI2cResetMode); + rootMenu.addChild(optionChannelBankConfig); + rootMenu.addChild(menuEncoderDirections); + rootMenu.addChild(menuVelocityIntervals); + rootMenu.addChild(menuAbsParams); + rootMenu.addChild(optionProgramToFlash); + rootMenu.addChild(optionSendToRAM); + rootMenu.addChild(optionExit); + + TelemetryMenu menu = new TelemetryMenu(telemetry, rootMenu); + + while (!isStopRequested()) + { + menu.loop(gamepad1); + telemetry.update(); + sleep(20); + } + } + + void sendSettingsToRam() + { + for(int i = 0; i < OctoQuad.NUM_ENCODERS; i++) + { + octoquad.setSingleEncoderDirection(i, optionsEncoderDirections[i].getValue() ? OctoQuad.EncoderDirection.REVERSE : OctoQuad.EncoderDirection.FORWARD); + octoquad.setSingleVelocitySampleInterval(i, optionsVelocityIntervals[i].getValue()); + + OctoQuad.ChannelPulseWidthParams params = new OctoQuad.ChannelPulseWidthParams(); + params.max_length_us = optionsAbsParamsMax[i].getValue(); + params.min_length_us = optionsAbsParamsMin[i].getValue(); + + octoquad.setSingleChannelPulseWidthParams(i, params); + octoquad.setSingleChannelPulseWidthTracksWrap(i, optionsAbsParamsWrapTracking[i].val); + } + + octoquad.setI2cRecoveryMode((OctoQuad.I2cRecoveryMode) optionI2cResetMode.getValue()); + octoquad.setChannelBankConfig((OctoQuad.ChannelBankConfig) optionChannelBankConfig.getValue()); + } + + /* + * Copyright (c) 2023 OpenFTC Team + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + + public static class TelemetryMenu + { + private final MenuElement root; + private MenuElement currentLevel; + + private boolean dpadUpPrev; + private boolean dpadDnPrev; + private boolean dpadRightPrev; + private boolean dpadLeftPrev; + private boolean xPrev; + private boolean lbPrev; + + private int selectedIdx = 0; + private Stack selectedIdxStack = new Stack<>(); + + private final Telemetry telemetry; + + /** + * TelemetryMenu constructor + * @param telemetry pass in 'telemetry' from your OpMode + * @param root the root menu element + */ + public TelemetryMenu(Telemetry telemetry, MenuElement root) + { + this.root = root; + this.currentLevel = root; + this.telemetry = telemetry; + + telemetry.setDisplayFormat(Telemetry.DisplayFormat.HTML); + telemetry.setMsTransmissionInterval(50); + } + + /** + * Call this from inside your loop to put the current menu state into + * telemetry, and process gamepad inputs for navigating the menu + * @param gamepad the gamepad you want to use to navigate the menu + */ + public void loop(Gamepad gamepad) + { + // Capture current state of the gamepad buttons we care about; + // We can only look once or we risk a race condition + boolean dpadUp = gamepad.dpad_up; + boolean dpadDn = gamepad.dpad_down; + boolean dpadRight = gamepad.dpad_right; + boolean dpadLeft = gamepad.dpad_left; + boolean x = gamepad.x; + boolean lb = gamepad.left_bumper; + + // Figure out who our children our at this level + // and figure out which item is currently highlighted + // with the selection pointer + ArrayList children = currentLevel.children(); + Element currentSelection = children.get(selectedIdx); + + // Left and right are inputs to the selected item (if it's an Option) + if (currentSelection instanceof OptionElement) + { + if (dpadRight && !dpadRightPrev) // rising edge + { + ((OptionElement) currentSelection).onRightInput(); + } + else if (dpadLeft && !dpadLeftPrev) // rising edge + { + ((OptionElement) currentSelection).onLeftInput(); + } + } + + // Up and down navigate the current selection pointer + if (dpadUp && !dpadUpPrev) // rising edge + { + selectedIdx--; // Move selection pointer up + } + else if (dpadDn && !dpadDnPrev) // rising edge + { + selectedIdx++; // Move selection pointer down + } + + // Make selected index sane (don't let it go out of bounds) :eyes: + if (selectedIdx >= children.size()) + { + selectedIdx = children.size()-1; + } + else if (selectedIdx < 0) + { + selectedIdx = 0; + } + + // Select: either enter submenu or input to option + else if (x && !xPrev) // rising edge + { + // Select up element + if (currentSelection instanceof SpecialUpElement) + { + // We can only go up if we're not at the root level + if (currentLevel != root) + { + // Restore selection pointer to where it was before + selectedIdx = selectedIdxStack.pop(); + + // Change to the parent level + currentLevel = currentLevel.parent(); + } + } + // Input to option + else if (currentSelection instanceof OptionElement) + { + ((OptionElement) currentSelection).onClick(); + } + // Enter submenu + else if (currentSelection instanceof MenuElement) + { + // Save our current selection pointer so we can restore it + // later if the user navigates back up a level + selectedIdxStack.push(selectedIdx); + + // We have no idea what's in the submenu :monkey: so best to + // just set the selection pointer to the first element + selectedIdx = 0; + + // Now the current level becomes the submenu that the selection + // pointer was on + currentLevel = (MenuElement) currentSelection; + } + } + + // Go up a level + else if (lb && !lbPrev) + { + // We can only go up if we're not at the root level + if (currentLevel != root) + { + // Restore selection pointer to where it was before + selectedIdx = selectedIdxStack.pop(); + + // Change to the parent level + currentLevel = currentLevel.parent(); + } + } + + // Save the current button states so that we can look for + // the rising edge the next time around the loop :) + dpadUpPrev = dpadUp; + dpadDnPrev = dpadDn; + dpadRightPrev = dpadRight; + dpadLeftPrev = dpadLeft; + xPrev = x; + lbPrev = lb; + + // Start building the text display. + // First, we add the static directions for gamepad operation + StringBuilder builder = new StringBuilder(); + builder.append(""); + builder.append("Navigate items.....dpad up/down\n") + .append("Select.............X or Square\n") + .append("Edit option........dpad left/right\n") + .append("Up one level.......left bumper\n"); + builder.append(""); + builder.append("\n"); + + // Now actually add the menu options. We start by adding the name of the current menu level. + builder.append(""); + builder.append("Current Menu: ").append(currentLevel.name).append("\n"); + + // Now we loop through all the child elements of this level and add them + for (int i = 0; i < children.size(); i++) + { + // If the selection pointer is at this index, put a green dot in the box :) + if (selectedIdx == i) + { + builder.append("[] "); + } + // Otherwise, just put an empty box + else + { + builder.append("[ ] "); + } + + // Figure out who the selection pointer is pointing at :eyes: + Element e = children.get(i); + + // If it's pointing at a submenu, indicate that it's a submenu to the user + // by prefixing "> " to the name. + if (e instanceof MenuElement) + { + builder.append("> "); + } + + // Finally, add the element's name + builder.append(e.getDisplayText()); + + // We musn't forget the newline + builder.append("\n"); + } + + // Don't forget to close the font tag either + builder.append(""); + + // Build the string!!!! :nerd: + String menu = builder.toString(); + + // Add it to telemetry + telemetry.addLine(menu); + } + + public static class MenuElement extends Element + { + private String name; + private ArrayList children = new ArrayList<>(); + + /** + * Create a new MenuElement; may either be the root menu, or a submenu (set isRoot accordingly) + * @param name the name for this menu + * @param isRoot whether this is a root menu, or a submenu + */ + public MenuElement(String name, boolean isRoot) + { + this.name = name; + + // If it's not the root menu, we add the up one level option as the first element + if (!isRoot) + { + children.add(new SpecialUpElement()); + } + } + + /** + * Add a child element to this menu (may either be an Option or another menu) + * @param child the child element to add + */ + public void addChild(Element child) + { + child.setParent(this); + children.add(child); + } + + /** + * Add multiple child elements to this menu (may either be option, or another menu) + * @param children the children to add + */ + public void addChildren(Element[] children) + { + for (Element e : children) + { + e.setParent(this); + this.children.add(e); + } + } + + @Override + protected String getDisplayText() + { + return name; + } + + private ArrayList children() + { + return children; + } + } + + public static abstract class OptionElement extends Element + { + /** + * Override this to get notified when the element is clicked + */ + void onClick() {} + + /** + * Override this to get notified when the element gets a "left edit" input + */ + protected void onLeftInput() {} + + /** + * Override this to get notified when the element gets a "right edit" input + */ + protected void onRightInput() {} + } + + public static class EnumOption extends OptionElement + { + protected int idx = 0; + protected Enum[] e; + protected String name; + + public EnumOption(String name, Enum[] e) + { + this.e = e; + this.name = name; + } + + public EnumOption(String name, Enum[] e, Enum def) + { + this(name, e); + idx = def.ordinal(); + } + + @Override + public void onLeftInput() + { + idx++; + + if(idx > e.length-1) + { + idx = 0; + } + } + + @Override + public void onRightInput() + { + idx--; + + if(idx < 0) + { + idx = e.length-1; + } + } + + @Override + public void onClick() + { + //onRightInput(); + } + + @Override + protected String getDisplayText() + { + return String.format("%s: %s", name, e[idx].name()); + } + + public Enum getValue() + { + return e[idx]; + } + } + + public static class IntegerOption extends OptionElement + { + protected int i; + protected int min; + protected int max; + protected String name; + + public IntegerOption(String name, int min, int max, int def) + { + this.name = name; + this.min = min; + this.max = max; + this.i = def; + } + + @Override + public void onLeftInput() + { + i--; + + if(i < min) + { + i = max; + } + } + + @Override + public void onRightInput() + { + i++; + + if(i > max) + { + i = min; + } + } + + @Override + public void onClick() + { + //onRightInput(); + } + + @Override + protected String getDisplayText() + { + return String.format("%s: %d", name, i); + } + + public int getValue() + { + return i; + } + } + + static class BooleanOption extends OptionElement + { + private String name; + private boolean val = true; + + private String customTrue; + private String customFalse; + + BooleanOption(String name, boolean def) + { + this.name = name; + this.val = def; + } + + BooleanOption(String name, boolean def, String customTrue, String customFalse) + { + this(name, def); + this.customTrue = customTrue; + this.customFalse = customFalse; + } + + @Override + public void onLeftInput() + { + val = !val; + } + + @Override + public void onRightInput() + { + val = !val; + } + + @Override + public void onClick() + { + val = !val; + } + + @Override + protected String getDisplayText() + { + String valStr; + + if(customTrue != null && customFalse != null) + { + valStr = val ? customTrue : customFalse; + } + else + { + valStr = val ? "true" : "false"; + } + + return String.format("%s: %s", name, valStr); + } + + public boolean getValue() + { + return val; + } + } + + /** + * + */ + public static class StaticItem extends OptionElement + { + private String name; + + public StaticItem(String name) + { + this.name = name; + } + + @Override + protected String getDisplayText() + { + return name; + } + } + + public static abstract class StaticClickableOption extends OptionElement + { + private String name; + + public StaticClickableOption(String name) + { + this.name = name; + } + + abstract void onClick(); + + @Override + protected String getDisplayText() + { + return name; + } + } + + private static abstract class Element + { + private MenuElement parent; + + protected void setParent(MenuElement parent) + { + this.parent = parent; + } + + protected MenuElement parent() + { + return parent; + } + + protected abstract String getDisplayText(); + } + + private static class SpecialUpElement extends Element + { + @Override + protected String getDisplayText() + { + return ".. ↰ Up One Level"; + } + } + } +} diff --git a/FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/externalhardware/ConceptExternalHardwareClass.java b/FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/externalhardware/ConceptExternalHardwareClass.java new file mode 100644 index 0000000..cee35f6 --- /dev/null +++ b/FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/externalhardware/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.externalhardware; + +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 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, select this sample, and select TeleOp. + * Also add another new file named RobotHardware.java, select the sample with that name, and 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 START) + 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/externalhardware/RobotHardware.java b/FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/externalhardware/RobotHardware.java new file mode 100644 index 0000000..64f2206 --- /dev/null +++ b/FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/externalhardware/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.externalhardware; + +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, select this sample, and select Not an OpMode. + * Also add a new OpMode, select the sample ConceptExternalHardwareClass.java, and 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/readme.md b/FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/readme.md new file mode 100644 index 0000000..326978d --- /dev/null +++ b/FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/readme.md @@ -0,0 +1,45 @@ + +## Caution +No Team-specific code should be placed or modified in this ``.../samples`` folder. + +Samples should be Copied from here, and then Pasted into the team's +[/TeamCode/src/main/java/org/firstinspires/ftc/teamcode](../../../../../../../../../../TeamCode/src/main/java/org/firstinspires/ftc/teamcode) + folder, using the Android Studio cut and paste commands. This automatically changes all file and +class names to be consistent. From there, the sample can be modified to suit the team's needs. + +For more detailed instructions see the /teamcode readme. + +### Naming of Samples + +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 + 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. + +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. 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 + 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 new file mode 100644 index 0000000..e85e625 --- /dev/null +++ b/FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/sample_conventions.md @@ -0,0 +1,113 @@ +## Sample Class/Opmode conventions +#### V 1.1.0 8/9/2017 + +This document defines the FTC Sample OpMode and Class conventions. + +### OpMode Name + +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. + +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 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. + +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. These OpModes may not produce a drivable robot. + +Utility: This sample type is provided as a useful tool, or aide, to perform some specific development task. + It is not expected to be something you would include in your own robot code. + To use the tool, comment out the @Disabled annotation and build the App. + Read the comments found in the sample for an explanation of its intended use. + +After the prefix, other conventions will apply: + +* Sensor class names should constructed as: Sensor - Company - Type +* Robot class names should be constructed as: Robot - Mode - Action - OpModetype +* Concept class names should be constructed as: Concept - Topic - OpModetype + +### Sample OpMode Content/Style + +Code is formatted as per the Google Style Guide: + +https://google.github.io/styleguide/javaguide.html + +With “Sensor” and “Hardware” samples, the code should demonstrate the essential function, +and not be embellished with too much additional “clever” code. If a sensor has special +addressing needs, or has a variety of modes or outputs, these should be demonstrated as +simply as possible. + +Special programming methods, or robot control techniques should be reserved for “Concept” Samples, +and where possible, Samples should strive to only demonstrate a single concept, +eg: State machine coding, or a User Menu system, and not combine them into a single “all inclusive” +sample. This will prevent an “all inclusive” Sample being deleted just because one part of it +becomes obsolete. + +### Device Configuration Names + +The following device names are used in the external samples + +** Motors: +left_drive +right_drive +left_arm + +** Servos: +left_hand +right_hand +arm +claw + +** Sensors: +sensor_color +sensor_ir +sensor_light +sensor_ods +sensor_range +sensor_touch +sensor_color_distance +sensor_digital +digin +digout + +** Localization: +compass +gyro +imu +navx + +### Device Object Names + +Device Object names should use the same words as the device’s configuration name, but they +should be re-structured to be a suitable Java variable name. This should keep the same word order, +but adopt the style of beginning with a lower case letter, and then each subsequent word +starting with an upper case letter. + +Eg: from the examples above: tool, leftMotor, rightClawServo, rearLightSensor. + +Note: Sometimes it’s helpful to put the device type first, followed by the variant. +eg: motorLeft and motorRight, but this should only be done if the same word order +is used on the device configuration name. + +### OpMode code Comments + +Sample comments should read like normal code comments, that is, as an explanation of what the +sample code is doing. They should NOT be directives to the user, +like: “insert your joystick code here” as these comments typically aren’t +detailed enough to be useful. They also often get left in the code and become garbage. + +Instead, an example of the joystick code should be shown with a comment describing what it is doing. diff --git a/FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/internal/FtcOpModeRegister.java b/FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/internal/FtcOpModeRegister.java new file mode 100644 index 0000000..ceab67d --- /dev/null +++ b/FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/internal/FtcOpModeRegister.java @@ -0,0 +1,68 @@ +/* Copyright (c) 2014, 2015 Qualcomm Technologies Inc + +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted (subject to the limitations in the disclaimer below) provided that +the following conditions are met: + +Redistributions of source code must retain the above copyright notice, this list +of conditions and the following disclaimer. + +Redistributions in binary form must reproduce the above copyright notice, this +list of conditions and the following disclaimer in the documentation and/or +other materials provided with the distribution. + +Neither the name of Qualcomm Technologies Inc nor the names of its contributors +may be used to endorse or promote products derived from this software without +specific prior written permission. + +NO EXPRESS OR IMPLIED LICENSES TO ANY PARTY'S PATENT RIGHTS ARE GRANTED BY THIS +LICENSE. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ + +package org.firstinspires.ftc.robotcontroller.internal; + +import com.qualcomm.robotcore.eventloop.opmode.OpModeManager; +import com.qualcomm.robotcore.eventloop.opmode.OpModeRegister; + +/** + * {@link FtcOpModeRegister} is responsible for registering OpModes for use in an FTC game. + * @see #register(OpModeManager) + */ +public class FtcOpModeRegister implements OpModeRegister { + + /** + * {@link #register(OpModeManager)} is called by the SDK game in order to register + * OpMode classes or instances that will participate in an FTC game. + * + * There are two mechanisms by which an OpMode may be registered. + * + * 1) The preferred method is by means of class annotations in the OpMode itself. + * See, for example the class annotations in {@link org.firstinspires.ftc.robotcontroller.external.samples.ConceptNullOp}. + * + * 2) The other, retired, method is to modify this {@link #register(OpModeManager)} + * method to include explicit calls to OpModeManager.register(). + * This method of modifying this file directly is discouraged, as it + * makes updates to the SDK harder to integrate into your code. + * + * @param manager the object which contains methods for carrying out OpMode registrations + * + * @see com.qualcomm.robotcore.eventloop.opmode.TeleOp + * @see com.qualcomm.robotcore.eventloop.opmode.Autonomous + */ + public void register(OpModeManager manager) { + + /** + * Any manual OpMode class registrations should go here. + */ + } +} 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 new file mode 100644 index 0000000..3f1f77c --- /dev/null +++ b/FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/internal/FtcRobotControllerActivity.java @@ -0,0 +1,845 @@ +/* Copyright (c) 2014, 2015 Qualcomm Technologies Inc + +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted (subject to the limitations in the disclaimer below) provided that +the following conditions are met: + +Redistributions of source code must retain the above copyright notice, this list +of conditions and the following disclaimer. + +Redistributions in binary form must reproduce the above copyright notice, this +list of conditions and the following disclaimer in the documentation and/or +other materials provided with the distribution. + +Neither the name of Qualcomm Technologies Inc nor the names of its contributors +may be used to endorse or promote products derived from this software without +specific prior written permission. + +NO EXPRESS OR IMPLIED LICENSES TO ANY PARTY'S PATENT RIGHTS ARE GRANTED BY THIS +LICENSE. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ + +package org.firstinspires.ftc.robotcontroller.internal; + +import android.app.ActionBar; +import android.app.Activity; +import android.app.ActivityManager; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.ServiceConnection; +import android.content.SharedPreferences; +import android.content.res.Configuration; +import android.hardware.usb.UsbDevice; +import android.hardware.usb.UsbManager; +import android.net.wifi.WifiManager; +import android.os.Bundle; +import android.os.IBinder; +import android.preference.PreferenceManager; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.StringRes; +import android.view.Menu; +import android.view.MenuItem; +import android.view.MotionEvent; +import android.view.View; +import android.view.WindowManager; +import android.webkit.WebView; +import android.widget.ImageButton; +import android.widget.LinearLayout; +import android.widget.LinearLayout.LayoutParams; +import android.widget.PopupMenu; +import android.widget.TextView; + +import com.google.blocks.ftcrobotcontroller.ProgrammingWebHandlers; +import com.google.blocks.ftcrobotcontroller.runtime.BlocksOpMode; +import com.qualcomm.ftccommon.ClassManagerFactory; +import com.qualcomm.ftccommon.FtcAboutActivity; +import com.qualcomm.ftccommon.FtcEventLoop; +import com.qualcomm.ftccommon.FtcEventLoopIdle; +import com.qualcomm.ftccommon.FtcRobotControllerService; +import com.qualcomm.ftccommon.FtcRobotControllerService.FtcRobotControllerBinder; +import com.qualcomm.ftccommon.FtcRobotControllerSettingsActivity; +import com.qualcomm.ftccommon.LaunchActivityConstantsList; +import com.qualcomm.ftccommon.LaunchActivityConstantsList.RequestCode; +import com.qualcomm.ftccommon.Restarter; +import com.qualcomm.ftccommon.UpdateUI; +import com.qualcomm.ftccommon.configuration.EditParameters; +import com.qualcomm.ftccommon.configuration.FtcLoadFileActivity; +import com.qualcomm.ftccommon.configuration.RobotConfigFile; +import com.qualcomm.ftccommon.configuration.RobotConfigFileManager; +import com.qualcomm.ftcrobotcontroller.BuildConfig; +import com.qualcomm.ftcrobotcontroller.R; +import com.qualcomm.hardware.HardwareFactory; +import com.qualcomm.robotcore.eventloop.EventLoopManager; +import com.qualcomm.robotcore.eventloop.opmode.FtcRobotControllerServiceState; +import com.qualcomm.robotcore.eventloop.opmode.OpModeRegister; +import com.qualcomm.robotcore.hardware.configuration.LynxConstants; +import com.qualcomm.robotcore.hardware.configuration.Utility; +import com.qualcomm.robotcore.robot.Robot; +import com.qualcomm.robotcore.robot.RobotState; +import com.qualcomm.robotcore.util.ClockWarningSource; +import com.qualcomm.robotcore.util.Device; +import com.qualcomm.robotcore.util.Dimmer; +import com.qualcomm.robotcore.util.ImmersiveMode; +import com.qualcomm.robotcore.util.RobotLog; +import com.qualcomm.robotcore.util.WebServer; +import com.qualcomm.robotcore.wifi.NetworkConnection; +import com.qualcomm.robotcore.wifi.NetworkConnectionFactory; +import com.qualcomm.robotcore.wifi.NetworkType; + +import org.firstinspires.ftc.ftccommon.external.SoundPlayingRobotMonitor; +import org.firstinspires.ftc.ftccommon.internal.AnnotatedHooksClassFilter; +import org.firstinspires.ftc.ftccommon.internal.FtcRobotControllerWatchdogService; +import org.firstinspires.ftc.ftccommon.internal.ProgramAndManageActivity; +import org.firstinspires.ftc.onbotjava.ExternalLibraries; +import org.firstinspires.ftc.onbotjava.OnBotJavaHelperImpl; +import org.firstinspires.ftc.onbotjava.OnBotJavaProgrammingMode; +import org.firstinspires.ftc.robotcore.external.navigation.MotionDetection; +import org.firstinspires.ftc.robotcore.internal.hardware.android.AndroidBoard; +import org.firstinspires.ftc.robotcore.internal.network.DeviceNameManagerFactory; +import org.firstinspires.ftc.robotcore.internal.network.PreferenceRemoterRC; +import org.firstinspires.ftc.robotcore.internal.network.StartResult; +import org.firstinspires.ftc.robotcore.internal.network.WifiDirectChannelChanger; +import org.firstinspires.ftc.robotcore.internal.network.WifiMuteEvent; +import org.firstinspires.ftc.robotcore.internal.network.WifiMuteStateMachine; +import org.firstinspires.ftc.robotcore.internal.opmode.ClassManager; +import org.firstinspires.ftc.robotcore.internal.opmode.OnBotJavaHelper; +import org.firstinspires.ftc.robotcore.internal.system.AppAliveNotifier; +import org.firstinspires.ftc.robotcore.internal.system.AppUtil; +import org.firstinspires.ftc.robotcore.internal.system.Assert; +import org.firstinspires.ftc.robotcore.internal.system.PreferencesHelper; +import org.firstinspires.ftc.robotcore.internal.system.ServiceController; +import org.firstinspires.ftc.robotcore.internal.ui.ThemedActivity; +import org.firstinspires.ftc.robotcore.internal.ui.UILocation; +import org.firstinspires.ftc.robotcore.internal.webserver.RobotControllerWebInfo; +import org.firstinspires.ftc.robotserver.internal.programmingmode.ProgrammingModeManager; +import org.firstinspires.inspection.RcInspectionActivity; +import org.threeten.bp.YearMonth; +import org.xmlpull.v1.XmlPullParserException; + +import java.io.FileNotFoundException; +import java.util.List; +import java.util.Queue; +import java.util.concurrent.ConcurrentLinkedQueue; + +@SuppressWarnings("WeakerAccess") +public class FtcRobotControllerActivity extends Activity + { + public static final String TAG = "RCActivity"; + public String getTag() { return TAG; } + + private static final int REQUEST_CONFIG_WIFI_CHANNEL = 1; + private static final int NUM_GAMEPADS = 2; + + protected WifiManager.WifiLock wifiLock; + protected RobotConfigFileManager cfgFileMgr; + + private OnBotJavaHelper onBotJavaHelper; + + protected ProgrammingModeManager programmingModeManager; + + protected UpdateUI.Callback callback; + protected Context context; + protected Utility utility; + protected StartResult prefRemoterStartResult = new StartResult(); + protected StartResult deviceNameStartResult = new StartResult(); + protected PreferencesHelper preferencesHelper; + protected final SharedPreferencesListener sharedPreferencesListener = new SharedPreferencesListener(); + + protected ImageButton buttonMenu; + protected TextView textDeviceName; + protected TextView textNetworkConnectionStatus; + protected TextView textRobotStatus; + protected TextView[] textGamepad = new TextView[NUM_GAMEPADS]; + protected TextView textOpMode; + protected TextView textErrorMessage; + protected ImmersiveMode immersion; + + protected UpdateUI updateUI; + protected Dimmer dimmer; + protected LinearLayout entireScreenLayout; + + protected FtcRobotControllerService controllerService; + protected NetworkType networkType; + + protected FtcEventLoop eventLoop; + protected Queue receivedUsbAttachmentNotifications; + + protected WifiMuteStateMachine wifiMuteStateMachine; + protected MotionDetection motionDetection; + + private static boolean permissionsValidated = false; + + private WifiDirectChannelChanger wifiDirectChannelChanger; + + protected class RobotRestarter implements Restarter { + + public void requestRestart() { + requestRobotRestart(); + } + + } + + protected boolean serviceShouldUnbind = false; + protected ServiceConnection connection = new ServiceConnection() { + @Override + public void onServiceConnected(ComponentName name, IBinder service) { + FtcRobotControllerBinder binder = (FtcRobotControllerBinder) service; + onServiceBind(binder.getService()); + } + + @Override + public void onServiceDisconnected(ComponentName name) { + RobotLog.vv(FtcRobotControllerService.TAG, "%s.controllerService=null", TAG); + controllerService = null; + } + }; + + @Override + protected void onNewIntent(Intent intent) { + super.onNewIntent(intent); + + if (UsbManager.ACTION_USB_DEVICE_ATTACHED.equals(intent.getAction())) { + UsbDevice usbDevice = intent.getParcelableExtra(UsbManager.EXTRA_DEVICE); + RobotLog.vv(TAG, "ACTION_USB_DEVICE_ATTACHED: %s", usbDevice.getDeviceName()); + + if (usbDevice != null) { // paranoia + // We might get attachment notifications before the event loop is set up, so + // we hold on to them and pass them along only when we're good and ready. + if (receivedUsbAttachmentNotifications != null) { // *total* paranoia + receivedUsbAttachmentNotifications.add(usbDevice); + passReceivedUsbAttachmentsToEventLoop(); + } + } + } + } + + protected void passReceivedUsbAttachmentsToEventLoop() { + if (this.eventLoop != null) { + for (;;) { + UsbDevice usbDevice = receivedUsbAttachmentNotifications.poll(); + if (usbDevice == null) + break; + this.eventLoop.onUsbDeviceAttached(usbDevice); + } + } + else { + // Paranoia: we don't want the pending list to grow without bound when we don't + // (yet) have an event loop + while (receivedUsbAttachmentNotifications.size() > 100) { + receivedUsbAttachmentNotifications.poll(); + } + } + } + + /** + * There are cases where a permission may be revoked and the system restart will restart the + * FtcRobotControllerActivity, instead of the launch activity. Detect when that happens, and throw + * the device back to the permission validator activity. + */ + protected boolean enforcePermissionValidator() { + if (!permissionsValidated) { + RobotLog.vv(TAG, "Redirecting to permission validator"); + Intent permissionValidatorIntent = new Intent(AppUtil.getDefContext(), PermissionValidatorWrapper.class); + startActivity(permissionValidatorIntent); + finish(); + return true; + } else { + RobotLog.vv(TAG, "Permissions validated already"); + return false; + } + } + + public static void setPermissionsValidated() { + permissionsValidated = true; + } + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + if (enforcePermissionValidator()) { + return; + } + + RobotLog.onApplicationStart(); // robustify against onCreate() following onDestroy() but using the same app instance, which apparently does happen + RobotLog.vv(TAG, "onCreate()"); + ThemedActivity.appAppThemeToActivity(getTag(), this); // do this way instead of inherit to help AppInventor + + // Oddly, sometimes after a crash & restart the root activity will be something unexpected, like from the before crash? We don't yet understand + RobotLog.vv(TAG, "rootActivity is of class %s", AppUtil.getInstance().getRootActivity().getClass().getSimpleName()); + RobotLog.vv(TAG, "launchActivity is of class %s", FtcRobotControllerWatchdogService.launchActivity()); + Assert.assertTrue(FtcRobotControllerWatchdogService.isLaunchActivity(AppUtil.getInstance().getRootActivity())); + Assert.assertTrue(AppUtil.getInstance().isRobotController()); + + // Quick check: should we pretend we're not here, and so allow the Lynx to operate as + // a stand-alone USB-connected module? + if (LynxConstants.isRevControlHub()) { + // Double-sure check that we can talk to the DB over the serial TTY + AndroidBoard.getInstance().getAndroidBoardIsPresentPin().setState(true); + } + + context = this; + utility = new Utility(this); + + DeviceNameManagerFactory.getInstance().start(deviceNameStartResult); + + PreferenceRemoterRC.getInstance().start(prefRemoterStartResult); + + receivedUsbAttachmentNotifications = new ConcurrentLinkedQueue(); + eventLoop = null; + + setContentView(R.layout.activity_ftc_controller); + + preferencesHelper = new PreferencesHelper(TAG, context); + 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 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(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. + preferencesHelper.writeBooleanPrefIfDifferent(getString(R.string.pref_warn_about_2_4_ghz_band), true); + preferencesHelper.writeBooleanPrefIfDifferent(getString(R.string.pref_warn_about_obsolete_software), true); + preferencesHelper.writeBooleanPrefIfDifferent(getString(R.string.pref_warn_about_mismatched_app_versions), true); + preferencesHelper.writeBooleanPrefIfDifferent(getString(R.string.pref_warn_about_incorrect_clocks), true); + } + + entireScreenLayout = (LinearLayout) findViewById(R.id.entire_screen); + buttonMenu = (ImageButton) findViewById(R.id.menu_buttons); + buttonMenu.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + PopupMenu popupMenu = new PopupMenu(FtcRobotControllerActivity.this, v); + popupMenu.setOnMenuItemClickListener(new PopupMenu.OnMenuItemClickListener() { + @Override + public boolean onMenuItemClick(MenuItem item) { + return onOptionsItemSelected(item); // Delegate to the handler for the hardware menu button + } + }); + popupMenu.inflate(R.menu.ftc_robot_controller); + AnnotatedHooksClassFilter.getInstance().callOnCreateMenuMethods( + FtcRobotControllerActivity.this, popupMenu.getMenu()); + popupMenu.show(); + } + }); + + updateMonitorLayout(getResources().getConfiguration()); + + BlocksOpMode.setActivityAndWebView(this, (WebView) findViewById(R.id.webViewBlocksRuntime)); + + ExternalLibraries.getInstance().onCreate(); + onBotJavaHelper = new OnBotJavaHelperImpl(); + + /* + * Paranoia as the ClassManagerFactory requires EXTERNAL_STORAGE permissions + * and we've seen on the DS where the finish() call above does not short-circuit + * the onCreate() call for the activity and then we crash here because we don't + * have permissions. So... + */ + if (permissionsValidated) { + ClassManager.getInstance().setOnBotJavaClassHelper(onBotJavaHelper); + ClassManagerFactory.registerFilters(); + ClassManagerFactory.processAllClasses(); + } + + cfgFileMgr = new RobotConfigFileManager(this); + + // Clean up 'dirty' status after a possible crash + RobotConfigFile configFile = cfgFileMgr.getActiveConfig(); + if (configFile.isDirty()) { + configFile.markClean(); + cfgFileMgr.setActiveConfig(false, configFile); + } + + textDeviceName = (TextView) findViewById(R.id.textDeviceName); + textNetworkConnectionStatus = (TextView) findViewById(R.id.textNetworkConnectionStatus); + textRobotStatus = (TextView) findViewById(R.id.textRobotStatus); + textOpMode = (TextView) findViewById(R.id.textOpMode); + textErrorMessage = (TextView) findViewById(R.id.textErrorMessage); + textGamepad[0] = (TextView) findViewById(R.id.textGamepad1); + textGamepad[1] = (TextView) findViewById(R.id.textGamepad2); + immersion = new ImmersiveMode(getWindow().getDecorView()); + dimmer = new Dimmer(this); + dimmer.longBright(); + + programmingModeManager = new ProgrammingModeManager(); + programmingModeManager.register(new ProgrammingWebHandlers()); + programmingModeManager.register(new OnBotJavaProgrammingMode()); + + updateUI = createUpdateUI(); + callback = createUICallback(updateUI); + + PreferenceManager.setDefaultValues(this, R.xml.app_settings, false); + + WifiManager wifiManager = (WifiManager) getApplicationContext().getSystemService(Context.WIFI_SERVICE); + wifiLock = wifiManager.createWifiLock(WifiManager.WIFI_MODE_FULL_HIGH_PERF, ""); + + hittingMenuButtonBrightensScreen(); + + wifiLock.acquire(); + callback.networkConnectionUpdate(NetworkConnection.NetworkEvent.DISCONNECTED); + readNetworkType(); + ServiceController.startService(FtcRobotControllerWatchdogService.class); + bindToService(); + RobotLog.logAppInfo(); + RobotLog.logDeviceInfo(); + AndroidBoard.getInstance().logAndroidBoardInfo(); + + if (preferencesHelper.readBoolean(getString(R.string.pref_wifi_automute), false)) { + initWifiMute(true); + } + + FtcAboutActivity.setBuildTimeFromBuildConfig(BuildConfig.APP_BUILD_TIME); + + // check to see if there is a preferred Wi-Fi to use. + checkPreferredChannel(); + + AnnotatedHooksClassFilter.getInstance().callOnCreateMethods(this); + } + + protected UpdateUI createUpdateUI() { + Restarter restarter = new RobotRestarter(); + UpdateUI result = new UpdateUI(this, dimmer); + result.setRestarter(restarter); + result.setTextViews(textNetworkConnectionStatus, textRobotStatus, textGamepad, textOpMode, textErrorMessage, textDeviceName); + return result; + } + + protected UpdateUI.Callback createUICallback(UpdateUI updateUI) { + UpdateUI.Callback result = updateUI.new Callback(); + result.setStateMonitor(new SoundPlayingRobotMonitor()); + return result; + } + + @Override + protected void onStart() { + super.onStart(); + RobotLog.vv(TAG, "onStart()"); + + entireScreenLayout.setOnTouchListener(new View.OnTouchListener() { + @Override + public boolean onTouch(View v, MotionEvent event) { + dimmer.handleDimTimer(); + return false; + } + }); + } + + @Override + protected void onResume() { + super.onResume(); + RobotLog.vv(TAG, "onResume()"); + + // In case the user just got back from fixing their clock, refresh ClockWarningSource + ClockWarningSource.getInstance().onPossibleRcClockUpdate(); + } + + @Override + protected void onPause() { + super.onPause(); + RobotLog.vv(TAG, "onPause()"); + } + + @Override + protected void onStop() { + // Note: this gets called even when the configuration editor is launched. That is, it gets + // called surprisingly often. So, we don't actually do much here. + super.onStop(); + RobotLog.vv(TAG, "onStop()"); + } + + @Override + protected void onDestroy() { + super.onDestroy(); + RobotLog.vv(TAG, "onDestroy()"); + + shutdownRobot(); // Ensure the robot is put away to bed + if (callback != null) callback.close(); + + PreferenceRemoterRC.getInstance().stop(prefRemoterStartResult); + DeviceNameManagerFactory.getInstance().stop(deviceNameStartResult); + + unbindFromService(); + // If the app manually (?) is stopped, then we don't need the auto-starting function (?) + ServiceController.stopService(FtcRobotControllerWatchdogService.class); + if (wifiLock != null) wifiLock.release(); + if (preferencesHelper != null) preferencesHelper.getSharedPreferences().unregisterOnSharedPreferenceChangeListener(sharedPreferencesListener); + + RobotLog.cancelWriteLogcatToDisk(); + + AnnotatedHooksClassFilter.getInstance().callOnDestroyMethods(this); + } + + protected void bindToService() { + readNetworkType(); + Intent intent = new Intent(this, FtcRobotControllerService.class); + intent.putExtra(NetworkConnectionFactory.NETWORK_CONNECTION_TYPE, networkType); + serviceShouldUnbind = bindService(intent, connection, Context.BIND_AUTO_CREATE); + } + + protected void unbindFromService() { + if (serviceShouldUnbind) { + unbindService(connection); + serviceShouldUnbind = false; + } + } + + 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. + if (Device.isRevControlHub() == true) { + networkType = NetworkType.RCWIRELESSAP; + } else { + networkType = NetworkType.fromString(preferencesHelper.readString(context.getString(R.string.pref_pairing_kind), NetworkType.globalDefaultAsString())); + } + + // update the app_settings + preferencesHelper.writeStringPrefIfDifferent(context.getString(R.string.pref_pairing_kind), networkType.toString()); + } + + @Override + public void onWindowFocusChanged(boolean hasFocus) { + super.onWindowFocusChanged(hasFocus); + + if (hasFocus) { + immersion.hideSystemUI(); + getWindow().setFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION, WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION); + } + } + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + getMenuInflater().inflate(R.menu.ftc_robot_controller, menu); + AnnotatedHooksClassFilter.getInstance().callOnCreateMenuMethods(this, menu); + return true; + } + + private boolean isRobotRunning() { + if (controllerService == null) { + return false; + } + + Robot robot = controllerService.getRobot(); + + if ((robot == null) || (robot.eventLoopManager == null)) { + return false; + } + + RobotState robotState = robot.eventLoopManager.state; + + if (robotState != RobotState.RUNNING) { + return false; + } else { + return true; + } + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + int id = item.getItemId(); + + if (id == R.id.action_program_and_manage) { + if (isRobotRunning()) { + Intent programmingModeIntent = new Intent(AppUtil.getDefContext(), ProgramAndManageActivity.class); + RobotControllerWebInfo webInfo = programmingModeManager.getWebServer().getConnectionInformation(); + programmingModeIntent.putExtra(LaunchActivityConstantsList.RC_WEB_INFO, webInfo.toJson()); + startActivity(programmingModeIntent); + } else { + AppUtil.getInstance().showToast(UILocation.ONLY_LOCAL, context.getString(R.string.toastWifiUpBeforeProgrammingMode)); + } + } else if (id == R.id.action_inspection_mode) { + Intent inspectionModeIntent = new Intent(AppUtil.getDefContext(), RcInspectionActivity.class); + startActivity(inspectionModeIntent); + return true; + } else if (id == R.id.action_restart_robot) { + dimmer.handleDimTimer(); + AppUtil.getInstance().showToast(UILocation.BOTH, context.getString(R.string.toastRestartingRobot)); + requestRobotRestart(); + return true; + } + else if (id == R.id.action_configure_robot) { + EditParameters parameters = new EditParameters(); + Intent intentConfigure = new Intent(AppUtil.getDefContext(), FtcLoadFileActivity.class); + parameters.putIntent(intentConfigure); + startActivityForResult(intentConfigure, RequestCode.CONFIGURE_ROBOT_CONTROLLER.ordinal()); + } + else if (id == R.id.action_settings) { + // historical: this once erroneously used FTC_CONFIGURE_REQUEST_CODE_ROBOT_CONTROLLER + Intent settingsIntent = new Intent(AppUtil.getDefContext(), FtcRobotControllerSettingsActivity.class); + startActivityForResult(settingsIntent, RequestCode.SETTINGS_ROBOT_CONTROLLER.ordinal()); + return true; + } + else if (id == R.id.action_about) { + Intent intent = new Intent(AppUtil.getDefContext(), FtcAboutActivity.class); + startActivity(intent); + return true; + } + else if (id == R.id.action_exit_app) { + + //Clear backstack and everything to prevent edge case where VM might be + //restarted (after it was exited) if more than one activity was on the + //backstack for some reason. + finishAffinity(); + + //For lollipop and up, we can clear ourselves from the recents list too + if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.LOLLIPOP) { + ActivityManager manager = (ActivityManager) getSystemService(ACTIVITY_SERVICE); + List tasks = manager.getAppTasks(); + + for (ActivityManager.AppTask task : tasks) { + task.finishAndRemoveTask(); + } + } + + // Allow the user to use the Control Hub operating system's UI, instead of relaunching the app + AppAliveNotifier.getInstance().disableAppWatchdogUntilNextAppStart(); + + //Finally, nuke the VM from orbit + AppUtil.getInstance().exitApplication(); + + return true; + } + + return super.onOptionsItemSelected(item); + } + + @Override + public void onConfigurationChanged(Configuration newConfig) { + super.onConfigurationChanged(newConfig); + // don't destroy assets on screen rotation + updateMonitorLayout(newConfig); + } + + /** + * Updates the orientation of monitorContainer (which contains cameraMonitorView) + * based on the given configuration. Makes the children split the space. + */ + private void updateMonitorLayout(Configuration configuration) { + LinearLayout monitorContainer = (LinearLayout) findViewById(R.id.monitorContainer); + if (configuration.orientation == Configuration.ORIENTATION_LANDSCAPE) { + // When the phone is landscape, lay out the monitor views horizontally. + monitorContainer.setOrientation(LinearLayout.HORIZONTAL); + for (int i = 0; i < monitorContainer.getChildCount(); i++) { + View view = monitorContainer.getChildAt(i); + view.setLayoutParams(new LayoutParams(0, LayoutParams.MATCH_PARENT, 1 /* weight */)); + } + } else { + // When the phone is portrait, lay out the monitor views vertically. + monitorContainer.setOrientation(LinearLayout.VERTICAL); + for (int i = 0; i < monitorContainer.getChildCount(); i++) { + View view = monitorContainer.getChildAt(i); + view.setLayoutParams(new LayoutParams(LayoutParams.MATCH_PARENT, 0, 1 /* weight */)); + } + } + monitorContainer.requestLayout(); + } + + @Override + protected void onActivityResult(int request, int result, Intent intent) { + if (request == REQUEST_CONFIG_WIFI_CHANNEL) { + if (result == RESULT_OK) { + AppUtil.getInstance().showToast(UILocation.BOTH, context.getString(R.string.toastWifiConfigurationComplete)); + } + } + // was some historical confusion about launch codes here, so we err safely + if (request == RequestCode.CONFIGURE_ROBOT_CONTROLLER.ordinal() || request == RequestCode.SETTINGS_ROBOT_CONTROLLER.ordinal()) { + // We always do a refresh, whether it was a cancel or an OK, for robustness + shutdownRobot(); + cfgFileMgr.getActiveConfigAndUpdateUI(); + updateUIAndRequestRobotSetup(); + } + } + + public void onServiceBind(final FtcRobotControllerService service) { + RobotLog.vv(FtcRobotControllerService.TAG, "%s.controllerService=bound", TAG); + controllerService = service; + updateUI.setControllerService(controllerService); + + controllerService.setOnBotJavaHelper(onBotJavaHelper); + + updateUIAndRequestRobotSetup(); + programmingModeManager.setState(new FtcRobotControllerServiceState() { + @NonNull + @Override + public WebServer getWebServer() { + return service.getWebServer(); + } + + @Nullable + @Override + public OnBotJavaHelper getOnBotJavaHelper() { + return service.getOnBotJavaHelper(); + } + + @Override + public EventLoopManager getEventLoopManager() { + return service.getRobot().eventLoopManager; + } + }); + + AnnotatedHooksClassFilter.getInstance().callWebHandlerRegistrarMethods(this, + service.getWebServer().getWebHandlerManager()); + } + + private void updateUIAndRequestRobotSetup() { + if (controllerService != null) { + callback.networkConnectionUpdate(controllerService.getNetworkConnectionStatus()); + callback.updateRobotStatus(controllerService.getRobotStatus()); + // Only show this first-time toast on headless systems: what we have now on non-headless suffices + requestRobotSetup(LynxConstants.isRevControlHub() + ? new Runnable() { + @Override public void run() { + showRestartRobotCompleteToast(R.string.toastRobotSetupComplete); + } + } + : null); + } + } + + private void requestRobotSetup(@Nullable Runnable runOnComplete) { + if (controllerService == null) return; + + RobotConfigFile file = cfgFileMgr.getActiveConfigAndUpdateUI(); + HardwareFactory hardwareFactory = new HardwareFactory(context); + try { + hardwareFactory.setXmlPullParser(file.getXml()); + } catch (FileNotFoundException | XmlPullParserException e) { + RobotLog.ww(TAG, e, "Unable to set configuration file %s. Falling back on noConfig.", file.getName()); + file = RobotConfigFile.noConfig(cfgFileMgr); + try { + hardwareFactory.setXmlPullParser(file.getXml()); + cfgFileMgr.setActiveConfigAndUpdateUI(false, file); + } catch (FileNotFoundException | XmlPullParserException e1) { + RobotLog.ee(TAG, e1, "Failed to fall back on noConfig"); + } + } + + OpModeRegister userOpModeRegister = createOpModeRegister(); + eventLoop = new FtcEventLoop(hardwareFactory, userOpModeRegister, callback, this); + FtcEventLoopIdle idleLoop = new FtcEventLoopIdle(hardwareFactory, userOpModeRegister, callback, this); + + controllerService.setCallback(callback); + controllerService.setupRobot(eventLoop, idleLoop, runOnComplete); + + passReceivedUsbAttachmentsToEventLoop(); + AndroidBoard.showErrorIfUnknownControlHub(); + + AnnotatedHooksClassFilter.getInstance().callOnCreateEventLoopMethods(this, eventLoop); + } + + protected OpModeRegister createOpModeRegister() { + return new FtcOpModeRegister(); + } + + private void shutdownRobot() { + if (controllerService != null) controllerService.shutdownRobot(); + } + + private void requestRobotRestart() { + AppUtil.getInstance().showToast(UILocation.BOTH, AppUtil.getDefContext().getString(R.string.toastRestartingRobot)); + // + RobotLog.clearGlobalErrorMsg(); + RobotLog.clearGlobalWarningMsg(); + shutdownRobot(); + requestRobotSetup(new Runnable() { + @Override public void run() { + showRestartRobotCompleteToast(R.string.toastRestartRobotComplete); + } + }); + } + + private void showRestartRobotCompleteToast(@StringRes int resid) { + AppUtil.getInstance().showToast(UILocation.BOTH, AppUtil.getDefContext().getString(resid)); + } + + private void checkPreferredChannel() { + // For P2P network, check to see what preferred channel is. + if (networkType == NetworkType.WIFIDIRECT) { + int prefChannel = preferencesHelper.readInt(getString(com.qualcomm.ftccommon.R.string.pref_wifip2p_channel), -1); + if (prefChannel == -1) { + prefChannel = 0; + RobotLog.vv(TAG, "pref_wifip2p_channel: No preferred channel defined. Will use a default value of %d", prefChannel); + } else { + RobotLog.vv(TAG, "pref_wifip2p_channel: Found existing preferred channel (%d).", prefChannel); + } + + // attempt to set the preferred channel. + RobotLog.vv(TAG, "pref_wifip2p_channel: attempting to set preferred channel..."); + wifiDirectChannelChanger = new WifiDirectChannelChanger(); + wifiDirectChannelChanger.changeToChannel(prefChannel); + } + } + + protected void hittingMenuButtonBrightensScreen() { + ActionBar actionBar = getActionBar(); + if (actionBar != null) { + actionBar.addOnMenuVisibilityListener(new ActionBar.OnMenuVisibilityListener() { + @Override + public void onMenuVisibilityChanged(boolean isVisible) { + if (isVisible) { + dimmer.handleDimTimer(); + } + } + }); + } + } + + protected class SharedPreferencesListener implements SharedPreferences.OnSharedPreferenceChangeListener { + @Override public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) { + if (key.equals(context.getString(R.string.pref_app_theme))) { + ThemedActivity.restartForAppThemeChange(getTag(), getString(R.string.appThemeChangeRestartNotifyRC)); + } else if (key.equals(context.getString(R.string.pref_wifi_automute))) { + if (preferencesHelper.readBoolean(context.getString(R.string.pref_wifi_automute), false)) { + initWifiMute(true); + } else { + initWifiMute(false); + } + } + } + } + + protected void initWifiMute(boolean enable) { + if (enable) { + wifiMuteStateMachine = new WifiMuteStateMachine(); + wifiMuteStateMachine.initialize(); + wifiMuteStateMachine.start(); + + motionDetection = new MotionDetection(2.0, 10); + motionDetection.startListening(); + motionDetection.registerListener(new MotionDetection.MotionDetectionListener() { + @Override + public void onMotionDetected(double vector) + { + wifiMuteStateMachine.consumeEvent(WifiMuteEvent.USER_ACTIVITY); + } + }); + } else { + wifiMuteStateMachine.stop(); + wifiMuteStateMachine = null; + motionDetection.stopListening(); + motionDetection.purgeListeners(); + motionDetection = null; + } + } + + @Override + public void onUserInteraction() { + if (wifiMuteStateMachine != null) { + wifiMuteStateMachine.consumeEvent(WifiMuteEvent.USER_ACTIVITY); + } + } +} diff --git a/FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/internal/PermissionValidatorWrapper.java b/FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/internal/PermissionValidatorWrapper.java new file mode 100644 index 0000000..a0094bc --- /dev/null +++ b/FtcRobotController/src/main/java/org/firstinspires/ftc/robotcontroller/internal/PermissionValidatorWrapper.java @@ -0,0 +1,91 @@ +/* + * Copyright (c) 2018 Craig MacFarlane + * + * 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 Craig MacFarlane 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.internal; + +import android.Manifest; +import android.os.Bundle; + +import com.qualcomm.ftcrobotcontroller.R; + +import org.firstinspires.ftc.robotcore.internal.system.Misc; +import org.firstinspires.ftc.robotcore.internal.system.PermissionValidatorActivity; + +import java.util.ArrayList; +import java.util.List; + +public class PermissionValidatorWrapper extends PermissionValidatorActivity { + + private final String TAG = "PermissionValidatorWrapper"; + + /* + * The list of dangerous permissions the robot controller needs. + */ + protected List robotControllerPermissions = new ArrayList() {{ + add(Manifest.permission.WRITE_EXTERNAL_STORAGE); + add(Manifest.permission.READ_EXTERNAL_STORAGE); + add(Manifest.permission.CAMERA); + add(Manifest.permission.ACCESS_COARSE_LOCATION); + add(Manifest.permission.ACCESS_FINE_LOCATION); + add(Manifest.permission.READ_PHONE_STATE); + }}; + + private final static Class startApplication = FtcRobotControllerActivity.class; + + public String mapPermissionToExplanation(final String permission) { + if (permission.equals(Manifest.permission.WRITE_EXTERNAL_STORAGE)) { + return Misc.formatForUser(R.string.permRcWriteExternalStorageExplain); + } else if (permission.equals(Manifest.permission.READ_EXTERNAL_STORAGE)) { + return Misc.formatForUser(R.string.permRcReadExternalStorageExplain); + } else if (permission.equals(Manifest.permission.CAMERA)) { + return Misc.formatForUser(R.string.permRcCameraExplain); + } else if (permission.equals(Manifest.permission.ACCESS_COARSE_LOCATION)) { + return Misc.formatForUser(R.string.permAccessLocationExplain); + } else if (permission.equals(Manifest.permission.ACCESS_FINE_LOCATION)) { + return Misc.formatForUser(R.string.permAccessLocationExplain); + } else if (permission.equals(Manifest.permission.READ_PHONE_STATE)) { + return Misc.formatForUser(R.string.permReadPhoneState); + } + return Misc.formatForUser(R.string.permGenericExplain); + } + + @Override + protected void onCreate(Bundle savedInstanceState) + { + super.onCreate(savedInstanceState); + + permissions = robotControllerPermissions; + } + + protected Class onStartApplication() + { + FtcRobotControllerActivity.setPermissionsValidated(); + return startApplication; + } +} diff --git a/FtcRobotController/src/main/res/drawable-xhdpi/icon_menu.png b/FtcRobotController/src/main/res/drawable-xhdpi/icon_menu.png new file mode 100644 index 0000000000000000000000000000000000000000..6b9e997c5624fad049a7192d54034336f5216eaa GIT binary patch literal 975 zcmaJ=&ui0A91l2#lnrl!!r+mCOj+|@vb9~p;^^|4t!PT=I_)m9G#pWdU$RplDe*&&7KHhQB($9^!a$R3dVwx}KYA+y&(+2IISW~#L0aB&Cn77IO5N#f zIjSoz+y?WB#tD4FY>@cdL98XZ*yYvuIlYD==(?~iT|7^!VO4=aBLZ?#KIkKb4NO_K;|1yE%`VEav~mzLJ8(!D>muioJkQlktRkF8ra7k=vRs-iE*7!L?Zr|q zUo=#?kHys4@kzk?Sa*Px(NJtE2tVYJF^RlK#5E)8gKpuPH#`?Sl&^<%hvtGxL$Q!2 z1Jm*THSYcu*HC&Kh?g4!`ICTaKH}nj-#mN6ABJAW#d>n~?W3F#z}Uz!{`Gr5_jnqA zUORkw1MD9>KQZ@4-u-yB1}Daz{yaHcMcW7Kr}vK0-lwbUg8E?e#p==5Tj>)kmn!)6 H;`)<6M=di_ literal 0 HcmV?d00001 diff --git a/FtcRobotController/src/main/res/drawable-xhdpi/icon_robotcontroller.png b/FtcRobotController/src/main/res/drawable-xhdpi/icon_robotcontroller.png new file mode 100644 index 0000000000000000000000000000000000000000..022552f409c06b5c93aec2300324913fadcaac51 GIT binary patch literal 4777 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 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/FtcRobotController/src/main/res/menu/ftc_robot_controller.xml b/FtcRobotController/src/main/res/menu/ftc_robot_controller.xml new file mode 100644 index 0000000..657c1aa --- /dev/null +++ b/FtcRobotController/src/main/res/menu/ftc_robot_controller.xml @@ -0,0 +1,78 @@ + + +

+ + + + + + + + + + + + + + + diff --git a/FtcRobotController/src/main/res/raw/gold.wav b/FtcRobotController/src/main/res/raw/gold.wav new file mode 100644 index 0000000000000000000000000000000000000000..3a7baf88159a373d490064e774d8796e02bcf0bd GIT binary patch literal 104016 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ɍ)X7ft-z(-0Z_L+ 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 Vv0|mMjxCTc%YVx6WRILE{~xn&YM%fA literal 0 HcmV?d00001 diff --git a/FtcRobotController/src/main/res/raw/silver.wav b/FtcRobotController/src/main/res/raw/silver.wav new file mode 100644 index 0000000000000000000000000000000000000000..25918a72ae51c412a9a6268279b449fd19639272 GIT binary patch literal 99068 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 literal 0 HcmV?d00001 diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..19cfad9 --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-bin.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew new file mode 100755 index 0000000..91a7e26 --- /dev/null +++ b/gradlew @@ -0,0 +1,164 @@ +#!/usr/bin/env bash + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS="" + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn ( ) { + echo "$*" +} + +die ( ) { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; +esac + +# For Cygwin, ensure paths are in UNIX format before anything is touched. +if $cygwin ; then + [ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --unix "$JAVA_HOME"` +fi + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >&- +APP_HOME="`pwd -P`" +cd "$SAVED" >&- + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin, switch paths to Windows format before running java +if $cygwin ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=$((i+1)) + done + case $i in + (0) set -- ;; + (1) set -- "$args0" ;; + (2) set -- "$args0" "$args1" ;; + (3) set -- "$args0" "$args1" "$args2" ;; + (4) set -- "$args0" "$args1" "$args2" "$args3" ;; + (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules +function splitJvmOpts() { + JVM_OPTS=("$@") +} +eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS +JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" + +exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 0000000..8a0b282 --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,90 @@ +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS= + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto init + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:init +@rem Get command-line arguments, handling Windowz variants + +if not "%OS%" == "Windows_NT" goto win9xME_args +if "%@eval[2+2]" == "4" goto 4NT_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* +goto execute + +:4NT_args +@rem Get arguments from the 4NT Shell from JP Software +set CMD_LINE_ARGS=%$ + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/libs/README.txt b/libs/README.txt new file mode 100644 index 0000000..3d34852 --- /dev/null +++ b/libs/README.txt @@ -0,0 +1 @@ +Location of external libs diff --git a/libs/ftc.debug.keystore b/libs/ftc.debug.keystore new file mode 100644 index 0000000000000000000000000000000000000000..a7204e66d948a1bcff16a75fe0a85f9577ae675e GIT binary patch literal 2146 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 B7J>i( literal 0 HcmV?d00001 diff --git a/FtcRobotController/src/main/res/values/dimens.xml b/FtcRobotController/src/main/res/values/dimens.xml new file mode 100644 index 0000000..63f1bab --- /dev/null +++ b/FtcRobotController/src/main/res/values/dimens.xml @@ -0,0 +1,40 @@ + + + + + + 16dp + 5dp + + \ No newline at end of file diff --git a/FtcRobotController/src/main/res/values/strings.xml b/FtcRobotController/src/main/res/values/strings.xml new file mode 100644 index 0000000..6ea191e --- /dev/null +++ b/FtcRobotController/src/main/res/values/strings.xml @@ -0,0 +1,72 @@ + + + + + + + FTC Robot Controller + + + Self Inspect + Program & Manage + Blocks + Settings + Restart Robot + Configure Robot + About + Exit + + + Configuration Complete + Restarting Robot + The Robot Controller must be fully up and running before entering Program and Manage Mode. + + + + @style/AppThemeRedRC + @style/AppThemeGreenRC + @style/AppThemeBlueRC + @style/AppThemePurpleRC + @style/AppThemeOrangeRC + @style/AppThemeTealRC + + + pref_ftc_season_year_of_current_rc_new + + @string/packageNameRobotController + + diff --git a/FtcRobotController/src/main/res/values/styles.xml b/FtcRobotController/src/main/res/values/styles.xml new file mode 100644 index 0000000..07689c0 --- /dev/null +++ b/FtcRobotController/src/main/res/values/styles.xml @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/FtcRobotController/src/main/res/xml/app_settings.xml b/FtcRobotController/src/main/res/xml/app_settings.xml new file mode 100644 index 0000000..58d3aa9 --- /dev/null +++ b/FtcRobotController/src/main/res/xml/app_settings.xml @@ -0,0 +1,93 @@ + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/FtcRobotController/src/main/res/xml/device_filter.xml b/FtcRobotController/src/main/res/xml/device_filter.xml new file mode 100644 index 0000000..7b75350 --- /dev/null +++ b/FtcRobotController/src/main/res/xml/device_filter.xml @@ -0,0 +1,49 @@ + + + + + + + + + + + + + + diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..88b776b --- /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/README.md b/README.md new file mode 100644 index 0000000..cf1ead0 --- /dev/null +++ b/README.md @@ -0,0 +1,117 @@ +# ftc-lib + +## Yes, ts was generated... No I do not believe in an AI takeover + +**ftc-lib** is a high-performance, state-machine-driven framework designed for FTC teams who prioritize clean code, modularity, and hardware optimization. It provides a robust abstraction layer over standard FTC OpModes, integrating **PedroPathing** for precision movement and **Panels (Sloth)** for live configuration and telemetry. + +--- + +## 🚀 Key Features + +* **Template Method Pattern:** A locked-down backend that handles the "boring stuff" (Bulk reads, FPS capping, Telemetry timing) so you only write robot logic. +* **Subsystem Architecture:** Fully isolated mechanisms with their own internal "Micro-States." +* **Hierarchical State Machines:** Orchestrate complex robot actions by mapping a "Global State" to specific "Subsystem States." +* **Automatic Hardware Optimization:** + * **Bulk Reads:** Automatically sets all expansion hubs to Manual Caching mode for the fastest possible loop times. + * **FPS Capping:** Prevents CPU/Battery waste by locking loop speeds to a target (e.g., 50 FPS). +* **Stateful PID Control:** A built-in PID utility that handles time-deltas ($dt$) and integral sums internally. +* **Unified Telemetry:** A joined engine that pipes data to both the Driver Station and the **Panels** dashboard simultaneously. +* **Live Tuning:** Centralized `Constants.java` utilizing `@Configurable` for real-time value editing without recompiling. + +--- + +## 📂 Project Structure + +```text +teamcode/ +├── lib/ # The Core Framework (Don't touch) +│ ├── BaseStateOpMode.java # The engine that runs the robot +│ ├── Subsystem.java # The blueprint for all mechanisms +│ ├── SubsystemManager.java # Automates the lifecycle of subsystems +│ └── PIDController.java # Stateful math utility +├── subsystems/ # Your robot parts (Drivetrain, Lift, Intake) +├── util/ # Utilities (AutoTransfer, FPSCounter) +├── Constants.java # The "Control Panel" for the entire robot +└── opmodes/ # Your actual TeleOp and Autonomous files +``` + +--- + +## 🛠 Usage Guide + +### 1. Define your Constants +Use the `Constants.java` file to store every hardware name, PID value, and speed multiplier. + +```java +@Configurable +public static class LIFT { + public static double kP = 0.015; + public static int SCORING_POS = 2500; +} +``` + +### 2. Create a Subsystem +Inherit from `Subsystem`. Define "Micro-States" for this specific mechanism. + +```java +public class LiftSubsystem extends Subsystem { + public enum LiftState { IDLE, EXTENDING } + private LiftState state = LiftState.IDLE; + + @Override + public void update() { + // Run PID logic here + } +} +``` + +### 3. Build your TeleOp +Inherit from `BaseStateOpMode`. This gives you the `stateMachineUpdate()` hook where you map gamepad inputs to states. + +```java +@TeleOp +public class MainTeleOp extends BaseStateOpMode { + + @Override + protected void setupSubsystems() { + manager.register(new DriveSubsystem(), new LiftSubsystem()); + } + + @Override + protected void stateMachineUpdate() { + if (gamepad1.aWasPressed()) { + robotState = GlobalState.SCORING; + } + } +} +``` + +--- + +## ⚡ Performance Details + +### The "Tick" Lifecycle +Every loop, `ftc-lib` executes in this strict order: +1. **Hardware Sync:** Clears Bulk Cache on all Hubs. +2. **Logic Tick:** Runs your `stateMachineUpdate()`. +3. **Subsystem Tick:** All subsystems calculate PIDs and update motor powers. +4. **Telemetry Gate:** If the `TELEMETRY_DELAY_MS` has passed, it pushes data to Panels and the Driver Station. +5. **Sync Sleep:** Adjusts thread sleep time to maintain a consistent `TARGET_FPS`. + +### Auton-to-TeleOp Persistence +Using the `AutoTransfer` utility, this library can automatically carry over the robot's end-of-auton position (from **PedroPathing**) and the Alliance color into TeleOp, ensuring your field-centric drive and automation remain seamless. + +--- + +## 📦 Dependencies +* [PedroPathing](https://github.com/pedropathing/pedro-pathing) +* [Panels/Configurables (Sloth)](https://panels.bylazar.com/) +* [Panels/Telemetry (Sloth)](https://panels.bylazar.com/) + +--- + +## 🤝 Contribution +When adding new subsystems: +1. Ensure all hardware names are in `Constants`. +2. Ensure `publishTelemetry` only sends data when `GLOBAL.DEBUG_MODE` is true. +3. Never use `sleep()` inside a subsystem; use state timers instead. \ No newline at end of file diff --git a/TeamCode/build.gradle b/TeamCode/build.gradle new file mode 100644 index 0000000..878287a --- /dev/null +++ b/TeamCode/build.gradle @@ -0,0 +1,28 @@ +// +// build.gradle in TeamCode +// +// Most of the definitions for building your module reside in a common, shared +// file 'build.common.gradle'. Being factored in this way makes it easier to +// integrate updates to the FTC into your code. If you really need to customize +// the build definitions, you can place those customizations in this file, but +// please think carefully as to whether such customizations are really necessary +// before doing so. + + +// Custom definitions may go here + +// Include common definitions from above. +apply from: '../build.common.gradle' +apply from: '../build.dependencies.gradle' + +android { + namespace = 'org.firstinspires.ftc.teamcode' + + packagingOptions { + jniLibs.useLegacyPackaging true + } +} + +dependencies { + implementation project(':FtcRobotController') +} diff --git a/TeamCode/lib/OpModeAnnotationProcessor.jar b/TeamCode/lib/OpModeAnnotationProcessor.jar new file mode 100644 index 0000000000000000000000000000000000000000..4825cc36d9606badc3c0eac0f61079f9c5a4d196 GIT binary patch literal 6032 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 literal 0 HcmV?d00001 diff --git a/TeamCode/src/main/AndroidManifest.xml b/TeamCode/src/main/AndroidManifest.xml new file mode 100644 index 0000000..3705b31 --- /dev/null +++ b/TeamCode/src/main/AndroidManifest.xml @@ -0,0 +1,11 @@ + + + + + + + + diff --git a/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/lib/BaseOpMode.java b/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/lib/BaseOpMode.java new file mode 100644 index 0000000..18104b7 --- /dev/null +++ b/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/lib/BaseOpMode.java @@ -0,0 +1,83 @@ +package org.firstinspires.ftc.teamcode.lib; + +import android.os.SystemClock; + +import com.qualcomm.hardware.lynx.LynxModule; +import com.qualcomm.robotcore.eventloop.opmode.OpMode; +import com.qualcomm.robotcore.util.ElapsedTime; +import org.firstinspires.ftc.robotcore.external.Telemetry; + +// PANELS IMPORTS +import com.bylazar.telemetry.JoinedTelemetry; + +import org.firstinspires.ftc.teamcode.teleOp.Constants; +import org.firstinspires.ftc.teamcode.util.FPSCounter; + +import java.util.List; + +public abstract class BaseOpMode extends OpMode { + protected final SubsysManager manager = new SubsysManager(); + protected final FPSCounter fps = new FPSCounter(); + private final ElapsedTime telemetryTimer = new ElapsedTime(); + private List allHubs; + + protected Telemetry unifiedTelemetry; + + protected abstract void setupSubsystems(); + protected abstract void stateMachineUpdate(); + protected void onInit() {} + + @Override + public final void init() { + unifiedTelemetry = new JoinedTelemetry(telemetry); + + allHubs = hardwareMap.getAll(LynxModule.class); + for (LynxModule hub : allHubs) { + hub.setBulkCachingMode(LynxModule.BulkCachingMode.MANUAL); + } + + setupSubsystems(); + + manager.initAll(hardwareMap); + + onInit(); + } + + @Override + public final void start() { + fps.reset(); + fps.startLoop(); + telemetryTimer.reset(); + } + + @Override + public final void loop() { + for (LynxModule hub : allHubs) { + hub.clearBulkCache(); + } + + fps.startLoop(); + stateMachineUpdate(); + + manager.updateAll(); + + if (telemetryTimer.milliseconds() >= Constants.GLOBAL.TELEMETRY_DELAY_MS) { + + manager.publishTelemetryAll(unifiedTelemetry); + + if (Constants.GLOBAL.DEBUG_MODE) { + unifiedTelemetry.addData("Loop Time (ms)", fps.getCurrentLoopTime()); + unifiedTelemetry.addData("Target FPS", Constants.GLOBAL.TARGET_FPS); + unifiedTelemetry.addData("Current FPS", fps.getAverageFps()); + } + + unifiedTelemetry.update(); + telemetryTimer.reset(); + } + + long sleepTime = fps.getSyncTime(Constants.GLOBAL.TARGET_FPS); + if (sleepTime > 0) { + SystemClock.sleep(sleepTime); + } + } +} \ No newline at end of file diff --git a/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/lib/PIDController.java b/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/lib/PIDController.java new file mode 100644 index 0000000..fe6bd7f --- /dev/null +++ b/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/lib/PIDController.java @@ -0,0 +1,51 @@ +package org.firstinspires.ftc.teamcode.lib; + +import com.qualcomm.robotcore.util.ElapsedTime; + +public class PIDController { + private double kP, kI, kD; + + private double lastError = 0; + private double integralSum = 0; + private final ElapsedTime timer = new ElapsedTime(); + private boolean hasRunOnce = false; + + public PIDController(double kP, double kI, double kD) { + setCoefficients(kP, kI, kD); + } + + public void setCoefficients(double kP, double kI, double kD) { + this.kP = kP; + this.kI = kI; + this.kD = kD; + } + + public double calculate(double target, double current) { + double error = target - current; + + if (!hasRunOnce) { + timer.reset(); + lastError = error; + hasRunOnce = true; + return kP * error; + } + + double dt = timer.seconds(); + timer.reset(); + + if (dt == 0.0) return 0.0; + + double derivative = (error - lastError) / dt; + integralSum += (error * dt); + lastError = error; + + return (kP * error) + (kI * integralSum) + (kD * derivative); + } + + public void reset() { + integralSum = 0; + lastError = 0; + hasRunOnce = false; + timer.reset(); + } +} \ No newline at end of file diff --git a/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/lib/SubsysManager.java b/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/lib/SubsysManager.java new file mode 100644 index 0000000..65a7811 --- /dev/null +++ b/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/lib/SubsysManager.java @@ -0,0 +1,34 @@ +package org.firstinspires.ftc.teamcode.lib; + +import com.qualcomm.robotcore.hardware.HardwareMap; +import org.firstinspires.ftc.robotcore.external.Telemetry; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +public class SubsysManager { + private final List subsystems = new ArrayList<>(); + + public void register(Subsystem... subs) { + subsystems.addAll(Arrays.asList(subs)); + } + + public void initAll(HardwareMap hwMap) { + for (Subsystem sub : subsystems) { + sub.init(hwMap); + } + } + + public void updateAll() { + for (Subsystem sub : subsystems) { + sub.update(); + } + } + + public void publishTelemetryAll(Telemetry telemetry) { + for (Subsystem sub : subsystems) { + sub.publishTelemetry(telemetry); + } + } +} \ No newline at end of file diff --git a/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/lib/Subsystem.java b/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/lib/Subsystem.java new file mode 100644 index 0000000..901200f --- /dev/null +++ b/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/lib/Subsystem.java @@ -0,0 +1,12 @@ +package org.firstinspires.ftc.teamcode.lib; + +import com.qualcomm.robotcore.hardware.HardwareMap; +import org.firstinspires.ftc.robotcore.external.Telemetry; + +public abstract class Subsystem { + public abstract void init(HardwareMap hwMap); + + public abstract void update(); + + public void publishTelemetry(Telemetry telemetry) {} +} \ No newline at end of file diff --git a/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/pedroPathing/Constants.java b/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/pedroPathing/Constants.java new file mode 100644 index 0000000..fa30420 --- /dev/null +++ b/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/pedroPathing/Constants.java @@ -0,0 +1,19 @@ +package org.firstinspires.ftc.teamcode.pedroPathing; + +import com.pedropathing.follower.Follower; +import com.pedropathing.follower.FollowerConstants; +import com.pedropathing.ftc.FollowerBuilder; +import com.pedropathing.paths.PathConstraints; +import com.qualcomm.robotcore.hardware.HardwareMap; + +public class Constants { + public static FollowerConstants followerConstants = new FollowerConstants(); + + public static PathConstraints pathConstraints = new PathConstraints(0.99, 100, 1, 1); + + public static Follower createFollower(HardwareMap hardwareMap) { + return new FollowerBuilder(followerConstants, hardwareMap) + .pathConstraints(pathConstraints) + .build(); + } +} diff --git a/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/pedroPathing/Tuning.java b/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/pedroPathing/Tuning.java new file mode 100644 index 0000000..d0942f4 --- /dev/null +++ b/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/pedroPathing/Tuning.java @@ -0,0 +1,1585 @@ +package org.firstinspires.ftc.teamcode.pedroPathing; + +import static com.pedropathing.math.MathFunctions.quadraticFit; +import static org.firstinspires.ftc.teamcode.pedroPathing.Tuning.*; + +import android.annotation.SuppressLint; +import com.bylazar.configurables.PanelsConfigurables; +import com.bylazar.configurables.annotations.Configurable; +import com.bylazar.configurables.annotations.IgnoreConfigurable; +import com.bylazar.field.FieldManager; +import com.bylazar.field.PanelsField; +import com.bylazar.field.Style; +import com.bylazar.telemetry.PanelsTelemetry; +import com.bylazar.telemetry.TelemetryManager; +import com.pedropathing.follower.Follower; +import com.pedropathing.geometry.*; +import com.pedropathing.math.*; +import com.pedropathing.paths.*; +import com.pedropathing.telemetry.SelectableOpMode; +import com.pedropathing.util.*; +import com.qualcomm.robotcore.eventloop.opmode.OpMode; +import com.qualcomm.robotcore.eventloop.opmode.TeleOp; +import com.qualcomm.robotcore.util.ElapsedTime; + +import java.util.ArrayList; +import java.util.List; + +/** + * This is the Tuning class. It contains a selection menu for various tuning OpModes. + * + * @author Baron Henderson - 20077 The Indubitables + * @version 1.0, 6/26/2025 + */ +@Configurable +@TeleOp(name = "Tuning", group = "Pedro Pathing") +public class Tuning extends SelectableOpMode { + public static Follower follower; + + @IgnoreConfigurable + static PoseHistory poseHistory; + + @IgnoreConfigurable + static TelemetryManager telemetryM; + + @IgnoreConfigurable + static ArrayList changes = new ArrayList<>(); + + public Tuning() { + super("Select a Tuning OpMode", s -> { + s.folder("Localization", l -> { + l.add("Localization Test", LocalizationTest::new); + l.add("Offsets Tuner", OffsetsTuner::new); + l.add("Forward Tuner", ForwardTuner::new); + l.add("Lateral Tuner", LateralTuner::new); + l.add("Turn Tuner", TurnTuner::new); + }); + s.folder("Automatic", a -> { + a.add("Forward Velocity Tuner", ForwardVelocityTuner::new); + a.add("Lateral Velocity Tuner", LateralVelocityTuner::new); + a.add("Forward Zero Power Acceleration Tuner", ForwardZeroPowerAccelerationTuner::new); + a.add("Lateral Zero Power Acceleration Tuner", LateralZeroPowerAccelerationTuner::new); + a.add("Predictive Braking", PredictiveBrakingTuner::new); + }); + s.folder("Manual", p -> { + p.add("Translational Tuner", TranslationalTuner::new); + p.add("Heading Tuner", HeadingTuner::new); + p.add("Drive Tuner", DriveTuner::new); + p.add("Line Tuner", Line::new); + p.add("Centripetal Tuner", CentripetalTuner::new); + }); + s.folder("Tests", p -> { + p.add("Line", Line::new); + p.add("Triangle", Triangle::new); + p.add("Circle", Circle::new); + }); + }); + } + + @Override + public void onSelect() { + if (follower == null) { + follower = Constants.createFollower(hardwareMap); + PanelsConfigurables.INSTANCE.refreshClass(this); + } else { + follower = Constants.createFollower(hardwareMap); + } + + follower.setStartingPose(new Pose()); + + poseHistory = follower.getPoseHistory(); + + telemetryM = PanelsTelemetry.INSTANCE.getTelemetry(); + + Drawing.init(); + } + + @Override + public void onLog(List lines) {} + + public static void drawOnlyCurrent() { + try { + Drawing.drawRobot(follower.getPose()); + Drawing.sendPacket(); + } catch (Exception e) { + throw new RuntimeException("Drawing failed " + e); + } + } + + public static void draw() { + Drawing.drawDebug(follower); + } + + /** This creates a full stop of the robot by setting the drive motors to run at 0 power. */ + public static void stopRobot() { + follower.startTeleopDrive(true); + follower.setTeleOpDrive(0,0,0,true); + } +} + +/** + * This is the LocalizationTest OpMode. This is basically just a simple mecanum drive attached to a + * PoseUpdater. The OpMode will print out the robot's pose to telemetry as well as draw the robot. + * You should use this to check the robot's localization. + * + * @author Anyi Lin - 10158 Scott's Bots + * @author Baron Henderson - 20077 The Indubitables + * @version 1.0, 5/6/2024 + */ +class LocalizationTest extends OpMode { + @Override + public void init() { + follower.setStartingPose(new Pose(72,72)); + } + + /** This initializes the PoseUpdater, the mecanum drive motors, and the Panels telemetry. */ + @Override + public void init_loop() { + telemetryM.debug("This will print your robot's position to telemetry while " + + "allowing robot control through a basic mecanum drive on gamepad 1."); + telemetryM.update(telemetry); + follower.update(); + drawOnlyCurrent(); + } + + @Override + public void start() { + follower.startTeleopDrive(); + follower.update(); + } + + /** + * This updates the robot's pose estimate, the simple mecanum drive, and updates the + * Panels telemetry with the robot's position as well as draws the robot's position. + */ + @Override + public void loop() { + follower.setTeleOpDrive(-gamepad1.left_stick_y, -gamepad1.left_stick_x, -gamepad1.right_stick_x, true); + follower.update(); + + telemetryM.debug("x:" + follower.getPose().getX()); + telemetryM.debug("y:" + follower.getPose().getY()); + telemetryM.debug("heading:" + follower.getPose().getHeading()); + telemetryM.debug("total heading:" + follower.getTotalHeading()); + telemetryM.update(telemetry); + + draw(); + } +} + +/** + * This is the ForwardTuner OpMode. This tracks the forward movement of the robot and displays the + * necessary ticks to inches multiplier. This displayed multiplier is what's necessary to scale the + * robot's current distance in ticks to the specified distance in inches. So, to use this, run the + * tuner, then pull/push the robot to the specified distance using a ruler on the ground. When you're + * at the end of the distance, record the ticks to inches multiplier. Feel free to run multiple trials + * and average the results. Then, input the multiplier into the forward ticks to inches in your + * localizer of choice. + * + * @author Anyi Lin - 10158 Scott's Bots + * @author Baron Henderson - 20077 The Indubitables + * @version 1.0, 5/6/2024 + */ +class ForwardTuner extends OpMode { + public static double DISTANCE = 48; + + @Override + public void init() { + follower.setStartingPose(new Pose(72,72)); + follower.update(); + drawOnlyCurrent(); + } + + /** This initializes the PoseUpdater as well as the Panels telemetry. */ + @Override + public void init_loop() { + telemetryM.debug("Pull your robot forward " + DISTANCE + " inches. Your forward ticks to inches will be shown on the telemetry."); + telemetryM.update(telemetry); + drawOnlyCurrent(); + } + + /** + * This updates the robot's pose estimate, and updates the Panels telemetry with the + * calculated multiplier and draws the robot. + */ + @Override + public void loop() { + follower.update(); + + telemetryM.debug("Distance Moved: " + (follower.getPose().getX() - 72)); + telemetryM.debug("The multiplier will display what your forward ticks to inches should be to scale your current distance to " + DISTANCE + " inches."); + telemetryM.debug("Multiplier: " + (DISTANCE / ((follower.getPose().getX() - 72) / follower.getPoseTracker().getLocalizer().getForwardMultiplier()))); + telemetryM.update(telemetry); + + draw(); + } +} + +/** + * This is the LateralTuner OpMode. This tracks the strafe movement of the robot and displays the + * necessary ticks to inches multiplier. This displayed multiplier is what's necessary to scale the + * robot's current distance in ticks to the specified distance in inches. So, to use this, run the + * tuner, then pull/push the robot to the specified distance using a ruler on the ground. When you're + * at the end of the distance, record the ticks to inches multiplier. Feel free to run multiple trials + * and average the results. Then, input the multiplier into the strafe ticks to inches in your + * localizer of choice. + * + * @author Anyi Lin - 10158 Scott's Bots + * @author Baron Henderson - 20077 The Indubitables + * @version 2.0, 6/26/2025 + */ +class LateralTuner extends OpMode { + public static double DISTANCE = 48; + + @Override + public void init() { + follower.setStartingPose(new Pose(72,72)); + follower.update(); + drawOnlyCurrent(); + } + + /** This initializes the PoseUpdater as well as the Panels telemetry. */ + @Override + public void init_loop() { + telemetryM.debug("Pull your robot to the right " + DISTANCE + " inches. Your strafe ticks to inches will be shown on the telemetry."); + telemetryM.update(telemetry); + drawOnlyCurrent(); + } + + /** + * This updates the robot's pose estimate, and updates the Panels telemetry with the + * calculated multiplier and draws the robot. + */ + @Override + public void loop() { + follower.update(); + + telemetryM.debug("Distance Moved: " + (follower.getPose().getY() - 72)); + telemetryM.debug("The multiplier will display what your strafe ticks to inches should be to scale your current distance to " + DISTANCE + " inches."); + telemetryM.debug("Multiplier: " + (DISTANCE / ((follower.getPose().getY() - 72) / follower.getPoseTracker().getLocalizer().getLateralMultiplier()))); + telemetryM.update(telemetry); + + draw(); + } +} + +/** + * This is the TurnTuner OpMode. This tracks the turning movement of the robot and displays the + * necessary ticks to inches multiplier. This displayed multiplier is what's necessary to scale the + * robot's current angle in ticks to the specified angle in radians. So, to use this, run the + * tuner, then pull/push the robot to the specified angle using a protractor or lines on the ground. + * When you're at the end of the angle, record the ticks to inches multiplier. Feel free to run + * multiple trials and average the results. Then, input the multiplier into the turning ticks to + * radians in your localizer of choice. + * + * @author Anyi Lin - 10158 Scott's Bots + * @author Baron Henderson - 20077 The Indubitables + * @version 1.0, 5/6/2024 + */ +class TurnTuner extends OpMode { + public static double ANGLE = 2 * Math.PI; + + @Override + public void init() { + follower.setStartingPose(new Pose(72,72)); + follower.update(); + drawOnlyCurrent(); + } + + /** This initializes the PoseUpdater as well as the Panels telemetry. */ + @Override + public void init_loop() { + telemetryM.debug("Turn your robot " + ANGLE + " radians. Your turn ticks to inches will be shown on the telemetry."); + telemetryM.update(telemetry); + + drawOnlyCurrent(); + } + + /** + * This updates the robot's pose estimate, and updates the Panels telemetry with the + * calculated multiplier and draws the robot. + */ + @Override + public void loop() { + follower.update(); + + telemetryM.debug("Total Angle: " + follower.getTotalHeading()); + telemetryM.debug("The multiplier will display what your turn ticks to inches should be to scale your current angle to " + ANGLE + " radians."); + telemetryM.debug("Multiplier: " + (ANGLE / (follower.getTotalHeading() / follower.getPoseTracker().getLocalizer().getTurningMultiplier()))); + telemetryM.update(telemetry); + + draw(); + } +} + +/** + * This is the ForwardVelocityTuner autonomous follower OpMode. This runs the robot forwards at max + * power until it reaches some specified distance. It records the most recent velocities, and on + * reaching the end of the distance, it averages them and prints out the velocity obtained. It is + * recommended to run this multiple times on a full battery to get the best results. What this does + * is, when paired with StrafeVelocityTuner, allows FollowerConstants to create a Vector that + * empirically represents the direction your mecanum wheels actually prefer to go in, allowing for + * more accurate following. + * + * @author Anyi Lin - 10158 Scott's Bots + * @author Aaron Yang - 10158 Scott's Bots + * @author Harrison Womack - 10158 Scott's Bots + * @author Baron Henderson - 20077 The Indubitables + * @version 1.0, 3/13/2024 + */ +class ForwardVelocityTuner extends OpMode { + private final ArrayList velocities = new ArrayList<>(); + public static double DISTANCE = 48; + public static double RECORD_NUMBER = 10; + + private boolean end; + + @Override + public void init() { + follower.setStartingPose(new Pose(72, 72)); + } + + /** This initializes the drive motors as well as the cache of velocities and the Panels telemetry. */ + @Override + public void init_loop() { + telemetryM.debug("The robot will run at 1 power until it reaches " + DISTANCE + " inches forward."); + telemetryM.debug("Make sure you have enough room, since the robot has inertia after cutting power."); + telemetryM.debug("After running the distance, the robot will cut power from the drivetrain and display the forward velocity."); + telemetryM.debug("Press B on game pad 1 to stop."); + telemetryM.debug("pose", follower.getPose()); + telemetryM.update(telemetry); + follower.update(); + drawOnlyCurrent(); + } + + /** This starts the OpMode by setting the drive motors to run forward at full power. */ + @Override + public void start() { + for (int i = 0; i < RECORD_NUMBER; i++) { + velocities.add(0.0); + } + follower.startTeleopDrive(true); + follower.update(); + end = false; + } + + /** + * This runs the OpMode. At any point during the running of the OpMode, pressing B on + * game pad 1 will stop the OpMode. This continuously records the RECORD_NUMBER most recent + * velocities, and when the robot has run forward enough, these last velocities recorded are + * averaged and printed. + */ + @Override + public void loop() { + if (gamepad1.bWasPressed()) { + stopRobot(); + requestOpModeStop(); + } + + follower.update(); + draw(); + + + if (!end) { + if (Math.abs(follower.getPose().getX()) > (DISTANCE + 72)) { + end = true; + stopRobot(); + } else { + follower.setTeleOpDrive(1,0,0,true); + //double currentVelocity = Math.abs(follower.getVelocity().getXComponent()); + double currentVelocity = Math.abs(follower.poseTracker.getLocalizer().getVelocity().getX()); + velocities.add(currentVelocity); + velocities.remove(0); + } + } else { + stopRobot(); + double average = 0; + for (double velocity : velocities) { + average += velocity; + } + average /= velocities.size(); + telemetryM.debug("Forward Velocity: " + average); + telemetryM.debug("\n"); + telemetryM.debug("Press A to set the Forward Velocity temporarily (while robot remains on)."); + + for (int i = 0; i < velocities.size(); i++) { + telemetry.addData(String.valueOf(i), velocities.get(i)); + } + + telemetryM.update(telemetry); + telemetry.update(); + + if (gamepad1.aWasPressed()) { + follower.setXVelocity(average); + String message = "XMovement: " + average; + changes.add(message); + } + } + } +} + +/** + * This is the StrafeVelocityTuner autonomous follower OpMode. This runs the robot left at max + * power until it reaches some specified distance. It records the most recent velocities, and on + * reaching the end of the distance, it averages them and prints out the velocity obtained. It is + * recommended to run this multiple times on a full battery to get the best results. What this does + * is, when paired with ForwardVelocityTuner, allows FollowerConstants to create a Vector that + * empirically represents the direction your mecanum wheels actually prefer to go in, allowing for + * more accurate following. + * + * @author Anyi Lin - 10158 Scott's Bots + * @author Aaron Yang - 10158 Scott's Bots + * @author Harrison Womack - 10158 Scott's Bots + * @author Baron Henderson - 20077 The Indubitables + * @version 1.0, 3/13/2024 + */ +class LateralVelocityTuner extends OpMode { + private final ArrayList velocities = new ArrayList<>(); + + public static double DISTANCE = 48; + public static double RECORD_NUMBER = 10; + + private boolean end; + + @Override + public void init() { + follower.setStartingPose(new Pose(72, 72)); + } + + /** + * This initializes the drive motors as well as the cache of velocities and the Panels + * telemetryM. + */ + @Override + public void init_loop() { + telemetryM.debug("The robot will run at 1 power until it reaches " + DISTANCE + " inches to the left."); + telemetryM.debug("Make sure you have enough room, since the robot has inertia after cutting power."); + telemetryM.debug("After running the distance, the robot will cut power from the drivetrain and display the strafe velocity."); + telemetryM.debug("Press B on Gamepad 1 to stop."); + telemetryM.update(telemetry); + follower.update(); + drawOnlyCurrent(); + } + + /** This starts the OpMode by setting the drive motors to run left at full power. */ + @Override + public void start() { + for (int i = 0; i < RECORD_NUMBER; i++) { + velocities.add(0.0); + } + follower.startTeleopDrive(true); + follower.update(); + } + + /** + * This runs the OpMode. At any point during the running of the OpMode, pressing B on + * game pad1 will stop the OpMode. This continuously records the RECORD_NUMBER most recent + * velocities, and when the robot has run sideways enough, these last velocities recorded are + * averaged and printed. + */ + @Override + public void loop() { + if (gamepad1.bWasPressed()) { + stopRobot(); + requestOpModeStop(); + } + + follower.update(); + draw(); + + if (!end) { + if (Math.abs(follower.getPose().getY()) > (DISTANCE + 72)) { + end = true; + stopRobot(); + } else { + follower.setTeleOpDrive(0,1,0,true); + double currentVelocity = Math.abs(follower.getVelocity().dot(new Vector(1, Math.PI / 2))); + velocities.add(currentVelocity); + velocities.remove(0); + } + } else { + stopRobot(); + double average = 0; + for (double velocity : velocities) { + average += velocity; + } + average /= velocities.size(); + + telemetryM.debug("Strafe Velocity: " + average); + telemetryM.debug("\n"); + telemetryM.debug("Press A to set the Lateral Velocity temporarily (while robot remains on)."); + telemetryM.update(telemetry); + + if (gamepad1.aWasPressed()) { + follower.setYVelocity(average); + String message = "YMovement: " + average; + changes.add(message); + } + } + } +} + +/** + * This is the ForwardZeroPowerAccelerationTuner autonomous follower OpMode. This runs the robot + * forward until a specified velocity is achieved. Then, the robot cuts power to the motors, setting + * them to zero power. The deceleration, or negative acceleration, is then measured until the robot + * stops. The accelerations across the entire time the robot is slowing down is then averaged and + * that number is then printed. This is used to determine how the robot will decelerate in the + * forward direction when power is cut, making the estimations used in the calculations for the + * drive Vector more accurate and giving better braking at the end of Paths. + * + * @author Anyi Lin - 10158 Scott's Bots + * @author Baron Henderson - 20077 The Indubitables + * @author Aaron Yang - 10158 Scott's Bots + * @author Harrison Womack - 10158 Scott's Bots + * @version 1.0, 3/13/2024 + */ +class ForwardZeroPowerAccelerationTuner extends OpMode { + private final ArrayList accelerations = new ArrayList<>(); + public static double VELOCITY = 30; + + private double previousVelocity; + private long previousTimeNano; + + private boolean stopping; + private boolean end; + + @Override + public void init() { + follower.setStartingPose(new Pose(72, 72)); + } + + /** This initializes the drive motors as well as the Panels telemetryM. */ + @Override + public void init_loop() { + telemetryM.debug("The robot will run forward until it reaches " + VELOCITY + " inches per second."); + telemetryM.debug("Then, it will cut power from the drivetrain and roll to a stop."); + telemetryM.debug("Make sure you have enough room."); + telemetryM.debug("After stopping, the forward zero power acceleration (natural deceleration) will be displayed."); + telemetryM.debug("Press B on Gamepad 1 to stop."); + telemetryM.update(telemetry); + follower.update(); + drawOnlyCurrent(); + } + + /** This starts the OpMode by setting the drive motors to run forward at full power. */ + @Override + public void start() { + follower.startTeleopDrive(false); + follower.update(); + follower.setTeleOpDrive(1,0,0,true); + } + + /** + * This runs the OpMode. At any point during the running of the OpMode, pressing B on + * game pad 1 will stop the OpMode. When the robot hits the specified velocity, the robot will + * record its deceleration / negative acceleration until it stops. Then, it will average all the + * recorded deceleration / negative acceleration and print that value. + */ + @Override + public void loop() { + if (gamepad1.bWasPressed()) { + stopRobot(); + requestOpModeStop(); + } + + follower.update(); + draw(); + + Vector heading = new Vector(1.0, follower.getPose().getHeading()); + if (!end) { + if (!stopping) { + if (follower.getVelocity().dot(heading) > VELOCITY) { + previousVelocity = follower.getVelocity().dot(heading); + previousTimeNano = System.nanoTime(); + stopping = true; + follower.setTeleOpDrive(0,0,0,true); + } + } else { + double currentVelocity = follower.getVelocity().dot(heading); + accelerations.add((currentVelocity - previousVelocity) / ((System.nanoTime() - previousTimeNano) / Math.pow(10.0, 9))); + previousVelocity = currentVelocity; + previousTimeNano = System.nanoTime(); + if (currentVelocity < follower.getConstraints().getVelocityConstraint()) { + end = true; + } + } + } else { + double average = 0; + for (double acceleration : accelerations) { + average += acceleration; + } + average /= accelerations.size(); + + telemetryM.debug("Forward Zero Power Acceleration (Deceleration): " + average); + telemetryM.debug("\n"); + telemetryM.debug("Press A to set the Forward Zero Power Acceleration temporarily (while robot remains on)."); + telemetryM.update(telemetry); + + if (gamepad1.aWasPressed()) { + follower.getConstants().setForwardZeroPowerAcceleration(average); + String message = "Forward Zero Power Acceleration: " + average; + changes.add(message); + } + } + } +} + +/** + * This is the LateralZeroPowerAccelerationTuner autonomous follower OpMode. This runs the robot + * to the left until a specified velocity is achieved. Then, the robot cuts power to the motors, setting + * them to zero power. The deceleration, or negative acceleration, is then measured until the robot + * stops. The accelerations across the entire time the robot is slowing down is then averaged and + * that number is then printed. This is used to determine how the robot will decelerate in the + * forward direction when power is cut, making the estimations used in the calculations for the + * drive Vector more accurate and giving better braking at the end of Paths. + * + * @author Anyi Lin - 10158 Scott's Bots + * @author Aaron Yang - 10158 Scott's Bots + * @author Harrison Womack - 10158 Scott's Bots + * @author Baron Henderson - 20077 The Indubitables + * @version 1.0, 3/13/2024 + */ +class LateralZeroPowerAccelerationTuner extends OpMode { + private final ArrayList accelerations = new ArrayList<>(); + public static double VELOCITY = 30; + private double previousVelocity; + private long previousTimeNano; + private boolean stopping; + private boolean end; + + @Override + public void init() { + follower.setStartingPose(new Pose(72, 72)); + } + + /** This initializes the drive motors as well as the Panels telemetry. */ + @Override + public void init_loop() { + telemetryM.debug("The robot will run to the left until it reaches " + VELOCITY + " inches per second."); + telemetryM.debug("Then, it will cut power from the drivetrain and roll to a stop."); + telemetryM.debug("Make sure you have enough room."); + telemetryM.debug("After stopping, the lateral zero power acceleration (natural deceleration) will be displayed."); + telemetryM.debug("Press B on game pad 1 to stop."); + telemetryM.update(telemetry); + follower.update(); + drawOnlyCurrent(); + } + + /** This starts the OpMode by setting the drive motors to run forward at full power. */ + @Override + public void start() { + follower.startTeleopDrive(false); + follower.update(); + follower.setTeleOpDrive(0,1,0,true); + } + + /** + * This runs the OpMode. At any point during the running of the OpMode, pressing B on + * game pad 1 will stop the OpMode. When the robot hits the specified velocity, the robot will + * record its deceleration / negative acceleration until it stops. Then, it will average all the + * recorded deceleration / negative acceleration and print that value. + */ + @Override + public void loop() { + if (gamepad1.bWasPressed()) { + stopRobot(); + requestOpModeStop(); + } + + follower.update(); + draw(); + + Vector heading = new Vector(1.0, follower.getPose().getHeading() - Math.PI / 2); + if (!end) { + if (!stopping) { + if (Math.abs(follower.getVelocity().dot(heading)) > VELOCITY) { + previousVelocity = Math.abs(follower.getVelocity().dot(heading)); + previousTimeNano = System.nanoTime(); + stopping = true; + follower.setTeleOpDrive(0,0,0,true); + } + } else { + double currentVelocity = Math.abs(follower.getVelocity().dot(heading)); + accelerations.add((currentVelocity - previousVelocity) / ((System.nanoTime() - previousTimeNano) / Math.pow(10.0, 9))); + previousVelocity = currentVelocity; + previousTimeNano = System.nanoTime(); + if (currentVelocity < follower.getConstraints().getVelocityConstraint()) { + end = true; + } + } + } else { + double average = 0; + for (double acceleration : accelerations) { + average += acceleration; + } + average /= accelerations.size(); + + telemetryM.debug("Lateral Zero Power Acceleration (Deceleration): " + average); + telemetryM.debug("\n"); + telemetryM.debug("Press A to set the Lateral Zero Power Acceleration temporarily (while robot remains on)."); + telemetryM.update(telemetry); + + if (gamepad1.aWasPressed()) { + follower.getConstants().setLateralZeroPowerAcceleration(average); + String message = "Lateral Zero Power Acceleration: " + average; + changes.add(message); + } + } + } +} + +/** + * This is the Translational PIDF Tuner OpMode. It will keep the robot in place. + * The user should push the robot laterally to test the PIDF and adjust the PIDF values accordingly. + * + * @author Baron Henderson - 20077 The Indubitables + * @author Anyi Lin - 10158 Scott's Bots + * @author Aaron Yang - 10158 Scott's Bots + * @author Harrison Womack - 10158 Scott's Bots + * @version 1.0, 3/12/2024 + */ +class TranslationalTuner extends OpMode { + public static double DISTANCE = 40; + private boolean forward = true; + + private Path forwards; + private Path backwards; + + @Override + public void init() { + follower.setStartingPose(new Pose(72, 72)); + } + + /** This initializes the Follower and creates the forward and backward Paths. */ + @Override + public void init_loop() { + telemetryM.debug("This will activate the translational PIDF(s)"); + telemetryM.debug("The robot will try to stay in place while you push it laterally."); + telemetryM.debug("You can adjust the PIDF values to tune the robot's translational PIDF(s)."); + telemetryM.update(telemetry); + follower.update(); + drawOnlyCurrent(); + } + + @Override + public void start() { + follower.deactivateAllPIDFs(); + follower.activateTranslational(); + forwards = new Path(new BezierLine(new Pose(72,72), new Pose(DISTANCE + 72,72))); + forwards.setConstantHeadingInterpolation(0); + backwards = new Path(new BezierLine(new Pose(DISTANCE + 72,72), new Pose(72,72))); + backwards.setConstantHeadingInterpolation(0); + follower.followPath(forwards); + } + + /** This runs the OpMode, updating the Follower as well as printing out the debug statements to the Telemetry */ + @Override + public void loop() { + follower.update(); + draw(); + + if (!follower.isBusy()) { + if (forward) { + forward = false; + follower.followPath(backwards); + } else { + forward = true; + follower.followPath(forwards); + } + } + + telemetryM.debug("Push the robot laterally to test the Translational PIDF(s)."); + telemetryM.addData("Zero Line", 0); + telemetryM.addData("Error X", follower.errorCalculator.getTranslationalError().getXComponent()); + telemetryM.addData("Error Y", follower.errorCalculator.getTranslationalError().getYComponent()); + telemetryM.update(telemetry); + } +} + +/** + * This is the Heading PIDF Tuner OpMode. It will keep the robot in place. + * The user should try to turn the robot to test the PIDF and adjust the PIDF values accordingly. + * It will try to keep the robot at a constant heading while the user tries to turn it. + * + * @author Baron Henderson - 20077 The Indubitables + * @author Anyi Lin - 10158 Scott's Bots + * @author Aaron Yang - 10158 Scott's Bots + * @author Harrison Womack - 10158 Scott's Bots + * @version 1.0, 3/12/2024 + */ +class HeadingTuner extends OpMode { + public static double DISTANCE = 40; + private boolean forward = true; + + private Path forwards; + private Path backwards; + + @Override + public void init() { + follower.setStartingPose(new Pose(72, 72)); + } + + /** + * This initializes the Follower and creates the forward and backward Paths. Additionally, this + * initializes the Panels telemetry. + */ + @Override + public void init_loop() { + telemetryM.debug("This will activate the heading PIDF(s)."); + telemetryM.debug("The robot will try to stay at a constant heading while you try to turn it."); + telemetryM.debug("You can adjust the PIDF values to tune the robot's heading PIDF(s)."); + telemetryM.update(telemetry); + follower.update(); + drawOnlyCurrent(); + } + + @Override + public void start() { + follower.deactivateAllPIDFs(); + follower.activateHeading(); + forwards = new Path(new BezierLine(new Pose(72,72), new Pose(DISTANCE + 72,72))); + forwards.setConstantHeadingInterpolation(0); + backwards = new Path(new BezierLine(new Pose(DISTANCE + 72,72), new Pose(72,72))); + backwards.setConstantHeadingInterpolation(0); + follower.followPath(forwards); + } + + /** + * This runs the OpMode, updating the Follower as well as printing out the debug statements to + * the Telemetry, as well as the Panels. + */ + @Override + public void loop() { + follower.update(); + draw(); + + if (!follower.isBusy()) { + if (forward) { + forward = false; + follower.followPath(backwards); + } else { + forward = true; + follower.followPath(forwards); + } + } + + telemetryM.debug("Turn the robot manually to test the Heading PIDF(s)."); + telemetryM.addData("Zero Line", 0); + telemetryM.addData("Error", follower.errorCalculator.getHeadingError()); + telemetryM.update(telemetry); + } +} + +/** + * This is the Drive PIDF Tuner OpMode. It will run the robot in a straight line going forward and back. + * + * @author Baron Henderson - 20077 The Indubitables + * @author Anyi Lin - 10158 Scott's Bots + * @author Aaron Yang - 10158 Scott's Bots + * @author Harrison Womack - 10158 Scott's Bots + * @version 1.0, 3/12/2024 + */ +class DriveTuner extends OpMode { + public static double DISTANCE = 40; + private boolean forward = true; + + private PathChain forwards; + private PathChain backwards; + + @Override + public void init() { + follower.setStartingPose(new Pose(72, 72)); + } + + /** + * This initializes the Follower and creates the forward and backward Paths. Additionally, this + * initializes the Panels telemetry. + */ + @Override + public void init_loop() { + telemetryM.debug("This will run the robot in a straight line going " + DISTANCE + "inches forward."); + telemetryM.debug("The robot will go forward and backward continuously along the path."); + telemetryM.debug("Make sure you have enough room."); + telemetryM.update(telemetry); + follower.update(); + drawOnlyCurrent(); + } + + @Override + public void start() { + follower.deactivateAllPIDFs(); + follower.activateDrive(); + + forwards = follower.pathBuilder() + .setGlobalDeceleration() + .addPath(new BezierLine(new Pose(72,72), new Pose(DISTANCE + 72,72))) + .setConstantHeadingInterpolation(0) + .build(); + + backwards = follower.pathBuilder() + .setGlobalDeceleration() + .addPath(new BezierLine(new Pose(DISTANCE + 72,72), new Pose(72,72))) + .setConstantHeadingInterpolation(0) + .build(); + + follower.followPath(forwards); + } + + /** + * This runs the OpMode, updating the Follower as well as printing out the debug statements to + * the Telemetry, as well as the Panels. + */ + @Override + public void loop() { + follower.update(); + draw(); + + if (!follower.isBusy()) { + if (forward) { + forward = false; + follower.followPath(backwards); + } else { + forward = true; + follower.followPath(forwards); + } + } + + telemetryM.debug("Driving forward?: " + forward); + telemetryM.addData("Zero Line", 0); + telemetryM.addData("Error", follower.errorCalculator.getDriveErrors()[1]); + telemetryM.update(telemetry); + } +} + +/** + * This is the Line Test Tuner OpMode. It will drive the robot forward and back + * The user should push the robot laterally and angular to test out the drive, heading, and translational PIDFs. + * + * @author Baron Henderson - 20077 The Indubitables + * @author Anyi Lin - 10158 Scott's Bots + * @author Aaron Yang - 10158 Scott's Bots + * @author Harrison Womack - 10158 Scott's Bots + * @version 1.0, 3/12/2024 + */ +class Line extends OpMode { + public static double DISTANCE = 40; + private boolean forward = true; + + private Path forwards; + private Path backwards; + + @Override + public void init() { + follower.setStartingPose(new Pose(72, 72)); + } + + /** This initializes the Follower and creates the forward and backward Paths. */ + @Override + public void init_loop() { + telemetryM.debug("This will activate all the PIDF(s)"); + telemetryM.debug("The robot will go forward and backward continuously along the path while correcting."); + telemetryM.debug("You can adjust the PIDF values to tune the robot's drive PIDF(s)."); + telemetryM.update(telemetry); + follower.update(); + drawOnlyCurrent(); + } + + @Override + public void start() { + follower.activateAllPIDFs(); + forwards = new Path(new BezierLine(new Pose(72,72), new Pose(DISTANCE + 72,72))); + forwards.setConstantHeadingInterpolation(0); + backwards = new Path(new BezierLine(new Pose(DISTANCE + 72,72), new Pose(72,72))); + backwards.setConstantHeadingInterpolation(0); + follower.followPath(forwards); + } + + /** This runs the OpMode, updating the Follower as well as printing out the debug statements to the Telemetry */ + @Override + public void loop() { + follower.update(); + draw(); + + if (!follower.isBusy()) { + if (forward) { + forward = false; + follower.followPath(backwards); + } else { + forward = true; + follower.followPath(forwards); + } + } + + telemetryM.debug("Driving Forward?: " + forward); + telemetryM.update(telemetry); + } +} + +/** + * This is the Centripetal Tuner OpMode. It runs the robot in a specified distance + * forward and to the left. On reaching the end of the forward Path, the robot runs the backward + * Path the same distance back to the start. Rinse and repeat! This is good for testing a variety + * of Vectors, like the drive Vector, the translational Vector, the heading Vector, and the + * centripetal Vector. + * + * @author Baron Henderson - 20077 The Indubitables + * @author Anyi Lin - 10158 Scott's Bots + * @author Aaron Yang - 10158 Scott's Bots + * @author Harrison Womack - 10158 Scott's Bots + * @version 1.0, 3/13/2024 + */ +class CentripetalTuner extends OpMode { + public static double DISTANCE = 20; + private boolean forward = true; + + private Path forwards; + private Path backwards; + + @Override + public void init() { + follower.setStartingPose(new Pose(72, 72)); + } + + /** + * This initializes the Follower and creates the forward and backward Paths. + * Additionally, this initializes the Panels telemetry. + */ + @Override + public void init_loop() { + telemetryM.debug("This will run the robot in a curve going " + DISTANCE + " inches to the left and the same number of inches forward."); + telemetryM.debug("The robot will go continuously along the path."); + telemetryM.debug("Make sure you have enough room."); + telemetryM.update(telemetry); + follower.update(); + drawOnlyCurrent(); + } + + @Override + public void start() { + follower.activateAllPIDFs(); + forwards = new Path(new BezierCurve(new Pose(72,72), new Pose(Math.abs(DISTANCE) + 72,72), new Pose(Math.abs(DISTANCE) + 72,DISTANCE + 72))); + backwards = new Path(new BezierCurve(new Pose(Math.abs(DISTANCE) + 72,DISTANCE + 72), new Pose(Math.abs(DISTANCE) + 72,72), new Pose(72,72))); + + backwards.setTangentHeadingInterpolation(); + backwards.reverseHeadingInterpolation(); + + follower.followPath(forwards); + } + + /** + * This runs the OpMode, updating the Follower as well as printing out the debug statements to + * the Telemetry, as well as the Panels. + */ + @Override + public void loop() { + follower.update(); + draw(); + if (!follower.isBusy()) { + if (forward) { + forward = false; + follower.followPath(backwards); + } else { + forward = true; + follower.followPath(forwards); + } + } + + telemetryM.debug("Driving away from the origin along the curve?: " + forward); + telemetryM.update(telemetry); + } +} + +/** + * This is the Triangle autonomous OpMode. + * It runs the robot in a triangle, with the starting point being the bottom-middle point. + * + * @author Baron Henderson - 20077 The Indubitables + * @author Samarth Mahapatra - 1002 CircuitRunners Robotics Surge + * @version 1.0, 12/30/2024 + */ +class Triangle extends OpMode { + + private final Pose startPose = new Pose(72, 72, Math.toRadians(0)); + private final Pose interPose = new Pose(24 + 72, -24 + 72, Math.toRadians(90)); + private final Pose endPose = new Pose(24 + 72, 24 + 72, Math.toRadians(45)); + + private PathChain triangle; + + /** + * This runs the OpMode, updating the Follower as well as printing out the debug statements to + * the Telemetry, as well as the Panels. + */ + @Override + public void loop() { + follower.update(); + draw(); + + if (follower.atParametricEnd()) { + follower.followPath(triangle, true); + } + } + + @Override + public void init() { + follower.setStartingPose(new Pose(72, 72)); + } + + @Override + public void init_loop() { + telemetryM.debug("This will run in a roughly triangular shape, starting on the bottom-middle point."); + telemetryM.debug("So, make sure you have enough space to the left, front, and right to run the OpMode."); + telemetryM.update(telemetry); + follower.update(); + drawOnlyCurrent(); + } + + /** Creates the PathChain for the "triangle".*/ + @Override + public void start() { + follower.setStartingPose(startPose); + + triangle = follower.pathBuilder() + .addPath(new BezierLine(startPose, interPose)) + .setLinearHeadingInterpolation(startPose.getHeading(), interPose.getHeading()) + .addPath(new BezierLine(interPose, endPose)) + .setLinearHeadingInterpolation(interPose.getHeading(), endPose.getHeading()) + .addPath(new BezierLine(endPose, startPose)) + .setLinearHeadingInterpolation(endPose.getHeading(), startPose.getHeading()) + .build(); + + follower.followPath(triangle); + } +} + +/** + * This is the Circle autonomous OpMode. It runs the robot in a PathChain that's actually not quite + * a circle, but some Bezier curves that have control points set essentially in a square. However, + * it turns enough to tune your centripetal force correction and some of your heading. Some lag in + * heading is to be expected. + * + * @author Anyi Lin - 10158 Scott's Bots + * @author Aaron Yang - 10158 Scott's Bots + * @author Harrison Womack - 10158 Scott's Bots + * @version 1.0, 3/12/2024 + */ +class Circle extends OpMode { + public static double RADIUS = 10; + private PathChain circle; + + public void start() { + circle = follower.pathBuilder() + .addPath(new BezierCurve(new Pose(72, 72), new Pose(RADIUS + 72, 72), new Pose(RADIUS + 72, RADIUS + 72))) + .setHeadingInterpolation(HeadingInterpolator.facingPoint(72, RADIUS + 72)) + .addPath(new BezierCurve(new Pose(RADIUS + 72, RADIUS + 72), new Pose(RADIUS + 72, (2 * RADIUS) + 72), new Pose(72, (2 * RADIUS) + 72))) + .setHeadingInterpolation(HeadingInterpolator.facingPoint(72, RADIUS + 72)) + .addPath(new BezierCurve(new Pose(72, (2 * RADIUS) + 72), new Pose(-RADIUS + 72, (2 * RADIUS) + 72), new Pose(-RADIUS + 72, RADIUS + 72))) + .setHeadingInterpolation(HeadingInterpolator.facingPoint(72, RADIUS + 72)) + .addPath(new BezierCurve(new Pose(-RADIUS + 72, RADIUS + 72), new Pose(-RADIUS + 72, 72), new Pose(72, 72))) + .setHeadingInterpolation(HeadingInterpolator.facingPoint(72, RADIUS + 72)) + .build(); + follower.followPath(circle); + } + + @Override + public void init_loop() { + telemetryM.debug("This will run in a roughly circular shape of radius " + RADIUS + ", starting on the right-most edge. "); + telemetryM.debug("So, make sure you have enough space to the left, front, and back to run the OpMode."); + telemetryM.debug("It will also continuously face the center of the circle to test your heading and centripetal correction."); + telemetryM.update(telemetry); + follower.update(); + drawOnlyCurrent(); + } + + @Override + public void init() { + follower.setStartingPose(new Pose(72, 72)); + } + + /** + * This runs the OpMode, updating the Follower as well as printing out the debug statements to + * the Telemetry, as well as the FTC Dashboard. + */ + @Override + public void loop() { + follower.update(); + draw(); + + if (follower.atParametricEnd()) { + follower.followPath(circle); + } + } +} + +/** + * This is the OffsetsTuner OpMode. This tracks the movement of the robot as it turns 180 degrees, + * and calculates what the robot's strafeX and forwardY offsets should be. Ensure that your strafeX and forwardY offsets + * are set to 0 before running this OpMode. After running, input the displayed offsets into your localizer constants. + * + * @author Havish Sripada - 12808 RevAmped Robotics + * @author Baron Henderson + */ +class OffsetsTuner extends OpMode { + @Override + public void init() { + follower.setStartingPose(new Pose(72,72)); + follower.update(); + drawOnlyCurrent(); + } + + /** This initializes the PoseUpdater as well as the Panels telemetry. */ + @Override + public void init_loop() { + telemetryM.debug("Prerequisite: Make sure both your offsets are set to 0 in your localizer constants."); + telemetryM.debug("Turn your robot " + Math.PI + " radians. Your offsets in inches will be shown on the telemetry."); + telemetryM.update(telemetry); + + drawOnlyCurrent(); + } + + /** + * This updates the robot's pose estimate, and updates the Panels telemetry with the + * calculated offsets and draws the robot. + */ + @Override + public void loop() { + follower.update(); + + telemetryM.debug("Total Angle: " + follower.getTotalHeading()); + + telemetryM.debug("The following values are the offsets in inches that should be applied to your localizer."); + telemetryM.debug("strafeX: " + ((72.0-follower.getPose().getX()) / 2.0)); + telemetryM.debug("forwardY: " + ((72.0-follower.getPose().getY()) / 2.0)); + telemetryM.update(telemetry); + + draw(); + } +} + +/** + * This is the Predictive Braking Tuner. It runs the robot forward and backward at various power + * levels, recording the robot’s velocity and position immediately before braking. The motors are + * then set to zero-power brake mode, which represents the fastest theoretical braking the robot + * can achieve. Once the robot comes to a complete stop, the tuner measures the stopping distance. + * Using the collected data, it generates a velocity-vs-stopping-distance graph and fits a + * quadratic curve to model the braking behavior. + * + * @author Ashay Sarda - 19745 Turtle Walkers + * @author Jacob Ophoven - 18535 Frozen Code + * @version 1.0, 12/26/2025 + */ +class PredictiveBrakingTuner extends OpMode { + private static final double[] TEST_POWERS = + {1, 1, 1, 0.9, 0.9, 0.8, 0.7, 0.6, 0.5, 0.4, 0.3, 0.2}; + + private static final int DRIVE_TIME_MS = 1000; + private static final int BRAKE_WAIT_MS = 500; + + private enum State { + START_MOVE, + WAIT_DRIVE_TIME, + APPLY_BRAKE, + WAIT_BRAKE_TIME, + RECORD, + DONE + } + + private static class BrakeRecord { + double timeMs; + Pose pose; + double velocity; + + BrakeRecord(double timeMs, Pose pose, double velocity) { + this.timeMs = timeMs; + this.pose = pose; + this.velocity = velocity; + } + } + + private State state = State.START_MOVE; + + private final ElapsedTime timer = new ElapsedTime(); + + private int iteration = 0; + + private Vector startPosition; + private double measuredVelocity; + + private final List velocityToBrakingDistance = new ArrayList<>(); + private final List brakeData = new ArrayList<>(); + + @Override + public void init() {} + + @Override + public void init_loop() { + telemetryM.debug("The robot will move forwards and backwards starting at max speed and slowing down."); + telemetryM.debug("Make sure you have enough room. Leave at least 4-5 feet."); + telemetryM.debug("After stopping, kFriction and kBraking will be displayed."); + telemetryM.debug("Make sure to turn the timer off."); + telemetryM.debug("Press B on game pad 1 to stop."); + telemetryM.update(telemetry); + follower.update(); + drawOnlyCurrent(); + } + + @Override + public void start() { + timer.reset(); + follower.update(); + follower.startTeleOpDrive(true); + } + + @SuppressLint("DefaultLocale") + @Override + public void loop() { + follower.update(); + + if (gamepad1.b) { + stopRobot(); + requestOpModeStop(); + return; + } + + switch (state) { + case START_MOVE: { + if (iteration >= TEST_POWERS.length) { + state = State.DONE; + break; + } + + double currentPower = TEST_POWERS[iteration]; + follower.setMaxPower(currentPower); + if (iteration % 2 != 0) { + follower.setTeleOpDrive(-1, 0, 0, true); + } else { + follower.setTeleOpDrive(1, 0, 0, true); + } + + timer.reset(); + state = State.WAIT_DRIVE_TIME; + break; + } + + case WAIT_DRIVE_TIME: { + if (timer.milliseconds() >= DRIVE_TIME_MS) { + measuredVelocity = follower.getVelocity().getMagnitude(); + startPosition = follower.getPose().getAsVector(); + state = State.APPLY_BRAKE; + } + break; + } + + case APPLY_BRAKE: { + stopRobot(); + + timer.reset(); + state = State.WAIT_BRAKE_TIME; + break; + } + + case WAIT_BRAKE_TIME: { + double t = timer.milliseconds(); + Pose currentPose = follower.getPose(); + double currentVelocity = follower.getVelocity().getMagnitude(); + + brakeData.add(new BrakeRecord(t, currentPose, currentVelocity)); + + if (timer.milliseconds() >= BRAKE_WAIT_MS || follower.getVelocity().getMagnitude() <= .05) { + state = State.RECORD; + } + break; + } + + case RECORD: { + Vector endPosition = follower.getPose().getAsVector(); + double brakingDistance = endPosition.minus(startPosition).getMagnitude(); + + velocityToBrakingDistance.add(new double[]{measuredVelocity, brakingDistance}); + + telemetryM.debug("Test " + iteration, + String.format("v=%.3f d=%.3f", measuredVelocity, + brakingDistance)); + telemetryM.update(telemetry); + + iteration++; + state = State.START_MOVE; + + break; + } + + case DONE: { + stopRobot(); + + double[] coefficients = quadraticFit(velocityToBrakingDistance); + + telemetryM.debug("Tuning Complete"); + telemetryM.debug("Braking Profile:"); + telemetryM.debug("kQuadratic", coefficients[1]); + telemetryM.debug("kLinear", coefficients[0]); + telemetryM.update(telemetry); + telemetryM.debug("Tuning Complete"); + telemetryM.debug("Braking Profile:"); + telemetryM.debug("kQuadraticFriction", coefficients[1]); + telemetryM.debug("kLinearBraking", coefficients[0]); + for (BrakeRecord record : brakeData) { + Pose p = record.pose; + telemetryM.debug(String.format("t=%.0f ms, x=%.2f, y=%.2f, θ=%.2f, v=%.2f", + record.timeMs, p.getX(), p.getY(), + p.getHeading(), + record.velocity)); + } + telemetryM.update(); + break; + } + } + + telemetry.update(); + } +} + +/** + * This is the Drawing class. It handles the drawing of stuff on Panels Dashboard, like the robot. + * + * @author Lazar - 19234 + * @version 1.1, 5/19/2025 + */ +class Drawing { + public static final double ROBOT_RADIUS = 9; // woah + private static final FieldManager panelsField = PanelsField.INSTANCE.getField(); + + private static final Style robotLook = new Style( + "", "#3F51B5", 0.75 + ); + private static final Style historyLook = new Style( + "", "#4CAF50", 0.75 + ); + + /** + * This prepares Panels Field for using Pedro Offsets + */ + public static void init() { + panelsField.setOffsets(PanelsField.INSTANCE.getPresets().getPEDRO_PATHING()); + } + + /** + * This draws everything that will be used in the Follower's telemetryDebug() method. This takes + * a Follower as an input, so an instance of the DashbaordDrawingHandler class is not needed. + * + * @param follower Pedro Follower instance. + */ + public static void drawDebug(Follower follower) { + if (follower.getCurrentPath() != null) { + drawPath(follower.getCurrentPath(), robotLook); + Pose closestPoint = follower.getPointFromPath(follower.getCurrentPath().getClosestPointTValue()); + drawRobot(new Pose(closestPoint.getX(), closestPoint.getY(), follower.getCurrentPath().getHeadingGoal(follower.getCurrentPath().getClosestPointTValue())), robotLook); + } + drawPoseHistory(follower.getPoseHistory(), historyLook); + drawRobot(follower.getPose(), historyLook); + + sendPacket(); + } + + /** + * This draws a robot at a specified Pose with a specified + * look. The heading is represented as a line. + * + * @param pose the Pose to draw the robot at + * @param style the parameters used to draw the robot with + */ + public static void drawRobot(Pose pose, Style style) { + if (pose == null || Double.isNaN(pose.getX()) || Double.isNaN(pose.getY()) || Double.isNaN(pose.getHeading())) { + return; + } + + panelsField.setStyle(style); + panelsField.moveCursor(pose.getX(), pose.getY()); + panelsField.circle(ROBOT_RADIUS); + + Vector v = pose.getHeadingAsUnitVector(); + v.setMagnitude(v.getMagnitude() * ROBOT_RADIUS); + double x1 = pose.getX() + v.getXComponent() / 2, y1 = pose.getY() + v.getYComponent() / 2; + double x2 = pose.getX() + v.getXComponent(), y2 = pose.getY() + v.getYComponent(); + + panelsField.setStyle(style); + panelsField.moveCursor(x1, y1); + panelsField.line(x2, y2); + } + + /** + * This draws a robot at a specified Pose. The heading is represented as a line. + * + * @param pose the Pose to draw the robot at + */ + public static void drawRobot(Pose pose) { + drawRobot(pose, robotLook); + } + + /** + * This draws a Path with a specified look. + * + * @param path the Path to draw + * @param style the parameters used to draw the Path with + */ + public static void drawPath(Path path, Style style) { + double[][] points = path.getPanelsDrawingPoints(); + + for (int i = 0; i < points[0].length; i++) { + for (int j = 0; j < points.length; j++) { + if (Double.isNaN(points[j][i])) { + points[j][i] = 0; + } + } + } + + panelsField.setStyle(style); + panelsField.moveCursor(points[0][0], points[0][1]); + panelsField.line(points[1][0], points[1][1]); + } + + /** + * This draws all the Paths in a PathChain with a + * specified look. + * + * @param pathChain the PathChain to draw + * @param style the parameters used to draw the PathChain with + */ + public static void drawPath(PathChain pathChain, Style style) { + for (int i = 0; i < pathChain.size(); i++) { + drawPath(pathChain.getPath(i), style); + } + } + + /** + * This draws the pose history of the robot. + * + * @param poseTracker the PoseHistory to get the pose history from + * @param style the parameters used to draw the pose history with + */ + public static void drawPoseHistory(PoseHistory poseTracker, Style style) { + panelsField.setStyle(style); + + int size = poseTracker.getXPositionsArray().length; + for (int i = 0; i < size - 1; i++) { + + panelsField.moveCursor(poseTracker.getXPositionsArray()[i], poseTracker.getYPositionsArray()[i]); + panelsField.line(poseTracker.getXPositionsArray()[i + 1], poseTracker.getYPositionsArray()[i + 1]); + } + } + + /** + * This draws the pose history of the robot. + * + * @param poseTracker the PoseHistory to get the pose history from + */ + public static void drawPoseHistory(PoseHistory poseTracker) { + drawPoseHistory(poseTracker, historyLook); + } + + /** + * This tries to send the current packet to FTControl Panels. + */ + public static void sendPacket() { + panelsField.update(); + } +} diff --git a/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/readme.md b/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/readme.md new file mode 100644 index 0000000..4d1da42 --- /dev/null +++ b/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/readme.md @@ -0,0 +1,131 @@ +## TeamCode Module + +Welcome! + +This module, TeamCode, is the place where you will write/paste the code for your team's +robot controller App. This module is currently empty (a clean slate) but the +process for adding OpModes is straightforward. + +## Creating your own OpModes + +The easiest way to create your own OpMode is to copy a Sample OpMode and make it your own. + +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 + +### Naming of Samples + +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 + 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. + +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. 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 +your TeamCode module to be used. + +This is done inside Android Studio directly, using the following steps: + + 1) Locate the desired sample class in the Project/Android tree. + + 2) Right click on the sample class and select "Copy" + + 3) Expand the TeamCode/java folder + + 4) Right click on the org.firstinspires.ftc.teamcode folder and select "Paste" + + 5) You will be prompted for a class name for the copy. + Choose something meaningful based on the purpose of this class. + Start with a capital letter, and remember that there may be more similar classes later. + +Once your copy has been created, you should prepare it for use on your robot. +This is done by adjusting the OpMode's name, and enabling it to be displayed on the +Driver Station's OpMode list. + +Each OpMode sample class begins with several lines of code like the ones shown below: + +``` + @TeleOp(name="Template: Linear OpMode", group="Linear Opmode") + @Disabled +``` + +The name that will appear on the driver station's "opmode list" is defined by the code: + ``name="Template: Linear OpMode"`` +You can change what appears between the quotes to better describe your opmode. +The "group=" portion of the code can be used to help organize your list of OpModes. + +As shown, the current OpMode will NOT appear on the driver station's OpMode list because of the + ``@Disabled`` annotation which has been included. +This line can simply be deleted , or commented out, to make the OpMode visible. + + + +## ADVANCED Multi-Team App management: Cloning the TeamCode Module + +In some situations, you have multiple teams in your club and you want them to all share +a common code organization, with each being able to *see* the others code but each having +their own team module with their own code that they maintain themselves. + +In this situation, you might wish to clone the TeamCode module, once for each of these teams. +Each of the clones would then appear along side each other in the Android Studio module list, +together with the FtcRobotController module (and the original TeamCode module). + +Selective Team phones can then be programmed by selecting the desired Module from the pulldown list +prior to clicking to the green Run arrow. + +Warning: This is not for the inexperienced Software developer. +You will need to be comfortable with File manipulations and managing Android Studio Modules. +These changes are performed OUTSIDE of Android Studios, so close Android Studios before you do this. + +Also.. Make a full project backup before you start this :) + +To clone TeamCode, do the following: + +Note: Some names start with "Team" and others start with "team". This is intentional. + +1) Using your operating system file management tools, copy the whole "TeamCode" + folder to a sibling folder with a corresponding new name, eg: "Team0417". + +2) In the new Team0417 folder, delete the TeamCode.iml file. + +3) the new Team0417 folder, rename the "src/main/java/org/firstinspires/ftc/teamcode" folder + to a matching name with a lowercase 'team' eg: "team0417". + +4) In the new Team0417/src/main folder, edit the "AndroidManifest.xml" file, change the line that contains + package="org.firstinspires.ftc.teamcode" + to be + package="org.firstinspires.ftc.team0417" + +5) Add: include ':Team0417' to the "/settings.gradle" file. + +6) Open up Android Studios and clean out any old files by using the menu to "Build/Clean Project"" \ No newline at end of file diff --git a/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/subsys/Drivetrain.java b/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/subsys/Drivetrain.java new file mode 100644 index 0000000..6cc342c --- /dev/null +++ b/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/subsys/Drivetrain.java @@ -0,0 +1,94 @@ +package org.firstinspires.ftc.teamcode.subsys; + +import com.qualcomm.robotcore.hardware.DcMotor; +import com.qualcomm.robotcore.hardware.HardwareMap; +import org.firstinspires.ftc.robotcore.external.Telemetry; + +import org.firstinspires.ftc.teamcode.teleOp.Constants; +import org.firstinspires.ftc.teamcode.lib.Subsystem; + +public class Drivetrain extends Subsystem { + + // Subsystem Micro-States + public enum DriveState { + NORMAL, + PRECISION, + LOCKED // Safety state to halt motors instantly + } + + private DriveState currentState = DriveState.NORMAL; + + private DcMotor frontLeft, frontRight, backLeft, backRight; + + private double driveY = 0.0; + private double driveX = 0.0; + private double driveTurn = 0.0; + + @Override + public void init(HardwareMap hwMap) { + frontLeft = hwMap.get(DcMotor.class, Constants.DRIVE.FL_NAME); + frontRight = hwMap.get(DcMotor.class, Constants.DRIVE.FR_NAME); + backLeft = hwMap.get(DcMotor.class, Constants.DRIVE.BL_NAME); + backRight = hwMap.get(DcMotor.class, Constants.DRIVE.BR_NAME); + + frontRight.setDirection(DcMotor.Direction.REVERSE); + backRight.setDirection(DcMotor.Direction.REVERSE); + + frontLeft.setZeroPowerBehavior(DcMotor.ZeroPowerBehavior.BRAKE); + frontRight.setZeroPowerBehavior(DcMotor.ZeroPowerBehavior.BRAKE); + backLeft.setZeroPowerBehavior(DcMotor.ZeroPowerBehavior.BRAKE); + backRight.setZeroPowerBehavior(DcMotor.ZeroPowerBehavior.BRAKE); + } + + // Command Hooks for the OpMode to control this subsystem + public void setTargetState(DriveState newState) { + this.currentState = newState; + } + + public void setDriveVectors(double y, double x, double turn) { + this.driveY = y; + this.driveX = x; + this.driveTurn = turn; + } + + @Override + public void update() { + if (currentState == DriveState.LOCKED) { + frontLeft.setPower(0); frontRight.setPower(0); + backLeft.setPower(0); backRight.setPower(0); + return; + } + + double speedMultiplier = (currentState == DriveState.PRECISION) + ? Constants.DRIVE.PRECISION_SPEED + : Constants.DRIVE.NORMAL_SPEED; + + double flPower = (driveY + driveX + driveTurn) * speedMultiplier; + double frPower = (driveY - driveX - driveTurn) * speedMultiplier; + double blPower = (driveY - driveX + driveTurn) * speedMultiplier; + double brPower = (driveY + driveX - driveTurn) * speedMultiplier; + + // Normalization (prevents math outputting numbers > 1.0) + double max = Math.max(Math.abs(flPower), Math.abs(frPower)); + max = Math.max(max, Math.abs(blPower)); + max = Math.max(max, Math.abs(brPower)); + + if (max > 1.0) { + flPower /= max; frPower /= max; + blPower /= max; brPower /= max; + } + + frontLeft.setPower(flPower); + frontRight.setPower(frPower); + backLeft.setPower(blPower); + backRight.setPower(brPower); + } + + @Override + public void publishTelemetry(Telemetry telemetry) { + if (Constants.GLOBAL.DEBUG_MODE) { + telemetry.addData("Drive State", currentState.toString()); + telemetry.addData("Inputs (Y/X/T)", String.format("%.2f / %.2f / %.2f", driveY, driveX, driveTurn)); + } + } +} \ No newline at end of file diff --git a/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/teleOp/Constants.java b/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/teleOp/Constants.java new file mode 100644 index 0000000..ac81a53 --- /dev/null +++ b/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/teleOp/Constants.java @@ -0,0 +1,27 @@ +package org.firstinspires.ftc.teamcode.teleOp; + +import com.bylazar.configurables.annotations.Configurable; + +@Configurable +public class Constants { + + @Configurable + public static class GLOBAL { + public static double TELEMETRY_DELAY_MS = 250.0; + public static boolean DEBUG_MODE = true; + public static int TARGET_FPS = 0; // No target FPS (UNCAPPED) + } + + @Configurable + public static class DRIVE { + // Hardware Names + public static final String FL_NAME = "fl"; + public static final String FR_NAME = "fr"; + public static final String BL_NAME = "bl"; + public static final String BR_NAME = "br"; + + // Speed Constants + public static double NORMAL_SPEED = 0.6; + public static double PRECISION_SPEED = 0.35; + } +} \ No newline at end of file diff --git a/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/teleOp/teleOp.java b/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/teleOp/teleOp.java new file mode 100644 index 0000000..9d8e830 --- /dev/null +++ b/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/teleOp/teleOp.java @@ -0,0 +1,56 @@ +package org.firstinspires.ftc.teamcode.teleOp; + +import com.qualcomm.robotcore.eventloop.opmode.TeleOp; +import org.firstinspires.ftc.teamcode.lib.BaseOpMode; +import org.firstinspires.ftc.teamcode.subsys.Drivetrain; +import org.firstinspires.ftc.teamcode.util.AutoTransfer; + +@TeleOp(name = "Competition TeleOp", group = "Main") +public class teleOp extends BaseOpMode { + + public enum GlobalState { + IDLE, + SCORING + } + + private GlobalState robotState = GlobalState.IDLE; + private Drivetrain drive; + + @Override + protected void setupSubsystems() { + drive = new Drivetrain(); + manager.register(drive); + } + + @Override + protected void onInit() { + if (AutoTransfer.hasValidData()) { + unifiedTelemetry.addData("Loaded Alliance", AutoTransfer.getAllianceString()); + unifiedTelemetry.update(); + } + } + + @Override + protected void stateMachineUpdate() { + drive.setDriveVectors(-gamepad1.left_stick_y, gamepad1.left_stick_x, gamepad1.right_stick_x); + + switch (robotState) { + + case IDLE: + drive.setTargetState(Drivetrain.DriveState.NORMAL); + + if (gamepad1.aWasPressed()) { + robotState = GlobalState.SCORING; + } + break; + + case SCORING: + drive.setTargetState(Drivetrain.DriveState.PRECISION); + + if (gamepad1.bWasPressed()) { + robotState = GlobalState.IDLE; + } + break; + } + } +} \ No newline at end of file diff --git a/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/util/AutoTransfer.java b/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/util/AutoTransfer.java new file mode 100755 index 0000000..fc91998 --- /dev/null +++ b/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/util/AutoTransfer.java @@ -0,0 +1,55 @@ +package org.firstinspires.ftc.teamcode.util; + +import com.pedropathing.geometry.Pose; + +public class AutoTransfer { + + public static Pose transferPose = new Pose(0, 0, 0); + + public static int alliance = 0; + + public static int motifPattern = 0; + + public static boolean isAutonRan = false; + + public static void reset() { + transferPose = new Pose(0, 0, 0); + alliance = 0; + motifPattern = 0; + isAutonRan = false; + } + + public static void initAuton(int allianceColor) { + reset(); + alliance = allianceColor; + isAutonRan = true; + } + + public static void setMotifPattern(int pattern) { + motifPattern = pattern; + } + + public static void updatePose(Pose pose) { + transferPose = pose; + } + + public static String getAllianceString() { + switch (alliance) { + case 1: return "BLUE"; + case 2: return "RED"; + default: return "UNKNOWN"; + } + } + public static String getMotifString() { + switch (motifPattern) { + case 1: return "Pattern 1 (GPP)"; + case 2: return "Pattern 2 (PGP)"; + case 3: return "Pattern 3 (PPG)"; + default: return "UNKNOWN"; + } + } + + public static boolean hasValidData() { + return isAutonRan && alliance != 0; + } +} \ No newline at end of file diff --git a/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/util/FPSCounter.java b/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/util/FPSCounter.java new file mode 100755 index 0000000..659813e --- /dev/null +++ b/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/util/FPSCounter.java @@ -0,0 +1,62 @@ +package org.firstinspires.ftc.teamcode.util; + +import com.qualcomm.robotcore.util.ElapsedTime; + +public class FPSCounter { + private final ElapsedTime loopTimer = new ElapsedTime(); + private double loopTimeSum = 0; + private int loopCount = 0; + private double currentLoopTime = 0; + private double lastTime = 0; + + public FPSCounter() { + reset(); + } + + public void startLoop() { + double now = loopTimer.milliseconds(); + if (loopCount > 0) { + currentLoopTime = now - lastTime; + loopTimeSum += currentLoopTime; + } + lastTime = now; + loopCount++; + } + + public long getSyncTime(int targetFps) { + if (targetFps <= 0) return 0; + + double targetMs = 1000.0 / targetFps; + double now = loopTimer.milliseconds(); + double elapsedWorkMs = now - lastTime; + double remainingMs = targetMs - elapsedWorkMs; + + return (remainingMs > 0) ? (long) remainingMs : 0; + } + + public double getCurrentFps() { + return (currentLoopTime > 0) ? 1000.0 / currentLoopTime : 0; + } + + public double getAverageFps() { + if (loopCount <= 1) return 0; + return 1000.0 / (loopTimeSum / (loopCount - 1)); + } + + public double getCurrentLoopTime() { + return currentLoopTime; + } + + public double getAverageLoopTime() { + if (loopCount <= 1) return 0; + return loopTimeSum / (loopCount - 1); + } + + public void reset() { + loopTimer.reset(); + lastTime = 0; + loopTimeSum = 0; + loopCount = 0; + currentLoopTime = 0; + } +} diff --git a/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/util/OATDrawing.java b/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/util/OATDrawing.java new file mode 100755 index 0000000..0c42eb0 --- /dev/null +++ b/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/util/OATDrawing.java @@ -0,0 +1,58 @@ +package org.firstinspires.ftc.teamcode.util; + +import com.bylazar.field.FieldManager; +import com.bylazar.field.PanelsField; +import com.bylazar.field.Style; +import com.pedropathing.geometry.*; +import com.pedropathing.math.*; +import com.pedropathing.paths.*; + +public class OATDrawing { + public static final double ROBOT_RADIUS = 9; + private static final FieldManager panelsField = PanelsField.INSTANCE.getField(); + + private static final Style robotLook = new Style("", "#3F51B5", 0.75); + + private static final Style goalLook = new Style("", "#F44336", 0.9); + + private static final Style vectorLook = new Style("", "#4CAF50", 0.8); + + private static final Style visionLook = new Style("", "#FFD700", 0.9); + + public static void init() { + panelsField.setOffsets(PanelsField.INSTANCE.getPresets().getPEDRO_PATHING()); + } + public static void drawOATState(Pose robotPose, Pose goalPose, boolean hasVisionLock) { + if (robotPose == null || goalPose == null) return; + + drawRobot(robotPose, robotLook); + + panelsField.setStyle(goalLook); + panelsField.moveCursor(goalPose.getX(), goalPose.getY()); + panelsField.circle(4); + + panelsField.setStyle(hasVisionLock ? visionLook : vectorLook); + panelsField.moveCursor(robotPose.getX(), robotPose.getY()); + panelsField.line(goalPose.getX(), goalPose.getY()); + + panelsField.update(); + } + + private static void drawRobot(Pose pose, Style style) { + if (pose == null || Double.isNaN(pose.getX()) || Double.isNaN(pose.getY()) || Double.isNaN(pose.getHeading())) { + return; + } + + panelsField.setStyle(style); + panelsField.moveCursor(pose.getX(), pose.getY()); + panelsField.circle(ROBOT_RADIUS); + + Vector v = pose.getHeadingAsUnitVector(); + v.setMagnitude(v.getMagnitude() * ROBOT_RADIUS); + double x2 = pose.getX() + v.getXComponent(); + double y2 = pose.getY() + v.getYComponent(); + + panelsField.moveCursor(pose.getX(), pose.getY()); + panelsField.line(x2, y2); + } +} \ No newline at end of file diff --git a/TeamCode/src/main/res/raw/readme.md b/TeamCode/src/main/res/raw/readme.md new file mode 100644 index 0000000..355a3c4 --- /dev/null +++ b/TeamCode/src/main/res/raw/readme.md @@ -0,0 +1 @@ +Place your sound files in this folder to use them as project resources. \ No newline at end of file diff --git a/TeamCode/src/main/res/values/strings.xml b/TeamCode/src/main/res/values/strings.xml new file mode 100644 index 0000000..d781ec5 --- /dev/null +++ b/TeamCode/src/main/res/values/strings.xml @@ -0,0 +1,4 @@ + + + + diff --git a/TeamCode/src/main/res/xml/teamwebcamcalibrations.xml b/TeamCode/src/main/res/xml/teamwebcamcalibrations.xml new file mode 100644 index 0000000..22ae7a8 --- /dev/null +++ b/TeamCode/src/main/res/xml/teamwebcamcalibrations.xml @@ -0,0 +1,161 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/build.common.gradle b/build.common.gradle new file mode 100644 index 0000000..160f84f --- /dev/null +++ b/build.common.gradle @@ -0,0 +1,117 @@ +/** + * build.common.gradle + * + * Try to avoid editing this file, as it may be updated from time to time as the FTC SDK + * evolves. Rather, if it is necessary to customize the build process, do those edits in + * the build.gradle file in TeamCode. + * + * This file contains the necessary content of the 'build.gradle' files for robot controller + * applications built using the FTC SDK. Each individual 'build.gradle' in those applications + * can simply contain the one line: + * + * apply from: '../build.common.gradle' + * + * which will pick up this file here. This approach allows makes it easier to integrate + * updates to the FTC SDK into your code. + */ + +import java.util.regex.Pattern + +apply plugin: 'com.android.application' + +android { + + compileSdk 34 + + signingConfigs { + release { + def apkStoreFile = System.getenv("APK_SIGNING_STORE_FILE") + if (apkStoreFile != null) { + keyAlias System.getenv("APK_SIGNING_KEY_ALIAS") + keyPassword System.getenv("APK_SIGNING_KEY_PASSWORD") + storeFile file(System.getenv("APK_SIGNING_STORE_FILE")) + storePassword System.getenv("APK_SIGNING_STORE_PASSWORD") + } else { + keyAlias 'androiddebugkey' + keyPassword 'android' + storeFile rootProject.file('libs/ftc.debug.keystore') + storePassword 'android' + } + } + + debug { + keyAlias 'androiddebugkey' + keyPassword 'android' + storeFile rootProject.file('libs/ftc.debug.keystore') + storePassword 'android' + } + } + + defaultConfig { + signingConfig signingConfigs.debug + applicationId 'com.qualcomm.ftcrobotcontroller' + minSdkVersion 24 + //noinspection ExpiredTargetSdkVersion + targetSdkVersion 28 + + /** + * We keep the versionCode and versionName of robot controller applications in sync with + * the master information published in the AndroidManifest.xml file of the FtcRobotController + * module. This helps avoid confusion that might arise from having multiple versions of + * a robot controller app simultaneously installed on a robot controller device. + * + * We accomplish this with the help of a funky little Groovy script that maintains that + * correspondence automatically. + * + * @see Configure Your Build + * @see Versioning Your App + */ + def manifestFile = project(':FtcRobotController').file('src/main/AndroidManifest.xml'); + def manifestText = manifestFile.getText() + // + def vCodePattern = Pattern.compile("versionCode=\"(\\d+(\\.\\d+)*)\"") + def matcher = vCodePattern.matcher(manifestText) + matcher.find() + def vCode = Integer.parseInt(matcher.group(1)) + // + def vNamePattern = Pattern.compile("versionName=\"(.*)\"") + matcher = vNamePattern.matcher(manifestText); + matcher.find() + def vName = matcher.group(1) + // + versionCode vCode + versionName vName + } + + // http://google.github.io/android-gradle-dsl/current/com.android.build.gradle.internal.dsl.BuildType.html + buildTypes { + release { + signingConfig signingConfigs.release + + ndk { + abiFilters "armeabi-v7a", "arm64-v8a" + } + } + debug { + debuggable true + jniDebuggable true + ndk { + abiFilters "armeabi-v7a", "arm64-v8a" + } + } + } + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + + packagingOptions { + pickFirst '**/*.so' + } + ndkVersion '21.3.6528147' +} + +repositories { +} + diff --git a/build.dependencies.gradle b/build.dependencies.gradle new file mode 100644 index 0000000..8a0a2b4 --- /dev/null +++ b/build.dependencies.gradle @@ -0,0 +1,22 @@ +repositories { + mavenCentral() + google() // Needed for androidx + maven { url = "https://mymaven.bylazar.com/releases" } +} + +dependencies { + implementation 'org.firstinspires.ftc:Inspection:11.1.0' + implementation 'org.firstinspires.ftc:Blocks:11.1.0' + implementation 'org.firstinspires.ftc:RobotCore:11.1.0' + implementation 'org.firstinspires.ftc:RobotServer:11.1.0' + implementation 'org.firstinspires.ftc:OnBotJava:11.1.0' + implementation 'org.firstinspires.ftc:Hardware:11.1.0' + implementation 'org.firstinspires.ftc:FtcCommon:11.1.0' + implementation 'org.firstinspires.ftc:Vision:11.1.0' + implementation 'androidx.appcompat:appcompat:1.2.0' + + implementation 'com.pedropathing:ftc:2.1.0' + implementation 'com.pedropathing:telemetry:1.0.0' + implementation 'com.bylazar:fullpanels:1.0.12' +} + diff --git a/build.gradle b/build.gradle new file mode 100644 index 0000000..e70f209 --- /dev/null +++ b/build.gradle @@ -0,0 +1,29 @@ +/** + * Top-level build file for ftc_app project. + * + * It is extraordinarily rare that you will ever need to edit this file. + */ + +buildscript { + repositories { + mavenCentral() + google() + } + dependencies { + // Note for FTC Teams: Do not modify this yourself. + classpath 'com.android.tools.build:gradle:8.7.0' + } +} + +// This is now required because aapt2 has to be downloaded from the +// google() repository beginning with version 3.2 of the Android Gradle Plugin +allprojects { + repositories { + mavenCentral() + google() + } +} + +repositories { + mavenCentral() +} diff --git a/doc/legal/AudioBlocksSounds.txt b/doc/legal/AudioBlocksSounds.txt new file mode 100644 index 0000000..4eab3bc --- /dev/null +++ b/doc/legal/AudioBlocksSounds.txt @@ -0,0 +1,21 @@ +The sound files listed below in this SDK were purchased from www.audioblocks.com under the +following license. + + http://support.audioblocks.com/customer/en/portal/topics/610636-licensing-faq-s/articles + + How am I allowed to use your content? + Last Updated: Aug 11, 2016 01:51PM EDT + Our content may be used for nearly any project, commercial or otherwise, including feature + films, broadcast, commercial, industrial, educational video, print projects, multimedia, + games, and the internet, so long as substantial value is added to the content. (For example, + incorporating an audio clip into a commercial qualifies, while reposting our audio clip on + YouTube with no modification or no visual component does not.) Once you download a file it is + yours to keep and use forever, royalty- free, even if you change your subscription or cancel + your account. + +List of applicable sound files + + chimeconnect.wav + chimedisconnect.wav + errormessage.wav + warningmessage.wav \ No newline at end of file diff --git a/doc/legal/Exhibit A - LEGO Open Source License Agreement.txt b/doc/legal/Exhibit A - LEGO Open Source License Agreement.txt new file mode 100644 index 0000000..10c13b9 --- /dev/null +++ b/doc/legal/Exhibit A - LEGO Open Source License Agreement.txt @@ -0,0 +1,15 @@ +EXHIBIT A - LEGO® Open Source License Agreement + +The contents of the file 'nxtstartupsound.wav' contained in this SDK are subject to the +LEGO® Open Source License Agreement Version 1.0 (the "License"); you may not use this +file except in compliance with the License. You may obtain a copy of the License +at "LEGO Open Source License.pdf" contained in the same directory as this exhibit. + +Software distributed under the License is distributed on an "AS IS" basis, WITHOUT +WARRANTY OF ANY KIND, either express or implied. See the License for the specific +language governing rights and limitations under the License. + +The Original Code is \AT91SAM7S256\Resource\SOUNDS\!Startup.rso. +LEGO is the owner of the Original Code. Portions created by Robert Atkinson are +Copyright (C) 2015. All Rights Reserved. +Contributor(s): Robert Atkinson. \ No newline at end of file diff --git a/doc/legal/LEGO Open Source License.pdf b/doc/legal/LEGO Open Source License.pdf new file mode 100644 index 0000000000000000000000000000000000000000..9188498471221a53237117c43b1630ed8c605358 GIT binary patch literal 34100 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≪93VoD#r=U^w|{NX(M#v$3KBH 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 QlVhh4X4WvG$sO!}02-HC^Z)<= literal 0 HcmV?d00001 diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 0000000..f5935e9 --- /dev/null +++ b/gradle.properties @@ -0,0 +1,12 @@ +# AndroidX package structure to make it clearer which packages are bundled with the +# Android operating system, and which are packaged with your app's APK +# https://developer.android.com/topic/libraries/support-library/androidx-rn +android.useAndroidX=true + +# We no longer need to auto-convert third-party libraries to use AndroidX, which slowed down the build +android.enableJetifier=false + +# Allow Gradle to use up to 1 GB of RAM +org.gradle.jvmargs=-Xmx1024M + +android.nonTransitiveRClass=false \ No newline at end of file diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..f3d88b1c2faf2fc91d853cd5d4242b5547257070 GIT binary patch literal 58695 zcma&OV~}Oh(k5J8>Mq;vvTfV8ZQE5{wr$(iDciPf+tV}m-if*I+;_h3N1nY;M6TF7 zBc7A_WUgl&IY|&uNFbnJzkq;%`2QLZ5b*!{1OkHidzBVe;-?mu5upVElKVGD>pC88 zzP}E3wRHBgaO?2nzdZ5pL;m-xf&RU>buj(E-s=DK zf%>P9se`_emGS@673tqyT^;o8?2H}$uO&&u^TlmHfPgSSfPiTK^AZ7DTPH`Szw4#- z&21E&^c|dx9f;^@46XDX9itS+ZRYuqx#wG*>5Bs&gxwSQbj8grds#xkl;ikls1%(2 zR-`Tn(#9}E_aQ!zu~_iyc0gXp2I`O?erY?=JK{M`Ew(*RP3vy^0=b2E0^PSZgm(P6 z+U<&w#)I=>0z=IC4 zh4Q;eq94OGttUh7AGWu7m){;^Qk*5F6eTn+Ky$x>9Ntl~n0KDzFmB0lBI6?o!({iX zQt=|-9TPjAmCP!eA{r|^71cIvI(1#UCSzPw(L2>8OG0O_RQeJ{{MG)tLQ*aSX{AMS zP-;|nj+9{J&c9UV5Ww|#OE*Ah6?9WaR?B04N|#`m0G-IqwdN~Z{8)!$@UsK>l9H81 z?z`Z@`dWZEvuABvItgYLk-FA(u-$4mfW@2(Eh(9fe`5?WUda#wQa54 z3dXE&-*@lsrR~U#4NqkGM7Yu4#pfGqAmxmGr&Ep?&MwQ9?Z*twtODbi;vK|nQ~d_N z;T5Gtj_HZKu&oTfqQ~i`K!L||U1U=EfW@FzKSx!_`brOs#}9d(!Cu>cN51(FstP_2dJh>IHldL~vIwjZChS-*KcKk5Gz zyoiecAu;ImgF&DPrY6!68)9CM-S8*T5$damK&KdK4S6yg#i9%YBH>Yuw0f280eAv3 za@9e0+I>F}6&QZE5*T8$5__$L>39+GL+Q(}j71dS!_w%B5BdDS56%xX1~(pKYRjT; zbVy6V@Go&vbd_OzK^&!o{)$xIfnHbMJZMOo``vQfBpg7dzc^+&gfh7_=oxk5n(SO3 zr$pV6O0%ZXyK~yn++5#x`M^HzFb3N>Vb-4J%(TAy#3qjo2RzzD*|8Y} z7fEdoY5x9b3idE~-!45v?HQ$IQWc(c>@OZ>p*o&Om#YU904cMNGuEfV=7=&sEBWEO z0*!=GVSv0>d^i9z7Sg{z#So+GM2TEu7$KXJ6>)Bor8P5J(xrxgx+fTLn1?Jlotz*U z(ekS*a2*ml5ft&R;h3Gc2ndTElB!bdMa>UptgIl{pA+&b+z_Y&aS7SWUlwJf-+PRv z$#v|!SP92+41^ppe}~aariwztUtwKA8BBLa5=?j3@~qHfjxkvID8CD`t5*+4s|u4T zLJ9iEfhO4YuAl$)?VsWcln|?(P=CA|!u}ab3c3fL8ej9fW;K|@3-c@y4I;^8?K!i0 zS(5Cm#i85BGZov}qp+<-5!Fh+KZev3(sA2D_4Z~ZLmB5B$_Yw2aY{kA$zuzggbD{T zE>#yd3ilpjM4F^dmfW#p#*;@RgBg{!_3b6cW?^iYcP!mjj!}pkNi{2da-ZCD2TKKz zH^x^+YgBb=dtg@_(Cy33D|#IZ&8t?w8$E8P0fmX#GIzq~w51uYmFs{aY76e0_~z2M z(o%PNTIipeOIq(H5O>OJ*v8KZE>U@kw5(LkumNrY>Rv7BlW7{_R9v@N63rK)*tu|S zKzq|aNs@81YUVZ5vm>+pc42CDPwQa>oxrsXkRdowWP!w?=M(fn3y6frEV*;WwfUV$s31D!S_;_~E@MEZ>|~wmIr05#z2J+& zBme6rnxfCp&kP@sP)NwG>!#WqzG>KN7VC~Gdg493So%%-P%Rk!<|~-U|L3VASMj9K zk(Pfm1oj~>$A>MFFdAC8M&X0i9-cV7Q($(R5C&nR5RH$T&7M=pCDl`MpAHPOha!4r zQnYz$7B1iLK$>_Ai%kZQaj-9)nH$)tESWUSDGs2|7plF4cq1Oj-U|+l4Ga}>k!efC z*ecEudbliG+%wI8J#qI!s@t%0y9R$MBUFB)4d47VmI`FjtzNd_xit&l1T@drx z&4>Aj<2{1gUW8&EihwT1mZeliwrCN{R|4@w4@@Btov?x5ZVzrs&gF0n4jGSE33ddUnBg_nO4Zw)yB$J-{@a8 z);m%fvX2fvXxogriNb}}A8HxA)1P-oK+Da4C3pofK3>U_6%DsXFpPX}3F8O`uIpLn zdKjq(QxJTJ4xh->(=lxWO#^XAa~<7UxQl8~8=izS!TcPmAiBP5Et7y?qEbFd9Q=%IJ;%Kn$lto-~3`}&`x=AVS+Uo7N*hbUxhqVH_w^sn!74z{Ka#*U6s z=8jIrHpUMBC@@9Jn~GS<$lse*EKuX%3Swl5&3~GiK_$vn8Vjqe{mjhBlH}m4I8qK+ ztU50COh7)d-gXpq-|}T;biGa^e=VjxjjFuoGIA8`2jJ}wNBRcsx24?7lJ7W4ksNPv zA7|gcXT@~7KTID#0|EX#OAXvgaBJ8Jg!7X#kc1^Tvl;I(=~(jtn-(5bhB=~J^w5bw z8^Hifeupm;nwsSDkT{?x?E(DgLC~Nh8HKQGv`~2jMYrz9PwS^8qs3@nz4ZBCP5}%i z=w}jr2*$X-f(zDhu%D8(hWCpix>TQpi{e`-{p^y?x4?9%)^wWc?L}UMcfp~lL|;g) zmtkcXGi9#?cFOQQi_!Z8b;4R%4y{$SN~fkFedDJ&3eBfHg|DRSx09!tjoDHgD510Z z_aJLHdS&7;Dl;X|WBVyl_+d+2_MK07^X1JEi_)v$Z*ny-()VrD6VWx|Un{)gO0*FQ zX{8Ss3JMrV15zXyfCTsVO@hs49m&mN(QMdL3&x@uQqOyh2gnGJYocz0G=?BX7qxA{ zXe0bn4ij^;wfZfnRlIYkWS^usYI@goI9PccI>}Ih*B!%zv6P$DoXsS%?G)|HHevkG z>`b#vtP=Lx$Ee(t??%_+jh(nuc0Q&mCU{E3U z1NqNK!XOE#H2Pybjg0_tYz^bzX`^RR{F2ML^+<8Q{a;t(#&af8@c6K2y2m zP|parK=qf`I`#YxwL=NTP>tMiLR(d|<#gEu=L-c!r&(+CpSMB5ChYW1pUmTVdCWw|!Ao?j&-*~50S`=) z9#Knf7GPA19g%Y7wip@`nj$aJcV|SakXZ*Q2k$_SZlNMx!eY8exF;navr&R)?NO9k z#V&~KLZ0c9m|Mf4Gic}+<=w9YPlY@|Pw*z?70dwOtb<9-(0GOg>{sZaMkZc9DVk0r zKt%g5B1-8xj$Z)>tWK-Gl4{%XF55_Ra3}pSY<@Y&9mw`1jW8|&Zm{BmHt^g=FlE{` z9Lu7fI2v3_0u~apyA;wa|S4NaaG>eHEw&3lNFVd_R9E=Y? zgpVQxc9{drFt2pP#ZiN~(PL%9daP4pWd*5ABZYK{a@e&Vb`TYiLt$1S>KceK36Ehz z;;MI%V;I`#VoSVAgK3I%-c>ViA>nt=5EZ zjr$Jv~$_vg<$q<@CpZ1gdqP_3v^)uaqZ`?RS_>f(pWx3(H;gWpjR?W8L++YPW;)Vw3)~tozdySrB3A2;O<%1F8?Il4G|rO0mEZYHDz!?ke!$^bEiWRC1B%j~ws0+hHS;B8l5Wh)e+Ms7f4M4CbL%Q_*i~cP}5-B(UkE&f7*pW6OtYk5okQCEoN4v|7;(+~~nyViqo5 z(bMGQi$)KN6EmfVHv4pf2zZMJbcAKyYy>jY@>LB5eId|2Vsp{>NMlsee-tmh({;@b z@g;wiv8@a1qrDf-@7$(MR^M^*dKYBewhIDFX%;*8s zR#u?E;DJO;VnTY6IfbO=dQ61V0DisUAs4~t|9`9ZE(jG}ax#-xikDhsO_4^RaK ziZ?9AJQP_{9WuzVk^s_U+3V8gOvVl5(#1>}a|RL>};+uJB%nQM-J>M4~yK)cioytFXtnmOaJZSiE+3g}C`Im~6H z*+-vjI>ng5w>>Y!L(+DwX2gs0!&-BFEaDie4i5ln*NGP$te7$F9iUlJl4`XpkAsPm z0l?GQ17uN^=g~u1*$)S`30xL%!`LW*flwT*#svAtY(kHXFfvA`dj*pDfr0pBZ`!La zWmX$Z@qyv|{nNsRS|+CzN-Pvb>47HEDeUGFhpp5C_NL0Vp~{Wc{bsm_5J!#tuqW@? z)Be zb&Gj&(l*bHQDq7w-b`F9MHEH*{Dh~0`Gn8t`pz}!R+q~4u$T@cVaUu`E^%0f-q*hM z1To6V31UGJN7a-QW5;nhk#C26vmHyjTVZkdV zqYMI9jQY)3oZt=V0L7JZQ=^c2k){Y_lHp&V_LIi*iX^Ih3vZ_K<@Di(hY<&g^f?c$wwF-wX1VLj>ZC4{0#e`XhbL_$a9uXS zKph*4LupSV2TQBCJ4AfOXD8fs2;bAGz-qU4=Qj$^1ZJX z2TtaVdq>OjaWGvv9)agwV)QW9eTZ-xv`us2!yXSARnD5DwX_Vg*@g4w!-zT|5<}-7 zsnllGRQz>k!LwdU`|i&!Bw^W7CTUU3x`Zg8>XgHj=bo!cd<#pI8*pa*1N`gg~I0ace!wzZoJ)oGScm~D_Sc;#wFed zUo;-*0LaWVCC2yqr6IbeW3`hvXyMfAH94qP2|cN``Z%dSuz8HcQ!WT0k38!X34<6l zHtMV%4fH5<6z-lYcK;CTvzzT6-^xSP>~a*8LfbByHyp$|X*#I6HCAi){gCu1nvN%& zvlSbNFJRCc&8>f`$2Qa`fb@w!C11v1KCn)P9<}ei0}g*cl~9A9h=7(}FO!=cVllq3 z7nD)E%gt;&AYdo{Ljb2~Fm5jy{I><%i*GUlU8crR4k(zwQf#nima@xb%O71M#t-4< z(yjX(m^mp_Y;5()naqt2-VibylPS)Oof9uBp$3Gj`>7@gjKwnwRCc>rx%$esn);gI z5B9;~uz57n7Rpm8K^o=_sFPyU?>liHM&8&#O%f)}C5F7gvj#n#TLp@!M~Q?iW~lS}(gy%d&G3p?iBP z(PZQUv07@7!o3~1_l|m5m;Xr)^QK_JaVAY3v1UREC*6>v;AT$BO`nA~KZa1x3kV2F z%iwG7SaaAcT8kalCa^Hg&|eINWmBQA_d8$}B+-Q_@6j_{>a- zwT3CMWG!A}Ef$EvQsjK>o)lJ;q!~#F%wo`k-_mT=+yo%6+`iGe9(XeUl;*-4(`G;M zc@+ep^Xv&<3e7l4wt48iwaLIC1RhSsYrf6>7zXfVD zNNJ1#zM;CjKgfqCabzacX7#oEN{koCnq1-stV+-CMQ=ZX7Fpd*n9`+AEg9=p&q7mTAKXvcbo?$AVvOOp{F>#a;S?joYZl_f}BECS%u&0x!95DR;|QkR9i}`FEAsPb=)I z8nb=4iwjiLRgAF}8WTwAb^eA>QjL4Srqb#n zTwx^-*Z38Uzh@bX$_1tq>m{o8PBX*t3Lqaf$EBqiOU*2NFp{LJX#3}p9{|v{^Hg4f zlhllKI>F+>*%mu6i9V7TT*Wx-zdK z(p8faUOwGOm5mBC%UGA1jO0@IKkG;i&+6Ur8XR2ZuRb$*a}R^-H6eKxcYodlXsF`& z{NkO+;_Yh-Ni@vV9iyzM43Yibn;oC7hPAzC24zs&+RYdY&r`3&&fg2hs62ysV^G`N zHMfBEFo8E3S$0C_m({bL8QCe$B@M{n1dLsaJYIU;(!n*V?0I1OvBB=iYh&`?u8 z&~n-$nbVIhO3mMhCQRlq%XRr1;Hvl=9E_F0sc9!VLnM>@mY~=Cx3K5}wxHKEZF9pC zIdyu1qucM!gEiomw7bW0-RwbX7?o=FE#K0l4`U2KhC8*kMWaEWJyVNZVu_tY2e&4F zb54Lh=Oz>(3?V$!ArXFXh8Cb3i;%KQGCrW$W#;kvx$YA2gofNeu?@nt>Yq8?2uJQp zUTo14hS%&dHF3Uhm~Z1>W)yb%&HoM!3z?%a%dmKT#>}}kKy2B=V3{Nu=bae%V%wU$ zb4%^m?&qn==QeHo`nAs3H}wtiK~!!&i|iBLfazh6!y9F)ToKNyE0B385!zq{p)5vB zvu`R#ULIS|2{3w52c*c$4}Pe>9Fw&U^>Bb_LUWn!xPx3X-uQsv(b1XFvFzn#voq0* z5~o`V_G805QXdgAOwOjoqmZ?uzwBVYSNP0Ie8FL`P0VK1J4CzV@t&%0duHB{;yIL$FZ9 zz#s#%ZG6ya&AwE;0_~^$1K