From 3610615b101ace33d2cd01ef5bfe93b340fe266b Mon Sep 17 00:00:00 2001 From: Sebastian Erives Date: Fri, 27 Sep 2024 17:03:08 -0600 Subject: [PATCH] Add missing Color methods, SDK color samples & Gamepad stub in OpModes --- .../ftc/robotcore/external/Telemetry.java | 74 ++- .../robotcore/robocol/TelemetryMessage.java | 22 + .../eocvsim/pipeline/PipelineManager.kt | 14 +- ...tryImpl.java => EOCVSimTelemetryImpl.java} | 101 +++-- .../resources/templates/default_workspace.zip | Bin 29308 -> 37207 bytes .../resources/templates/gradle_workspace.zip | Bin 91053 -> 99008 bytes .../samples/ConceptVisionColorLocator.java | 191 ++++++++ .../samples/ConceptVisionColorSensor.java | 136 ++++++ .../src/main/java/android/graphics/Color.java | 72 ++- .../robotcore/eventloop/opmode/OpMode.java | 5 + .../qualcomm/robotcore/hardware/Gamepad.java | 424 ++++++++++++++++++ .../robotcore/hardware/HardwareMap.java | 2 +- 12 files changed, 984 insertions(+), 57 deletions(-) create mode 100644 Common/src/main/java/org/firstinspires/ftc/robotcore/robocol/TelemetryMessage.java rename EOCV-Sim/src/main/java/org/firstinspires/ftc/robotcore/internal/opmode/{TelemetryImpl.java => EOCVSimTelemetryImpl.java} (93%) create mode 100644 TeamCode/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/ConceptVisionColorLocator.java create mode 100644 TeamCode/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/ConceptVisionColorSensor.java create mode 100644 Vision/src/main/java/com/qualcomm/robotcore/hardware/Gamepad.java diff --git a/Common/src/main/java/org/firstinspires/ftc/robotcore/external/Telemetry.java b/Common/src/main/java/org/firstinspires/ftc/robotcore/external/Telemetry.java index d862ac22..b15b12e9 100644 --- a/Common/src/main/java/org/firstinspires/ftc/robotcore/external/Telemetry.java +++ b/Common/src/main/java/org/firstinspires/ftc/robotcore/external/Telemetry.java @@ -1,17 +1,23 @@ /* Copyright (c) 2016 Robert Atkinson + 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 Robert Atkinson nor the names of his 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, @@ -26,6 +32,7 @@ are permitted (subject to the limitations in the disclaimer below) provided that */ package org.firstinspires.ftc.robotcore.external; +import androidx.annotation.Nullable; import java.util.Locale; @@ -43,12 +50,18 @@ are permitted (subject to the limitations in the disclaimer below) provided that * telemetry.update(); * * + *

In the 2015/16 season, the call to {@link #update()} was not required; now, however, + * in a LinearOpMode, unless {@link #update()} is called, nothing will appear on the + * driver station screen. In other, loop-based OpModes, {@link #update()} continues to be called + * automatically at the end of OpMode#loop() and OpMode#init_loop(); no call to + * {@link #update()} is required in loop-based OpModes.

* *
- *     // loop-based opmode
+ *     // loop-based OpMode
  *     telemetry.addData("count", currentCount);
  *     telemetry.addData("elapsedTime", "%.3f", elapsedSeconds);
  * 
+ *

By default (but see {@link #setAutoClear(boolean) setAutoClear()}), data is cleared from the * telemetry after each call to {@link #update()}; thus, you need to issue {@link #addData(String, * Object) addData()} for the entire contents of the telemetry screen on each update cycle. @@ -69,6 +82,7 @@ are permitted (subject to the limitations in the disclaimer below) provided that * telemetry.update(); * ... * } + * void anotherPartOfYourCode() { * ... * elapsedItem.setValue("%.3f", elapsedSeconds); @@ -268,6 +282,30 @@ public interface Telemetry */ boolean removeAction(Object token); + //---------------------------------------------------------------------------------------------- + // Text to Speech + //---------------------------------------------------------------------------------------------- + + /** + * Directs the Driver Station device to speak the given text using TextToSpeech functionality, + * with the same language and country codes that were previously used, or the default language + * and country. + * + * @param text the text to be spoken + */ + void speak(String text); + + /** + * Directs the Driver Station device to speak the given text using TextToSpeech functionality, + * with the given language and country codes. + * + * @param text the text to be spoken + * @param languageCode an ISO 639 alpha-2 or alpha-3 language code, or a language subtag up to + * 8 characters in length + * @param countryCode an ISO 3166 alpha-2 country code, or a UN M.49 numeric-3 area code + */ + void speak(String text, String languageCode, String countryCode); + //---------------------------------------------------------------------------------------------- // Transmission //---------------------------------------------------------------------------------------------- @@ -339,7 +377,7 @@ interface Line /** * Instances of {@link Item} represent an item of data on the drive station telemetry display. * - * @see {@link #addData(String, Object)} + * @see #addData(String, Object) */ interface Item { @@ -404,7 +442,7 @@ interface Item * @see #clear() * @see #isRetained() */ - Item setRetained(Boolean retained); + Item setRetained(@Nullable Boolean retained); /** * Returns whether the item is to be retained in a clear() operation. @@ -498,6 +536,30 @@ interface Item */ void setCaptionValueSeparator(String captionValueSeparator); + enum DisplayFormat + { + CLASSIC, // What you've all come to know and love (or not) since 2015 + MONOSPACE, // Same as classic, except uses a monospaced font so you can column align data + HTML; // Allows use of a subset of HTML tags, enabling "rich text" display (e.g. color & size) + } + + /** + * Sets the telemetry display format on the Driver Station. See the comments on {@link DisplayFormat}. + * + * @param displayFormat the telemetry display format the Driver Station should use + */ + void setDisplayFormat(DisplayFormat displayFormat); + + /** + * Sets the number of decimal places for Double and Float + * + * @param minDecimalPlaces - the minimum number of places to show when Double or Float is passed in without a Format + * @param maxDecimalPlaces - the maximum number of places to show when Double or Float is passed in without a Format + */ + default void setNumDecimalPlaces(int minDecimalPlaces, int maxDecimalPlaces){ + // does nothing just so we don't break existing Telemetry + } + //---------------------------------------------------------------------------------------------- // Properties //---------------------------------------------------------------------------------------------- @@ -516,9 +578,9 @@ interface Log enum DisplayOrder { NEWEST_FIRST, OLDEST_FIRST } /** - * Returns the maximum number of lines which will be retained in a {@link #log()()} and + * Returns the maximum number of lines which will be retained in a {@link #log()} and * shown on the driver station display. - * @return the maximum number of lines which will be retained in a {@link #log()()} + * @return the maximum number of lines which will be retained in a {@link #log()} * @see #setCapacity(int) */ int getCapacity(); @@ -567,4 +629,4 @@ enum DisplayOrder { NEWEST_FIRST, OLDEST_FIRST } * @see Log#addData(String, Object) */ Log log(); -} \ No newline at end of file +} diff --git a/Common/src/main/java/org/firstinspires/ftc/robotcore/robocol/TelemetryMessage.java b/Common/src/main/java/org/firstinspires/ftc/robotcore/robocol/TelemetryMessage.java new file mode 100644 index 00000000..41a62006 --- /dev/null +++ b/Common/src/main/java/org/firstinspires/ftc/robotcore/robocol/TelemetryMessage.java @@ -0,0 +1,22 @@ +package org.firstinspires.ftc.robotcore.robocol; + +/** + * Placeholder for telemetry message constants + */ +public class TelemetryMessage { + + static final int cbTimestamp = 8; + static final int cbSorted = 1; + static final int cbRobotState = 1; + static final int cbTagLen = 1; + static final int cbCountLen = 1; + static final int cbKeyLen = 2; + static final int cbValueLen = 2; + static final int cbFloat = 4; + + public final static int cbTagMax = (1 << (cbTagLen*8)) - 1; + public final static int cCountMax = (1 << (cbCountLen*8)) - 1; + public final static int cbKeyMax = (1 << (cbKeyLen*8)) - 1; + public final static int cbValueMax = (1 << (cbValueLen*8)) - 1; + +} diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/pipeline/PipelineManager.kt b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/pipeline/PipelineManager.kt index d6eabacc..c9a910dd 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/pipeline/PipelineManager.kt +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/pipeline/PipelineManager.kt @@ -35,26 +35,22 @@ import com.github.serivesmejia.eocvsim.pipeline.util.PipelineSnapshot import com.github.serivesmejia.eocvsim.util.ReflectUtil import com.github.serivesmejia.eocvsim.util.StrUtil import com.github.serivesmejia.eocvsim.util.event.EventHandler -import com.github.serivesmejia.eocvsim.util.exception.MaxActiveContextsException import com.github.serivesmejia.eocvsim.util.fps.FpsCounter import com.github.serivesmejia.eocvsim.util.loggerForThis import io.github.deltacv.common.image.MatPoster import io.github.deltacv.common.pipeline.util.PipelineStatisticsCalculator import io.github.deltacv.eocvsim.virtualreflect.VirtualField -import io.github.deltacv.eocvsim.virtualreflect.VirtualReflectContext import io.github.deltacv.eocvsim.virtualreflect.VirtualReflection import io.github.deltacv.eocvsim.virtualreflect.jvm.JvmVirtualReflection import kotlinx.coroutines.* import org.firstinspires.ftc.robotcore.external.Telemetry -import org.firstinspires.ftc.robotcore.internal.opmode.TelemetryImpl +import org.firstinspires.ftc.robotcore.internal.opmode.EOCVSimTelemetryImpl import org.firstinspires.ftc.vision.VisionProcessor import org.opencv.core.Mat import org.openftc.easyopencv.OpenCvPipeline import org.openftc.easyopencv.OpenCvViewport import org.openftc.easyopencv.processFrameInternal import java.lang.RuntimeException -import java.lang.reflect.Constructor -import java.lang.reflect.Field import java.util.* import kotlin.coroutines.EmptyCoroutineContext import kotlin.math.roundToLong @@ -202,7 +198,7 @@ class PipelineManager( openedPipelineOutputCount++ } - if(telemetry is TelemetryImpl) { + if(telemetry is EOCVSimTelemetryImpl) { telemetry.errItem.caption = "[/!\\]" telemetry.errItem.setValue("Uncaught exception thrown, check Workspace -> Output.") telemetry.forceTelemetryTransmission() @@ -212,7 +208,7 @@ class PipelineManager( pipelineExceptionTracker.onPipelineExceptionClear { val telemetry = currentTelemetry - if(telemetry is TelemetryImpl) { + if(telemetry is EOCVSimTelemetryImpl) { telemetry.errItem.caption = "" telemetry.errItem.setValue("") telemetry.forceTelemetryTransmission() @@ -275,7 +271,7 @@ class PipelineManager( } } - if(telemetry is TelemetryImpl) { + if(telemetry is EOCVSimTelemetryImpl) { if (compiledPipelineManager.isBuildRunning) { telemetry.infoItem.caption = "[>]" telemetry.infoItem.setValue("Building java files in workspace...") @@ -581,7 +577,7 @@ class PipelineManager( val instantiator = getInstantiatorFor(pipelineClass) try { - nextTelemetry = TelemetryImpl().apply { + nextTelemetry = EOCVSimTelemetryImpl().apply { // send telemetry updates to the ui addTransmissionReceiver(eocvSim.visualizer.telemetryPanel) } diff --git a/EOCV-Sim/src/main/java/org/firstinspires/ftc/robotcore/internal/opmode/TelemetryImpl.java b/EOCV-Sim/src/main/java/org/firstinspires/ftc/robotcore/internal/opmode/EOCVSimTelemetryImpl.java similarity index 93% rename from EOCV-Sim/src/main/java/org/firstinspires/ftc/robotcore/internal/opmode/TelemetryImpl.java rename to EOCV-Sim/src/main/java/org/firstinspires/ftc/robotcore/internal/opmode/EOCVSimTelemetryImpl.java index 8e21e920..1af5a2f2 100644 --- a/EOCV-Sim/src/main/java/org/firstinspires/ftc/robotcore/internal/opmode/TelemetryImpl.java +++ b/EOCV-Sim/src/main/java/org/firstinspires/ftc/robotcore/internal/opmode/EOCVSimTelemetryImpl.java @@ -1,18 +1,23 @@ /* Copyright (c) 2016 Robert Atkinson + All rights reserved. -Adapted for EOCV-Sim (c) 2021 Sebastian Erives + 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 Robert Atkinson nor the names of his 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, @@ -27,24 +32,29 @@ are permitted (subject to the limitations in the disclaimer below) provided that */ package org.firstinspires.ftc.robotcore.internal.opmode; -import com.github.serivesmejia.eocvsim.gui.component.visualizer.TelemetryPanel; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.qualcomm.robotcore.eventloop.opmode.OpMode; import com.qualcomm.robotcore.util.ElapsedTime; import org.firstinspires.ftc.robotcore.external.Func; import org.firstinspires.ftc.robotcore.external.Predicate; import org.firstinspires.ftc.robotcore.external.Telemetry; +import org.firstinspires.ftc.robotcore.robocol.TelemetryMessage; -import javax.swing.*; +import java.text.DecimalFormat; import java.util.ArrayList; import java.util.Iterator; import java.util.LinkedList; import java.util.List; /** - * {@link TelemetryImpl} is the system-provided implementation of the {@link Telemetry} interface. + * {@link EOCVSimTelemetryImpl} is the system-provided implementation of the {@link Telemetry} interface. */ -public class TelemetryImpl implements Telemetry, TelemetryInternal +public class EOCVSimTelemetryImpl implements Telemetry, TelemetryInternal { + //---------------------------------------------------------------------------------------------- // Types //---------------------------------------------------------------------------------------------- @@ -78,9 +88,12 @@ protected class Value this.valueProducer = valueProducer; } - Value(Object value) - { - this.value = value; + Value(Object value) { + if ((value instanceof Double) || (value instanceof Float)) { + this.value = decimalFormat.format(value); + } else { + this.value = value; + } } Value(Func valueProducer) @@ -97,7 +110,7 @@ boolean isProducer() return this.valueProducer != null; } - String getComposed(boolean recompose) + @NonNull String getComposed(boolean recompose) { if (recompose || composed==null) { @@ -142,13 +155,7 @@ void boundedAddToList(int index, Lineable data) // Using the max # of data items that can be actually transmitted, ever, seems like // a practical choice as an upper bound. - - // 255 was inlined from the original calculations in robocol's TelemetryMessage.cCountMax: - // - // static final int cbCountLen = 1; - // ... - // public final static int cCountMax = (1 << (cbCountLen*8)) - 1; - if (list.size() < 255) + if (list.size() < TelemetryMessage.cCountMax) { list.add(index, data); } @@ -270,6 +277,7 @@ protected class ItemImpl implements Item, Lineable String caption = null; Value value = null; Boolean retained = null; + boolean showIfEmpty = true; //------------------------------------------------------------------------------------------ @@ -294,7 +302,7 @@ protected class ItemImpl implements Item, Lineable { String composed = this.value.getComposed(recompose); - if(!showIfEmpty && this.caption.trim().equals("") && composed.trim().equals("")) { + if(!showIfEmpty && this.caption.trim().isEmpty() && composed.trim().isEmpty()) { return ""; } @@ -323,7 +331,7 @@ protected class ItemImpl implements Item, Lineable } } - @Override public Item setRetained(Boolean retained) + @Override public Item setRetained(@Nullable Boolean retained) { synchronized (theLock) { @@ -418,14 +426,14 @@ protected class LineImpl implements Line, Lineable // Operations //------------------------------------------------------------------------------------------ - public String getComposed(boolean recompose, boolean appendSeparator) + @Override public String getComposed(boolean recompose) { StringBuilder result = new StringBuilder(); result.append(this.lineCaption); boolean firstTime = true; for (Lineable lineable : lineables) { - if (!firstTime && appendSeparator) + if (!firstTime) { result.append(getItemSeparator()); } @@ -435,11 +443,6 @@ public String getComposed(boolean recompose, boolean appendSeparator) return result.toString(); } - @Override - public String getComposed(boolean recompose) { - return getComposed(recompose, true); - } - @Override public Item addData(String caption, String format, Object... args) { return lineables.addItemAfter(null, caption, new Value(format, args)); @@ -459,10 +462,6 @@ public String getComposed(boolean recompose) { { return lineables.addItemAfter(null, caption, new Value(format, valueProducer)); } - - public boolean isEmpty() { - return getComposed(false, false).trim().equals(""); - } } protected class LogImpl implements Log @@ -510,7 +509,7 @@ boolean isDirty() // Use the outer class to mindlessly avoid any potential deadlocks Object getLock() { - return TelemetryImpl.this; + return EOCVSimTelemetryImpl.this; } int size() @@ -614,12 +613,16 @@ void reset() protected ElapsedTime transmissionTimer; protected boolean isDirty; protected boolean clearOnAdd; + protected OpMode opMode; protected boolean isAutoClear; protected int msTransmissionInterval; protected String captionValueSeparator; protected String itemSeparator; - protected StringBuilder currentSb; + protected DecimalFormat decimalFormat = new DecimalFormat("0.####"); + /* + * EOCV-Sim + */ protected ArrayList transmissionReceivers = new ArrayList<>(); public Item errItem; @@ -629,7 +632,7 @@ void reset() // Construction //---------------------------------------------------------------------------------------------- - public TelemetryImpl() + public EOCVSimTelemetryImpl() { this.log = new LogImpl(); resetTelemetryForOpMode(); @@ -649,8 +652,7 @@ public void resetTelemetryForOpMode() this.msTransmissionInterval = 250; this.captionValueSeparator = " : "; this.itemSeparator = " | "; - this.currentSb = new StringBuilder(); - this.transmissionReceivers = new ArrayList<>(); + errItem = addData("", "").setRetained(true); ((ItemImpl)errItem).showIfEmpty = false; @@ -678,6 +680,11 @@ boolean isDirty() return this.isDirty; } + public void setNumDecimalPlaces(int minDecimalPlaces, int maxDecimalPlaces){ + decimalFormat.setMinimumFractionDigits(minDecimalPlaces); + decimalFormat.setMaximumFractionDigits(maxDecimalPlaces); + } + //---------------------------------------------------------------------------------------------- // Updating //---------------------------------------------------------------------------------------------- @@ -753,11 +760,14 @@ else if (updateReason==UpdateReason.USER) return result; } + + } + protected void saveToTransmitter(boolean recompose) { - currentSb = new StringBuilder(); + StringBuilder currentSb = new StringBuilder(); // When we recompose, we save the composed lines. Thus, they will stick around // even after we might get clear()'d. In that way, they'll still be there to @@ -870,6 +880,11 @@ public boolean removeTransmissionReceiver(TelemetryTransmissionReceiver transmis } } + @Override public void setDisplayFormat(DisplayFormat displayFormat) + { + // no-op in eocv-sim + } + //---------------------------------------------------------------------------------------------- // Adding and removing data //---------------------------------------------------------------------------------------------- @@ -984,9 +999,17 @@ protected void onAddData() } } - @Override - public String toString() { - return currentSb.toString(); + //---------------------------------------------------------------------------------------------- + // Text to Speech + //---------------------------------------------------------------------------------------------- + + @Override public void speak(String text) + { + speak(text, null, null); } -} \ No newline at end of file + @Override public void speak(String text, String languageCode, String countryCode) + { + // no-op + } +} diff --git a/EOCV-Sim/src/main/resources/templates/default_workspace.zip b/EOCV-Sim/src/main/resources/templates/default_workspace.zip index f7e41a86ca994c71cd7d36f9bb6dde905c3bc42f..9d41330a33c9c598bad420dbcb2a416c0cc26c5c 100644 GIT binary patch delta 7755 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 diff --git a/EOCV-Sim/src/main/resources/templates/gradle_workspace.zip b/EOCV-Sim/src/main/resources/templates/gradle_workspace.zip index 209e6f648468c4e79e5fbfb571717eb6fb9cc833..29a5cc7d28ae2391d8f246b1fb251079f3517655 100644 GIT binary patch delta 7784 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 MAdsLl!avjh0FBfX_y7O^ delta 38 pcmX@m%C`16Yr_^s@6heG#f*M@BCKo-K%fAGetZlJo}r9D9stzU2e<$L diff --git a/TeamCode/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/ConceptVisionColorLocator.java b/TeamCode/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/ConceptVisionColorLocator.java new file mode 100644 index 00000000..312120c6 --- /dev/null +++ b/TeamCode/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/ConceptVisionColorLocator.java @@ -0,0 +1,191 @@ +/* + * 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 an object in the field of view, 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 process is created first, and then the VisionPortal is built to use this process. + * The ColorBlobLocatorProcessor 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 Blob contains the contour and the boxFit data. + * Note: The default sort order for Blobs is ContourArea, in descending order, so the biggest contours are listed first. + * + * To aid the user, 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: Vision Color-Locator", group = "Concept") +public class ConceptVisionColorLocator 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. You can use a predefined color, or create you own color range + * .setTargetColorRange(ColorRange.BLUE) // use a predefined color match + * Available predefined colors are: RED, BLUE YELLOW GREEN + * .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 pixel square near the upper left corner + * ImageRegion.asUnityCenterCoordinates(-0.5, 0.5, 0.5, -0.5) 50% width/height square centered on screen + * + * - Define which contours are included. + * You can get ALL the contours, or you can skip any contours that are completely inside another contour. + * .setContourMode(ColorBlobLocatorProcessor.ContourMode.ALL_FLATTENED_HIERARCHY) // return all contours + * .setContourMode(ColorBlobLocatorProcessor.ContourMode.EXTERNAL_ONLY) // exclude contours inside other contours + * note: EXTERNAL_ONLY helps to avoid bright reflection spots from breaking up areas of solid color. + * + * - turn the display of contours ON or OFF. Turning this on 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 of pixels, 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 "pixels" 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 more visible by filling in small holes, making lines appear thicker, + * 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.BLUE) // use a predefined color match + .setContourMode(ColorBlobLocatorProcessor.ContourMode.EXTERNAL_ONLY) // exclude blobs inside blobs + .setRoi(ImageRegion.asUnityCenterCoordinates(-0.5, 0.5, 0.5, -0.5)) // search central 1/4 of camera view + .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. + * 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(50); // Speed up telemetry updates, Just use for debugging. + telemetry.setDisplayFormat(Telemetry.DisplayFormat.MONOSPACE); + + // WARNING: To be able 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. + * + * Use any of the following filters. + * + * ColorBlobLocatorProcessor.Util.filterByArea(minArea, maxArea, blobs); + * 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.Util.filterByDensity(minDensity, maxDensity, blobs); + * 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.Util.filterByAspectRatio(minAspect, maxAspect, blobs); + * 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.Util.filterByArea(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 use ascending or descending order. + * ColorBlobLocatorProcessor.Util.sortByArea(SortOrder.DESCENDING, blobs); // Default + * ColorBlobLocatorProcessor.Util.sortByDensity(SortOrder.DESCENDING, blobs); + * ColorBlobLocatorProcessor.Util.sortByAspectRatio(SortOrder.DESCENDING, blobs); + */ + + telemetry.addLine(" Area Density Aspect Center"); + + // Display the size (area) and center location for each Blob. + for(ColorBlobLocatorProcessor.Blob b : blobs) + { + RotatedRect boxFit = b.getBoxFit(); + telemetry.addLine(String.format("%5d %4.2f %5.2f (%3d,%3d)", + b.getContourArea(), b.getDensity(), b.getAspectRatio(), (int) boxFit.center.x, (int) boxFit.center.y)); + } + + telemetry.update(); + sleep(50); + } + } +} \ No newline at end of file diff --git a/TeamCode/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/ConceptVisionColorSensor.java b/TeamCode/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/ConceptVisionColorSensor.java new file mode 100644 index 00000000..33afccf1 --- /dev/null +++ b/TeamCode/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/ConceptVisionColorSensor.java @@ -0,0 +1,136 @@ +/* + * 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.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 process is created first, and then the VisionPortal is built to use this process. + * The PredominantColorProcessor 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 verify that the color is reasonable. + * + * 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 pixel square near the upper left corner + * ImageRegion.asUnityCenterCoordinates(-0.1, 0.1, 0.1, -0.1) 10% width/height square centered on screen + * + * - 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 the surrounding. + * Possible choices are: + * RED, ORANGE, YELLOW, GREEN, CYAN, BLUE, PURPLE, MAGENTA, BLACK, WHITE; + * + * 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.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 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(colorSensor) + .setCameraResolution(new Size(320, 240)) + .setCamera(hardwareMap.get(WebcamName.class, "Webcam 1")) + .build(); + + telemetry.setMsTransmissionInterval(50); // Speed up telemetry updates, Just use for debugging. + + // WARNING: To be able to view the stream preview on the Driver Station, this code runs in INIT mode. + while (opModeIsActive() || opModeInInit()) + { + telemetry.addData("DS preview on/off", "3 dots, Camera Stream\n"); + + // Request the most recent color analysis. + // This will return the closest matching colorSwatch and the predominant RGB color. + // Note: to take actions based on the detected color, simply use the colorSwatch in a comparison or switch. + // eg: + // if (result.closestSwatch == PredominantColorProcessor.Swatch.RED) {... some code ...} + PredominantColorProcessor.Result result = colorSensor.getAnalysis(); + + // Display the Color Sensor result. + telemetry.addData("Best Match:", result.closestSwatch); + telemetry.addLine(String.format("R %3d, G %3d, B %3d", Color.red(result.rgb), Color.green(result.rgb), Color.blue(result.rgb))); + telemetry.update(); + + sleep(20); + } + } +} \ No newline at end of file diff --git a/Vision/src/main/java/android/graphics/Color.java b/Vision/src/main/java/android/graphics/Color.java index 7460ec3e..02243cc7 100644 --- a/Vision/src/main/java/android/graphics/Color.java +++ b/Vision/src/main/java/android/graphics/Color.java @@ -1383,9 +1383,77 @@ public static int HSVToColor(@IntRange(from = 0, to = 255) int alpha, @Size(3) f } return nativeHSVToColor(alpha, hsv); } - private static native void nativeRGBToHSV(int red, int greed, int blue, float hsv[]); - private static native int nativeHSVToColor(int alpha, float hsv[]); + private static void nativeRGBToHSV(int red, int green, int blue, float hsv[]) { + int min = Math.min(red, Math.min(green, blue)); + int max = Math.max(red, Math.max(green, blue)); + int delta = max - min; + + float v = (float) max / 255; + if (v < 0 || v > 1.0f) + throw new RuntimeException("RGB max is out of range"); + + if (delta == 0) { // we're a shade of gray + hsv[0] = 0; + hsv[1] = 0; + hsv[2] = v; + return; + } + + float s = (float) delta / max; + if (s < 0 || s > 1.0f) + throw new RuntimeException("RGB delta is out of range"); + + float h; + if (red == max) + h = (float) (green - blue) / delta; + else if (green == max) + h = 2.0f + (float) (blue - red) / delta; + else // blue == max + h = 4.0f + (float) (red - green) / delta; + + h *= 60; + if (h < 0) + h += 360.0f; + + if (h < 0 || h >= 360.0f) + throw new RuntimeException("h is out of range"); + + hsv[0] = h; + hsv[1] = s; + hsv[2] = v; + } + + private static int nativeHSVToColor(int alpha, float hsv[]) { + int s = hsv[1] < 0 ? 0 : hsv[1] >= 1.0f ? 255 : ((int) (hsv[1] * (1 << 16))) >> 8; + int v = hsv[2] < 0 ? 0 : hsv[2] >= 1.0f ? 255 : ((int) (hsv[2] * (1 << 16))) >> 8; + + if (s == 0) // shade of gray + return (alpha << 24) | (v << 16) | (v << 8) | v; + int hx = (hsv[0] < 0 || hsv[0] >= 360.0f) ? 0 : ((int) (hsv[0]/60 * (1 << 16))); + int f = hx & 0xFFFF; + + int v_scale = v+1; + int p = ((255 - s) * v_scale) >> 8; + int q = ((255 - (s * f >> 16)) * v_scale) >> 8; + int t = ((255 - (s * (1 << 16 - f) >> 16)) * v_scale) >> 8; + + int r, g, b; + + if (hx >> 16 >= 6) + throw new RuntimeException("hx is out of range"); + switch (hx >> 16) { + case 0: r = v; g = t; b = p; break; + case 1: r = q; g = v; b = p; break; + case 2: r = p; g = v; b = t; break; + case 3: r = p; g = q; b = v; break; + case 4: r = t; g = p; b = v; break; + default: r = v; g = p; b = q; break; + } + return (alpha << 24) | (r << 16) | (g << 8) | b; + } + private static final HashMap sColorNameMap; + static { sColorNameMap = new HashMap<>(); sColorNameMap.put("black", BLACK); diff --git a/Vision/src/main/java/com/qualcomm/robotcore/eventloop/opmode/OpMode.java b/Vision/src/main/java/com/qualcomm/robotcore/eventloop/opmode/OpMode.java index 779bfe32..ec861393 100644 --- a/Vision/src/main/java/com/qualcomm/robotcore/eventloop/opmode/OpMode.java +++ b/Vision/src/main/java/com/qualcomm/robotcore/eventloop/opmode/OpMode.java @@ -31,6 +31,7 @@ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE package com.qualcomm.robotcore.eventloop.opmode; +import com.qualcomm.robotcore.hardware.Gamepad; import com.qualcomm.robotcore.hardware.HardwareMap; import io.github.deltacv.vision.external.util.FrameQueue; import io.github.deltacv.vision.internal.opmode.OpModeNotification; @@ -53,6 +54,10 @@ public abstract class OpMode extends TimestampedOpenCvPipeline { // never in my volatile boolean isStarted = false; volatile boolean stopRequested = false; + // Stubs! + public Gamepad gamepad1 = new Gamepad(); + public Gamepad gamepad2 = new Gamepad(); + protected FrameQueue inputQueue; public HardwareMap hardwareMap; diff --git a/Vision/src/main/java/com/qualcomm/robotcore/hardware/Gamepad.java b/Vision/src/main/java/com/qualcomm/robotcore/hardware/Gamepad.java new file mode 100644 index 00000000..bfef1dab --- /dev/null +++ b/Vision/src/main/java/com/qualcomm/robotcore/hardware/Gamepad.java @@ -0,0 +1,424 @@ +/* + * Copyright (c) 2014, 2015 Qualcomm Technologies Inc + * + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, are permitted + * (subject to the limitations in the disclaimer below) provided that the following conditions are + * met: + * + * Redistributions of source code must retain the above copyright notice, this list of conditions + * and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, this list of conditions + * and the following disclaimer in the documentation and/or other materials provided with the + * distribution. + * + * Neither the name of Qualcomm Technologies Inc nor the names of its contributors may be used to + * endorse or promote products derived from this software without specific prior written permission. + * + * NO EXPRESS OR IMPLIED LICENSES TO ANY PARTY'S PATENT RIGHTS ARE GRANTED BY THIS LICENSE. THIS + * SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED + * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS + * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, + * OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF + * THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package com.qualcomm.robotcore.hardware; + +/** + * Monitor a hardware gamepad. In the case of EOCV-Sim, this is a stub class that does nothing. + *

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

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

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

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

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

+ * The dpad is represented as 4 buttons, dpad_up, dpad_down, dpad_left, and dpad_right + */ +@SuppressWarnings("unused") +public class Gamepad { + + /** + * A gamepad with an ID equal to ID_UNASSOCIATED has not been associated with any device. + */ + public static final int ID_UNASSOCIATED = -1; + + /** + * A gamepad with a phantom id a synthetic one made up by the system + */ + public static final int ID_SYNTHETIC = -2; + + public enum Type { + // Do NOT change the order/names of existing entries, + // you will break backwards compatibility!! + UNKNOWN(LegacyType.UNKNOWN), + LOGITECH_F310(LegacyType.LOGITECH_F310), + XBOX_360(LegacyType.XBOX_360), + SONY_PS4(LegacyType.SONY_PS4), // This indicates a PS4-compatible controller that is being used through our compatibility mode + SONY_PS4_SUPPORTED_BY_KERNEL(LegacyType.SONY_PS4); // This indicates a PS4-compatible controller that is being used through the DualShock 4 Linux kernel driver. + + private final LegacyType correspondingLegacyType; + Type(LegacyType correspondingLegacyType) { + this.correspondingLegacyType = correspondingLegacyType; + } + } + + // LegacyType is necessary because robocol gamepad version 3 was written in a way that was not + // forwards-compatible, so we have to keep sending V3-compatible values. + public enum LegacyType { + // Do NOT change the order or names of existing entries, or add new entries. + // You will break backwards compatibility!! + UNKNOWN, + LOGITECH_F310, + XBOX_360, + SONY_PS4 + } + + @SuppressWarnings("UnusedAssignment") + public volatile Type type = Type.UNKNOWN; // IntelliJ thinks this is redundant, but it is NOT. Must be a bug in the analyzer? + + /** + * left analog stick horizontal axis + */ + public volatile float left_stick_x = 0f; + + /** + * left analog stick vertical axis + */ + public volatile float left_stick_y = 0f; + + /** + * right analog stick horizontal axis + */ + public volatile float right_stick_x = 0f; + + /** + * right analog stick vertical axis + */ + public volatile float right_stick_y = 0f; + + /** + * dpad up + */ + public volatile boolean dpad_up = false; + + /** + * dpad down + */ + public volatile boolean dpad_down = false; + + /** + * dpad left + */ + public volatile boolean dpad_left = false; + + /** + * dpad right + */ + public volatile boolean dpad_right = false; + + /** + * button a + */ + public volatile boolean a = false; + + /** + * button b + */ + public volatile boolean b = false; + + /** + * button x + */ + public volatile boolean x = false; + + /** + * button y + */ + public volatile boolean y = false; + + /** + * button guide - often the large button in the middle of the controller. The OS may + * capture this button before it is sent to the app; in which case you'll never + * receive it. + */ + public volatile boolean guide = false; + + /** + * button start + */ + public volatile boolean start = false; + + /** + * button back + */ + public volatile boolean back = false; + + /** + * button left bumper + */ + public volatile boolean left_bumper = false; + + /** + * button right bumper + */ + public volatile boolean right_bumper = false; + + /** + * left stick button + */ + public volatile boolean left_stick_button = false; + + /** + * right stick button + */ + public volatile boolean right_stick_button = false; + + /** + * left trigger + */ + public volatile float left_trigger = 0f; + + /** + * right trigger + */ + public volatile float right_trigger = 0f; + + /** + * PS4 Support - Circle + */ + public volatile boolean circle = false; + + /** + * PS4 Support - cross + */ + public volatile boolean cross = false; + + /** + * PS4 Support - triangle + */ + public volatile boolean triangle = false; + + /** + * PS4 Support - square + */ + public volatile boolean square = false; + + /** + * PS4 Support - share + */ + public volatile boolean share = false; + + /** + * PS4 Support - options + */ + public volatile boolean options = false; + + /** + * PS4 Support - touchpad + */ + public volatile boolean touchpad = false; + public volatile boolean touchpad_finger_1; + public volatile boolean touchpad_finger_2; + public volatile float touchpad_finger_1_x; + public volatile float touchpad_finger_1_y; + public volatile float touchpad_finger_2_x; + public volatile float touchpad_finger_2_y; + + /** + * PS4 Support - PS Button + */ + public volatile boolean ps = false; + + /** + * ID assigned to this gamepad by the OS. This value can change each time the device is plugged in. + */ + public volatile int id = ID_UNASSOCIATED; // public only for historical reasons + + public void setGamepadId(int id) { + this.id = id; + } + public int getGamepadId() { + return this.id; + } + + /** + * Relative timestamp of the last time an event was detected + */ + public volatile long timestamp = 0; + + public Gamepad() { + this.type = type(); + } + + /** + * Reset this gamepad into its initial state + */ + public void reset() { + left_stick_x = 0f; + left_stick_y = 0f; + right_stick_x = 0f; + right_stick_y = 0f; + dpad_up = false; + dpad_down = false; + dpad_left = false; + dpad_right = false; + a = false; + b = false; + x = false; + y = false; + guide = false; + start = false; + back = false; + left_bumper = false; + right_bumper = false; + left_stick_button = false; + right_stick_button = false; + left_trigger = 0f; + right_trigger = 0f; + circle = false; + cross = false; + triangle = false; + square = false; + share = false; + options = false; + touchpad = false; + touchpad_finger_1 = false; + touchpad_finger_2 = false; + touchpad_finger_1_x = 0f; + touchpad_finger_1_y = 0f; + touchpad_finger_2_x = 0f; + touchpad_finger_2_y = 0f; + ps = false; + timestamp = 0; + } + + /** + * Are all analog sticks and triggers in their rest position? + * @return true if all analog sticks and triggers are at rest; otherwise false + */ + public boolean atRest() { + return ( + left_stick_x == 0f && left_stick_y == 0f && + right_stick_x == 0f && right_stick_y == 0f && + left_trigger == 0f && right_trigger == 0f); + } + + /** + * Get the type of gamepad as a {@link Type}. This method defaults to "UNKNOWN". + * @return gamepad type + */ + public Type type() { + return type; + } + + /** + * Get the type of gamepad as a {@link LegacyType}. This method defaults to "UNKNOWN". + * @return gamepad type + */ + private LegacyType legacyType() { + return type.correspondingLegacyType; + } + + + /** + * Display a summary of this gamepad, including the state of all buttons, analog sticks, and triggers + * @return a summary + */ + @Override + public String toString() { + + switch (type) { + case SONY_PS4: + case SONY_PS4_SUPPORTED_BY_KERNEL: + return ps4ToString(); + + case UNKNOWN: + case LOGITECH_F310: + case XBOX_360: + default: + return genericToString(); + } + } + + + protected String ps4ToString() { + String buttons = new String(); + if (dpad_up) buttons += "dpad_up "; + if (dpad_down) buttons += "dpad_down "; + if (dpad_left) buttons += "dpad_left "; + if (dpad_right) buttons += "dpad_right "; + if (cross) buttons += "cross "; + if (circle) buttons += "circle "; + if (square) buttons += "square "; + if (triangle) buttons += "triangle "; + if (ps) buttons += "ps "; + if (share) buttons += "share "; + if (options) buttons += "options "; + if (touchpad) buttons += "touchpad "; + if (left_bumper) buttons += "left_bumper "; + if (right_bumper) buttons += "right_bumper "; + if (left_stick_button) buttons += "left stick button "; + if (right_stick_button) buttons += "right stick button "; + + return String.format("ID: %2d user: %2d lx: % 1.2f ly: % 1.2f rx: % 1.2f ry: % 1.2f lt: %1.2f rt: %1.2f %s", + id, 0, left_stick_x, left_stick_y, + right_stick_x, right_stick_y, left_trigger, right_trigger, buttons); + } + + protected String genericToString() { + String buttons = new String(); + if (dpad_up) buttons += "dpad_up "; + if (dpad_down) buttons += "dpad_down "; + if (dpad_left) buttons += "dpad_left "; + if (dpad_right) buttons += "dpad_right "; + if (a) buttons += "a "; + if (b) buttons += "b "; + if (x) buttons += "x "; + if (y) buttons += "y "; + if (guide) buttons += "guide "; + if (start) buttons += "start "; + if (back) buttons += "back "; + if (left_bumper) buttons += "left_bumper "; + if (right_bumper) buttons += "right_bumper "; + if (left_stick_button) buttons += "left stick button "; + if (right_stick_button) buttons += "right stick button "; + + return String.format("ID: %2d user: %2d lx: % 1.2f ly: % 1.2f rx: % 1.2f ry: % 1.2f lt: %1.2f rt: %1.2f %s", + id, 0, left_stick_x, left_stick_y, + right_stick_x, right_stick_y, left_trigger, right_trigger, buttons); + } + + /** + * Alias buttons so that XBOX & PS4 native button labels can be used in use code. + * Should allow a team to program with whatever controllers they prefer, but + * be able to swap controllers easily without changing code. + */ + protected void updateButtonAliases(){ + // There is no assignment for touchpad because there is no equivalent on XBOX controllers. + circle = b; + cross = a; + triangle = y; + square = x; + share = back; + options = start; + ps = guide; + } +} diff --git a/Vision/src/main/java/com/qualcomm/robotcore/hardware/HardwareMap.java b/Vision/src/main/java/com/qualcomm/robotcore/hardware/HardwareMap.java index f0c39b30..1a58f1a0 100644 --- a/Vision/src/main/java/com/qualcomm/robotcore/hardware/HardwareMap.java +++ b/Vision/src/main/java/com/qualcomm/robotcore/hardware/HardwareMap.java @@ -21,7 +21,7 @@ public T get(Class classType, String deviceName) { return (T) new SourcedCameraNameImpl(ThreadSourceHander.hand(deviceName)); } - return null; + throw new IllegalArgumentException("Unknown device type " + classType.getName()); } } \ No newline at end of file