From 2e1b3d0f83ce3e5a66cf38bc0a95f81f8159635d Mon Sep 17 00:00:00 2001 From: Matt Date: Sat, 16 Jan 2021 20:41:47 -0800 Subject: [PATCH] Add Photonlib (#231) Merges Photonlib into Photonvision, along with the Photonlib code examples. Also creates a new PhotonTargeting library teams can depend on. --- .github/workflows/main.yml | 100 ++- .gitignore | 3 + photon-core/build.gradle | 29 +- photon-core/gradle/wrapper/gradle-wrapper.jar | Bin 58702 -> 0 bytes .../gradle/wrapper/gradle-wrapper.properties | 6 - .../networktables/NTDataPublisher.java | 20 +- .../common/hardware/HardwareManager.java | 3 +- .../common/hardware/VisionLED.java | 58 +- photon-core/versioningHelper.gradle | 46 -- photon-lib/.clang-format | 167 +++++ photon-lib/.styleguide | 20 + photon-lib/.styleguide-license | 16 + photon-lib/LICENSE | 674 ++++++++++++++++++ photon-lib/README.md | 3 + photon-lib/build.gradle | 129 ++++ photon-lib/clang-format.sh | 62 ++ photon-lib/config.gradle | 181 +++++ photon-lib/publish.gradle | 191 +++++ photon-lib/settings.gradle | 8 + photon-lib/src/generate/photonlib.json.in | 40 ++ photon-lib/src/main/driver/cpp/VendorJNI.cpp | 46 ++ .../src/main/driver/cpp/driversource.cpp | 22 + .../src/main/driver/include/driverheader.h | 22 + photon-lib/src/main/driver/symbols.txt | 4 + .../java/org/photonvision/PhotonCamera.java | 198 +++++ .../java/org/photonvision/PhotonUtils.java | 166 +++++ .../org/photonvision/SimPhotonCamera.java | 70 ++ .../org/photonvision/SimVisionSystem.java | 181 +++++ .../org/photonvision/SimVisionTarget.java | 51 ++ .../native/cpp/photonlib/PhotonCamera.cpp | 90 +++ .../cpp/photonlib/PhotonPipelineResult.cpp | 67 ++ .../cpp/photonlib/PhotonTrackedTarget.cpp | 60 ++ .../native/cpp/photonlib/SimPhotonCamera.cpp | 42 ++ .../native/cpp/photonlib/SimVisionSystem.cpp | 119 ++++ .../native/cpp/photonlib/SimVisionTarget.cpp | 33 + .../main/native/include/photonlib/Packet.h | 121 ++++ .../native/include/photonlib/PhotonCamera.h | 141 ++++ .../include/photonlib/PhotonPipelineResult.h | 100 +++ .../include/photonlib/PhotonTrackedTarget.h | 93 +++ .../native/include/photonlib/PhotonUtils.h | 173 +++++ .../include/photonlib/SimPhotonCamera.h | 65 ++ .../include/photonlib/SimVisionSystem.h | 74 ++ .../include/photonlib/SimVisionTarget.h | 45 ++ .../java/org/photonvision/PacketTest.java | 98 +++ .../org/photonvision/PhotonCameraTest.java | 39 + .../java/org/photonvision/PhotonUtilTest.java | 78 ++ .../org/photonvision/SimVisionSystemTest.java | 307 ++++++++ photon-lib/src/test/native/cpp/PacketTest.cpp | 91 +++ .../src/test/native/cpp/PhotonUtilsTest.cpp | 21 + .../test/native/cpp/SimVisionSystemTest.cpp | 401 +++++++++++ photon-lib/src/test/native/cpp/main.cpp | 24 + photon-targeting/.gitignore | 13 + photon-targeting/build.gradle | 33 + photon-targeting/publish.gradle | 28 + photon-targeting/settings.gradle | 2 + .../common/dataflow/structures/Packet.java | 4 + .../common/hardware/VisionLEDMode.java | 46 ++ .../targeting/PhotonPipelineResult.java | 85 ++- .../targeting/PhotonTrackedTarget.java | 15 +- photonlib-cpp-examples/build.gradle | 77 ++ photonlib-cpp-examples/settings.gradle | 1 + .../cpp/examples/aimandrange/cpp/Robot.cpp | 64 ++ .../cpp/examples/aimandrange/include/Robot.h | 64 ++ .../cpp/examples/aimattarget/cpp/Robot.cpp | 51 ++ .../cpp/examples/aimattarget/include/Robot.h | 46 ++ .../src/main/cpp/examples/examples.json | 38 + .../cpp/examples/getinrange/cpp/Robot.cpp | 57 ++ .../cpp/examples/getinrange/include/Robot.h | 60 ++ photonlib-java-examples/build.gradle | 35 + photonlib-java-examples/settings.gradle | 1 + .../photonlib/examples/aimandrange/Main.java | 38 + .../photonlib/examples/aimandrange/Robot.java | 103 +++ .../photonlib/examples/aimattarget/Main.java | 38 + .../photonlib/examples/aimattarget/Robot.java | 103 +++ .../java/org/photonlib/examples/examples.json | 38 + .../photonlib/examples/getinrange/Main.java | 38 + .../photonlib/examples/getinrange/Robot.java | 100 +++ settings.gradle | 6 +- versioningHelper.gradle | 27 + 79 files changed, 5867 insertions(+), 142 deletions(-) delete mode 100644 photon-core/gradle/wrapper/gradle-wrapper.jar delete mode 100644 photon-core/gradle/wrapper/gradle-wrapper.properties delete mode 100644 photon-core/versioningHelper.gradle create mode 100644 photon-lib/.clang-format create mode 100644 photon-lib/.styleguide create mode 100644 photon-lib/.styleguide-license create mode 100644 photon-lib/LICENSE create mode 100644 photon-lib/README.md create mode 100644 photon-lib/build.gradle create mode 100644 photon-lib/clang-format.sh create mode 100644 photon-lib/config.gradle create mode 100644 photon-lib/publish.gradle create mode 100644 photon-lib/settings.gradle create mode 100644 photon-lib/src/generate/photonlib.json.in create mode 100644 photon-lib/src/main/driver/cpp/VendorJNI.cpp create mode 100644 photon-lib/src/main/driver/cpp/driversource.cpp create mode 100644 photon-lib/src/main/driver/include/driverheader.h create mode 100644 photon-lib/src/main/driver/symbols.txt create mode 100644 photon-lib/src/main/java/org/photonvision/PhotonCamera.java create mode 100644 photon-lib/src/main/java/org/photonvision/PhotonUtils.java create mode 100644 photon-lib/src/main/java/org/photonvision/SimPhotonCamera.java create mode 100644 photon-lib/src/main/java/org/photonvision/SimVisionSystem.java create mode 100644 photon-lib/src/main/java/org/photonvision/SimVisionTarget.java create mode 100644 photon-lib/src/main/native/cpp/photonlib/PhotonCamera.cpp create mode 100644 photon-lib/src/main/native/cpp/photonlib/PhotonPipelineResult.cpp create mode 100644 photon-lib/src/main/native/cpp/photonlib/PhotonTrackedTarget.cpp create mode 100644 photon-lib/src/main/native/cpp/photonlib/SimPhotonCamera.cpp create mode 100644 photon-lib/src/main/native/cpp/photonlib/SimVisionSystem.cpp create mode 100644 photon-lib/src/main/native/cpp/photonlib/SimVisionTarget.cpp create mode 100644 photon-lib/src/main/native/include/photonlib/Packet.h create mode 100644 photon-lib/src/main/native/include/photonlib/PhotonCamera.h create mode 100644 photon-lib/src/main/native/include/photonlib/PhotonPipelineResult.h create mode 100644 photon-lib/src/main/native/include/photonlib/PhotonTrackedTarget.h create mode 100644 photon-lib/src/main/native/include/photonlib/PhotonUtils.h create mode 100644 photon-lib/src/main/native/include/photonlib/SimPhotonCamera.h create mode 100644 photon-lib/src/main/native/include/photonlib/SimVisionSystem.h create mode 100644 photon-lib/src/main/native/include/photonlib/SimVisionTarget.h create mode 100644 photon-lib/src/test/java/org/photonvision/PacketTest.java create mode 100644 photon-lib/src/test/java/org/photonvision/PhotonCameraTest.java create mode 100644 photon-lib/src/test/java/org/photonvision/PhotonUtilTest.java create mode 100644 photon-lib/src/test/java/org/photonvision/SimVisionSystemTest.java create mode 100644 photon-lib/src/test/native/cpp/PacketTest.cpp create mode 100644 photon-lib/src/test/native/cpp/PhotonUtilsTest.cpp create mode 100644 photon-lib/src/test/native/cpp/SimVisionSystemTest.cpp create mode 100644 photon-lib/src/test/native/cpp/main.cpp create mode 100644 photon-targeting/.gitignore create mode 100644 photon-targeting/build.gradle create mode 100644 photon-targeting/publish.gradle create mode 100644 photon-targeting/settings.gradle rename {photon-core => photon-targeting}/src/main/java/org/photonvision/common/dataflow/structures/Packet.java (98%) create mode 100644 photon-targeting/src/main/java/org/photonvision/common/hardware/VisionLEDMode.java rename photon-core/src/main/java/org/photonvision/vision/pipeline/result/SimplePipelineResult.java => photon-targeting/src/main/java/org/photonvision/targeting/PhotonPipelineResult.java (59%) rename photon-core/src/main/java/org/photonvision/vision/pipeline/result/SimpleTrackedTarget.java => photon-targeting/src/main/java/org/photonvision/targeting/PhotonTrackedTarget.java (88%) create mode 100644 photonlib-cpp-examples/build.gradle create mode 100644 photonlib-cpp-examples/settings.gradle create mode 100644 photonlib-cpp-examples/src/main/cpp/examples/aimandrange/cpp/Robot.cpp create mode 100644 photonlib-cpp-examples/src/main/cpp/examples/aimandrange/include/Robot.h create mode 100644 photonlib-cpp-examples/src/main/cpp/examples/aimattarget/cpp/Robot.cpp create mode 100644 photonlib-cpp-examples/src/main/cpp/examples/aimattarget/include/Robot.h create mode 100644 photonlib-cpp-examples/src/main/cpp/examples/examples.json create mode 100644 photonlib-cpp-examples/src/main/cpp/examples/getinrange/cpp/Robot.cpp create mode 100644 photonlib-cpp-examples/src/main/cpp/examples/getinrange/include/Robot.h create mode 100644 photonlib-java-examples/build.gradle create mode 100644 photonlib-java-examples/settings.gradle create mode 100644 photonlib-java-examples/src/main/java/org/photonlib/examples/aimandrange/Main.java create mode 100644 photonlib-java-examples/src/main/java/org/photonlib/examples/aimandrange/Robot.java create mode 100644 photonlib-java-examples/src/main/java/org/photonlib/examples/aimattarget/Main.java create mode 100644 photonlib-java-examples/src/main/java/org/photonlib/examples/aimattarget/Robot.java create mode 100644 photonlib-java-examples/src/main/java/org/photonlib/examples/examples.json create mode 100644 photonlib-java-examples/src/main/java/org/photonlib/examples/getinrange/Main.java create mode 100644 photonlib-java-examples/src/main/java/org/photonlib/examples/getinrange/Robot.java create mode 100644 versioningHelper.gradle diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 71c928d4b..5d8947820 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -14,7 +14,7 @@ on: jobs: # This job builds the client (web view). - build-client: + photonclient-build: # Let all steps run within the photon-client dir. defaults: @@ -49,7 +49,7 @@ jobs: name: built-client path: photon-client/dist/ - build-server: + photon-build-all: # The type of runner that the job will run on. runs-on: ubuntu-latest @@ -89,7 +89,7 @@ jobs: with: file: ./photon-core/build/reports/jacoco/test/jacocoTestReport.xml - build-offline-docs: + photonserver-build-offline-docs: runs-on: ubuntu-latest steps: @@ -125,8 +125,8 @@ jobs: name: built-docs path: build/html - build-package: - needs: [build-client, build-server, build-offline-docs] + photon-build-package: + needs: [photonclient-build, photon-build-all, photonserver-build-offline-docs] # The type of runner that the job will run on. runs-on: ubuntu-latest @@ -177,7 +177,7 @@ jobs: photon-server/build/libs/*.jar if: github.event_name == 'push' - check-lint: + photonserver-check-lint: # The type of runner that the job will run on. runs-on: ubuntu-latest @@ -195,9 +195,9 @@ jobs: chmod +x gradlew ./gradlew spotlessCheck - release: + photon-release: if: startsWith(github.ref, 'refs/tags/v') - needs: [build-package] + needs: [photon-build-package] runs-on: ubuntu-latest steps: - uses: actions/download-artifact@v2 @@ -208,3 +208,87 @@ jobs: files: '**/*' env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + # Building photonlib + photonlib-build-host: + strategy: + fail-fast: false + matrix: + include: + - os: windows-latest + artifact-name: Win64 + - os: macos-latest + artifact-name: macOS + - os: ubuntu-latest + artifact-name: Linux + + runs-on: ${{ matrix.os }} + name: "Photonlib - Build - ${{ matrix.artifact-name }}" + steps: + - uses: actions/checkout@v2 + - uses: actions/setup-java@v1 + with: + java-version: 11 + - run: | + chmod +x gradlew + ./gradlew photon-lib:build + - run: ./gradlew photonlib:publish + name: Publish + env: + ARTIFACTORY_API_KEY: ${{ secrets.ARTIFACTORY_API_KEY }} + if: github.event_name == 'push' + + photonlib-build-docker: + strategy: + fail-fast: false + matrix: + include: + - container: wpilib/roborio-cross-ubuntu:2020-18.04 + artifact-name: Athena + - container: wpilib/raspbian-cross-ubuntu:10-18.04 + artifact-name: Raspbian + - container: wpilib/aarch64-cross-ubuntu:bionic-18.04 + artifact-name: Aarch64 + + runs-on: ubuntu-latest + container: ${{ matrix.container }} + name: "Photonlib - Build - ${{ matrix.artifact-name }}" + steps: + - uses: actions/checkout@v2 + - uses: actions/setup-java@v1 + with: + java-version: 11 + - run: | + chmod +x gradlew + ./gradlew photon-lib:build + - run: | + chmod +x gradlew + ./gradlew photon-lib:publish + env: + ARTIFACTORY_API_KEY: ${{ secrets.ARTIFACTORY_API_KEY }} + if: github.event_name == 'push' + + photonlib-wpiformat: + name: "wpiformat" + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - name: Fetch all history and metadata + run: | + git fetch --prune --unshallow + git checkout -b pr + git branch -f master origin/master + - name: Set up Python 3.8 + uses: actions/setup-python@v2 + with: + python-version: 3.8 + - name: Install clang-format + run: sudo apt-get update -q && sudo apt-get install clang-format-10 + - name: Install wpiformat + run: pip3 install wpiformat + - name: Run + run: | + ls -la + wpiformat -clang 10 -f photon-lib + - name: Check Output + run: git --no-pager diff --exit-code HEAD diff --git a/.gitignore b/.gitignore index 3e8fcce9f..21cd24014 100644 --- a/.gitignore +++ b/.gitignore @@ -131,3 +131,6 @@ photon-server/src/main/generated/native/include/org_photonvision_raspi_PicamJNI. .gradle/* photonvision_config build/spotlessJava +build/* +build +photon-lib/src/main/java/org/photonvision/PhotonVersion.java diff --git a/photon-core/build.gradle b/photon-core/build.gradle index fd6eb19a0..44674159d 100644 --- a/photon-core/build.gradle +++ b/photon-core/build.gradle @@ -1,7 +1,12 @@ +import java.time.LocalDateTime +import java.time.format.DateTimeFormatter + apply from: '../common.gradle' -apply from: 'versioningHelper.gradle' +apply from: '../versioningHelper.gradle' dependencies { + implementation project(':photon-targeting') + implementation 'io.javalin:javalin:3.7.0' implementation 'org.msgpack:msgpack-core:0.8.20' @@ -17,3 +22,25 @@ dependencies { // Zip compile 'org.zeroturnaround:zt-zip:1.14' } + +task writeCurrentVersionJava { + String date = DateTimeFormatter.ofPattern("yyyy-M-d hh:mm:ss").format(LocalDateTime.now()) + File versionFile = new File(java.nio.file.Path.of("$projectDir", "src", "main", "java", "org", "photonvision", "PhotonVersion.java") + .toAbsolutePath().toString()) + versionFile.delete() + versionFile << "package org.photonvision;\n" + + "\n" + + "/*\n" + + " * Autogenerated file! Do not manually edit this file. This version is regenerated\n" + + " * any time the publish task is run, or when this file is deleted.\n" + + " */\n" + + "\n" + + "@SuppressWarnings(\"ALL\")\n" + + "public final class PhotonVersion {\n" + + " public static final String versionString = \"${versionString}\";\n" + + " public static final String buildDate = \"${date}\";\n" + + " public static final boolean isRelease = !versionString.startsWith(\"dev\");\n" + + "}" +} + +build.dependsOn writeCurrentVersionJava diff --git a/photon-core/gradle/wrapper/gradle-wrapper.jar b/photon-core/gradle/wrapper/gradle-wrapper.jar deleted file mode 100644 index cc4fdc293d0e50b0ad9b65c16e7ddd1db2f6025b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 58702 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

@up^y4&I8N>~wvL%f(S2W%NN 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 D613UA diff --git a/photon-core/gradle/wrapper/gradle-wrapper.properties b/photon-core/gradle/wrapper/gradle-wrapper.properties deleted file mode 100644 index b16a683a7..000000000 --- a/photon-core/gradle/wrapper/gradle-wrapper.properties +++ /dev/null @@ -1,6 +0,0 @@ -distributionBase=GRADLE_USER_HOME -distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-6.7.1-bin.zip -distributionSha256Sum=3239b5ed86c3838a37d983ac100573f64c1f3fd8e1eb6c89fa5f9529b5ec091d -zipStoreBase=GRADLE_USER_HOME -zipStorePath=wrapper/dists diff --git a/photon-core/src/main/java/org/photonvision/common/dataflow/networktables/NTDataPublisher.java b/photon-core/src/main/java/org/photonvision/common/dataflow/networktables/NTDataPublisher.java index 0fb875283..710aebb25 100644 --- a/photon-core/src/main/java/org/photonvision/common/dataflow/networktables/NTDataPublisher.java +++ b/photon-core/src/main/java/org/photonvision/common/dataflow/networktables/NTDataPublisher.java @@ -20,13 +20,17 @@ package org.photonvision.common.dataflow.networktables; import edu.wpi.first.networktables.EntryNotification; import edu.wpi.first.networktables.NetworkTable; import edu.wpi.first.networktables.NetworkTableEntry; +import java.util.ArrayList; +import java.util.List; import java.util.function.BooleanSupplier; import java.util.function.Consumer; import java.util.function.Supplier; import org.photonvision.common.dataflow.CVPipelineResultConsumer; import org.photonvision.common.dataflow.structures.Packet; +import org.photonvision.targeting.PhotonPipelineResult; +import org.photonvision.targeting.PhotonTrackedTarget; import org.photonvision.vision.pipeline.result.CVPipelineResult; -import org.photonvision.vision.pipeline.result.SimplePipelineResult; +import org.photonvision.vision.target.TrackedTarget; public class NTDataPublisher implements CVPipelineResultConsumer { @@ -163,7 +167,9 @@ public class NTDataPublisher implements CVPipelineResultConsumer { @Override public void accept(CVPipelineResult result) { - var simplified = new SimplePipelineResult(result); + var simplified = + new PhotonPipelineResult( + result.getLatencyMillis(), simpleFromTrackedTargets(result.targets)); Packet packet = new Packet(simplified.getPacketSize()); simplified.populatePacket(packet); @@ -201,4 +207,14 @@ public class NTDataPublisher implements CVPipelineResultConsumer { } rootTable.getInstance().flush(); } + + public static List simpleFromTrackedTargets(List targets) { + var ret = new ArrayList(); + for (var t : targets) { + ret.add( + new PhotonTrackedTarget( + t.getYaw(), t.getPitch(), t.getArea(), t.getSkew(), t.getCameraToTarget())); + } + return ret; + } } diff --git a/photon-core/src/main/java/org/photonvision/common/hardware/HardwareManager.java b/photon-core/src/main/java/org/photonvision/common/hardware/HardwareManager.java index da42f72c8..fbaebc4cc 100644 --- a/photon-core/src/main/java/org/photonvision/common/hardware/HardwareManager.java +++ b/photon-core/src/main/java/org/photonvision/common/hardware/HardwareManager.java @@ -27,7 +27,6 @@ import org.photonvision.common.dataflow.networktables.NTDataChangeListener; import org.photonvision.common.dataflow.networktables.NetworkTablesManager; import org.photonvision.common.hardware.GPIO.CustomGPIO; import org.photonvision.common.hardware.GPIO.pi.PigpioSocket; -import org.photonvision.common.hardware.VisionLED.VisionLEDMode; import org.photonvision.common.hardware.metrics.MetricsBase; import org.photonvision.common.logging.LogGroup; import org.photonvision.common.logging.Logger; @@ -91,7 +90,7 @@ public class HardwareManager { pigpioSocket); ledModeEntry = NetworkTablesManager.getInstance().kRootTable.getEntry("ledMode"); - ledModeEntry.setNumber(VisionLEDMode.VLM_DEFAULT.value); + ledModeEntry.setNumber(VisionLEDMode.kDefault.value); ledModeListener = visionLED == null ? null diff --git a/photon-core/src/main/java/org/photonvision/common/hardware/VisionLED.java b/photon-core/src/main/java/org/photonvision/common/hardware/VisionLED.java index ba9ff9599..880619e23 100644 --- a/photon-core/src/main/java/org/photonvision/common/hardware/VisionLED.java +++ b/photon-core/src/main/java/org/photonvision/common/hardware/VisionLED.java @@ -40,7 +40,7 @@ public class VisionLED { private final int brightnessMax; private final PigpioSocket pigpioSocket; - private VisionLEDMode currentLedMode = VisionLEDMode.VLM_DEFAULT; + private VisionLEDMode currentLedMode = VisionLEDMode.kDefault; private BooleanSupplier pipelineModeSupplier; private int mappedBrightnessPercentage; @@ -111,7 +111,7 @@ public class VisionLED { } public void setState(boolean on) { - setInternal(on ? VisionLEDMode.VLM_ON : VisionLEDMode.VLM_OFF, false); + setInternal(on ? VisionLEDMode.kOn : VisionLEDMode.kOff, false); } void onLedModeChange(EntryNotification entryNotification) { @@ -120,20 +120,20 @@ public class VisionLED { VisionLEDMode newLedMode; switch (newLedModeRaw) { case -1: - newLedMode = VisionLEDMode.VLM_DEFAULT; + newLedMode = VisionLEDMode.kDefault; break; case 0: - newLedMode = VisionLEDMode.VLM_OFF; + newLedMode = VisionLEDMode.kOff; break; case 1: - newLedMode = VisionLEDMode.VLM_ON; + newLedMode = VisionLEDMode.kOn; break; case 2: - newLedMode = VisionLEDMode.VLM_BLINK; + newLedMode = VisionLEDMode.kBlink; break; default: logger.warn("User supplied invalid LED mode, falling back to Default"); - newLedMode = VisionLEDMode.VLM_DEFAULT; + newLedMode = VisionLEDMode.kDefault; break; } setInternal(newLedMode, true); @@ -145,16 +145,16 @@ public class VisionLED { if (fromNT) { switch (newLedMode) { - case VLM_DEFAULT: + case kDefault: setStateImpl(pipelineModeSupplier.getAsBoolean()); break; - case VLM_OFF: + case kOff: setStateImpl(false); break; - case VLM_ON: + case kOn: setStateImpl(true); break; - case VLM_BLINK: + case kBlink: blinkImpl(85, -1); break; } @@ -166,15 +166,15 @@ public class VisionLED { + newLedMode.toString() + "\""); } else { - if (currentLedMode == VisionLEDMode.VLM_DEFAULT) { + if (currentLedMode == VisionLEDMode.kDefault) { switch (newLedMode) { - case VLM_DEFAULT: + case kDefault: setStateImpl(pipelineModeSupplier.getAsBoolean()); break; - case VLM_OFF: + case kOff: setStateImpl(false); break; - case VLM_ON: + case kOn: setStateImpl(true); break; } @@ -182,32 +182,4 @@ public class VisionLED { logger.info("Changing LED internal state to " + newLedMode.toString()); } } - - public enum VisionLEDMode { - VLM_DEFAULT(-1), - VLM_OFF(0), - VLM_ON(1), - VLM_BLINK(2); - - public final int value; - - VisionLEDMode(int value) { - this.value = value; - } - - @Override - public String toString() { - switch (this) { - case VLM_DEFAULT: - return "Default"; - case VLM_OFF: - return "Off"; - case VLM_ON: - return "On"; - case VLM_BLINK: - return "Blink"; - } - return ""; - } - } } diff --git a/photon-core/versioningHelper.gradle b/photon-core/versioningHelper.gradle deleted file mode 100644 index 84fa29875..000000000 --- a/photon-core/versioningHelper.gradle +++ /dev/null @@ -1,46 +0,0 @@ -import java.time.LocalDateTime -import java.time.format.DateTimeFormatter -import java.nio.file.Path - -gradle.allprojects { - ext.getCurrentVersion = { -> - def stdout = new ByteArrayOutputStream() - String tagIsh - try { - exec { - commandLine 'git', 'describe', '--tags', '--exclude="Dev"' - standardOutput = stdout - } - tagIsh = stdout.toString().trim().toLowerCase() - } catch(Exception e) { - tagIsh = "dev-Unknown" - } - boolean isDev = tagIsh.matches(".*-[0-9]*-g[0-9a-f]*") - if(isDev) tagIsh = "dev-" + tagIsh - println("Picked up version: " + tagIsh) - return tagIsh - } - ext.versionString = getCurrentVersion() -} - -task writeCurrentVersionJava { - String date = DateTimeFormatter.ofPattern("yyyy-M-d hh:mm:ss").format(LocalDateTime.now()) - File versionFile = new File(Path.of("$projectDir", "src", "main", "java", "org", "photonvision", "PhotonVersion.java") - .toAbsolutePath().toString()) - versionFile.delete() - versionFile << "package org.photonvision;\n" + - "\n" + - "/*\n" + - " * Autogenerated file! Do not manually edit this file. This version is regenerated\n" + - " * any time the publish task is run, or when this file is deleted.\n" + - " */\n" + - "\n" + - "@SuppressWarnings(\"ALL\")\n" + - "public final class PhotonVersion {\n" + - " public static final String versionString = \"${versionString}\";\n" + - " public static final String buildDate = \"${date}\";\n" + - " public static final boolean isRelease = !versionString.startsWith(\"dev\");\n" + - "}" -} - -build.dependsOn writeCurrentVersionJava diff --git a/photon-lib/.clang-format b/photon-lib/.clang-format new file mode 100644 index 000000000..14ff7a4a8 --- /dev/null +++ b/photon-lib/.clang-format @@ -0,0 +1,167 @@ +--- +Language: Cpp +BasedOnStyle: Google +AccessModifierOffset: -1 +AlignAfterOpenBracket: Align +AlignConsecutiveMacros: false +AlignConsecutiveAssignments: false +AlignConsecutiveDeclarations: false +AlignEscapedNewlines: Left +AlignOperands: true +AlignTrailingComments: true +AllowAllArgumentsOnNextLine: true +AllowAllConstructorInitializersOnNextLine: true +AllowAllParametersOfDeclarationOnNextLine: true +AllowShortBlocksOnASingleLine: Never +AllowShortCaseLabelsOnASingleLine: false +AllowShortFunctionsOnASingleLine: All +AllowShortLambdasOnASingleLine: All +AllowShortIfStatementsOnASingleLine: WithoutElse +AllowShortLoopsOnASingleLine: true +AlwaysBreakAfterDefinitionReturnType: None +AlwaysBreakAfterReturnType: None +AlwaysBreakBeforeMultilineStrings: true +AlwaysBreakTemplateDeclarations: Yes +BinPackArguments: true +BinPackParameters: true +BraceWrapping: + AfterCaseLabel: false + AfterClass: false + AfterControlStatement: false + AfterEnum: false + AfterFunction: false + AfterNamespace: false + AfterObjCDeclaration: false + AfterStruct: false + AfterUnion: false + AfterExternBlock: false + BeforeCatch: false + BeforeElse: false + IndentBraces: false + SplitEmptyFunction: true + SplitEmptyRecord: true + SplitEmptyNamespace: true +BreakBeforeBinaryOperators: None +BreakBeforeBraces: Attach +BreakBeforeInheritanceComma: false +BreakInheritanceList: BeforeColon +BreakBeforeTernaryOperators: true +BreakConstructorInitializersBeforeComma: false +BreakConstructorInitializers: BeforeColon +BreakAfterJavaFieldAnnotations: false +BreakStringLiterals: true +ColumnLimit: 80 +CommentPragmas: '^ IWYU pragma:' +CompactNamespaces: false +ConstructorInitializerAllOnOneLineOrOnePerLine: true +ConstructorInitializerIndentWidth: 4 +ContinuationIndentWidth: 4 +Cpp11BracedListStyle: true +DeriveLineEnding: false +DerivePointerAlignment: false +DisableFormat: false +ExperimentalAutoDetectBinPacking: false +FixNamespaceComments: true +ForEachMacros: + - foreach + - Q_FOREACH + - BOOST_FOREACH +IncludeBlocks: Regroup +IncludeCategories: + - Regex: '^' + Priority: 2 + SortPriority: 0 + - Regex: '^<.*\.h>' + Priority: 1 + SortPriority: 0 + - Regex: '^<.*' + Priority: 2 + SortPriority: 0 + - Regex: '.*' + Priority: 3 + SortPriority: 0 +IncludeIsMainRegex: '([-_](test|unittest))?$' +IncludeIsMainSourceRegex: '' +IndentCaseLabels: true +IndentGotoLabels: true +IndentPPDirectives: None +IndentWidth: 2 +IndentWrappedFunctionNames: false +JavaScriptQuotes: Leave +JavaScriptWrapImports: true +KeepEmptyLinesAtTheStartOfBlocks: false +MacroBlockBegin: '' +MacroBlockEnd: '' +MaxEmptyLinesToKeep: 1 +NamespaceIndentation: None +ObjCBinPackProtocolList: Never +ObjCBlockIndentWidth: 2 +ObjCSpaceAfterProperty: false +ObjCSpaceBeforeProtocolList: true +PenaltyBreakAssignment: 2 +PenaltyBreakBeforeFirstCallParameter: 1 +PenaltyBreakComment: 300 +PenaltyBreakFirstLessLess: 120 +PenaltyBreakString: 1000 +PenaltyBreakTemplateDeclaration: 10 +PenaltyExcessCharacter: 1000000 +PenaltyReturnTypeOnItsOwnLine: 200 +PointerAlignment: Left +RawStringFormats: + - Language: Cpp + Delimiters: + - cc + - CC + - cpp + - Cpp + - CPP + - 'c++' + - 'C++' + CanonicalDelimiter: '' + BasedOnStyle: google + - Language: TextProto + Delimiters: + - pb + - PB + - proto + - PROTO + EnclosingFunctions: + - EqualsProto + - EquivToProto + - PARSE_PARTIAL_TEXT_PROTO + - PARSE_TEST_PROTO + - PARSE_TEXT_PROTO + - ParseTextOrDie + - ParseTextProtoOrDie + CanonicalDelimiter: '' + BasedOnStyle: google +ReflowComments: true +SortIncludes: false +SortUsingDeclarations: true +SpaceAfterCStyleCast: false +SpaceAfterLogicalNot: false +SpaceAfterTemplateKeyword: true +SpaceBeforeAssignmentOperators: true +SpaceBeforeCpp11BracedList: false +SpaceBeforeCtorInitializerColon: true +SpaceBeforeInheritanceColon: true +SpaceBeforeParens: ControlStatements +SpaceBeforeRangeBasedForLoopColon: true +SpaceInEmptyBlock: false +SpaceInEmptyParentheses: false +SpacesBeforeTrailingComments: 2 +SpacesInAngles: false +SpacesInConditionalStatement: false +SpacesInContainerLiterals: true +SpacesInCStyleCastParentheses: false +SpacesInParentheses: false +SpacesInSquareBrackets: false +SpaceBeforeSquareBrackets: false +Standard: Auto +StatementMacros: + - Q_UNUSED + - QT_REQUIRE_VERSION +TabWidth: 8 +UseCRLF: false +UseTab: Never +... diff --git a/photon-lib/.styleguide b/photon-lib/.styleguide new file mode 100644 index 000000000..645ca1a39 --- /dev/null +++ b/photon-lib/.styleguide @@ -0,0 +1,20 @@ +cppHeaderFileInclude { + \.h$ + \.hpp$ + \.inc$ + \.inl$ +} + +cppSrcFileInclude { + \.cpp$ +} + +includeProject { + ^photonLib/ +} + +includeOtherLibs { + ^frc/ + ^units/ + ^wpi/ +} diff --git a/photon-lib/.styleguide-license b/photon-lib/.styleguide-license new file mode 100644 index 000000000..bcc3fc01e --- /dev/null +++ b/photon-lib/.styleguide-license @@ -0,0 +1,16 @@ +/* + * Copyright (C) Photon Vision. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ diff --git a/photon-lib/LICENSE b/photon-lib/LICENSE new file mode 100644 index 000000000..f288702d2 --- /dev/null +++ b/photon-lib/LICENSE @@ -0,0 +1,674 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + Copyright (C) + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. diff --git a/photon-lib/README.md b/photon-lib/README.md new file mode 100644 index 000000000..34e975fdb --- /dev/null +++ b/photon-lib/README.md @@ -0,0 +1,3 @@ +# PhotonLib + +The vendor dependency for [PhotonVision](https://github.com/photonvision/photonvision). Just add the vendor JSON to your robot project and you're good! diff --git a/photon-lib/build.gradle b/photon-lib/build.gradle new file mode 100644 index 000000000..a8a1f0fab --- /dev/null +++ b/photon-lib/build.gradle @@ -0,0 +1,129 @@ +plugins { + id 'cpp' + id 'java' + id 'edu.wpi.first.wpilib.repositories.WPILibRepositoriesPlugin' version '2020.2' + id 'google-test-test-suite' + id 'edu.wpi.first.NativeUtils' version '2020.10.1' + id 'edu.wpi.first.GradleJni' version '0.10.1' + id 'edu.wpi.first.GradleVsCode' version '0.12.0' +} + +repositories { + mavenCentral() + maven { url "https://frcmaven.wpi.edu/artifactory/development" } +} + +if (project.hasProperty('releaseMode')) { + wpilibRepositories.addAllReleaseRepositories(project) +} else { + wpilibRepositories.addAllDevelopmentRepositories(project) +} + +apply from: '../versioningHelper.gradle' + +ext { + pubVersion = versionString + wpilibVersion = '2020.3.2-99-g9f4de91' +} + +// Apply C++ configuration +apply from: 'config.gradle' + +test { + useJUnitPlatform() +} + +// Apply Java configuration +dependencies { + + // TODO C++ + compile project(':photon-core') + compile project(':photon-targeting') + + implementation 'edu.wpi.first.cscore:cscore-java:2020.+' + implementation 'edu.wpi.first.cameraserver:cameraserver-java:2020.+' + implementation 'edu.wpi.first.wpilibj:wpilibj-java:2020.+' + implementation 'edu.wpi.first.wpiutil:wpiutil-java:2020.+' + implementation 'edu.wpi.first.wpimath:wpimath-java:2020.+' + implementation 'edu.wpi.first.hal:hal-java:2020.+' + implementation 'edu.wpi.first.thirdparty.frc2020.opencv:opencv-java:3.4.7-2' + + implementation "edu.wpi.first.ntcore:ntcore-java:$wpilibVersion" + compile "edu.wpi.first.ntcore:ntcore-jni:$wpilibVersion:linuxaarch64bionic" + compile "edu.wpi.first.ntcore:ntcore-jni:$wpilibVersion:linuxraspbian" + compile "edu.wpi.first.ntcore:ntcore-jni:$wpilibVersion:linuxx86-64" + compile "edu.wpi.first.ntcore:ntcore-jni:$wpilibVersion:osxx86-64" + compile "edu.wpi.first.ntcore:ntcore-jni:$wpilibVersion:windowsx86-64" + + testImplementation("org.junit.jupiter:junit-jupiter-api:5.6.2") + testImplementation("org.junit.jupiter:junit-jupiter-params:5.6.2") + testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:5.6.2") +} + +// Set up exports properly +nativeUtils { + exportsConfigs { + // Main library is just default empty. This will export everything + Photon { + } + } +} + +model { + components { + Photon(NativeLibrarySpec) { + sources { + cpp { + source { + srcDirs 'src/main/native/cpp' + include '**/*.cpp' + } + exportedHeaders { + srcDirs 'src/main/native/include' + } + } + } + nativeUtils.useRequiredLibrary(it, 'wpilib_shared') + } + } + testSuites { + cppTest(GoogleTestTestSuiteSpec) { + testing $.components.Photon + + sources.cpp { + source { + srcDir 'src/test/native/cpp' + include '**/*.cpp' + } + } + + nativeUtils.useRequiredLibrary(it, 'wpilib_executable_shared') + nativeUtils.useRequiredLibrary(it, 'googletest_static') + } + } +} + +def photonlibFileInput = file("src/generate/photonlib.json.in") +ext.photonlibFileOutput = file("$buildDir/generated/vendordeps/photonlib.json") + +task generateVendorJson() { + description = 'Generates the vendor JSON file' + group = 'PhotonVision' + + outputs.file photonlibFileOutput + inputs.file photonlibFileInput + + doLast { + println "Writing version ${pubVersion} to $photonlibFileOutput" + + if (photonlibFileOutput.exists()) { + photonlibFileOutput.delete() + } + def read = photonlibFileInput.text.replace('${photon_version}', pubVersion) + photonlibFileOutput.write(read) + } +} + +build.dependsOn generateVendorJson + +apply from: 'publish.gradle' diff --git a/photon-lib/clang-format.sh b/photon-lib/clang-format.sh new file mode 100644 index 000000000..0b72f3b51 --- /dev/null +++ b/photon-lib/clang-format.sh @@ -0,0 +1,62 @@ +#!/bin/bash +################################################################################ +# Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. +# See https://llvm.org/LICENSE.txt for license information. +# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +################################################################################ +# +# This script will install the llvm toolchain on the different +# Debian and Ubuntu versions + +set -eux + +# read optional command line argument +LLVM_VERSION=10 +if [ "$#" -eq 1 ]; then + LLVM_VERSION=$1 +fi + +DISTRO=$(lsb_release -is) +VERSION=$(lsb_release -sr) +DIST_VERSION="${DISTRO}_${VERSION}" + +if [[ $EUID -ne 0 ]]; then + echo "This script must be run as root!" + exit 1 +fi + +declare -A LLVM_VERSION_PATTERNS +LLVM_VERSION_PATTERNS[9]="-9" +LLVM_VERSION_PATTERNS[10]="-10" +LLVM_VERSION_PATTERNS[11]="" + +if [ ! ${LLVM_VERSION_PATTERNS[$LLVM_VERSION]+_} ]; then + echo "This script does not support LLVM version $LLVM_VERSION" + exit 3 +fi + +LLVM_VERSION_STRING=${LLVM_VERSION_PATTERNS[$LLVM_VERSION]} + +# find the right repository name for the distro and version +case "$DIST_VERSION" in + Debian_9* ) REPO_NAME="deb http://apt.llvm.org/stretch/ llvm-toolchain-stretch$LLVM_VERSION_STRING main" ;; + Debian_10* ) REPO_NAME="deb http://apt.llvm.org/buster/ llvm-toolchain-buster$LLVM_VERSION_STRING main" ;; + Debian_unstable ) REPO_NAME="deb http://apt.llvm.org/unstable/ llvm-toolchain$LLVM_VERSION_STRING main" ;; + Debian_testing ) REPO_NAME="deb http://apt.llvm.org/unstable/ llvm-toolchain$LLVM_VERSION_STRING main" ;; + Ubuntu_16.04 ) REPO_NAME="deb http://apt.llvm.org/xenial/ llvm-toolchain-xenial$LLVM_VERSION_STRING main" ;; + Ubuntu_18.04 ) REPO_NAME="deb http://apt.llvm.org/bionic/ llvm-toolchain-bionic$LLVM_VERSION_STRING main" ;; + Ubuntu_18.10 ) REPO_NAME="deb http://apt.llvm.org/cosmic/ llvm-toolchain-cosmic$LLVM_VERSION_STRING main" ;; + Ubuntu_19.04 ) REPO_NAME="deb http://apt.llvm.org/disco/ llvm-toolchain-disco$LLVM_VERSION_STRING main" ;; + Ubuntu_19.10 ) REPO_NAME="deb http://apt.llvm.org/eoan/ llvm-toolchain-eoan$LLVM_VERSION_STRING main" ;; + Ubuntu_20.04 ) REPO_NAME="deb http://apt.llvm.org/focal/ llvm-toolchain-focal$LLVM_VERSION_STRING main" ;; + * ) + echo "Distribution '$DISTRO' in version '$VERSION' is not supported by this script (${DIST_VERSION})." + exit 2 +esac + + +# install everything +wget -O - https://apt.llvm.org/llvm-snapshot.gpg.key | apt-key add - +add-apt-repository "${REPO_NAME}" +apt-get update +apt-get install -y clang-format-$LLVM_VERSION diff --git a/photon-lib/config.gradle b/photon-lib/config.gradle new file mode 100644 index 000000000..ec82167db --- /dev/null +++ b/photon-lib/config.gradle @@ -0,0 +1,181 @@ +import org.gradle.internal.os.OperatingSystem + +nativeUtils.addWpiNativeUtils() +nativeUtils.withRoboRIO() +nativeUtils.withRaspbian() +nativeUtils.withBionic() + +nativeUtils { + wpi { + configureDependencies { + wpiVersion = "2020.+" + niLibVersion = "2020.10.1" + wpimathVersion = "2020.+" + opencvVersion = "3.4.7-2" + niLibVersion = "2020.10.1" + googleTestVersion = "1.9.0-3-437e100" + opencvVersion = "3.4.7-2" + googleTestVersion = "1.9.0-3-437e100" + } + } +} + +nativeUtils.wpi.addWarnings() +nativeUtils.wpi.addWarningsAsErrors() + +nativeUtils.setSinglePrintPerPlatform() + +model { + // components { + // all { + // targetPlatform nativeUtils.wpi.platforms.roborio + // } + // } + // Uncomment this, and remove above lines to enable builds for all platforms + components { + all { + nativeUtils.useAllPlatforms(it) + } + } + binaries { + withType(NativeBinarySpec).all { + nativeUtils.usePlatformArguments(it) + } + } +} + +ext.appendDebugPathToBinaries = { binaries-> + binaries.withType(StaticLibraryBinarySpec) { + if (it.buildType.name.contains('debug')) { + def staticFileDir = it.staticLibraryFile.parentFile + def staticFileName = it.staticLibraryFile.name + def staticFileExtension = staticFileName.substring(staticFileName.lastIndexOf('.')) + staticFileName = staticFileName.substring(0, staticFileName.lastIndexOf('.')) + staticFileName = staticFileName + 'd' + staticFileExtension + def newStaticFile = new File(staticFileDir, staticFileName) + it.staticLibraryFile = newStaticFile + } + } + binaries.withType(SharedLibraryBinarySpec) { + if (it.buildType.name.contains('debug')) { + def sharedFileDir = it.sharedLibraryFile.parentFile + def sharedFileName = it.sharedLibraryFile.name + def sharedFileExtension = sharedFileName.substring(sharedFileName.lastIndexOf('.')) + sharedFileName = sharedFileName.substring(0, sharedFileName.lastIndexOf('.')) + sharedFileName = sharedFileName + 'd' + sharedFileExtension + def newSharedFile = new File(sharedFileDir, sharedFileName) + + def sharedLinkFileDir = it.sharedLibraryLinkFile.parentFile + def sharedLinkFileName = it.sharedLibraryLinkFile.name + def sharedLinkFileExtension = sharedLinkFileName.substring(sharedLinkFileName.lastIndexOf('.')) + sharedLinkFileName = sharedLinkFileName.substring(0, sharedLinkFileName.lastIndexOf('.')) + sharedLinkFileName = sharedLinkFileName + 'd' + sharedLinkFileExtension + def newLinkFile = new File(sharedLinkFileDir, sharedLinkFileName) + + it.sharedLibraryLinkFile = newLinkFile + it.sharedLibraryFile = newSharedFile + } + } +} + +ext.createComponentZipTasks = { components, names, base, type, project, func -> + def stringNames = names.collect {it.toString()} + def configMap = [:] + components.each { + if (it in NativeLibrarySpec && stringNames.contains(it.name)) { + it.binaries.each { + if (!it.buildable) return + def target = nativeUtils.getPublishClassifier(it) + if (configMap.containsKey(target)) { + configMap.get(target).add(it) + } else { + configMap.put(target, []) + configMap.get(target).add(it) + } + } + } + } + def taskList = [] + def outputsFolder = file("$project.buildDir/outputs") + configMap.each { key, value -> + def task = project.tasks.create(base + "-${key}", type) { + description = 'Creates component archive for platform ' + key + destinationDirectory = outputsFolder + classifier = key + archiveBaseName = '_M_' + base + duplicatesStrategy = 'exclude' + + from(licenseFile) { + into '/' + } + + func(it, value) + } + taskList.add(task) + + project.build.dependsOn task + + project.artifacts { + task + } + addTaskToCopyAllOutputs(task) + } + return taskList +} + +ext.createAllCombined = { list, name, base, type, project -> + def outputsFolder = file("$project.buildDir/outputs") + + def task = project.tasks.create(base + "-all", type) { + description = "Creates component archive for all classifiers" + destinationDirectory = outputsFolder + classifier = "all" + archiveBaseName = base + duplicatesStrategy = 'exclude' + + list.each { + if (it.name.endsWith('debug')) return + from project.zipTree(it.archivePath) + dependsOn it + } + } + + project.build.dependsOn task + + project.artifacts { + task + } + + return task + +} + +ext.includeStandardZipFormat = { task, value -> + value.each { binary -> + if (binary.buildable) { + if (binary instanceof SharedLibraryBinarySpec) { + task.dependsOn binary.tasks.link + task.from(new File(binary.sharedLibraryFile.absolutePath + ".debug")) { + into nativeUtils.getPlatformPath(binary) + '/shared' + } + def sharedPath = binary.sharedLibraryFile.absolutePath + sharedPath = sharedPath.substring(0, sharedPath.length() - 4) + + task.from(new File(sharedPath + '.pdb')) { + into nativeUtils.getPlatformPath(binary) + '/shared' + } + task.from(binary.sharedLibraryFile) { + into nativeUtils.getPlatformPath(binary) + '/shared' + } + task.from(binary.sharedLibraryLinkFile) { + into nativeUtils.getPlatformPath(binary) + '/shared' + } + } else if (binary instanceof StaticLibraryBinarySpec) { + task.dependsOn binary.tasks.createStaticLib + task.from(binary.staticLibraryFile) { + into nativeUtils.getPlatformPath(binary) + '/static' + } + } + } + } +} diff --git a/photon-lib/publish.gradle b/photon-lib/publish.gradle new file mode 100644 index 000000000..7bd75031d --- /dev/null +++ b/photon-lib/publish.gradle @@ -0,0 +1,191 @@ +apply plugin: 'maven-publish' + +ext.licenseFile = files("$rootDir/LICENSE.txt") + +def outputsFolder = file("$buildDir/outputs") +def allOutputsFolder = file("$buildDir/allOutputs") + +def versionFile = file("$allOutputsFolder/version.txt") + +task outputVersions() { + description = 'Prints the versions of wpilib to a file for use by the downstream packaging project' + group = 'Build' + outputs.files(versionFile) + + doFirst { + buildDir.mkdir() + outputsFolder.mkdir() + allOutputsFolder.mkdir() + } + + doLast { + versionFile.write pubVersion + } +} + +task libraryBuild() {} + +build.dependsOn outputVersions + +task copyAllOutputs(type: Copy) { + destinationDir allOutputsFolder +} + +build.dependsOn copyAllOutputs +copyAllOutputs.dependsOn outputVersions + +ext.addTaskToCopyAllOutputs = { task -> + copyAllOutputs.dependsOn task + copyAllOutputs.inputs.file task.archivePath + copyAllOutputs.from task.archivePath +} + +def artifactGroupId = 'org.photonvision' +def baseArtifactId = 'PhotonLib' +def zipBaseName = "_GROUP_org_photonvision_photonlib_ID_${baseArtifactId}-cpp_CLS" +def javaBaseName = "_GROUP_org_photonvision_photonlib_ID_${baseArtifactId}-java_CLS" + +task cppHeadersZip(type: Zip) { + destinationDirectory = outputsFolder + archiveBaseName = zipBaseName + classifier = "headers" + + from(licenseFile) { + into '/' + } + + from('src/main/native/include/') { + into '/' + } +} + +task cppSourceZip(type: Zip) { + destinationDirectory = outputsFolder + archiveBaseName = zipBaseName + classifier = "source" + + from(licenseFile) { + into '/' + } + + from('src/main/native/cpp') { + into '/' + } +} + +build.dependsOn cppHeadersZip +addTaskToCopyAllOutputs(cppHeadersZip) +build.dependsOn cppSourceZip +addTaskToCopyAllOutputs(cppSourceZip) + +task sourcesJar(type: Jar, dependsOn: classes) { + classifier = 'sources' + from sourceSets.main.allSource +} + +task javadocJar(type: Jar, dependsOn: javadoc) { + classifier = 'javadoc' + from javadoc.destinationDir +} + +task outputJar(type: Jar, dependsOn: classes) { + archiveBaseName = javaBaseName + destinationDirectory = outputsFolder + from sourceSets.main.output +} + +task outputSourcesJar(type: Jar, dependsOn: classes) { + archiveBaseName = javaBaseName + destinationDirectory = outputsFolder + classifier = 'sources' + from sourceSets.main.allSource +} + +task outputJavadocJar(type: Jar, dependsOn: javadoc) { + archiveBaseName = javaBaseName + destinationDirectory = outputsFolder + classifier = 'javadoc' + from javadoc.destinationDir +} + +def vendorJson = artifacts.add('archives', file("$photonlibFileOutput")) + +artifacts { + archives sourcesJar + archives javadocJar + archives outputJar + archives outputSourcesJar + archives outputJavadocJar +} + +addTaskToCopyAllOutputs(outputSourcesJar) +addTaskToCopyAllOutputs(outputJavadocJar) +addTaskToCopyAllOutputs(outputJar) + +build.dependsOn outputSourcesJar +build.dependsOn outputJavadocJar +build.dependsOn outputJar + +libraryBuild.dependsOn build + +def releasesRepoUrl = "$buildDir/repos/releases" + +publishing { + repositories { + maven { + url = releasesRepoUrl + } + maven { + url 'https://maven.photonvision.org/repository/internal' + credentials { + username 'ghactions' + password System.getenv("ARTIFACTORY_API_KEY") + } + } + } +} + +task cleanReleaseRepo(type: Delete) { + delete releasesRepoUrl +} + +tasks.matching {it != cleanReleaseRepo}.all {it.dependsOn cleanReleaseRepo} + +model { + publishing { + def taskList = createComponentZipTasks($.components, ['Photon'], zipBaseName, Zip, project, includeStandardZipFormat) + + publications { + cpp(MavenPublication) { + taskList.each { + artifact it + } + artifact cppHeadersZip + artifact cppSourceZip + + artifactId = "${baseArtifactId}-cpp" + groupId artifactGroupId + version pubVersion + } + java(MavenPublication) { + artifact jar + artifact sourcesJar + artifact javadocJar + + artifactId = "${baseArtifactId}-java" + groupId artifactGroupId + version pubVersion + } + vendorjson(MavenPublication) { + artifact vendorJson + + artifactId = "${baseArtifactId}-json" + groupId = artifactGroupId + version "1.0" + } + } + } +} + +publishToMavenLocal.dependsOn libraryBuild +publish.dependsOn libraryBuild diff --git a/photon-lib/settings.gradle b/photon-lib/settings.gradle new file mode 100644 index 000000000..212a8d608 --- /dev/null +++ b/photon-lib/settings.gradle @@ -0,0 +1,8 @@ +pluginManagement { + repositories { + mavenLocal() + gradlePluginPortal() + } +} + +rootProject.name = 'photon-lib' diff --git a/photon-lib/src/generate/photonlib.json.in b/photon-lib/src/generate/photonlib.json.in new file mode 100644 index 000000000..e21397477 --- /dev/null +++ b/photon-lib/src/generate/photonlib.json.in @@ -0,0 +1,40 @@ +{ + "fileName": "photonlib.json", + "name": "photonlib", + "version": "${photon_version}", + "uuid": "515fe07e-bfc6-11fa-b3de-0242ac130004 ", + "mavenUrls": [ + "https://maven.photonvision.org/repository/internal" + ], + "jsonUrl": "https://maven.photonvision.org/repository/internal/org/photonvision/lib/PhotonLib-json/1.0/PhotonLib-json-1.0.json", + "jniDependencies": [], + "cppDependencies": [ + { + "groupId": "org.photonvision.lib", + "artifactId": "PhotonLib-cpp", + "version": "${photon_version}", + "libName": "Photon", + "headerClassifier": "headers", + "sharedLibrary": true, + "skipInvalidPlatforms": true, + "binaryPlatforms": [ + "windowsx86-64", + "linuxathena", + "linuxx86-64", + "osxx86-64" + ] + } + ], + "javaDependencies": [ + { + "groupId": "org.photonvision", + "artifactId": "PhotonLib-java", + "version": "${photon_version}" + }, + { + "groupId": "org.photonvision", + "artifactId": "PhotonTargeting-java", + "version": "${photon_version}" + } + ] +} diff --git a/photon-lib/src/main/driver/cpp/VendorJNI.cpp b/photon-lib/src/main/driver/cpp/VendorJNI.cpp new file mode 100644 index 000000000..1f2833e48 --- /dev/null +++ b/photon-lib/src/main/driver/cpp/VendorJNI.cpp @@ -0,0 +1,46 @@ +/* + * Copyright (C) Photon Vision. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "com_vendor_jni_VendorJNI.h" +#include "jni.h" + +JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM* vm, void* reserved) { + // Check to ensure the JNI version is valid + + JNIEnv* env; + if (vm->GetEnv(reinterpret_cast(&env), JNI_VERSION_1_6) != JNI_OK) + return JNI_ERR; + + // In here is also where you store things like class references + // if they are ever needed + + return JNI_VERSION_1_6; +} + +JNIEXPORT void JNICALL JNI_OnUnload(JavaVM* vm, void* reserved) {} + +/* + * Class: com_vendor_jni_VendorJNI + * Method: initialize + * Signature: ()I + */ +JNIEXPORT jint JNICALL +Java_com_vendor_jni_VendorJNI_initialize + (JNIEnv*, jclass) +{ + return 0; +} diff --git a/photon-lib/src/main/driver/cpp/driversource.cpp b/photon-lib/src/main/driver/cpp/driversource.cpp new file mode 100644 index 000000000..a9e7f69df --- /dev/null +++ b/photon-lib/src/main/driver/cpp/driversource.cpp @@ -0,0 +1,22 @@ +/* + * Copyright (C) Photon Vision. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "driverheader.h" + +extern "C" { +void c_doThing(void) {} +} // extern "C" diff --git a/photon-lib/src/main/driver/include/driverheader.h b/photon-lib/src/main/driver/include/driverheader.h new file mode 100644 index 000000000..4b706229f --- /dev/null +++ b/photon-lib/src/main/driver/include/driverheader.h @@ -0,0 +1,22 @@ +/* + * Copyright (C) Photon Vision. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#pragma once + +extern "C" { +void c_doThing(void); +} // extern "C" diff --git a/photon-lib/src/main/driver/symbols.txt b/photon-lib/src/main/driver/symbols.txt new file mode 100644 index 000000000..2f9b64148 --- /dev/null +++ b/photon-lib/src/main/driver/symbols.txt @@ -0,0 +1,4 @@ +JNI_OnLoad +JNI_OnUnload +Java_com_vendor_jni_VendorJNI_initialize +c_doThing diff --git a/photon-lib/src/main/java/org/photonvision/PhotonCamera.java b/photon-lib/src/main/java/org/photonvision/PhotonCamera.java new file mode 100644 index 000000000..2557b06c8 --- /dev/null +++ b/photon-lib/src/main/java/org/photonvision/PhotonCamera.java @@ -0,0 +1,198 @@ +/* + * Copyright (C) Photon Vision. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.photonvision; + +import edu.wpi.first.networktables.NetworkTable; +import edu.wpi.first.networktables.NetworkTableEntry; +import edu.wpi.first.networktables.NetworkTableInstance; +import org.photonvision.common.dataflow.structures.Packet; +import org.photonvision.common.hardware.VisionLEDMode; +import org.photonvision.targeting.PhotonPipelineResult; + +/** Represents a camera that is connected to PhotonVision. */ +public class PhotonCamera { + final NetworkTableEntry rawBytesEntry; + final NetworkTableEntry driverModeEntry; + final NetworkTableEntry inputSaveImgEntry; + final NetworkTableEntry outputSaveImgEntry; + final NetworkTableEntry pipelineIndexEntry; + final NetworkTableEntry ledModeEntry; + + final NetworkTable mainTable = NetworkTableInstance.getDefault().getTable("photonvision"); + + boolean driverMode; + int pipelineIndex; + VisionLEDMode mode; + + Packet packet = new Packet(1); + + /** + * Constructs a PhotonCamera from a root table. + * + * @param rootTable The root table that the camera is broadcasting information over. + */ + public PhotonCamera(NetworkTable rootTable) { + rawBytesEntry = rootTable.getEntry("rawBytes"); + driverModeEntry = rootTable.getEntry("driverMode"); + inputSaveImgEntry = rootTable.getEntry("inputSaveImgCmd"); + outputSaveImgEntry = rootTable.getEntry("outputSaveImgCmd"); + pipelineIndexEntry = rootTable.getEntry("pipelineIndex"); + ledModeEntry = mainTable.getEntry("ledMode"); + + driverMode = driverModeEntry.getBoolean(false); + pipelineIndex = pipelineIndexEntry.getNumber(0).intValue(); + getLEDMode(); + } + + /** + * Constructs a PhotonCamera from the name of the camera. + * + * @param cameraName The nickname of the camera (found in the PhotonVision UI). + */ + public PhotonCamera(String cameraName) { + this(NetworkTableInstance.getDefault().getTable("photonvision").getSubTable(cameraName)); + } + + /** + * Returns the latest pipeline result. + * + * @return The latest pipeline result. + */ + public PhotonPipelineResult getLatestResult() { + // Clear the packet. + packet.clear(); + + // Create latest result. + var ret = new PhotonPipelineResult(); + + // Populate packet and create result. + packet.setData(rawBytesEntry.getRaw(new byte[] {})); + if (packet.getSize() < 1) return ret; + ret.createFromPacket(packet); + + // Return result. + return ret; + } + + /** + * Returns whether the camera is in driver mode. + * + * @return Whether the camera is in driver mode. + */ + public boolean getDriverMode() { + return driverMode; + } + + /** + * Toggles driver mode. + * + * @param driverMode Whether to set driver mode. + */ + public void setDriverMode(boolean driverMode) { + if (this.driverMode != driverMode) { + this.driverMode = driverMode; + driverModeEntry.setBoolean(this.driverMode); + } + } + + /** + * Request the camera to save a new image file from the input camera stream with overlays. Images + * take up space in the filesystem of the PhotonCamera. Calling it frequently will fill up disk + * space and eventually cause the system to stop working. Clear out images in + * /opt/photonvision/photonvision_config/imgSaves frequently to prevent issues. + */ + public void takeInputSnapshot() { + inputSaveImgEntry.setBoolean(true); + } + + /** + * Request the camera to save a new image file from the output stream with overlays. Images take + * up space in the filesystem of the PhotonCamera. Calling it frequently will fill up disk space + * and eventually cause the system to stop working. Clear out images in + * /opt/photonvision/photonvision_config/imgSaves frequently to prevent issues. + */ + public void takeOutputSnapshot() { + outputSaveImgEntry.setBoolean(true); + } + + /** + * Returns the active pipeline index. + * + * @return The active pipeline index. + */ + public int getPipelineIndex() { + return pipelineIndex; + } + + /** + * Allows the user to select the active pipeline index. + * + * @param index The active pipeline index. + */ + public void setPipelineIndex(int index) { + if (pipelineIndex != index) { + pipelineIndex = index; + pipelineIndexEntry.setNumber(pipelineIndex); + } + } + + /** + * Returns the current LED mode. + * + * @return The current LED mode. + */ + public VisionLEDMode getLEDMode() { + int value = ledModeEntry.getNumber(-1).intValue(); + switch (value) { + case 0: + mode = VisionLEDMode.kOff; + break; + case 1: + mode = VisionLEDMode.kOn; + break; + case 2: + mode = VisionLEDMode.kBlink; + break; + case -1: + default: + mode = VisionLEDMode.kDefault; + break; + } + return mode; + } + + /** + * Sets the LED mode. + * + * @param led The mode to set to. + */ + public void setLED(VisionLEDMode led) { + if (led != mode) { + ledModeEntry.setNumber(led.value); + } + } + + /** + * Returns whether the latest target result has targets. + * + * @return Whether the latest target result has targets. + */ + public boolean hasTargets() { + return getLatestResult().hasTargets(); + } +} diff --git a/photon-lib/src/main/java/org/photonvision/PhotonUtils.java b/photon-lib/src/main/java/org/photonvision/PhotonUtils.java new file mode 100644 index 000000000..d1b5182f2 --- /dev/null +++ b/photon-lib/src/main/java/org/photonvision/PhotonUtils.java @@ -0,0 +1,166 @@ +/* + * Copyright (C) Photon Vision. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.photonvision; + +import edu.wpi.first.wpilibj.geometry.Pose2d; +import edu.wpi.first.wpilibj.geometry.Rotation2d; +import edu.wpi.first.wpilibj.geometry.Transform2d; +import edu.wpi.first.wpilibj.geometry.Translation2d; + +public final class PhotonUtils { + private PhotonUtils() { + // Utility class + } + + /** + * Algorithm from https://docs.limelightvision.io/en/latest/cs_estimating_distance.html Estimates + * range to a target using the target's elevation. This method can produce more stable results + * than SolvePNP when well tuned, if the full 6d robot pose is not required. Note that this method + * requires the camera to have 0 roll (not be skewed clockwise or CCW relative to the floor), and + * for there to exist a height differential between goal and camera. The larger this differential, + * the more accurate the distance estimate will be. + * + *

Units can be converted using the {@link edu.wpi.first.wpilibj.util.Units} class. + * + * @param cameraHeightMeters The physical height of the camera off the floor in meters. + * @param targetHeightMeters The physical height of the target off the floor in meters. This + * should be the height of whatever is being targeted (i.e. if the targeting region is set to + * top, this should be the height of the top of the target). + * @param cameraPitchRadians The pitch of the camera from the horizontal plane in radians. + * Positive values up. + * @param targetPitchRadians The pitch of the target in the camera's lens in radians. Positive + * values up. + * @return The estimated distance to the target in meters. + */ + public static double calculateDistanceToTargetMeters( + double cameraHeightMeters, + double targetHeightMeters, + double cameraPitchRadians, + double targetPitchRadians) { + return (targetHeightMeters - cameraHeightMeters) + / Math.tan(cameraPitchRadians + targetPitchRadians); + } + + /** + * Estimate the {@link Translation2d} of the target relative to the camera. + * + * @param targetDistanceMeters The distance to the target in meters. + * @param yaw The observed yaw of the target. + * @return The target's camera-relative translation. + */ + public static Translation2d estimateCameraToTargetTranslation( + double targetDistanceMeters, Rotation2d yaw) { + return new Translation2d( + yaw.getCos() * targetDistanceMeters, yaw.getSin() * targetDistanceMeters); + } + + /** + * Estimate the position of the robot in the field. + * + * @param cameraHeightMeters The physical height of the camera off the floor in meters. + * @param targetHeightMeters The physical height of the target off the floor in meters. This + * should be the height of whatever is being targeted (i.e. if the targeting region is set to + * top, this should be the height of the top of the target). + * @param cameraPitchRadians The pitch of the camera from the horizontal plane in radians. + * Positive values up. + * @param targetPitchRadians The pitch of the target in the camera's lens in radians. Positive + * values up. + * @param targetYaw The observed yaw of the target. Note that this *must* be CCW-positive, and + * Photon returns CW-positive. + * @param gyroAngle The current robot gyro angle, likely from odometry. + * @param fieldToTarget A Pose2d representing the target position in the field coordinate system. + * @param cameraToRobot The position of the robot relative to the camera. If the camera was + * mounted 3 inches behind the "origin" (usually physical center) of the robot, this would be + * Transform2d(3 inches, 0 inches, 0 degrees). + * @return The position of the robot in the field. + */ + public static Pose2d estimateFieldToRobot( + double cameraHeightMeters, + double targetHeightMeters, + double cameraPitchRadians, + double targetPitchRadians, + Rotation2d targetYaw, + Rotation2d gyroAngle, + Pose2d fieldToTarget, + Transform2d cameraToRobot) { + return PhotonUtils.estimateFieldToRobot( + PhotonUtils.estimateCameraToTarget( + PhotonUtils.estimateCameraToTargetTranslation( + PhotonUtils.calculateDistanceToTargetMeters( + cameraHeightMeters, targetHeightMeters, cameraPitchRadians, targetPitchRadians), + targetYaw), + fieldToTarget, + gyroAngle), + fieldToTarget, + cameraToRobot); + } + + /** + * Estimates a {@link Transform2d} that maps the camera position to the target position, using the + * robot's gyro. Note that the gyro angle provided *must* line up with the field coordinate system + * -- that is, it should read zero degrees when pointed towards the opposing alliance station, and + * increase as the robot rotates CCW. + * + * @param cameraToTargetTranslation A Translation2d that encodes the x/y position of the target + * relative to the camera. + * @param fieldToTarget A Pose2d representing the target position in the field coordinate system. + * @param gyroAngle The current robot gyro angle, likely from odometry. + * @return A Transform2d that takes us from the camera to the target. + */ + public static Transform2d estimateCameraToTarget( + Translation2d cameraToTargetTranslation, Pose2d fieldToTarget, Rotation2d gyroAngle) { + // This pose maps our camera at the origin out to our target, in the robot + // reference frame + // The translation part of this Transform2d is from the above step, and the + // rotation uses our robot's + // gyro. + return new Transform2d( + cameraToTargetTranslation, gyroAngle.times(-1).minus(fieldToTarget.getRotation())); + } + + /** + * Estimates the pose of the robot in the field coordinate system, given the position of the + * target relative to the camera, the target relative to the field, and the robot relative to the + * camera. + * + * @param cameraToTarget The position of the target relative to the camera. + * @param fieldToTarget The position of the target in the field. + * @param cameraToRobot The position of the robot relative to the camera. If the camera was + * mounted 3 inches behind the "origin" (usually physical center) of the robot, this would be + * Transform2d(3 inches, 0 inches, 0 degrees). + * @return The position of the robot in the field. + */ + public static Pose2d estimateFieldToRobot( + Transform2d cameraToTarget, Pose2d fieldToTarget, Transform2d cameraToRobot) { + return estimateFieldToCamera(cameraToTarget, fieldToTarget).transformBy(cameraToRobot); + } + + /** + * Estimates the pose of the camera in the field coordinate system, given the position of the + * target relative to the camera, and the target relative to the field. This *only* tracks the + * position of the camera, not the position of the robot itself. + * + * @param cameraToTarget The position of the target relative to the camera. + * @param fieldToTarget The position of the target in the field. + * @return The position of the camera in the field. + */ + public static Pose2d estimateFieldToCamera(Transform2d cameraToTarget, Pose2d fieldToTarget) { + var targetToCamera = cameraToTarget.inverse(); + return fieldToTarget.transformBy(targetToCamera); + } +} diff --git a/photon-lib/src/main/java/org/photonvision/SimPhotonCamera.java b/photon-lib/src/main/java/org/photonvision/SimPhotonCamera.java new file mode 100644 index 000000000..29bdf855e --- /dev/null +++ b/photon-lib/src/main/java/org/photonvision/SimPhotonCamera.java @@ -0,0 +1,70 @@ +/* + * Copyright (C) Photon Vision. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.photonvision; + +import edu.wpi.first.networktables.NetworkTable; +import java.util.Arrays; +import java.util.List; +import org.photonvision.common.dataflow.structures.Packet; +import org.photonvision.targeting.PhotonPipelineResult; +import org.photonvision.targeting.PhotonTrackedTarget; + +public class SimPhotonCamera extends PhotonCamera { + /** + * Constructs a Simulated PhotonCamera from a root table. + * + * @param rootTable The root table that the camera is broadcasting information over. + */ + public SimPhotonCamera(NetworkTable rootTable) { + super(rootTable); + } + + /** + * Constructs a Simulated PhotonCamera from the name of the camera. + * + * @param cameraName The nickname of the camera (found in the PhotonVision UI). + */ + public SimPhotonCamera(String cameraName) { + super(cameraName); + } + + /** + * Simulate one processed frame of vision data, putting one result to NT. + * + * @param latencyMillis + * @param targets Each target detected + */ + public void submitProcessedFrame(double latencyMillis, PhotonTrackedTarget... targets) { + submitProcessedFrame(latencyMillis, Arrays.asList(targets)); + } + + /** + * Simulate one processed frame of vision data, putting one result to NT. + * + * @param latencyMillis + * @param tgtList List of targets detected + */ + public void submitProcessedFrame(double latencyMillis, List tgtList) { + if (!getDriverMode()) { + PhotonPipelineResult newResult = new PhotonPipelineResult(latencyMillis, tgtList); + var newPacket = new Packet(newResult.getPacketSize()); + newResult.populatePacket(newPacket); + rawBytesEntry.setRaw(newPacket.getData()); + } + } +} diff --git a/photon-lib/src/main/java/org/photonvision/SimVisionSystem.java b/photon-lib/src/main/java/org/photonvision/SimVisionSystem.java new file mode 100644 index 000000000..5e85483f7 --- /dev/null +++ b/photon-lib/src/main/java/org/photonvision/SimVisionSystem.java @@ -0,0 +1,181 @@ +/* + * Copyright (C) Photon Vision. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.photonvision; + +import edu.wpi.first.wpilibj.geometry.Pose2d; +import edu.wpi.first.wpilibj.geometry.Transform2d; +import edu.wpi.first.wpilibj.util.Units; +import java.util.ArrayList; +import org.photonvision.targeting.PhotonTrackedTarget; + +public class SimVisionSystem { + SimPhotonCamera cam; + + double camDiagFOVDegrees; + double camHorizFOVDegrees; + double camVertFOVDegrees; + double cameraHeightOffGroundMeters; + double maxLEDRangeMeters; + double camPitchDegrees; + int cameraResWidth; + int cameraResHeight; + double minTargetArea; + Transform2d cameraToRobot; + + ArrayList tgtList; + + /** + * Create a simulated vision system involving a camera and coprocessor mounted on a mobile robot + * running Photonvision, detecting one or more targets scattered around the field. This assumes a + * fairly simple and distortionless pinhole camera model. + * + * @param camName Name of the photonvision camera to create. Align it with the settings you use in + * the PhotonVision GUI. + * @param camDiagFOVDegrees Diagonal Field of View of the camera used. Align it with the + * manufacturer specifications, and/or whatever is configured in the PhotonVision Setting + * page. + * @param camPitchDegrees pitch of the camera's view axis back from horizontal. Make this the same + * as whatever is configured in the PhotonVision Setting page. + * @param cameraToRobot Pose Transform to move from the camera's mount position to the robot's + * position + * @param cameraHeightOffGroundMeters Height of the camera off the ground in meters + * @param maxLEDRangeMeters Maximum distance at which your camera can illuminate the target and + * make it visible. Set to 9000 or more if your vision system does not rely on LED's. + * @param cameraResWidth Width of your camera's image sensor in pixels + * @param cameraResHeight Height of your camera's image sensor in pixels + * @param minTargetArea Minimum area that that the target should be before it's recognized as a + * target by the camera. Match this with your contour filtering settings in the PhotonVision + * GUI. + */ + public SimVisionSystem( + String camName, + double camDiagFOVDegrees, + double camPitchDegrees, + Transform2d cameraToRobot, + double cameraHeightOffGroundMeters, + double maxLEDRangeMeters, + int cameraResWidth, + int cameraResHeight, + double minTargetArea) { + this.camDiagFOVDegrees = camDiagFOVDegrees; + this.camPitchDegrees = camPitchDegrees; + this.cameraToRobot = cameraToRobot; + this.cameraHeightOffGroundMeters = cameraHeightOffGroundMeters; + this.maxLEDRangeMeters = maxLEDRangeMeters; + this.cameraResWidth = cameraResWidth; + this.cameraResHeight = cameraResHeight; + this.minTargetArea = minTargetArea; + + // Calculate horizontal/vertical FOV by similar triangles + double hypotPixels = Math.hypot(cameraResWidth, cameraResHeight); + this.camHorizFOVDegrees = camDiagFOVDegrees * cameraResWidth / hypotPixels; + this.camVertFOVDegrees = camDiagFOVDegrees * cameraResHeight / hypotPixels; + + cam = new SimPhotonCamera(camName); + tgtList = new ArrayList(); + } + + /** + * Add a target on the field which your vision system is designed to detect. The photoncamera from + * this system will report the location of the robot relative to the subste of these targets which + * are visible from the given robot position. + * + * @param tgt + */ + public void addSimVisionTarget(SimVisionTarget tgt) { + tgtList.add(tgt); + } + + /** + * Adjust the camera position relative to the robot. Use this if your camera is on a gimbal or + * turret or some other mobile platform. + * + * @param newCameraToRobot New Tranform from the robot to the camera + * @param newCamHeightMeters New height of the camera off the floor + * @param newCamPitchDegrees New pitch of the camera axis back from horizontal + */ + public void moveCamera( + Transform2d newCameraToRobot, double newCamHeightMeters, double newCamPitchDegrees) { + this.cameraToRobot = newCameraToRobot; + this.cameraHeightOffGroundMeters = newCamHeightMeters; + this.camPitchDegrees = newCamPitchDegrees; + } + + /** + * Periodic update. Call this once per frame of image data you wish to process and send to + * NetworkTables + * + * @param robotPoseMeters current pose of the robot on the field. Will be used to calcualte which + * targets are actually in view, where they are at relative to the robot, and relevant + * PhotonVision parameters. + */ + public void processFrame(Pose2d robotPoseMeters) { + + Pose2d cameraPos = robotPoseMeters.transformBy(cameraToRobot.inverse()); + + ArrayList visibleTgtList = new ArrayList<>(tgtList.size()); + + tgtList.forEach( + (tgt) -> { + var camToTargetTrans = new Transform2d(cameraPos, tgt.targetPos); + + double distAlongGroundMeters = camToTargetTrans.getTranslation().getNorm(); + double distVerticalMeters = + tgt.targetHeightAboveGroundMeters - this.cameraHeightOffGroundMeters; + double distMeters = Math.hypot(distAlongGroundMeters, distVerticalMeters); + + double area = tgt.tgtAreaMeters2 / getM2PerPx(distAlongGroundMeters); + + // 2D yaw mode considers the target as a point, and should ignore target rotation. + // Photon reports it in the correct robot reference frame. + // IE: targets to the left of the image should report negative yaw. + double yawDegrees = + -1.0 + * Units.radiansToDegrees( + Math.atan2( + camToTargetTrans.getTranslation().getY(), + camToTargetTrans.getTranslation().getX())); + double pitchDegrees = + Units.radiansToDegrees(Math.atan2(distVerticalMeters, distAlongGroundMeters)) + - this.camPitchDegrees; + + if (camCanSeeTarget(distMeters, yawDegrees, pitchDegrees, area)) { + visibleTgtList.add( + new PhotonTrackedTarget(yawDegrees, pitchDegrees, area, 0.0, camToTargetTrans)); + } + }); + + cam.submitProcessedFrame(0.0, visibleTgtList); + } + + double getM2PerPx(double dist) { + double widthMPerPx = + 2 * dist * Math.tan(Units.degreesToRadians(this.camHorizFOVDegrees) / 2) / cameraResWidth; + double heightMPerPx = + 2 * dist * Math.tan(Units.degreesToRadians(this.camVertFOVDegrees) / 2) / cameraResHeight; + return widthMPerPx * heightMPerPx; + } + + boolean camCanSeeTarget(double distMeters, double yaw, double pitch, double area) { + boolean inRange = (distMeters < this.maxLEDRangeMeters); + boolean inHorizAngle = Math.abs(yaw) < (this.camHorizFOVDegrees / 2); + boolean inVertAngle = Math.abs(pitch) < (this.camVertFOVDegrees / 2); + boolean targetBigEnough = area > this.minTargetArea; + return (inRange && inHorizAngle && inVertAngle && targetBigEnough); + } +} diff --git a/photon-lib/src/main/java/org/photonvision/SimVisionTarget.java b/photon-lib/src/main/java/org/photonvision/SimVisionTarget.java new file mode 100644 index 000000000..60b896d02 --- /dev/null +++ b/photon-lib/src/main/java/org/photonvision/SimVisionTarget.java @@ -0,0 +1,51 @@ +/* + * Copyright (C) Photon Vision. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.photonvision; + +import edu.wpi.first.wpilibj.geometry.Pose2d; + +public class SimVisionTarget { + Pose2d targetPos; + double targetWidthMeters; + double targetHeightMeters; + double targetHeightAboveGroundMeters; + double targetInfill_pct; + double tgtAreaMeters2; + + /** + * Describes a vision target located somewhere on the field that your SimVisionSystem can detect. + * + * @param targetPos Pose2d of the target on the field. Define it such that, if you are standing on + * the middle of the field facing the target, the Y axis points to your left, and the X axis + * points away from you. + * @param targetHeightAboveGroundMeters Height of the target above the field plane, in meters. + * @param targetWidthMeters Width of the outter bounding box of the target in meters. + * @param targetHeightMeters Pair Height of the outter bounding box of the target in meters. + */ + public SimVisionTarget( + Pose2d targetPos, + double targetHeightAboveGroundMeters, + double targetWidthMeters, + double targetHeightMeters) { + this.targetPos = targetPos; + this.targetHeightAboveGroundMeters = targetHeightAboveGroundMeters; + this.targetWidthMeters = targetWidthMeters; + this.targetHeightMeters = targetHeightMeters; + this.tgtAreaMeters2 = targetWidthMeters * targetHeightMeters; + } +} diff --git a/photon-lib/src/main/native/cpp/photonlib/PhotonCamera.cpp b/photon-lib/src/main/native/cpp/photonlib/PhotonCamera.cpp new file mode 100644 index 000000000..b0a0ce472 --- /dev/null +++ b/photon-lib/src/main/native/cpp/photonlib/PhotonCamera.cpp @@ -0,0 +1,90 @@ +/* + * Copyright (C) Photon Vision. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "photonlib/PhotonCamera.h" + +#include "photonlib/Packet.h" + +namespace photonlib { +PhotonCamera::PhotonCamera(std::shared_ptr rootTable) + : rawBytesEntry(rootTable->GetEntry("rawBytes")), + driverModeEntry(rootTable->GetEntry("driverMode")), + inputSaveImgEntry(rootTable->GetEntry("inputSaveImgCmd")), + outputSaveImgEntry(rootTable->GetEntry("outputSaveImgCmd")), + pipelineIndexEntry(rootTable->GetEntry("pipelineIndex")), + ledModeEntry(mainTable->GetEntry("ledMode")) { + driverMode = driverModeEntry.GetBoolean(false); + pipelineIndex = static_cast(pipelineIndexEntry.GetDouble(0.0)); + mode = GetLEDMode(); +} + +PhotonCamera::PhotonCamera(const std::string& cameraName) + : PhotonCamera(nt::NetworkTableInstance::GetDefault() + .GetTable("photonvision") + ->GetSubTable(cameraName)) {} + +PhotonPipelineResult PhotonCamera::GetLatestResult() const { + // Clear the current packet. + packet.Clear(); + + // Create the new result; + PhotonPipelineResult result; + + // Fill the packet with latest data and populate result. + std::string value = rawBytesEntry.GetValue()->GetRaw(); + std::vector bytes{value.begin(), value.end()}; + + photonlib::Packet packet{bytes}; + + packet >> result; + return result; +} + +void PhotonCamera::SetDriverMode(bool driverMode) { + if (this->driverMode != driverMode) { + this->driverMode = driverMode; + driverModeEntry.SetBoolean(this->driverMode); + } +} + +void PhotonCamera::TakeInputSnapshot() { inputSaveImgEntry.SetBoolean(true); } + +void PhotonCamera::TakeOutputSnapshot() { outputSaveImgEntry.SetBoolean(true); } + +bool PhotonCamera::GetDriverMode() const { return driverMode; } + +void PhotonCamera::SetPipelineIndex(int index) { + if (index != pipelineIndex) { + pipelineIndex = index; + pipelineIndexEntry.SetDouble(static_cast(pipelineIndex)); + } +} + +int PhotonCamera::GetPipelineIndex() const { return pipelineIndex; } + +LEDMode PhotonCamera::GetLEDMode() const { + mode = static_cast(static_cast(ledModeEntry.GetDouble(-1.0))); + return mode; +} + +void PhotonCamera::SetLEDMode(LEDMode led) { + if (led != mode) { + mode = led; + ledModeEntry.SetDouble(static_cast(static_cast(mode))); + } +} +} // namespace photonlib diff --git a/photon-lib/src/main/native/cpp/photonlib/PhotonPipelineResult.cpp b/photon-lib/src/main/native/cpp/photonlib/PhotonPipelineResult.cpp new file mode 100644 index 000000000..69354da9f --- /dev/null +++ b/photon-lib/src/main/native/cpp/photonlib/PhotonPipelineResult.cpp @@ -0,0 +1,67 @@ +/* + * Copyright (C) Photon Vision. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "photonlib/PhotonPipelineResult.h" + +namespace photonlib { +PhotonPipelineResult::PhotonPipelineResult( + units::second_t latency, wpi::ArrayRef targets) + : latency(latency), + targets(targets.data(), targets.data() + targets.size()) { + hasTargets = targets.size() != 0; +} + +bool PhotonPipelineResult::operator==(const PhotonPipelineResult& other) const { + return latency == other.latency && hasTargets == other.hasTargets && + targets == other.targets; +} + +bool PhotonPipelineResult::operator!=(const PhotonPipelineResult& other) const { + return !operator==(other); +} + +Packet& operator<<(Packet& packet, const PhotonPipelineResult& result) { + // Encode latency, existence of targets, and number of targets. + packet << result.latency.to() * 1000 << result.hasTargets + << static_cast(result.targets.size()); + + // Encode the information of each target. + for (auto& target : result.targets) packet << target; + + // Return the packet + return packet; +} + +Packet& operator>>(Packet& packet, PhotonPipelineResult& result) { + // Decode latency, existence of targets, and number of targets. + int8_t targetCount = 0; + double latencyMillis = 0; + packet >> latencyMillis >> result.hasTargets >> targetCount; + result.latency = units::second_t(latencyMillis / 1000.0); + + result.targets.clear(); + + // Decode the information of each target. + for (int i = 0; i < targetCount; ++i) { + PhotonTrackedTarget target; + packet >> target; + result.targets.push_back(target); + } + return packet; +} + +} // namespace photonlib diff --git a/photon-lib/src/main/native/cpp/photonlib/PhotonTrackedTarget.cpp b/photon-lib/src/main/native/cpp/photonlib/PhotonTrackedTarget.cpp new file mode 100644 index 000000000..d5005e747 --- /dev/null +++ b/photon-lib/src/main/native/cpp/photonlib/PhotonTrackedTarget.cpp @@ -0,0 +1,60 @@ +/* + * Copyright (C) Photon Vision. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "photonlib/PhotonTrackedTarget.h" + +#include + +#include + +namespace photonlib { + +PhotonTrackedTarget::PhotonTrackedTarget(double yaw, double pitch, double area, + double skew, + const frc::Transform2d& pose) + : yaw(yaw), pitch(pitch), area(area), skew(skew), cameraToTarget(pose) {} + +bool PhotonTrackedTarget::operator==(const PhotonTrackedTarget& other) const { + return other.yaw == yaw && other.pitch == pitch && other.area == area && + other.skew == skew && other.cameraToTarget == cameraToTarget; +} + +bool PhotonTrackedTarget::operator!=(const PhotonTrackedTarget& other) const { + return !operator==(other); +} + +Packet& operator<<(Packet& packet, const PhotonTrackedTarget& target) { + return packet << target.yaw << target.pitch << target.area << target.skew + << target.cameraToTarget.Translation().X().to() + << target.cameraToTarget.Translation().Y().to() + << target.cameraToTarget.Rotation().Degrees().to(); +} + +Packet& operator>>(Packet& packet, PhotonTrackedTarget& target) { + packet >> target.yaw >> target.pitch >> target.area >> target.skew; + double x = 0; + double y = 0; + double rot = 0; + packet >> x >> y >> rot; + + target.cameraToTarget = + frc::Transform2d(frc::Translation2d(units::meter_t(x), units::meter_t(y)), + units::degree_t(rot)); + return packet; +} + +} // namespace photonlib diff --git a/photon-lib/src/main/native/cpp/photonlib/SimPhotonCamera.cpp b/photon-lib/src/main/native/cpp/photonlib/SimPhotonCamera.cpp new file mode 100644 index 000000000..ce8f4e2c6 --- /dev/null +++ b/photon-lib/src/main/native/cpp/photonlib/SimPhotonCamera.cpp @@ -0,0 +1,42 @@ +/* + * Copyright (C) Photon Vision. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "photonlib/SimPhotonCamera.h" + +namespace photonlib { + +SimPhotonCamera::SimPhotonCamera(std::shared_ptr rootTable) + : PhotonCamera(rootTable) {} + +SimPhotonCamera::SimPhotonCamera(const std::string& cameraName) + : PhotonCamera(cameraName) {} + +void SimPhotonCamera::SubmitProcessedFrame( + units::second_t latency, wpi::ArrayRef tgtList) { + if (!GetDriverMode()) { + // Clear the current packet. + simPacket.Clear(); + + // Create the new result and pump it into the packet + simPacket << PhotonPipelineResult(latency, tgtList); + + rawBytesEntry.SetRaw( + wpi::StringRef(simPacket.GetData().data(), simPacket.GetData().size())); + } +} + +} // namespace photonlib diff --git a/photon-lib/src/main/native/cpp/photonlib/SimVisionSystem.cpp b/photon-lib/src/main/native/cpp/photonlib/SimVisionSystem.cpp new file mode 100644 index 000000000..971dc6fb0 --- /dev/null +++ b/photon-lib/src/main/native/cpp/photonlib/SimVisionSystem.cpp @@ -0,0 +1,119 @@ +/* + * Copyright (C) Photon Vision. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "photonlib/SimVisionSystem.h" + +#include + +#include +#include + +namespace photonlib { + +SimVisionSystem::SimVisionSystem(const std::string& name, + units::degree_t camDiagFOV, + units::degree_t camPitch, + frc::Transform2d cameraToRobot, + units::meter_t cameraHeightOffGround, + units::meter_t maxLEDRange, int cameraResWidth, + int cameraResHeight, double minTargetArea) + : camDiagFOV(camDiagFOV), + camPitch(camPitch), + cameraToRobot(cameraToRobot), + cameraHeightOffGround(cameraHeightOffGround), + maxLEDRange(maxLEDRange), + cameraResWidth(cameraResWidth), + cameraResHeight(cameraResHeight), + minTargetArea(minTargetArea) { + double hypotPixels = std::hypot(cameraResWidth, cameraResHeight); + camHorizFOV = camDiagFOV * cameraResWidth / hypotPixels; + camVertFOV = camDiagFOV * cameraResHeight / hypotPixels; + + cam = SimPhotonCamera(name); + tgtList.clear(); +} + +void SimVisionSystem::AddSimVisionTarget(SimVisionTarget tgt) { + tgtList.push_back(tgt); +} + +void SimVisionSystem::MoveCamera(frc::Transform2d newCameraToRobot, + units::meter_t newCamHeight, + units::degree_t newCamPitch) { + cameraToRobot = newCameraToRobot; + cameraHeightOffGround = newCamHeight; + camPitch = newCamPitch; +} + +void SimVisionSystem::ProcessFrame(frc::Pose2d robotPose) { + frc::Pose2d cameraPos = robotPose.TransformBy(cameraToRobot.Inverse()); + std::vector visibleTgtList = {}; + + for (auto&& tgt : tgtList) { + frc::Transform2d camToTargetTrans = + frc::Transform2d(cameraPos, tgt.targetPos); + + units::meter_t distAlongGround = camToTargetTrans.Translation().Norm(); + units::meter_t distVertical = + tgt.targetHeightAboveGround - cameraHeightOffGround; + units::meter_t distHypot = + units::math::hypot(distAlongGround, distVertical); + + double area = tgt.tgtArea.to() / GetM2PerPx(distAlongGround); + + // 2D yaw mode considers the target as a point, and should ignore target + // rotation. + // Photon reports it in the correct robot reference frame. + // IE: targets to the left of the image should report negative yaw. + units::degree_t yawAngle = + -1.0 * units::math::atan2(camToTargetTrans.Translation().Y(), + camToTargetTrans.Translation().X()); + units::degree_t pitchAngle = + units::math::atan2(distVertical, distAlongGround) - camPitch; + + if (CamCanSeeTarget(distHypot, yawAngle, pitchAngle, area)) { + PhotonTrackedTarget newTgt = + PhotonTrackedTarget(yawAngle.to(), pitchAngle.to(), + area, 0.0, camToTargetTrans); + visibleTgtList.push_back(newTgt); + } + } + + units::second_t procDelay(0.0); // Future - tie this to something meaningful + cam.SubmitProcessedFrame( + procDelay, wpi::MutableArrayRef(visibleTgtList)); +} + +double SimVisionSystem::GetM2PerPx(units::meter_t dist) { + double heightMPerPx = 2 * dist.to() * + units::math::tan(camVertFOV / 2) / cameraResHeight; + double widthMPerPx = 2 * dist.to() * + units::math::tan(camHorizFOV / 2) / cameraResWidth; + return widthMPerPx * heightMPerPx; +} + +bool SimVisionSystem::CamCanSeeTarget(units::meter_t distHypot, + units::degree_t yaw, + units::degree_t pitch, double area) { + bool inRange = (distHypot < maxLEDRange); + bool inHorizAngle = units::math::abs(yaw) < (camHorizFOV / 2); + bool inVertAngle = units::math::abs(pitch) < (camVertFOV / 2); + bool targetBigEnough = area > minTargetArea; + return (inRange && inHorizAngle && inVertAngle && targetBigEnough); +} + +} // namespace photonlib diff --git a/photon-lib/src/main/native/cpp/photonlib/SimVisionTarget.cpp b/photon-lib/src/main/native/cpp/photonlib/SimVisionTarget.cpp new file mode 100644 index 000000000..13620843d --- /dev/null +++ b/photon-lib/src/main/native/cpp/photonlib/SimVisionTarget.cpp @@ -0,0 +1,33 @@ +/* + * Copyright (C) Photon Vision. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "photonlib/SimVisionTarget.h" + +namespace photonlib { + +SimVisionTarget::SimVisionTarget(frc::Pose2d& targetPos, + units::meter_t targetHeightAboveGround, + units::meter_t targetWidth, + units::meter_t targetHeight) + : targetPos(targetPos), + targetHeightAboveGround(targetHeightAboveGround), + targetWidth(targetWidth), + targetHeight(targetHeight) { + tgtArea = targetWidth * targetHeight; +} + +} // namespace photonlib diff --git a/photon-lib/src/main/native/include/photonlib/Packet.h b/photon-lib/src/main/native/include/photonlib/Packet.h new file mode 100644 index 000000000..03bf55d20 --- /dev/null +++ b/photon-lib/src/main/native/include/photonlib/Packet.h @@ -0,0 +1,121 @@ +/* + * Copyright (C) Photon Vision. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#pragma once + +#include +#include +#include + +#include + +namespace photonlib { + +/** + * A packet that holds byte-packed data to be sent over NetworkTables. + */ +class Packet { + public: + /** + * Constructs an empty packet. + */ + Packet() = default; + + /** + * Constructs a packet with the given data. + * @param data The packet data. + */ + explicit Packet(std::vector data) : packetData(data) {} + + /** + * Clears the packet and resets the read and write positions. + */ + void Clear() { + packetData.clear(); + readPos = 0; + writePos = 0; + } + + /** + * Returns the packet data. + * @return The packet data. + */ + const std::vector& GetData() { return packetData; } + + /** + * Returns the number of bytes in the data. + * @return The number of bytes in the data. + */ + size_t GetDataSize() const { return packetData.size(); } + + /** + * Adds a value to the data buffer. This should only be used with PODs. + * @tparam T The data type. + * @param src The data source. + * @return A reference to the current object. + */ + template + Packet& operator<<(T src) { + packetData.resize(packetData.size() + sizeof(T)); + std::memcpy(packetData.data() + writePos, &src, sizeof(T)); + + if constexpr (wpi::support::endian::system_endianness() == + wpi::support::endianness::little) { + // Reverse to big endian for network conventions. + std::reverse(packetData.data() + writePos, + packetData.data() + writePos + sizeof(T)); + } + + writePos += sizeof(T); + return *this; + } + + /** + * Extracts a value to the provided destination. + * @tparam T The type of value to extract. + * @param value The value to extract. + * @return A reference to the current object. + */ + template + Packet& operator>>(T& value) { + std::memcpy(&value, packetData.data() + readPos, sizeof(T)); + + if constexpr (wpi::support::endian::system_endianness() == + wpi::support::endianness::little) { + // Reverse to little endian for host. + char& raw = reinterpret_cast(value); + std::reverse(&raw, &raw + sizeof(T)); + } + + readPos += sizeof(T); + return *this; + } + + bool operator==(const Packet& right) const { + return packetData == right.packetData; + } + bool operator!=(const Packet& right) const { return !operator==(right); } + + private: + // Data stored in the packet + std::vector packetData; + + size_t readPos = 0; + size_t writePos = 0; +}; + +} // namespace photonlib diff --git a/photon-lib/src/main/native/include/photonlib/PhotonCamera.h b/photon-lib/src/main/native/include/photonlib/PhotonCamera.h new file mode 100644 index 000000000..b6c87aefa --- /dev/null +++ b/photon-lib/src/main/native/include/photonlib/PhotonCamera.h @@ -0,0 +1,141 @@ +/* + * Copyright (C) Photon Vision. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#pragma once + +#include +#include +#include + +#include +#include + +#include "photonlib/PhotonPipelineResult.h" + +namespace photonlib { + +enum LEDMode : int { kDefault = -1, kOff = 0, kOn = 1, kBlink = 2 }; + +/** + * Represents a camera that is connected to PhotonVision.ß + */ +class PhotonCamera { + public: + /** + * Constructs a PhotonCamera from a root table. + * @param rootTable The root table that the camera is broadcasting information + * over. + */ + explicit PhotonCamera(std::shared_ptr rootTable); + + /** + * Constructs a PhotonCamera from the name of the camera. + * @param cameraName The nickname of the camera (found in the PhotonVision + * UI). + */ + explicit PhotonCamera(const std::string& cameraName); + + /** + * Returns the latest pipeline result. + * @return The latest pipeline result. + */ + PhotonPipelineResult GetLatestResult() const; + + /** + * Toggles driver mode. + * @param driverMode Whether to set driver mode. + */ + void SetDriverMode(bool driverMode); + + /** + * Returns whether the camera is in driver mode. + * @return Whether the camera is in driver mode. + */ + bool GetDriverMode() const; + + /** + * Request the camera to save a new image file from the input + * camera stream with overlays. + * Images take up space in the filesystem of the PhotonCamera. + * Calling it frequently will fill up disk space and eventually + * cause the system to stop working. + * Clear out images in /opt/photonvision/photonvision_config/imgSaves + * frequently to prevent issues. + */ + void TakeInputSnapshot(void); + + /** + * Request the camera to save a new image file from the output + * stream with overlays. + * Images take up space in the filesystem of the PhotonCamera. + * Calling it frequently will fill up disk space and eventually + * cause the system to stop working. + * Clear out images in /opt/photonvision/photonvision_config/imgSaves + * frequently to prevent issues. + */ + void TakeOutputSnapshot(void); + + /** + * Allows the user to select the active pipeline index. + * @param index The active pipeline index. + */ + void SetPipelineIndex(int index); + + /** + * Returns the active pipeline index. + * @return The active pipeline index. + */ + int GetPipelineIndex() const; + + /** + * Returns the current LED mode. + * @return The current LED mode. + */ + LEDMode GetLEDMode() const; + + /** + * Sets the LED mode. + * @param led The mode to set to. + */ + void SetLEDMode(LEDMode led); + + /** + * Returns whether the latest target result has targets. + * @return Whether the latest target result has targets. + */ + bool HasTargets() const { return GetLatestResult().HasTargets(); } + + private: + std::shared_ptr mainTable = + nt::NetworkTableInstance::GetDefault().GetTable("photonvision"); + + protected: + nt::NetworkTableEntry rawBytesEntry; + nt::NetworkTableEntry driverModeEntry; + nt::NetworkTableEntry inputSaveImgEntry; + nt::NetworkTableEntry outputSaveImgEntry; + nt::NetworkTableEntry pipelineIndexEntry; + nt::NetworkTableEntry ledModeEntry; + + mutable Packet packet; + + bool driverMode; + double pipelineIndex; + mutable LEDMode mode; +}; + +} // namespace photonlib diff --git a/photon-lib/src/main/native/include/photonlib/PhotonPipelineResult.h b/photon-lib/src/main/native/include/photonlib/PhotonPipelineResult.h new file mode 100644 index 000000000..3a20b7d09 --- /dev/null +++ b/photon-lib/src/main/native/include/photonlib/PhotonPipelineResult.h @@ -0,0 +1,100 @@ +/* + * Copyright (C) Photon Vision. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#pragma once + +#include + +#include +#include +#include +#include + +#include "photonlib/Packet.h" +#include "photonlib/PhotonTrackedTarget.h" + +namespace photonlib { +/** + * Represents a pipeline result from a PhotonCamera. + */ +class PhotonPipelineResult { + public: + /** + * Constructs an empty pipeline result. + */ + PhotonPipelineResult() = default; + + /** + * Constructs a pipeline result. + * @param latency The latency in the pipeline. + * @param targets The list of targets identified by the pipeline. + */ + PhotonPipelineResult(units::second_t latency, + wpi::ArrayRef targets); + + /** + * Returns the best target in this pipeline result. If there are no targets, + * this method will return an empty target with all values set to zero. The + * best target is determined by the target sort mode in the PhotonVision UI. + * + * @return The best target of the pipeline result. + */ + PhotonTrackedTarget GetBestTarget() const { + if (!HasTargets() && !HAS_WARNED) { + ::frc::DriverStation::ReportError( + "This PhotonPipelineResult object has no targets associated with it! " + "Please check hasTargets() before calling this method. For more " + "information, please review the PhotonLib documentation at " + "http://docs.photonvision.org"); + HAS_WARNED = true; + } + return hasTargets ? targets[0] : PhotonTrackedTarget(); + } + + /** + * Returns the latency in the pipeline. + * @return The latency in the pipeline. + */ + units::second_t GetLatency() const { return latency; } + + /** + * Returns whether the pipeline has targets. + * @return Whether the pipeline has targets. + */ + bool HasTargets() const { return hasTargets; } + + /** + * Returns a reference to the vector of targets. + * @return A reference to the vector of targets. + */ + const wpi::ArrayRef GetTargets() const { + return targets; + } + + bool operator==(const PhotonPipelineResult& other) const; + bool operator!=(const PhotonPipelineResult& other) const; + + friend Packet& operator<<(Packet& packet, const PhotonPipelineResult& result); + friend Packet& operator>>(Packet& packet, PhotonPipelineResult& result); + + private: + units::second_t latency; + bool hasTargets; + wpi::SmallVector targets; + inline static bool HAS_WARNED = false; +}; +} // namespace photonlib diff --git a/photon-lib/src/main/native/include/photonlib/PhotonTrackedTarget.h b/photon-lib/src/main/native/include/photonlib/PhotonTrackedTarget.h new file mode 100644 index 000000000..d4ddbb3b4 --- /dev/null +++ b/photon-lib/src/main/native/include/photonlib/PhotonTrackedTarget.h @@ -0,0 +1,93 @@ +/* + * Copyright (C) Photon Vision. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#pragma once + +#include +#include +#include + +#include + +#include "photonlib/Packet.h" + +namespace photonlib { +/** + * Represents a tracked target within a pipeline. + */ +class PhotonTrackedTarget { + public: + /** + * Constructs an empty target. + */ + PhotonTrackedTarget() = default; + + /** + * Constructs a target. + * @param yaw The yaw of the target. + * @param pitch The pitch of the target. + * @param area The area of the target. + * @param skew The skew of the target. + * @param pose The camera-relative pose of the target. + */ + PhotonTrackedTarget(double yaw, double pitch, double area, double skew, + const frc::Transform2d& pose); + + /** + * Returns the target yaw (positive-left). + * @return The target yaw. + */ + double GetYaw() const { return yaw; } + + /** + * Returns the target pitch (positive-up) + * @return The target pitch. + */ + double GetPitch() const { return pitch; } + + /** + * Returns the target area (0-100). + * @return The target area. + */ + double GetArea() const { return area; } + + /** + * Returns the target skew (counter-clockwise positive). + * @return The target skew. + */ + double GetSkew() const { return skew; } + + /** + * Returns the pose of the target relative to the robot. + * @return The pose of the target relative to the robot. + */ + frc::Transform2d GetCameraRelativePose() const { return cameraToTarget; } + + bool operator==(const PhotonTrackedTarget& other) const; + bool operator!=(const PhotonTrackedTarget& other) const; + + friend Packet& operator<<(Packet& packet, const PhotonTrackedTarget& target); + friend Packet& operator>>(Packet& packet, PhotonTrackedTarget& target); + + private: + double yaw = 0; + double pitch = 0; + double area = 0; + double skew = 0; + frc::Transform2d cameraToTarget; +}; +} // namespace photonlib diff --git a/photon-lib/src/main/native/include/photonlib/PhotonUtils.h b/photon-lib/src/main/native/include/photonlib/PhotonUtils.h new file mode 100644 index 000000000..bf9c4b29b --- /dev/null +++ b/photon-lib/src/main/native/include/photonlib/PhotonUtils.h @@ -0,0 +1,173 @@ +/* + * Copyright (C) Photon Vision. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#pragma once + +#include +#include +#include +#include +#include +#include +#include + +namespace photonlib { +class PhotonUtils { + public: + /** + * Algorithm from + * https://docs.limelightvision.io/en/latest/cs_estimating_distance.html + * Estimates range to a target using the target's elevation. This method can + * produce more stable results than SolvePNP when well tuned, if the full 6d + * robot pose is not required. + * + * @param cameraHeight The height of the camera off the floor. + * @param targetHeight The height of the target off the floor. + * @param cameraPitch The pitch of the camera from the horizontal plane. + * Positive valueBytes up. + * @param targetPitch The pitch of the target in the camera's lens. Positive + * values up. + * @return The estimated distance to the target. + */ + static units::meter_t CalculateDistanceToTarget(units::meter_t cameraHeight, + units::meter_t targetHeight, + units::radian_t cameraPitch, + units::radian_t targetPitch) { + return (targetHeight - cameraHeight) / + units::math::tan(cameraPitch + targetPitch); + } + + /** + * Estimate the Translation2d of the target relative to the camera. + * + * @param targetDistance The distance to the target. + * @param yaw The observed yaw of the target. + * + * @return The target's camera-relative translation. + */ + static frc::Translation2d EstimateCameraToTargetTranslation( + units::meter_t targetDistance, const frc::Rotation2d& yaw) { + return {targetDistance * yaw.Cos(), targetDistance * yaw.Sin()}; + } + + /** + * Estimate the position of the robot in the field. + * + * @param cameraHeightMeters The physical height of the camera off the floor + * in meters. + * @param targetHeightMeters The physical height of the target off the floor + * in meters. This should be the height of whatever is being targeted (i.e. if + * the targeting region is set to top, this should be the height of the top of + * the target). + * @param cameraPitchRadians The pitch of the camera from the horizontal plane + * in radians. Positive values up. + * @param targetPitchRadians The pitch of the target in the camera's lens in + * radians. Positive values up. + * @param targetYaw The observed yaw of the target. Note that this + * *must* be CCW-positive, and Photon returns + * CW-positive. + * @param gyroAngle The current robot gyro angle, likely from + * odometry. + * @param fieldToTarget A frc::Pose2d representing the target position in + * the field coordinate system. + * @param cameraToRobot The position of the robot relative to the camera. + * If the camera was mounted 3 inches behind the + * "origin" (usually physical center) of the robot, + * this would be frc::Transform2d(3 inches, 0 + * inches, 0 degrees). + * @return The position of the robot in the field. + */ + static frc::Pose2d EstimateFieldToRobot( + units::meter_t cameraHeight, units::meter_t targetHeight, + units::radian_t cameraPitch, units::radian_t targetPitch, + const frc::Rotation2d& targetYaw, const frc::Rotation2d& gyroAngle, + const frc::Pose2d& fieldToTarget, const frc::Transform2d& cameraToRobot) { + return EstimateFieldToRobot( + EstimateCameraToTarget( + EstimateCameraToTargetTranslation( + CalculateDistanceToTarget(cameraHeight, targetHeight, + cameraPitch, targetPitch), + targetYaw), + fieldToTarget, gyroAngle), + fieldToTarget, cameraToRobot); + } + + /** + * Estimates a {@link frc::Transform2d} that maps the camera position to the + * target position, using the robot's gyro. Note that the gyro angle provided + * *must* line up with the field coordinate system -- that is, it should read + * zero degrees when pointed towards the opposing alliance station, and + * increase as the robot rotates CCW. + * + * @param cameraToTargetTranslation A Translation2d that encodes the x/y + * position of the target relative to the + * camera. + * @param fieldToTarget A frc::Pose2d representing the target + * position in the field coordinate system. + * @param gyroAngle The current robot gyro angle, likely from + * odometry. + * @return A frc::Transform2d that takes us from the camera to the target. + */ + static frc::Transform2d EstimateCameraToTarget( + const frc::Translation2d& cameraToTargetTranslation, + const frc::Pose2d& fieldToTarget, const frc::Rotation2d& gyroAngle) { + // This pose maps our camera at the origin out to our target, in the robot + // reference frame + // The translation part of this frc::Transform2d is from the above step, and + // the rotation uses our robot's gyro. + return frc::Transform2d(cameraToTargetTranslation, + -gyroAngle - fieldToTarget.Rotation()); + } + + /** + * Estimates the pose of the robot in the field coordinate system, given the + * position of the target relative to the camera, the target relative to the + * field, and the robot relative to the camera. + * + * @param cameraToTarget The position of the target relative to the camera. + * @param fieldToTarget The position of the target in the field. + * @param cameraToRobot The position of the robot relative to the camera. If + * the camera was mounted 3 inches behind the "origin" + * (usually physical center) of the robot, this would be + * frc::Transform2d(3 inches, 0 inches, 0 degrees). + * @return The position of the robot in the field. + */ + static frc::Pose2d EstimateFieldToRobot( + const frc::Transform2d& cameraToTarget, const frc::Pose2d& fieldToTarget, + const frc::Transform2d& cameraToRobot) { + return EstimateFieldToCamera(cameraToTarget, fieldToTarget) + .TransformBy(cameraToRobot); + } + + /** + * Estimates the pose of the camera in the field coordinate system, given the + * position of the target relative to the camera, and the target relative to + * the field. This *only* tracks the position of the camera, not the position + * of the robot itself. + * + * @param cameraToTarget The position of the target relative to the camera. + * @param fieldToTarget The position of the target in the field. + * @return The position of the camera in the field. + */ + static frc::Pose2d EstimateFieldToCamera( + const frc::Transform2d& cameraToTarget, + const frc::Pose2d& fieldToTarget) { + auto targetToCamera = cameraToTarget.Inverse(); + return fieldToTarget.TransformBy(targetToCamera); + } +}; +} // namespace photonlib diff --git a/photon-lib/src/main/native/include/photonlib/SimPhotonCamera.h b/photon-lib/src/main/native/include/photonlib/SimPhotonCamera.h new file mode 100644 index 000000000..1a53138b4 --- /dev/null +++ b/photon-lib/src/main/native/include/photonlib/SimPhotonCamera.h @@ -0,0 +1,65 @@ +/* + * Copyright (C) Photon Vision. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#pragma once + +#include +#include + +#include +#include +#include + +#include "photonlib/Packet.h" +#include "photonlib/PhotonCamera.h" + +namespace photonlib { + +/** + * Represents a camera that is connected to PhotonVision.ß + */ +class SimPhotonCamera : public PhotonCamera { + public: + /** + * Constructs a Simulated PhotonCamera from a root table. + * + * @param rootTable The root table that the camera is broadcasting information + * over. + */ + explicit SimPhotonCamera(std::shared_ptr rootTable); + + /** + * Constructs a Simulated PhotonCamera from the name of the camera. + * + * @param cameraName The nickname of the camera (found in the PhotonVision + * UI). + */ + explicit SimPhotonCamera(const std::string& cameraName); + + /** + * Simulate one processed frame of vision data, putting one result to NT. + * @param latency Latency of frame processing + * @param tgtList Set of targets detected + */ + void SubmitProcessedFrame(units::second_t latency, + wpi::ArrayRef tgtList); + + private: + mutable Packet simPacket; +}; + +} // namespace photonlib diff --git a/photon-lib/src/main/native/include/photonlib/SimVisionSystem.h b/photon-lib/src/main/native/include/photonlib/SimVisionSystem.h new file mode 100644 index 000000000..419ac63f6 --- /dev/null +++ b/photon-lib/src/main/native/include/photonlib/SimVisionSystem.h @@ -0,0 +1,74 @@ +/* + * Copyright (C) Photon Vision. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#pragma once + +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +#include "photonlib/SimPhotonCamera.h" +#include "photonlib/SimVisionTarget.h" + +namespace photonlib { + +/** + * Represents a camera that is connected to PhotonVision. + */ +class SimVisionSystem { + public: + explicit SimVisionSystem(const std::string& name, units::degree_t camDiagFOV, + units::degree_t camPitch, + frc::Transform2d cameraToRobot, + units::meter_t cameraHeightOffGround, + units::meter_t maxLEDRange, int cameraResWidth, + int cameraResHeight, double minTargetArea); + + void AddSimVisionTarget(SimVisionTarget tgt); + void MoveCamera(frc::Transform2d newcameraToRobot, + units::meter_t newCamHeight, units::degree_t newCamPitch); + void ProcessFrame(frc::Pose2d robotPose); + + private: + units::degree_t camDiagFOV; + units::degree_t camPitch; + frc::Transform2d cameraToRobot; + units::meter_t cameraHeightOffGround; + units::meter_t maxLEDRange; + int cameraResWidth; + int cameraResHeight; + double minTargetArea; + units::degree_t camHorizFOV; + units::degree_t camVertFOV; + std::vector tgtList = {}; + + double GetM2PerPx(units::meter_t dist); + bool CamCanSeeTarget(units::meter_t distHypot, units::degree_t yaw, + units::degree_t pitch, double area); + + public: + SimPhotonCamera cam = photonlib::SimPhotonCamera("Default"); +}; + +} // namespace photonlib diff --git a/photon-lib/src/main/native/include/photonlib/SimVisionTarget.h b/photon-lib/src/main/native/include/photonlib/SimVisionTarget.h new file mode 100644 index 000000000..3ebb66d26 --- /dev/null +++ b/photon-lib/src/main/native/include/photonlib/SimVisionTarget.h @@ -0,0 +1,45 @@ +/* + * Copyright (C) Photon Vision. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#pragma once + +#include +#include +#include + +namespace photonlib { + +/** + * Represents a target on the field which the vision processing system could + * detect. + */ +class SimVisionTarget { + public: + explicit SimVisionTarget(frc::Pose2d& targetPos, + units::meter_t targetHeightAboveGround, + units::meter_t targetWidth, + units::meter_t targetHeight); + + frc::Pose2d targetPos; + units::meter_t targetHeightAboveGround; + units::meter_t targetWidth; + units::meter_t targetHeight; + double targetInfill_pct; + units::square_meter_t tgtArea; +}; + +} // namespace photonlib diff --git a/photon-lib/src/test/java/org/photonvision/PacketTest.java b/photon-lib/src/test/java/org/photonvision/PacketTest.java new file mode 100644 index 000000000..1574ccf72 --- /dev/null +++ b/photon-lib/src/test/java/org/photonvision/PacketTest.java @@ -0,0 +1,98 @@ +/* + * Copyright (C) Photon Vision. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.photonvision; + +import edu.wpi.first.wpilibj.geometry.Rotation2d; +import edu.wpi.first.wpilibj.geometry.Transform2d; +import edu.wpi.first.wpilibj.geometry.Translation2d; +import java.util.ArrayList; +import java.util.List; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.photonvision.common.dataflow.structures.Packet; +import org.photonvision.targeting.PhotonPipelineResult; +import org.photonvision.targeting.PhotonTrackedTarget; + +class PacketTest { + @Test + void testSimpleTrackedTarget() { + var target = + new PhotonTrackedTarget( + 3.0, 4.0, 9.0, -5.0, new Transform2d(new Translation2d(1, 2), new Rotation2d(1.5))); + var p = new Packet(PhotonTrackedTarget.PACK_SIZE_BYTES); + target.populatePacket(p); + + var b = new PhotonTrackedTarget(); + b.createFromPacket(p); + + Assertions.assertEquals(target, b); + } + + @Test + void testSimplePipelineResult() { + var result = new PhotonPipelineResult(1, new ArrayList<>()); + var p = new Packet(result.getPacketSize()); + result.populatePacket(p); + + var b = new PhotonPipelineResult(); + b.createFromPacket(p); + + Assertions.assertEquals(result, b); + + var result2 = + new PhotonPipelineResult( + 2, + List.of( + new PhotonTrackedTarget( + 3.0, + -4.0, + 9.0, + 4.0, + new Transform2d(new Translation2d(1, 2), new Rotation2d(1.5))), + new PhotonTrackedTarget( + 3.0, + -4.0, + 9.1, + 6.7, + new Transform2d(new Translation2d(1, 5), new Rotation2d(1.5))))); + var p2 = new Packet(result2.getPacketSize()); + result2.populatePacket(p2); + + var b2 = new PhotonPipelineResult(); + b2.createFromPacket(p2); + + Assertions.assertEquals(result2, b2); + } + + @Test + void testBytePackFromCpp() { + byte[] bytePack = { + 64, 8, 0, 0, 0, 0, 0, 0, 64, 16, 0, 0, 0, 0, 0, 0, 64, 34, 0, 0, 0, 0, 0, 0, -64, 20, 0, 0, 0, + 0, 0, 0, 63, -16, 0, 0, 0, 0, 0, 0, 64, 0, 0, 0, 0, 0, 0, 0, 64, 85, 124, 101, 19, -54, -47, + 122 + }; + var t = new PhotonTrackedTarget(); + t.createFromPacket(new Packet(bytePack)); + + var target = + new PhotonTrackedTarget( + 3.0, 4.0, 9.0, -5.0, new Transform2d(new Translation2d(1, 2), new Rotation2d(1.5))); + + Assertions.assertEquals(t, target); + } +} diff --git a/photon-lib/src/test/java/org/photonvision/PhotonCameraTest.java b/photon-lib/src/test/java/org/photonvision/PhotonCameraTest.java new file mode 100644 index 000000000..b405415bd --- /dev/null +++ b/photon-lib/src/test/java/org/photonvision/PhotonCameraTest.java @@ -0,0 +1,39 @@ +/* + * Copyright (C) Photon Vision. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.photonvision; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.photonvision.common.dataflow.structures.Packet; +import org.photonvision.targeting.PhotonPipelineResult; + +class PhotonCameraTest { + @Test + public void testEmpty() { + Assertions.assertDoesNotThrow( + () -> { + var packet = new Packet(1); + var ret = new PhotonPipelineResult(); + packet.setData(new byte[0]); + if (packet.getSize() < 1) { + return; + } + ret.createFromPacket(packet); + }); + } +} diff --git a/photon-lib/src/test/java/org/photonvision/PhotonUtilTest.java b/photon-lib/src/test/java/org/photonvision/PhotonUtilTest.java new file mode 100644 index 000000000..ebcc8626d --- /dev/null +++ b/photon-lib/src/test/java/org/photonvision/PhotonUtilTest.java @@ -0,0 +1,78 @@ +/* + * Copyright (C) Photon Vision. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.photonvision; + +import edu.wpi.first.wpilibj.geometry.Pose2d; +import edu.wpi.first.wpilibj.geometry.Rotation2d; +import edu.wpi.first.wpilibj.geometry.Transform2d; +import edu.wpi.first.wpilibj.util.Units; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +class PhotonUtilTest { + @Test + public void testDistance() { + var camHeight = 1; + var targetHeight = 3; + var camPitch = Units.degreesToRadians(0); + var targetPitch = Units.degreesToRadians(30); + + var dist = + PhotonUtils.calculateDistanceToTargetMeters(camHeight, targetHeight, camPitch, targetPitch); + + Assertions.assertEquals(3.464, dist, 0.01); + + camHeight = 1; + targetHeight = 2; + camPitch = Units.degreesToRadians(20); + targetPitch = Units.degreesToRadians(-10); + + dist = + PhotonUtils.calculateDistanceToTargetMeters(camHeight, targetHeight, camPitch, targetPitch); + Assertions.assertEquals(5.671, dist, 0.01); + } + + @Test + public void testTransform() { + + var camHeight = 1; + var tgtHeight = 3; + var camPitch = 0; + var tgtPitch = Units.degreesToRadians(30); + var tgtYaw = new Rotation2d(); + var gyroAngle = new Rotation2d(); + var fieldToTarget = new Pose2d(); + var cameraToRobot = new Transform2d(); + + var fieldToRobot = + PhotonUtils.estimateFieldToRobot( + PhotonUtils.estimateCameraToTarget( + PhotonUtils.estimateCameraToTargetTranslation( + PhotonUtils.calculateDistanceToTargetMeters( + camHeight, tgtHeight, camPitch, tgtPitch), + tgtYaw), + fieldToTarget, + gyroAngle), + fieldToTarget, + cameraToRobot); + + Assertions.assertEquals(-3.464, fieldToRobot.getX(), 0.1); + Assertions.assertEquals(0, fieldToRobot.getY(), 0.1); + Assertions.assertEquals(0, fieldToRobot.getRotation().getDegrees(), 0.1); + } +} diff --git a/photon-lib/src/test/java/org/photonvision/SimVisionSystemTest.java b/photon-lib/src/test/java/org/photonvision/SimVisionSystemTest.java new file mode 100644 index 000000000..600a2e7e4 --- /dev/null +++ b/photon-lib/src/test/java/org/photonvision/SimVisionSystemTest.java @@ -0,0 +1,307 @@ +/* + * Copyright (C) Photon Vision. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.photonvision; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import edu.wpi.first.wpilibj.geometry.Pose2d; +import edu.wpi.first.wpilibj.geometry.Rotation2d; +import edu.wpi.first.wpilibj.geometry.Transform2d; +import edu.wpi.first.wpilibj.geometry.Translation2d; +import edu.wpi.first.wpilibj.util.Units; +import java.util.List; +import java.util.stream.Stream; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.junit.jupiter.params.provider.ValueSource; +import org.photonvision.targeting.PhotonTrackedTarget; + +class SimVisionSystemTest { + @Test + public void testEmpty() { + Assertions.assertDoesNotThrow( + () -> { + var sysUnderTest = + new SimVisionSystem("Test", 80.0, 0.0, new Transform2d(), 1, 99999, 320, 240, 0); + sysUnderTest.addSimVisionTarget(new SimVisionTarget(new Pose2d(), 0.0, 1.0, 1.0)); + for (int loopIdx = 0; loopIdx < 100; loopIdx++) { + sysUnderTest.processFrame(new Pose2d()); + } + }); + } + + @ParameterizedTest + @ValueSource(doubles = {5, 10, 15, 20, 25, 30}) + public void testDistanceAligned(double dist) { + + final var targetPose = new Pose2d(new Translation2d(35, 0), new Rotation2d()); + var sysUnderTest = + new SimVisionSystem("Test", 80.0, 0.0, new Transform2d(), 1, 99999, 320, 240, 0); + sysUnderTest.addSimVisionTarget(new SimVisionTarget(targetPose, 0.0, 1.0, 1.0)); + + final var robotPose = new Pose2d(new Translation2d(35 - dist, 0), new Rotation2d()); + sysUnderTest.processFrame(robotPose); + + var result = sysUnderTest.cam.getLatestResult(); + + assertTrue(result.hasTargets()); + assertEquals(result.getBestTarget().getCameraToTarget().getTranslation().getNorm(), dist); + } + + @Test + public void testVisibilityCupidShuffle() { + final var targetPose = new Pose2d(new Translation2d(35, 0), new Rotation2d()); + var sysUnderTest = + new SimVisionSystem("Test", 80.0, 0.0, new Transform2d(), 1, 99999, 640, 480, 0); + sysUnderTest.addSimVisionTarget(new SimVisionTarget(targetPose, 1.0, 3.0, 3.0)); + + // To the right, to the right + var robotPose = new Pose2d(new Translation2d(5, 0), Rotation2d.fromDegrees(-70)); + sysUnderTest.processFrame(robotPose); + assertFalse(sysUnderTest.cam.getLatestResult().hasTargets()); + + // To the right, to the right + robotPose = new Pose2d(new Translation2d(5, 0), Rotation2d.fromDegrees(-95)); + sysUnderTest.processFrame(robotPose); + assertFalse(sysUnderTest.cam.getLatestResult().hasTargets()); + + // To the left, to the left + robotPose = new Pose2d(new Translation2d(5, 0), Rotation2d.fromDegrees(90)); + sysUnderTest.processFrame(robotPose); + assertFalse(sysUnderTest.cam.getLatestResult().hasTargets()); + + // To the left, to the left + robotPose = new Pose2d(new Translation2d(5, 0), Rotation2d.fromDegrees(65)); + sysUnderTest.processFrame(robotPose); + assertFalse(sysUnderTest.cam.getLatestResult().hasTargets()); + + // now kick, now kick + robotPose = new Pose2d(new Translation2d(2, 0), Rotation2d.fromDegrees(5)); + sysUnderTest.processFrame(robotPose); + assertTrue(sysUnderTest.cam.getLatestResult().hasTargets()); + + // now kick, now kick + robotPose = new Pose2d(new Translation2d(2, 0), Rotation2d.fromDegrees(-5)); + sysUnderTest.processFrame(robotPose); + assertTrue(sysUnderTest.cam.getLatestResult().hasTargets()); + + // now walk it by yourself + robotPose = new Pose2d(new Translation2d(2, 0), Rotation2d.fromDegrees(-179)); + sysUnderTest.processFrame(robotPose); + assertFalse(sysUnderTest.cam.getLatestResult().hasTargets()); + + // now walk it by yourself + sysUnderTest.moveCamera( + new Transform2d(new Translation2d(), Rotation2d.fromDegrees(180)), 0, 1.0); + sysUnderTest.processFrame(robotPose); + assertTrue(sysUnderTest.cam.getLatestResult().hasTargets()); + } + + @Test + public void testNotVisibleVert1() { + final var targetPose = new Pose2d(new Translation2d(35, 0), new Rotation2d()); + var sysUnderTest = + new SimVisionSystem("Test", 80.0, 0.0, new Transform2d(), 1, 99999, 640, 480, 0); + sysUnderTest.addSimVisionTarget(new SimVisionTarget(targetPose, 1.0, 3.0, 3.0)); + + var robotPose = new Pose2d(new Translation2d(5, 0), Rotation2d.fromDegrees(5)); + sysUnderTest.processFrame(robotPose); + assertTrue(sysUnderTest.cam.getLatestResult().hasTargets()); + + sysUnderTest.moveCamera(new Transform2d(), 5000, 1.0); // vooop selfie stick + sysUnderTest.processFrame(robotPose); + assertFalse(sysUnderTest.cam.getLatestResult().hasTargets()); + } + + @Test + public void testNotVisibleVert2() { + final var targetPose = new Pose2d(new Translation2d(35, 0), new Rotation2d()); + var sysUnderTest = + new SimVisionSystem("Test", 80.0, 45.0, new Transform2d(), 1, 99999, 1234, 1234, 0); + sysUnderTest.addSimVisionTarget(new SimVisionTarget(targetPose, 3.0, 0.5, 0.5)); + + var robotPose = new Pose2d(new Translation2d(32, 0), Rotation2d.fromDegrees(5)); + sysUnderTest.processFrame(robotPose); + assertTrue(sysUnderTest.cam.getLatestResult().hasTargets()); + + // Pitched back camera should mean target goes out of view below the robot as distance increases + robotPose = new Pose2d(new Translation2d(0, 0), Rotation2d.fromDegrees(5)); + sysUnderTest.processFrame(robotPose); + assertFalse(sysUnderTest.cam.getLatestResult().hasTargets()); + } + + @Test + public void testNotVisibleTgtSize() { + final var targetPose = new Pose2d(new Translation2d(35, 0), new Rotation2d()); + var sysUnderTest = + new SimVisionSystem("Test", 80.0, 0.0, new Transform2d(), 1, 99999, 640, 480, 20.0); + sysUnderTest.addSimVisionTarget(new SimVisionTarget(targetPose, 1.0, 0.25, 0.1)); + + var robotPose = new Pose2d(new Translation2d(32, 0), Rotation2d.fromDegrees(5)); + sysUnderTest.processFrame(robotPose); + assertTrue(sysUnderTest.cam.getLatestResult().hasTargets()); + + robotPose = new Pose2d(new Translation2d(0, 0), Rotation2d.fromDegrees(5)); + sysUnderTest.processFrame(robotPose); + assertFalse(sysUnderTest.cam.getLatestResult().hasTargets()); + } + + @Test + public void testNotVisibleTooFarForLEDs() { + final var targetPose = new Pose2d(new Translation2d(35, 0), new Rotation2d()); + var sysUnderTest = + new SimVisionSystem("Test", 80.0, 0.0, new Transform2d(), 1, 10, 640, 480, 1.0); + sysUnderTest.addSimVisionTarget(new SimVisionTarget(targetPose, 1.0, 0.25, 0.1)); + + var robotPose = new Pose2d(new Translation2d(28, 0), Rotation2d.fromDegrees(5)); + sysUnderTest.processFrame(robotPose); + assertTrue(sysUnderTest.cam.getLatestResult().hasTargets()); + + robotPose = new Pose2d(new Translation2d(0, 0), Rotation2d.fromDegrees(5)); + sysUnderTest.processFrame(robotPose); + assertFalse(sysUnderTest.cam.getLatestResult().hasTargets()); + } + + @ParameterizedTest + @ValueSource(doubles = {-10, -5, -0, -1, -2, 5, 7, 10.23}) + public void testYawAngles(double testYaw) { + final var targetPose = new Pose2d(new Translation2d(35, 0), new Rotation2d(Math.PI / 4)); + var sysUnderTest = + new SimVisionSystem("Test", 80.0, 0.0, new Transform2d(), 1, 99999, 640, 480, 0); + sysUnderTest.addSimVisionTarget(new SimVisionTarget(targetPose, 0.0, 0.5, 0.5)); + + var robotPose = new Pose2d(new Translation2d(32, 0), Rotation2d.fromDegrees(testYaw)); + sysUnderTest.processFrame(robotPose); + var res = sysUnderTest.cam.getLatestResult(); + assertTrue(res.hasTargets()); + var tgt = res.getBestTarget(); + assertEquals(tgt.getYaw(), testYaw, 0.0001); + } + + @ParameterizedTest + @ValueSource(doubles = {-10, -5, -0, -1, -2, 5, 7, 10.23, 20.21, -19.999}) + public void testCameraPitch(double testPitch) { + final var targetPose = new Pose2d(new Translation2d(35, 0), new Rotation2d(Math.PI / 4)); + final var robotPose = new Pose2d(new Translation2d(30, 0), new Rotation2d(0)); + var sysUnderTest = + new SimVisionSystem("Test", 80.0, 0.0, new Transform2d(), 0.0, 99999, 640, 480, 0); + sysUnderTest.addSimVisionTarget(new SimVisionTarget(targetPose, 0.0, 0.5, 0.5)); + + sysUnderTest.moveCamera(new Transform2d(), 0.0, testPitch); + sysUnderTest.processFrame(robotPose); + var res = sysUnderTest.cam.getLatestResult(); + assertTrue(res.hasTargets()); + var tgt = res.getBestTarget(); + // If the camera is pitched down by 10 degrees, the target should appear + // in the upper part of the image (ie, pitch positive). Therefor, + // pass/fail involves -1.0. + assertEquals(tgt.getPitch(), -1.0 * testPitch, 0.0001); + } + + private static Stream distCalCParamProvider() { + // Arbitrary and fairly random assortment of distances, camera pitches, and heights + return Stream.of( + Arguments.of(5, 35, 0), + Arguments.of(6, 35, 1), + Arguments.of(10, 35, 0), + Arguments.of(15, 35, 2), + Arguments.of(19.95, 35, 0), + Arguments.of(20, 35, 0), + Arguments.of(5, 42, 1), + Arguments.of(6, 42, 0), + Arguments.of(10, 42, 2), + Arguments.of(15, 42, 0.5), + Arguments.of(19.42, 35, 0), + Arguments.of(20, 42, 0), + Arguments.of(5, 55, 2), + Arguments.of(6, 55, 0), + Arguments.of(10, 54, 2.2), + Arguments.of(15, 53, 0), + Arguments.of(19.52, 35, 1.1), + Arguments.of(20, 51, 2.87), + Arguments.of(20, 55, 3)); + } + + @ParameterizedTest + @MethodSource("distCalCParamProvider") + public void testDistanceCalc(double testDist, double testPitch, double testHeight) { + // Assume dist along ground and tgt height the same. Iterate over other parameters. + + final var targetPose = new Pose2d(new Translation2d(35, 0), new Rotation2d(Math.PI / 42)); + final var robotPose = new Pose2d(new Translation2d(35 - testDist, 0), new Rotation2d(0)); + var sysUnderTest = + new SimVisionSystem( + "absurdlylongnamewhichshouldneveractuallyhappenbuteehwelltestitanywaysohowsyourdaygoingihopegoodhaveagreatrestofyourlife!", + 160.0, + testPitch, + new Transform2d(), + testHeight, + 99999, + 640, + 480, + 0); + sysUnderTest.addSimVisionTarget(new SimVisionTarget(targetPose, testDist, 0.5, 0.5)); + + sysUnderTest.processFrame(robotPose); + var res = sysUnderTest.cam.getLatestResult(); + assertTrue(res.hasTargets()); + var tgt = res.getBestTarget(); + assertEquals(tgt.getYaw(), 0.0, 0.0001); + double distMeas = + PhotonUtils.calculateDistanceToTargetMeters( + testHeight, + testDist, + Units.degreesToRadians(testPitch), + Units.degreesToRadians(tgt.getPitch())); + assertEquals(distMeas, testDist, 0.001); + } + + @Test + public void testMultipleTargets() { + final var targetPoseL = new Pose2d(new Translation2d(35, 2), new Rotation2d()); + final var targetPoseC = new Pose2d(new Translation2d(35, 0), new Rotation2d()); + final var targetPoseR = new Pose2d(new Translation2d(35, -2), new Rotation2d()); + var sysUnderTest = + new SimVisionSystem("Test", 160.0, 0.0, new Transform2d(), 5.0, 99999, 640, 480, 20.0); + + sysUnderTest.addSimVisionTarget(new SimVisionTarget(targetPoseL, 0.0, 0.25, 0.1)); + sysUnderTest.addSimVisionTarget(new SimVisionTarget(targetPoseC, 1.0, 0.25, 0.1)); + sysUnderTest.addSimVisionTarget(new SimVisionTarget(targetPoseR, 2.0, 0.25, 0.1)); + sysUnderTest.addSimVisionTarget(new SimVisionTarget(targetPoseL, 3.0, 0.25, 0.1)); + sysUnderTest.addSimVisionTarget(new SimVisionTarget(targetPoseC, 4.0, 0.25, 0.1)); + sysUnderTest.addSimVisionTarget(new SimVisionTarget(targetPoseR, 5.0, 0.25, 0.1)); + sysUnderTest.addSimVisionTarget(new SimVisionTarget(targetPoseL, 6.0, 0.25, 0.1)); + sysUnderTest.addSimVisionTarget(new SimVisionTarget(targetPoseC, 7.0, 0.25, 0.1)); + sysUnderTest.addSimVisionTarget(new SimVisionTarget(targetPoseL, 8.0, 0.25, 0.1)); + sysUnderTest.addSimVisionTarget(new SimVisionTarget(targetPoseR, 9.0, 0.25, 0.1)); + sysUnderTest.addSimVisionTarget(new SimVisionTarget(targetPoseL, 10.0, 0.25, 0.1)); + + var robotPose = new Pose2d(new Translation2d(30, 0), Rotation2d.fromDegrees(5)); + sysUnderTest.processFrame(robotPose); + var res = sysUnderTest.cam.getLatestResult(); + assertTrue(res.hasTargets()); + List tgtList; + tgtList = res.getTargets(); + assertEquals(tgtList.size(), 11); + } +} diff --git a/photon-lib/src/test/native/cpp/PacketTest.cpp b/photon-lib/src/test/native/cpp/PacketTest.cpp new file mode 100644 index 000000000..ba31e23de --- /dev/null +++ b/photon-lib/src/test/native/cpp/PacketTest.cpp @@ -0,0 +1,91 @@ +/* + * Copyright (C) Photon Vision. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include + +#include + +#include "gtest/gtest.h" +#include "photonlib/PhotonPipelineResult.h" +#include "photonlib/PhotonTrackedTarget.h" + +TEST(PacketTest, PhotonTrackedTarget) { + photonlib::PhotonTrackedTarget target{ + 3.0, 4.0, 9.0, -5.0, + frc::Transform2d(frc::Translation2d(1_m, 2_m), 1.5_rad)}; + photonlib::Packet p; + p << target; + + photonlib::PhotonTrackedTarget b; + p >> b; + + for (auto& c : p.GetData()) { + std::cout << static_cast(c) << ","; + } + + EXPECT_EQ(target, b); +} + +TEST(PacketTest, PhotonPipelineResult) { + photonlib::PhotonPipelineResult result{1_s, {}}; + photonlib::Packet p; + p << result; + + photonlib::PhotonPipelineResult b; + p >> b; + + EXPECT_EQ(result, b); + + wpi::SmallVector targets{ + photonlib::PhotonTrackedTarget{ + 3.0, -4.0, 9.0, 4.0, + frc::Transform2d(frc::Translation2d(1_m, 2_m), 1.5_rad)}, + photonlib::PhotonTrackedTarget{ + 3.0, -4.0, 9.1, 6.7, + frc::Transform2d(frc::Translation2d(1_m, 5_m), 1.5_rad)}}; + + photonlib::PhotonPipelineResult result2{2_s, targets}; + photonlib::Packet p2; + p2 << result2; + + photonlib::PhotonPipelineResult b2; + p2 >> b2; + + EXPECT_EQ(result2, b2); +} + +TEST(PacketTest, BytePackFromJava) { + std::vector bytePack{ + 64, 8, 0, 0, 0, 0, 0, 0, 64, 16, 0, 0, 0, 0, + 0, 0, 64, 34, 0, 0, 0, 0, 0, 0, -64, 20, 0, 0, + 0, 0, 0, 0, 63, -16, 0, 0, 0, 0, 0, 0, 64, 0, + 0, 0, 0, 0, 0, 0, 64, 85, 124, 101, 19, -54, -47, 122}; + + std::vector bytes; + for (auto a : bytePack) bytes.emplace_back(static_cast(a)); + + photonlib::Packet packet{bytes}; + + photonlib::PhotonTrackedTarget res; + packet >> res; + + photonlib::PhotonTrackedTarget target{ + 3.0, 4.0, 9.0, -5.0, + frc::Transform2d(frc::Translation2d(1_m, 2_m), 1.5_rad)}; + + EXPECT_EQ(res, target); +} diff --git a/photon-lib/src/test/native/cpp/PhotonUtilsTest.cpp b/photon-lib/src/test/native/cpp/PhotonUtilsTest.cpp new file mode 100644 index 000000000..db061a33d --- /dev/null +++ b/photon-lib/src/test/native/cpp/PhotonUtilsTest.cpp @@ -0,0 +1,21 @@ +/* + * Copyright (C) Photon Vision. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "gtest/gtest.h" +#include "photonlib/PhotonUtils.h" + +TEST(PhotonUtilsTest, TestInclude) {} diff --git a/photon-lib/src/test/native/cpp/SimVisionSystemTest.cpp b/photon-lib/src/test/native/cpp/SimVisionSystemTest.cpp new file mode 100644 index 000000000..b6389aa38 --- /dev/null +++ b/photon-lib/src/test/native/cpp/SimVisionSystemTest.cpp @@ -0,0 +1,401 @@ +/* + * Copyright (C) Photon Vision. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include +#include +#include + +#include +#include + +#include "gtest/gtest.h" +#include "photonlib/PhotonCamera.h" +#include "photonlib/PhotonUtils.h" +#include "photonlib/SimVisionSystem.h" + +TEST(SimVisionSystemTest, testEmpty) { + photonlib::SimVisionSystem sysUnderTest("Test", 80.0_deg, 0.0_deg, + frc::Transform2d(), 1.0_m, 99999.0_m, + 320, 240, 0.0); + + for (int loopIdx = 0; loopIdx < 100; loopIdx++) { + sysUnderTest.ProcessFrame(frc::Pose2d()); + } +} + +class SimVisionSystemTestDistParam : public testing::TestWithParam {}; +INSTANTIATE_TEST_SUITE_P(SimVisionSystemTestDistParamInst, + SimVisionSystemTestDistParam, + testing::Values(5, 10, 15, 20, 25, 30)); + +TEST_P(SimVisionSystemTestDistParam, testDistanceAligned) { + double dist = GetParam(); + + auto targetPose = + frc::Pose2d(frc::Translation2d(35_m, 0_m), frc::Rotation2d()); + + photonlib::SimVisionSystem sysUnderTest("Test", 80.0_deg, 0.0_deg, + frc::Transform2d(), 1.0_m, 99999.0_m, + 320, 240, 0.0); + + sysUnderTest.AddSimVisionTarget( + photonlib::SimVisionTarget(targetPose, 0.0_m, 1.0_m, 1.0_m)); + + auto robotPose = frc::Pose2d( + frc::Translation2d(units::meter_t(35.0 - dist), 0_m), frc::Rotation2d()); + + sysUnderTest.ProcessFrame(robotPose); + auto result = sysUnderTest.cam.GetLatestResult(); + ASSERT_TRUE(result.HasTargets()); + ASSERT_EQ(result.GetBestTarget() + .GetCameraRelativePose() + .Translation() + .Norm() + .to(), + dist); +} + +TEST(SimVisionSystemTest, testVisibilityCupidShuffle) { + auto targetPose = + frc::Pose2d(frc::Translation2d(35_m, 0_m), frc::Rotation2d()); + + photonlib::SimVisionSystem sysUnderTest("Test", 80.0_deg, 0.0_deg, + frc::Transform2d(), 1.0_m, 99999.0_m, + 320, 240, 0.0); + + sysUnderTest.AddSimVisionTarget( + photonlib::SimVisionTarget(targetPose, 0.0_m, 3.0_m, 3.0_m)); + + // To the right, to the right + auto robotPose = + frc::Pose2d(frc::Translation2d(5.0_m, 0.0_m), frc::Rotation2d(-70.0_deg)); + sysUnderTest.ProcessFrame(robotPose); + auto result = sysUnderTest.cam.GetLatestResult(); + EXPECT_FALSE(result.HasTargets()); + + // To the right, to the right + robotPose = + frc::Pose2d(frc::Translation2d(5.0_m, 0.0_m), frc::Rotation2d(-95.0_deg)); + sysUnderTest.ProcessFrame(robotPose); + result = sysUnderTest.cam.GetLatestResult(); + EXPECT_FALSE(result.HasTargets()); + + // To the left, to the left + robotPose = + frc::Pose2d(frc::Translation2d(5.0_m, 0.0_m), frc::Rotation2d(90.0_deg)); + sysUnderTest.ProcessFrame(robotPose); + result = sysUnderTest.cam.GetLatestResult(); + EXPECT_FALSE(result.HasTargets()); + + // To the left, to the left + robotPose = + frc::Pose2d(frc::Translation2d(5.0_m, 0.0_m), frc::Rotation2d(65.0_deg)); + sysUnderTest.ProcessFrame(robotPose); + result = sysUnderTest.cam.GetLatestResult(); + EXPECT_FALSE(result.HasTargets()); + + // now kick, now kick + robotPose = + frc::Pose2d(frc::Translation2d(2.0_m, 0.0_m), frc::Rotation2d(5.0_deg)); + sysUnderTest.ProcessFrame(robotPose); + result = sysUnderTest.cam.GetLatestResult(); + EXPECT_TRUE(result.HasTargets()); + + // now kick, now kick + robotPose = + frc::Pose2d(frc::Translation2d(2.0_m, 0.0_m), frc::Rotation2d(-5.0_deg)); + sysUnderTest.ProcessFrame(robotPose); + result = sysUnderTest.cam.GetLatestResult(); + EXPECT_TRUE(result.HasTargets()); + + // now walk it by yourself + robotPose = frc::Pose2d(frc::Translation2d(2.0_m, 0.0_m), + frc::Rotation2d(-179.0_deg)); + sysUnderTest.ProcessFrame(robotPose); + result = sysUnderTest.cam.GetLatestResult(); + EXPECT_FALSE(result.HasTargets()); + + // now walk it by yourself + sysUnderTest.MoveCamera( + frc::Transform2d(frc::Translation2d(), frc::Rotation2d(180_deg)), 0.0_m, + 1.0_deg); + sysUnderTest.ProcessFrame(robotPose); + result = sysUnderTest.cam.GetLatestResult(); + EXPECT_TRUE(result.HasTargets()); +} + +TEST(SimVisionSystemTest, testNotVisibleVert1) { + auto targetPose = + frc::Pose2d(frc::Translation2d(35_m, 0_m), frc::Rotation2d()); + + photonlib::SimVisionSystem sysUnderTest("Test", 80.0_deg, 0.0_deg, + frc::Transform2d(), 1.0_m, 99999.0_m, + 640, 480, 0.0); + + sysUnderTest.AddSimVisionTarget( + photonlib::SimVisionTarget(targetPose, 1.0_m, 3.0_m, 2.0_m)); + + auto robotPose = + frc::Pose2d(frc::Translation2d(5.0_m, 0.0_m), frc::Rotation2d(5.0_deg)); + + sysUnderTest.ProcessFrame(robotPose); + auto result = sysUnderTest.cam.GetLatestResult(); + ASSERT_TRUE(result.HasTargets()); + + sysUnderTest.MoveCamera( + frc::Transform2d(frc::Translation2d(), frc::Rotation2d(0_deg)), 5000.0_m, + 1.0_deg); + sysUnderTest.ProcessFrame(robotPose); + result = sysUnderTest.cam.GetLatestResult(); + EXPECT_FALSE(result.HasTargets()); +} + +TEST(SimVisionSystemTest, testNotVisibleVert2) { + auto targetPose = + frc::Pose2d(frc::Translation2d(35_m, 0_m), frc::Rotation2d()); + + photonlib::SimVisionSystem sysUnderTest("Test", 80.0_deg, 45.0_deg, + frc::Transform2d(), 1.0_m, 99999.0_m, + 1234, 1234, 0.5); + + sysUnderTest.AddSimVisionTarget( + photonlib::SimVisionTarget(targetPose, 3.0_m, 0.5_m, 0.5_m)); + + auto robotPose = + frc::Pose2d(frc::Translation2d(32.0_m, 0.0_m), frc::Rotation2d(5.0_deg)); + + sysUnderTest.ProcessFrame(robotPose); + auto result = sysUnderTest.cam.GetLatestResult(); + EXPECT_TRUE(result.HasTargets()); + + robotPose = + frc::Pose2d(frc::Translation2d(0.0_m, 0.0_m), frc::Rotation2d(5.0_deg)); + sysUnderTest.ProcessFrame(robotPose); + result = sysUnderTest.cam.GetLatestResult(); + EXPECT_FALSE(result.HasTargets()); +} + +TEST(SimVisionSystemTest, testNotVisibleTgtSize) { + auto targetPose = + frc::Pose2d(frc::Translation2d(35_m, 0_m), frc::Rotation2d()); + + photonlib::SimVisionSystem sysUnderTest("Test", 80.0_deg, 0.0_deg, + frc::Transform2d(), 1.0_m, 99999.0_m, + 640, 480, 20.0); + + sysUnderTest.AddSimVisionTarget( + photonlib::SimVisionTarget(targetPose, 1.10_m, 0.25_m, 0.1_m)); + + auto robotPose = + frc::Pose2d(frc::Translation2d(32.0_m, 0.0_m), frc::Rotation2d(5.0_deg)); + sysUnderTest.ProcessFrame(robotPose); + auto result = sysUnderTest.cam.GetLatestResult(); + EXPECT_TRUE(result.HasTargets()); + + robotPose = + frc::Pose2d(frc::Translation2d(0.0_m, 0.0_m), frc::Rotation2d(5.0_deg)); + sysUnderTest.ProcessFrame(robotPose); + result = sysUnderTest.cam.GetLatestResult(); + EXPECT_FALSE(result.HasTargets()); +} + +TEST(SimVisionSystemTest, testNotVisibleTooFarForLEDs) { + auto targetPose = + frc::Pose2d(frc::Translation2d(35_m, 0_m), frc::Rotation2d()); + + photonlib::SimVisionSystem sysUnderTest("Test", 80.0_deg, 0.0_deg, + frc::Transform2d(), 1.0_m, 10.0_m, + 640, 480, 1.0); + + sysUnderTest.AddSimVisionTarget( + photonlib::SimVisionTarget(targetPose, 1.10_m, 0.25_m, 0.1_m)); + + auto robotPose = + frc::Pose2d(frc::Translation2d(28.0_m, 0.0_m), frc::Rotation2d(5.0_deg)); + sysUnderTest.ProcessFrame(robotPose); + auto result = sysUnderTest.cam.GetLatestResult(); + EXPECT_TRUE(result.HasTargets()); + + robotPose = + frc::Pose2d(frc::Translation2d(0.0_m, 0.0_m), frc::Rotation2d(5.0_deg)); + sysUnderTest.ProcessFrame(robotPose); + result = sysUnderTest.cam.GetLatestResult(); + EXPECT_FALSE(result.HasTargets()); +} + +class SimVisionSystemTestYawParam : public testing::TestWithParam {}; +INSTANTIATE_TEST_SUITE_P(SimVisionSystemTestYawParamInst, + SimVisionSystemTestYawParam, + testing::Values(-10, -5, -0, -1, -2, 5, 7, 10.23)); +TEST_P(SimVisionSystemTestYawParam, testYawAngles) { + double testYaw = GetParam(); // Nope, Chuck testYaw + auto targetPose = + frc::Pose2d(frc::Translation2d(35_m, 0_m), frc::Rotation2d(45_deg)); + + photonlib::SimVisionSystem sysUnderTest("Test", 80.0_deg, 0.0_deg, + frc::Transform2d(), 1.0_m, 99999.0_m, + 640, 480, 0.0); + + sysUnderTest.AddSimVisionTarget( + photonlib::SimVisionTarget(targetPose, 0.0_m, 0.5_m, 0.5_m)); + + auto robotPose = frc::Pose2d(frc::Translation2d(32_m, 0.0_m), + frc::Rotation2d(units::degree_t(testYaw))); + + sysUnderTest.ProcessFrame(robotPose); + auto result = sysUnderTest.cam.GetLatestResult(); + ASSERT_TRUE(result.HasTargets()); + auto tgt = result.GetBestTarget(); + EXPECT_DOUBLE_EQ(tgt.GetYaw(), testYaw); +} + +class SimVisionSystemTestCameraPitchParam + : public testing::TestWithParam {}; +INSTANTIATE_TEST_SUITE_P(SimVisionSystemTestCameraPitchParamInst, + SimVisionSystemTestCameraPitchParam, + testing::Values(-10, -5, -0, -1, -2, 5, 7, 10.23, + 20.21, -19.999)); +TEST_P(SimVisionSystemTestCameraPitchParam, testCameraPitch) { + double testPitch = GetParam(); + auto targetPose = + frc::Pose2d(frc::Translation2d(35_m, 0_m), frc::Rotation2d(45_deg)); + + auto robotPose = + frc::Pose2d(frc::Translation2d(30_m, 0.0_m), frc::Rotation2d(0.0_deg)); + + photonlib::SimVisionSystem sysUnderTest("Test", 80.0_deg, 0.0_deg, + frc::Transform2d(), 1.0_m, 99999.0_m, + 640, 480, 0.0); + + sysUnderTest.AddSimVisionTarget( + photonlib::SimVisionTarget(targetPose, 0.0_m, 0.5_m, 0.5_m)); + + sysUnderTest.MoveCamera( + frc::Transform2d(frc::Translation2d(), frc::Rotation2d()), 0.0_m, + units::degree_t(testPitch)); + + sysUnderTest.ProcessFrame(robotPose); + auto result = sysUnderTest.cam.GetLatestResult(); + ASSERT_TRUE(result.HasTargets()); + auto tgt = result.GetBestTarget(); + // If the camera is pitched down by 10 degrees, the target should appear + // in the upper part of the image (ie, pitch positive). Therefor, + // pass/fail involves -1.0. + EXPECT_DOUBLE_EQ(tgt.GetPitch(), -1.0 * testPitch); +} + +class SimVisionSystemTestDistCalcParam + : public testing::TestWithParam> {}; +INSTANTIATE_TEST_SUITE_P( + SimVisionSystemTestDistCalcParamInst, SimVisionSystemTestDistCalcParam, + testing::Values(std::tuple(5, 35, 0), + std::tuple(6, 35, 1), + std::tuple(10, 35, 0), + std::tuple(15, 35, 2), + std::tuple(19.95, 35, 0), + std::tuple(20, 35, 0), + std::tuple(5, 42, 1), + std::tuple(6, 42, 0), + std::tuple(10, 42, 2), + std::tuple(15, 42, 0.5), + std::tuple(19.42, 35, 0), + std::tuple(20, 42, 0), + std::tuple(5, 55, 2), + std::tuple(6, 55, 0), + std::tuple(10, 54, 2.2), + std::tuple(15, 53, 0), + std::tuple(19.52, 35, 1.1), + std::tuple(20, 51, 2.87), + std::tuple(20, 55, 3))); +TEST_P(SimVisionSystemTestDistCalcParam, testDistanceCalc) { + std::tuple testArgs = GetParam(); + double testDist = std::get<0>(testArgs); + double testPitch = std::get<1>(testArgs); + double testHeight = std::get<2>(testArgs); + + auto targetPose = frc::Pose2d(frc::Translation2d(35_m, 0_m), + frc::Rotation2d(units::radian_t(3.14159 / 42))); + + auto robotPose = + frc::Pose2d(frc::Translation2d(units::meter_t(35 - testDist), 0.0_m), + frc::Rotation2d(0.0_deg)); + + photonlib::SimVisionSystem sysUnderTest( + "absurdlylongnamewhichshouldneveractuallyhappenbuteehwelltestitanywaysoho" + "wsyourdaygoingihopegoodhaveagreatrestofyourlife", + 160.0_deg, units::degree_t(testPitch), frc::Transform2d(), + units::meter_t(testHeight), 99999.0_m, 640, 480, 0.0); + + sysUnderTest.AddSimVisionTarget(photonlib::SimVisionTarget( + targetPose, units::meter_t(testDist), 0.5_m, 0.5_m)); + + sysUnderTest.ProcessFrame(robotPose); + auto result = sysUnderTest.cam.GetLatestResult(); + ASSERT_TRUE(result.HasTargets()); + auto tgt = result.GetBestTarget(); + EXPECT_DOUBLE_EQ(tgt.GetYaw(), 0.0); + units::meter_t distMeas = photonlib::PhotonUtils::CalculateDistanceToTarget( + units::meter_t(testHeight), units::meter_t(testDist), + units::degree_t(testPitch), units::degree_t(tgt.GetPitch())); + EXPECT_DOUBLE_EQ(distMeas.to(), testDist); +} + +TEST(SimVisionSystemTest, testMultipleTargets) { + auto targetPoseL = + frc::Pose2d(frc::Translation2d(35_m, 2_m), frc::Rotation2d()); + auto targetPoseC = + frc::Pose2d(frc::Translation2d(35_m, 0_m), frc::Rotation2d()); + auto targetPoseR = + frc::Pose2d(frc::Translation2d(35_m, -2_m), frc::Rotation2d()); + + photonlib::SimVisionSystem sysUnderTest("test", 160.0_deg, 0.0_deg, + frc::Transform2d(), 5.0_m, 99999.0_m, + 640, 480, 20.0); + + sysUnderTest.AddSimVisionTarget( + photonlib::SimVisionTarget(targetPoseL, 0.0_m, 0.25_m, 0.1_m)); + sysUnderTest.AddSimVisionTarget( + photonlib::SimVisionTarget(targetPoseC, 1.0_m, 0.25_m, 0.1_m)); + sysUnderTest.AddSimVisionTarget( + photonlib::SimVisionTarget(targetPoseR, 2.0_m, 0.25_m, 0.1_m)); + sysUnderTest.AddSimVisionTarget( + photonlib::SimVisionTarget(targetPoseL, 3.0_m, 0.25_m, 0.1_m)); + sysUnderTest.AddSimVisionTarget( + photonlib::SimVisionTarget(targetPoseC, 4.0_m, 0.25_m, 0.1_m)); + sysUnderTest.AddSimVisionTarget( + photonlib::SimVisionTarget(targetPoseR, 5.0_m, 0.25_m, 0.1_m)); + sysUnderTest.AddSimVisionTarget( + photonlib::SimVisionTarget(targetPoseL, 6.0_m, 0.25_m, 0.1_m)); + sysUnderTest.AddSimVisionTarget( + photonlib::SimVisionTarget(targetPoseC, 7.0_m, 0.25_m, 0.1_m)); + sysUnderTest.AddSimVisionTarget( + photonlib::SimVisionTarget(targetPoseL, 8.0_m, 0.25_m, 0.1_m)); + sysUnderTest.AddSimVisionTarget( + photonlib::SimVisionTarget(targetPoseR, 9.0_m, 0.25_m, 0.1_m)); + sysUnderTest.AddSimVisionTarget( + photonlib::SimVisionTarget(targetPoseL, 10.0_m, 0.25_m, 0.1_m)); + + auto robotPose = + frc::Pose2d(frc::Translation2d(30_m, 0.0_m), frc::Rotation2d(5.0_deg)); + + sysUnderTest.ProcessFrame(robotPose); + auto result = sysUnderTest.cam.GetLatestResult(); + ASSERT_TRUE(result.HasTargets()); + + auto tgtList = result.GetTargets(); + EXPECT_EQ(11ul, tgtList.size()); +} diff --git a/photon-lib/src/test/native/cpp/main.cpp b/photon-lib/src/test/native/cpp/main.cpp new file mode 100644 index 000000000..b03cde4ea --- /dev/null +++ b/photon-lib/src/test/native/cpp/main.cpp @@ -0,0 +1,24 @@ +/* + * Copyright (C) Photon Vision. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "gtest/gtest.h" + +int main(int argc, char** argv) { + ::testing::InitGoogleTest(&argc, argv); + int ret = RUN_ALL_TESTS(); + return ret; +} diff --git a/photon-targeting/.gitignore b/photon-targeting/.gitignore new file mode 100644 index 000000000..933a7e534 --- /dev/null +++ b/photon-targeting/.gitignore @@ -0,0 +1,13 @@ +bin/* +.settings/* +.project +.classpath +*.prefs +.gradle +.gradle/* +build +build/* +photonvision/* +photonvision_config/* + +src/main/java/org/photonvision/PhotonVersion.java \ No newline at end of file diff --git a/photon-targeting/build.gradle b/photon-targeting/build.gradle new file mode 100644 index 000000000..f43133d4f --- /dev/null +++ b/photon-targeting/build.gradle @@ -0,0 +1,33 @@ +apply plugin: 'java' + +repositories { + maven { + url = 'https://frcmaven.wpi.edu:443/artifactory/development' + } + maven { + url = 'https://frcmaven.wpi.edu:443/artifactory/release' + } +} + +apply from: '../versioningHelper.gradle' + +ext { + pubVersion = versionString +} + +def openCVVersion = '3.4.7-2' + +dependencies { + implementation 'edu.wpi.first.wpimath:wpimath-java:2021.1.2' + implementation "edu.wpi.first.thirdparty.frc2020.opencv:opencv-java:$openCVVersion" + implementation "com.fasterxml.jackson.core:jackson-core:2.10.0" + implementation "com.fasterxml.jackson.core:jackson-annotations:2.10.0" + implementation 'org.apache.commons:commons-math3:3.6.1' +} + +java { + withJavadocJar() + withSourcesJar() +} + +apply from: 'publish.gradle' diff --git a/photon-targeting/publish.gradle b/photon-targeting/publish.gradle new file mode 100644 index 000000000..c54cf2988 --- /dev/null +++ b/photon-targeting/publish.gradle @@ -0,0 +1,28 @@ +apply plugin: 'maven-publish' + +def artifactGroupId = 'org.photonvision' +def baseArtifactId = 'PhotonTargeting' + +def outputsFolder = file("$buildDir/outputs") + +publishing { + repositories { + maven { + url 'https://maven.photonvision.org/repository/internal' + credentials { + username 'ghactions' + password System.getenv("ARTIFACTORY_API_KEY") + } + } + } + + publications { + mavenJava(MavenPublication) { + groupId = artifactGroupId + artifactId = "${baseArtifactId}-java" + version = pubVersion + + from components.java + } + } +} diff --git a/photon-targeting/settings.gradle b/photon-targeting/settings.gradle new file mode 100644 index 000000000..674b1ce85 --- /dev/null +++ b/photon-targeting/settings.gradle @@ -0,0 +1,2 @@ +rootProject.name = 'photon-targeting' + diff --git a/photon-core/src/main/java/org/photonvision/common/dataflow/structures/Packet.java b/photon-targeting/src/main/java/org/photonvision/common/dataflow/structures/Packet.java similarity index 98% rename from photon-core/src/main/java/org/photonvision/common/dataflow/structures/Packet.java rename to photon-targeting/src/main/java/org/photonvision/common/dataflow/structures/Packet.java index a09e9bd0c..07d9abc9f 100644 --- a/photon-core/src/main/java/org/photonvision/common/dataflow/structures/Packet.java +++ b/photon-targeting/src/main/java/org/photonvision/common/dataflow/structures/Packet.java @@ -49,6 +49,10 @@ public class Packet { writePos = 0; } + public int getSize() { + return size; + } + /** * Returns the packet data. * diff --git a/photon-targeting/src/main/java/org/photonvision/common/hardware/VisionLEDMode.java b/photon-targeting/src/main/java/org/photonvision/common/hardware/VisionLEDMode.java new file mode 100644 index 000000000..af95fc2cb --- /dev/null +++ b/photon-targeting/src/main/java/org/photonvision/common/hardware/VisionLEDMode.java @@ -0,0 +1,46 @@ +/* + * Copyright (C) Photon Vision. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.photonvision.common.hardware; + +public enum VisionLEDMode { + kDefault(-1), + kOff(0), + kOn(1), + kBlink(2); + + public final int value; + + VisionLEDMode(int value) { + this.value = value; + } + + @Override + public String toString() { + switch (this) { + case kDefault: + return "Default"; + case kOff: + return "Off"; + case kOn: + return "On"; + case kBlink: + return "Blink"; + } + return ""; + } +} diff --git a/photon-core/src/main/java/org/photonvision/vision/pipeline/result/SimplePipelineResult.java b/photon-targeting/src/main/java/org/photonvision/targeting/PhotonPipelineResult.java similarity index 59% rename from photon-core/src/main/java/org/photonvision/vision/pipeline/result/SimplePipelineResult.java rename to photon-targeting/src/main/java/org/photonvision/targeting/PhotonPipelineResult.java index 4f52997d2..5764b7a24 100644 --- a/photon-core/src/main/java/org/photonvision/vision/pipeline/result/SimplePipelineResult.java +++ b/photon-targeting/src/main/java/org/photonvision/targeting/PhotonPipelineResult.java @@ -15,55 +15,102 @@ * along with this program. If not, see . */ -package org.photonvision.vision.pipeline.result; +package org.photonvision.targeting; import java.util.ArrayList; import java.util.List; import java.util.Objects; import org.photonvision.common.dataflow.structures.Packet; -import org.photonvision.vision.target.TrackedTarget; -public class SimplePipelineResult { +/** Represents a pipeline result from a PhotonCamera. */ +public class PhotonPipelineResult { + private static boolean HAS_WARNED = false; + + // Targets to store. + public final List targets = new ArrayList<>(); + + // Latency in milliseconds. private double latencyMillis; + + // Whether targets exist. private boolean hasTargets; - public final List targets = new ArrayList<>(); - public SimplePipelineResult() {} + /** Constructs an empty pipeline result. */ + public PhotonPipelineResult() {} - public SimplePipelineResult( - double latencyMillis, boolean hasTargets, List targets) { + /** + * Constructs a pipeline result. + * + * @param latencyMillis The latency in the pipeline. + * @param targets The list of targets identified by the pipeline. + */ + public PhotonPipelineResult(double latencyMillis, List targets) { this.latencyMillis = latencyMillis; - this.hasTargets = hasTargets; + this.hasTargets = targets.size() != 0; this.targets.addAll(targets); } - public SimplePipelineResult(CVPipelineResult r) { - this(r.processingMillis, r.hasTargets(), simpleFromTrackedTargets(r.targets)); - } - /** * Returns the size of the packet needed to store this pipeline result. * * @return The size of the packet needed to store this pipeline result. */ public int getPacketSize() { - return targets.size() * SimpleTrackedTarget.PACK_SIZE_BYTES + 8 + 2; + return targets.size() * PhotonTrackedTarget.PACK_SIZE_BYTES + 8 + 2; } + /** + * Returns the best target in this pipeline result. If there are no targets, this method will + * return null. The best target is determined by the target sort mode in the PhotonVision UI. + * + * @return The best target of the pipeline result. + */ + public PhotonTrackedTarget getBestTarget() { + if (!hasTargets && !HAS_WARNED) { + String errStr = + "This PhotonPipelineResult object has no targets associated with it! Please check hasTargets() " + + "before calling this method. For more information, please review the PhotonLib " + + "documentation at http://docs.photonvision.org"; + System.err.println(errStr); + new Exception().printStackTrace(); + HAS_WARNED = true; + } + return hasTargets ? targets.get(0) : null; + } + + /** + * Returns the latency in the pipeline. + * + * @return The latency in the pipeline. + */ public double getLatencyMillis() { return latencyMillis; } + /** + * Returns whether the pipeline has targets. + * + * @return Whether the pipeline has targets. + */ public boolean hasTargets() { return hasTargets; } + /** + * Returns a copy of the vector of targets. + * + * @return A copy of the vector of targets. + */ + public List getTargets() { + return new ArrayList<>(targets); + } + @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; - SimplePipelineResult that = (SimplePipelineResult) o; + PhotonPipelineResult that = (PhotonPipelineResult) o; boolean latencyMatch = Double.compare(that.latencyMillis, latencyMillis) == 0; boolean hasTargetsMatch = that.hasTargets == hasTargets; boolean targetsMatch = that.targets.equals(targets); @@ -91,7 +138,7 @@ public class SimplePipelineResult { // Decode the information of each target. for (int i = 0; i < (int) targetCount; ++i) { - var target = new SimpleTrackedTarget(); + var target = new PhotonTrackedTarget(); target.createFromPacket(packet); targets.add(target); } @@ -117,12 +164,4 @@ public class SimplePipelineResult { // Return the packet. return packet; } - - private static List simpleFromTrackedTargets(List targets) { - var ret = new ArrayList(); - for (var t : targets) { - ret.add(new SimpleTrackedTarget(t)); - } - return ret; - } } diff --git a/photon-core/src/main/java/org/photonvision/vision/pipeline/result/SimpleTrackedTarget.java b/photon-targeting/src/main/java/org/photonvision/targeting/PhotonTrackedTarget.java similarity index 88% rename from photon-core/src/main/java/org/photonvision/vision/pipeline/result/SimpleTrackedTarget.java rename to photon-targeting/src/main/java/org/photonvision/targeting/PhotonTrackedTarget.java index 1f0ba9665..3baf4f2b3 100644 --- a/photon-core/src/main/java/org/photonvision/vision/pipeline/result/SimpleTrackedTarget.java +++ b/photon-targeting/src/main/java/org/photonvision/targeting/PhotonTrackedTarget.java @@ -15,16 +15,15 @@ * along with this program. If not, see . */ -package org.photonvision.vision.pipeline.result; +package org.photonvision.targeting; import edu.wpi.first.wpilibj.geometry.Rotation2d; import edu.wpi.first.wpilibj.geometry.Transform2d; import edu.wpi.first.wpilibj.geometry.Translation2d; import java.util.Objects; import org.photonvision.common.dataflow.structures.Packet; -import org.photonvision.vision.target.TrackedTarget; -public class SimpleTrackedTarget { +public class PhotonTrackedTarget { public static final int PACK_SIZE_BYTES = Double.BYTES * 7; private double yaw; @@ -33,9 +32,9 @@ public class SimpleTrackedTarget { private double skew; private Transform2d cameraToTarget = new Transform2d(); - public SimpleTrackedTarget() {} + public PhotonTrackedTarget() {} - public SimpleTrackedTarget(double yaw, double pitch, double area, double skew, Transform2d pose) { + public PhotonTrackedTarget(double yaw, double pitch, double area, double skew, Transform2d pose) { this.yaw = yaw; this.pitch = pitch; this.area = area; @@ -43,10 +42,6 @@ public class SimpleTrackedTarget { cameraToTarget = pose; } - public SimpleTrackedTarget(TrackedTarget t) { - this(t.getYaw(), t.getPitch(), t.getArea(), t.getSkew(), t.getCameraToTarget()); - } - public double getYaw() { return yaw; } @@ -67,7 +62,7 @@ public class SimpleTrackedTarget { public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; - SimpleTrackedTarget that = (SimpleTrackedTarget) o; + PhotonTrackedTarget that = (PhotonTrackedTarget) o; return Double.compare(that.yaw, yaw) == 0 && Double.compare(that.pitch, pitch) == 0 && Double.compare(that.area, area) == 0 diff --git a/photonlib-cpp-examples/build.gradle b/photonlib-cpp-examples/build.gradle new file mode 100644 index 000000000..379e268a3 --- /dev/null +++ b/photonlib-cpp-examples/build.gradle @@ -0,0 +1,77 @@ +plugins { + id 'cpp' + id 'java' + id 'edu.wpi.first.wpilib.repositories.WPILibRepositoriesPlugin' version '2020.2' + id 'google-test-test-suite' + id 'edu.wpi.first.NativeUtils' version '2020.10.1' + id 'edu.wpi.first.GradleJni' version '0.10.1' + id 'edu.wpi.first.GradleVsCode' version '0.12.0' +} + +repositories { + maven { + url = 'https://frcmaven.wpi.edu:443/artifactory/release' + maven { url "https://frcmaven.wpi.edu/artifactory/development" } + mavenCentral() + } +} + +apply from: '../photon-lib/config.gradle' + +ext.examplesMap = [:] + +File examplesTree = file("$projectDir/src/main/cpp/examples") +examplesTree.list(new FilenameFilter() { + @Override + public boolean accept(File current, String name) { + return new File(current, name).isDirectory(); + } +}).each { + examplesMap.put(it, []) +} + +nativeUtils.platformConfigs.named(nativeUtils.wpi.platforms.roborio).configure { + cppCompiler.args.remove('-Wno-error=deprecated-declarations') + cppCompiler.args.add('-Werror=deprecated-declarations') +} + +ext { + sharedCvConfigs = examplesMap + staticCvConfigs = [:] + useJava = false + useCpp = true +} + +model { + components { + examplesMap.each { key, value -> + "${key}"(NativeExecutableSpec) { + targetBuildTypes 'debug' + binaries.all { binary -> + lib project: ':photon-lib', library: 'Photon', linkage: 'shared' + if (binary.targetPlatform.name == nativeUtils.wpi.platforms.roborio) { + nativeUtils.useRequiredLibrary(binary, 'netcomm_shared', 'chipobject_shared', 'visa_shared', 'ni_runtime_shared') + } + } + sources { + cpp { + source { + srcDirs 'src/main/cpp/examples/' + "${key}" + "/cpp" + include '**/*.cpp' + } + exportedHeaders { + srcDirs 'src/main/cpp/examples/' + "${key}" + "/include" + include '**/*.h' + } + } + } + nativeUtils.useRequiredLibrary(it, 'wpilib_shared') + } + } + } +} + +ext { + exampleDirectory = new File("$projectDir/src/main/java/edu/wpi/first/wpilibj/examples/") + exampleFile = new File("$projectDir/src/main/java/edu/wpi/first/wpilibj/examples/examples.json") +} \ No newline at end of file diff --git a/photonlib-cpp-examples/settings.gradle b/photonlib-cpp-examples/settings.gradle new file mode 100644 index 000000000..325404797 --- /dev/null +++ b/photonlib-cpp-examples/settings.gradle @@ -0,0 +1 @@ +rootproject.name = 'photonlib-cpp-examples' \ No newline at end of file diff --git a/photonlib-cpp-examples/src/main/cpp/examples/aimandrange/cpp/Robot.cpp b/photonlib-cpp-examples/src/main/cpp/examples/aimandrange/cpp/Robot.cpp new file mode 100644 index 000000000..a9b077233 --- /dev/null +++ b/photonlib-cpp-examples/src/main/cpp/examples/aimandrange/cpp/Robot.cpp @@ -0,0 +1,64 @@ +/** + * Copyright (C) 2018-2020 Photon Vision. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "Robot.h" + +#include + +void Robot::TeleopPeriodic() { + double forwardSpeed; + double rotationSpeed; + + if (xboxController.GetAButton()) { + // Vision-alignment mode + // Query the latest result from PhotonVision + const auto &result = camera.GetLatestResult(); + + if (result.HasTargets()) { + // First calculate range + units::meter_t range = + photonlib::PhotonUtils::CalculateDistanceToTarget( + CAMERA_HEIGHT, TARGET_HEIGHT, CAMERA_PITCH, + units::degree_t{result.GetBestTarget().GetPitch()}); + + // Use this range as the measurement we give to the PID controller. + forwardSpeed = forwardController.Calculate( + range.to(), GOAL_RANGE_METERS.to()); + + // Also calculate angular power + rotationSpeed = + turnController.Calculate(result.GetBestTarget().GetYaw(), 0); + } else { + // If we have no targets, stay still. + forwardSpeed = 0; + rotationSpeed = 0; + } + } else { + // Manual Driver Mode + forwardSpeed = + xboxController.GetY(frc::GenericHID::JoystickHand::kRightHand); + rotationSpeed = + xboxController.GetX(frc::GenericHID::JoystickHand::kLeftHand); + } + + // Use our forward/turn speeds to control the drivetrain + drive.ArcadeDrive(forwardSpeed, rotationSpeed); +} + +#ifndef RUNNING_FRC_TESTS +int main() { return frc::StartRobot(); } +#endif diff --git a/photonlib-cpp-examples/src/main/cpp/examples/aimandrange/include/Robot.h b/photonlib-cpp-examples/src/main/cpp/examples/aimandrange/include/Robot.h new file mode 100644 index 000000000..9a42f4fcf --- /dev/null +++ b/photonlib-cpp-examples/src/main/cpp/examples/aimandrange/include/Robot.h @@ -0,0 +1,64 @@ +/** + * Copyright (C) 2018-2020 Photon Vision. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#pragma once + +#include + +#include +#include +#include +#include +#include +#include +#include + +class Robot : public frc::TimedRobot { +public: + void TeleopPeriodic() override; + +private: + // Constants such as camera and target height stored. Change per robot and + // goal! + const units::meter_t CAMERA_HEIGHT = 24_in; + const units::meter_t TARGET_HEIGHT = 5_ft; + + // Angle between horizontal and the camera. + const units::radian_t CAMERA_PITCH = 0_deg; + + // How far from the target we want to be + const units::meter_t GOAL_RANGE_METERS = 3_ft; + + // PID constants should be tuned per robot + const double LINEAR_P = 0.1; + const double LINEAR_D = 0.0; + frc2::PIDController forwardController{LINEAR_P, 0.0, LINEAR_D}; + + const double ANGULAR_P = 0.1; + const double ANGULAR_D = 0.0; + frc2::PIDController turnController{ANGULAR_P, 0.0, ANGULAR_D}; + + // Change this to match the name of your camera + photonlib::PhotonCamera camera{"photonvision"}; + + frc::XboxController xboxController{0}; + + // Drive motors + frc::PWMVictorSPX leftMotor{0}; + frc::PWMVictorSPX rightMotor{1}; + frc::DifferentialDrive drive{leftMotor, rightMotor}; +}; diff --git a/photonlib-cpp-examples/src/main/cpp/examples/aimattarget/cpp/Robot.cpp b/photonlib-cpp-examples/src/main/cpp/examples/aimattarget/cpp/Robot.cpp new file mode 100644 index 000000000..6b068dec4 --- /dev/null +++ b/photonlib-cpp-examples/src/main/cpp/examples/aimattarget/cpp/Robot.cpp @@ -0,0 +1,51 @@ +/** + * Copyright (C) 2018-2020 Photon Vision. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "Robot.h" + +#include + +void Robot::TeleopPeriodic() { + double forwardSpeed = + xboxController.GetY(frc::GenericHID::JoystickHand::kRightHand); + double rotationSpeed; + + if (xboxController.GetAButton()) { + // Vision-alignment mode + // Query the latest result from PhotonVision + photonlib::PhotonPipelineResult result = camera.GetLatestResult(); + + if (result.HasTargets()) { + // Rotation speed is the output of the PID controller + rotationSpeed = controller.Calculate(result.GetBestTarget().GetYaw(), 0); + } else { + // If we have no targets, stay still. + rotationSpeed = 0; + } + } else { + // Manual Driver Mode + rotationSpeed = + xboxController.GetX(frc::GenericHID::JoystickHand::kLeftHand); + } + + // Use our forward/turn speeds to control the drivetrain + drive.ArcadeDrive(forwardSpeed, rotationSpeed); +} + +#ifndef RUNNING_FRC_TESTS +int main() { return frc::StartRobot(); } +#endif diff --git a/photonlib-cpp-examples/src/main/cpp/examples/aimattarget/include/Robot.h b/photonlib-cpp-examples/src/main/cpp/examples/aimattarget/include/Robot.h new file mode 100644 index 000000000..b0cac12a4 --- /dev/null +++ b/photonlib-cpp-examples/src/main/cpp/examples/aimattarget/include/Robot.h @@ -0,0 +1,46 @@ +/** + * Copyright (C) 2018-2020 Photon Vision. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#pragma once + +#include + +#include +#include +#include +#include +#include +#include +#include + +class Robot : public frc::TimedRobot { +public: + void TeleopPeriodic() override; + +private: + // Change this to match the name of your camera + photonlib::PhotonCamera camera{"photonvision"}; + // PID constants should be tuned per robot + frc2::PIDController controller{.1, 0, 0}; + + frc::XboxController xboxController{0}; + + // Drive motors + frc::PWMVictorSPX leftMotor{0}; + frc::PWMVictorSPX rightMotor{1}; + frc::DifferentialDrive drive{leftMotor, rightMotor}; +}; diff --git a/photonlib-cpp-examples/src/main/cpp/examples/examples.json b/photonlib-cpp-examples/src/main/cpp/examples/examples.json new file mode 100644 index 000000000..2460afe15 --- /dev/null +++ b/photonlib-cpp-examples/src/main/cpp/examples/examples.json @@ -0,0 +1,38 @@ +[ + { + "name": "AimAtTarget", + "description": "Aim at a target", + "tags": [], + "gradlebase": "cpp", + "language": "cpp", + "commandversion": 1, + "mainclass": "Main", + "packagetoreplace": null, + "dependencies": [], + "foldername": "aimattarget" + }, + { + "name": "GetInRange", + "description": "Get in range of a target", + "tags": [], + "gradlebase": "cpp", + "language": "cpp", + "commandversion": 1, + "mainclass": "Main", + "packagetoreplace": null, + "dependencies": [], + "foldername": "getinrange" + }, + { + "name": "AimAndRange", + "description": "Aim at a target while at a desired range", + "tags": [], + "gradlebase": "cpp", + "language": "cpp", + "commandversion": 1, + "mainclass": "Main", + "packagetoreplace": null, + "dependencies": [], + "foldername": "aimandrange" + } +] diff --git a/photonlib-cpp-examples/src/main/cpp/examples/getinrange/cpp/Robot.cpp b/photonlib-cpp-examples/src/main/cpp/examples/getinrange/cpp/Robot.cpp new file mode 100644 index 000000000..e0f7c6caa --- /dev/null +++ b/photonlib-cpp-examples/src/main/cpp/examples/getinrange/cpp/Robot.cpp @@ -0,0 +1,57 @@ +/** + * Copyright (C) 2018-2020 Photon Vision. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "Robot.h" + +#include + +void Robot::TeleopPeriodic() { + double forwardSpeed; + double rotationSpeed = + xboxController.GetX(frc::GenericHID::JoystickHand::kLeftHand); + + if (xboxController.GetAButton()) { + // Vision-alignment mode + // Query the latest result from PhotonVision + photonlib::PhotonPipelineResult result = camera.GetLatestResult(); + + if (result.HasTargets()) { + // First calculate range + units::meter_t range = photonlib::PhotonUtils::CalculateDistanceToTarget( + CAMERA_HEIGHT, TARGET_HEIGHT, CAMERA_PITCH, + units::degree_t{result.GetBestTarget().GetPitch()}); + + // Use this range as the measurement we give to the PID controller. + forwardSpeed = controller.Calculate(range.to(), + GOAL_RANGE_METERS.to()); + } else { + // If we have no targets, stay still. + forwardSpeed = 0; + } + } else { + // Manual Driver Mode + forwardSpeed = + xboxController.GetY(frc::GenericHID::JoystickHand::kRightHand); + } + + // Use our forward/turn speeds to control the drivetrain + drive.ArcadeDrive(forwardSpeed, rotationSpeed); +} + +#ifndef RUNNING_FRC_TESTS +int main() { return frc::StartRobot(); } +#endif diff --git a/photonlib-cpp-examples/src/main/cpp/examples/getinrange/include/Robot.h b/photonlib-cpp-examples/src/main/cpp/examples/getinrange/include/Robot.h new file mode 100644 index 000000000..10272a4af --- /dev/null +++ b/photonlib-cpp-examples/src/main/cpp/examples/getinrange/include/Robot.h @@ -0,0 +1,60 @@ +/** + * Copyright (C) 2018-2020 Photon Vision. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#pragma once + +#include + +#include +#include +#include +#include +#include +#include +#include + +class Robot : public frc::TimedRobot { +public: + void TeleopPeriodic() override; + +private: + // Constants such as camera and target height stored. Change per robot and + // goal! + const units::meter_t CAMERA_HEIGHT = 24_in; + const units::meter_t TARGET_HEIGHT = 5_ft; + + // Angle between horizontal and the camera. + const units::radian_t CAMERA_PITCH = 0_deg; + + // How far from the target we want to be + const units::meter_t GOAL_RANGE_METERS = 3_ft; + + // PID constants should be tuned per robot + const double P_GAIN = 0.1; + const double D_GAIN = 0.0; + frc2::PIDController controller{P_GAIN, 0.0, D_GAIN}; + + // Change this to match the name of your camera + photonlib::PhotonCamera camera{"photonvision"}; + + frc::XboxController xboxController{0}; + + // Drive motors + frc::PWMVictorSPX leftMotor{0}; + frc::PWMVictorSPX rightMotor{1}; + frc::DifferentialDrive drive{leftMotor, rightMotor}; +}; diff --git a/photonlib-java-examples/build.gradle b/photonlib-java-examples/build.gradle new file mode 100644 index 000000000..75473cf58 --- /dev/null +++ b/photonlib-java-examples/build.gradle @@ -0,0 +1,35 @@ +apply plugin: 'java' + +String wpilibVersion = "2021.1.2" +String opencvVersion = "3.4.7-5" +String ejmlVersion = "0.38" +String jacksonVersion = "2.10.0" + +repositories { + maven { + url = 'https://frcmaven.wpi.edu:443/artifactory/release' + } +} + +dependencies { + implementation project(':photon-lib') + implementation project(':photon-targeting') + + implementation "edu.wpi.first.wpilibj:wpilibj-java:${wpilibVersion}" + implementation "edu.wpi.first.wpimath:wpimath-java:${wpilibVersion}" + implementation "edu.wpi.first.ntcore:ntcore-java:${wpilibVersion}" + implementation "edu.wpi.first.wpiutil:wpiutil-java:${wpilibVersion}" + implementation "edu.wpi.first.thirdparty.frc2021.opencv:opencv-java:${opencvVersion}" + implementation "edu.wpi.first.cscore:cscore-java:${wpilibVersion}" + implementation "edu.wpi.first.cameraserver:cameraserver-java:${wpilibVersion}" + implementation "edu.wpi.first.hal:hal-java:${wpilibVersion}" + implementation "org.ejml:ejml-simple:${ejmlVersion}" + implementation "com.fasterxml.jackson.core:jackson-annotations:${jacksonVersion}" + implementation "com.fasterxml.jackson.core:jackson-core:${jacksonVersion}" + implementation "com.fasterxml.jackson.core:jackson-databind:${jacksonVersion}" +} + +ext { + exampleDirectory = new File("$projectDir/src/main/java/edu/wpi/first/wpilibj/examples/") + exampleFile = new File("$projectDir/src/main/java/edu/wpi/first/wpilibj/examples/examples.json") +} \ No newline at end of file diff --git a/photonlib-java-examples/settings.gradle b/photonlib-java-examples/settings.gradle new file mode 100644 index 000000000..fe6bf76ca --- /dev/null +++ b/photonlib-java-examples/settings.gradle @@ -0,0 +1 @@ +rootproject.name = 'photonlib-java-examples' \ No newline at end of file diff --git a/photonlib-java-examples/src/main/java/org/photonlib/examples/aimandrange/Main.java b/photonlib-java-examples/src/main/java/org/photonlib/examples/aimandrange/Main.java new file mode 100644 index 000000000..b11b5b18a --- /dev/null +++ b/photonlib-java-examples/src/main/java/org/photonlib/examples/aimandrange/Main.java @@ -0,0 +1,38 @@ +/* + * Copyright (C) Photon Vision. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.photonlib.examples.aimandrange; + +import edu.wpi.first.wpilibj.RobotBase; + +/** +* Do NOT add any static variables to this class, or any initialization at all. Unless you know what +* you are doing, do not modify this file except to change the parameter class to the startRobot +* call. +*/ +public final class Main { + private Main() {} + + /** + * Main initialization function. Do not perform any initialization here. + * + *

If you change your main robot class, change the parameter type. + */ + public static void main(String... args) { + RobotBase.startRobot(Robot::new); + } +} diff --git a/photonlib-java-examples/src/main/java/org/photonlib/examples/aimandrange/Robot.java b/photonlib-java-examples/src/main/java/org/photonlib/examples/aimandrange/Robot.java new file mode 100644 index 000000000..0e40a393e --- /dev/null +++ b/photonlib-java-examples/src/main/java/org/photonlib/examples/aimandrange/Robot.java @@ -0,0 +1,103 @@ +/* + * Copyright (C) Photon Vision. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.photonlib.examples.aimandrange; + +import edu.wpi.first.wpilibj.GenericHID; +import edu.wpi.first.wpilibj.PWMVictorSPX; +import edu.wpi.first.wpilibj.TimedRobot; +import edu.wpi.first.wpilibj.XboxController; +import edu.wpi.first.wpilibj.controller.PIDController; +import edu.wpi.first.wpilibj.drive.DifferentialDrive; +import edu.wpi.first.wpilibj.util.Units; +import org.photonvision.PhotonCamera; +import org.photonvision.PhotonUtils; + +/** +* The VM is configured to automatically run this class, and to call the functions corresponding to +* each mode, as described in the TimedRobot documentation. If you change the name of this class or +* the package after creating this project, you must also update the build.gradle file in the +* project. +*/ +public class Robot extends TimedRobot { + // Constants such as camera and target height stored. Change per robot and goal! + final double CAMERA_HEIGHT_METERS = Units.inchesToMeters(24); + final double TARGET_HEIGHT_METERS = Units.feetToMeters(5); + // Angle between horizontal and the camera. + final double CAMERA_PITCH_RADIANS = Units.degreesToRadians(0); + + // How far from the target we want to be + final double GOAL_RANGE_METERS = Units.feetToMeters(3); + + // Change this to match the name of your camera + PhotonCamera camera = new PhotonCamera("photonvision"); + + // PID constants should be tuned per robot + final double LINEAR_P = 0.1; + final double LINEAR_D = 0.0; + PIDController forwardController = new PIDController(LINEAR_P, 0, LINEAR_D); + + final double ANGULAR_P = 0.1; + final double ANGULAR_D = 0.0; + PIDController turnController = new PIDController(ANGULAR_P, 0, ANGULAR_D); + + XboxController xboxController = new XboxController(0); + + // Drive motors + PWMVictorSPX leftMotor = new PWMVictorSPX(0); + PWMVictorSPX rightMotor = new PWMVictorSPX(1); + DifferentialDrive drive = new DifferentialDrive(leftMotor, rightMotor); + + @Override + public void teleopPeriodic() { + double forwardSpeed; + double rotationSpeed; + + if (xboxController.getAButton()) { + // Vision-alignment mode + // Query the latest result from PhotonVision + var result = camera.getLatestResult(); + + if (result.hasTargets()) { + // First calculate range + double range = + PhotonUtils.calculateDistanceToTargetMeters( + CAMERA_HEIGHT_METERS, + TARGET_HEIGHT_METERS, + CAMERA_PITCH_RADIANS, + result.getBestTarget().getPitch()); + + // Use this range as the measurement we give to the PID controller. + forwardSpeed = forwardController.calculate(range, GOAL_RANGE_METERS); + + // Also calculate angular power + rotationSpeed = turnController.calculate(result.getBestTarget().getYaw(), 0); + } else { + // If we have no targets, stay still. + forwardSpeed = 0; + rotationSpeed = 0; + } + } else { + // Manual Driver Mode + forwardSpeed = xboxController.getY(GenericHID.Hand.kRight); + rotationSpeed = xboxController.getX(GenericHID.Hand.kLeft); + } + + // Use our forward/turn speeds to control the drivetrain + drive.arcadeDrive(forwardSpeed, rotationSpeed); + } +} diff --git a/photonlib-java-examples/src/main/java/org/photonlib/examples/aimattarget/Main.java b/photonlib-java-examples/src/main/java/org/photonlib/examples/aimattarget/Main.java new file mode 100644 index 000000000..bdb793801 --- /dev/null +++ b/photonlib-java-examples/src/main/java/org/photonlib/examples/aimattarget/Main.java @@ -0,0 +1,38 @@ +/* + * Copyright (C) Photon Vision. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.photonlib.examples.aimattarget; + +import edu.wpi.first.wpilibj.RobotBase; + +/** +* Do NOT add any static variables to this class, or any initialization at all. Unless you know what +* you are doing, do not modify this file except to change the parameter class to the startRobot +* call. +*/ +public final class Main { + private Main() {} + + /** + * Main initialization function. Do not perform any initialization here. + * + *

If you change your main robot class, change the parameter type. + */ + public static void main(String... args) { + RobotBase.startRobot(Robot::new); + } +} diff --git a/photonlib-java-examples/src/main/java/org/photonlib/examples/aimattarget/Robot.java b/photonlib-java-examples/src/main/java/org/photonlib/examples/aimattarget/Robot.java new file mode 100644 index 000000000..50ad44b26 --- /dev/null +++ b/photonlib-java-examples/src/main/java/org/photonlib/examples/aimattarget/Robot.java @@ -0,0 +1,103 @@ +/* + * Copyright (C) Photon Vision. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.photonlib.examples.aimattarget; + +import edu.wpi.first.wpilibj.GenericHID; +import edu.wpi.first.wpilibj.PWMVictorSPX; +import edu.wpi.first.wpilibj.TimedRobot; +import edu.wpi.first.wpilibj.XboxController; +import edu.wpi.first.wpilibj.controller.PIDController; +import edu.wpi.first.wpilibj.drive.DifferentialDrive; +import edu.wpi.first.wpilibj.util.Units; +import org.photonvision.PhotonCamera; +import org.photonvision.PhotonUtils; + +/** +* The VM is configured to automatically run this class, and to call the functions corresponding to +* each mode, as described in the TimedRobot documentation. If you change the name of this class or +* the package after creating this project, you must also update the build.gradle file in the +* project. +*/ +public class Robot extends TimedRobot { + // Constants such as camera and target height stored. Change per robot and goal! + final double CAMERA_HEIGHT_METERS = Units.inchesToMeters(24); + final double TARGET_HEIGHT_METERS = Units.feetToMeters(5); + // Angle between horizontal and the camera. + final double CAMERA_PITCH_RADIANS = Units.degreesToRadians(0); + + // How far from the target we want to be + final double GOAL_RANGE_METERS = Units.feetToMeters(3); + + // Change this to match the name of your camera + PhotonCamera camera = new PhotonCamera("photonvision"); + + // PID constants should be tuned per robot + final double LINEAR_P = 0.1; + final double LINEAR_D = 0.0; + PIDController forwardController = new PIDController(LINEAR_P, 0, LINEAR_D); + + final double ANGULAR_P = 0.1; + final double ANGULAR_D = 0.0; + PIDController turnController = new PIDController(ANGULAR_P, 0, ANGULAR_D); + + XboxController xboxController = new XboxController(0); + + // Drive motors + PWMVictorSPX leftMotor = new PWMVictorSPX(0); + PWMVictorSPX rightMotor = new PWMVictorSPX(1); + DifferentialDrive drive = new DifferentialDrive(leftMotor, rightMotor); + + @Override + public void teleopPeriodic() { + double forwardSpeed; + double rotationSpeed; + + if (xboxController.getAButton()) { + // Vision-alignment mode + // Query the latest result from PhotonVision + var result = camera.getLatestResult(); + + if (result.hasTargets()) { + // First calculate range + double range = + PhotonUtils.calculateDistanceToTargetMeters( + CAMERA_HEIGHT_METERS, + TARGET_HEIGHT_METERS, + CAMERA_PITCH_RADIANS, + result.getBestTarget().getPitch()); + + // Use this range as the measurement we give to the PID controller. + forwardSpeed = forwardController.calculate(range, GOAL_RANGE_METERS); + + // Also calculate angular power + rotationSpeed = turnController.calculate(result.getBestTarget().getYaw(), 0); + } else { + // If we have no targets, stay still. + forwardSpeed = 0; + rotationSpeed = 0; + } + } else { + // Manual Driver Mode + forwardSpeed = xboxController.getY(GenericHID.Hand.kRight); + rotationSpeed = xboxController.getX(GenericHID.Hand.kLeft); + } + + // Use our forward/turn speeds to control the drivetrain + drive.arcadeDrive(forwardSpeed, rotationSpeed); + } +} diff --git a/photonlib-java-examples/src/main/java/org/photonlib/examples/examples.json b/photonlib-java-examples/src/main/java/org/photonlib/examples/examples.json new file mode 100644 index 000000000..21570e837 --- /dev/null +++ b/photonlib-java-examples/src/main/java/org/photonlib/examples/examples.json @@ -0,0 +1,38 @@ +[ + { + "name": "AimAtTarget", + "description": "Aim at a target", + "tags": [], + "gradlebase": "java", + "language": "java", + "commandversion": 1, + "mainclass": "Main", + "packagetoreplace": null, + "dependencies": [], + "foldername": "aimattarget" + }, + { + "name": "GetInRange", + "description": "Get in range of a target", + "tags": [], + "gradlebase": "java", + "language": "java", + "commandversion": 1, + "mainclass": "Main", + "packagetoreplace": null, + "dependencies": [], + "foldername": "getinrange" + }, + { + "name": "AimAndRange", + "description": "Aim at a target while at a desired range", + "tags": [], + "gradlebase": "java", + "language": "java", + "commandversion": 1, + "mainclass": "Main", + "packagetoreplace": null, + "dependencies": [], + "foldername": "aimandrange" + } +] \ No newline at end of file diff --git a/photonlib-java-examples/src/main/java/org/photonlib/examples/getinrange/Main.java b/photonlib-java-examples/src/main/java/org/photonlib/examples/getinrange/Main.java new file mode 100644 index 000000000..12e41d08e --- /dev/null +++ b/photonlib-java-examples/src/main/java/org/photonlib/examples/getinrange/Main.java @@ -0,0 +1,38 @@ +/* + * Copyright (C) Photon Vision. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.photonlib.examples.getinrange; + +import edu.wpi.first.wpilibj.RobotBase; + +/** +* Do NOT add any static variables to this class, or any initialization at all. Unless you know what +* you are doing, do not modify this file except to change the parameter class to the startRobot +* call. +*/ +public final class Main { + private Main() {} + + /** + * Main initialization function. Do not perform any initialization here. + * + *

If you change your main robot class, change the parameter type. + */ + public static void main(String... args) { + RobotBase.startRobot(Robot::new); + } +} diff --git a/photonlib-java-examples/src/main/java/org/photonlib/examples/getinrange/Robot.java b/photonlib-java-examples/src/main/java/org/photonlib/examples/getinrange/Robot.java new file mode 100644 index 000000000..ba7785925 --- /dev/null +++ b/photonlib-java-examples/src/main/java/org/photonlib/examples/getinrange/Robot.java @@ -0,0 +1,100 @@ +/* + * Copyright (C) Photon Vision. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.photonlib.examples.getinrange; + +import edu.wpi.first.wpilibj.GenericHID; +import edu.wpi.first.wpilibj.PWMVictorSPX; +import edu.wpi.first.wpilibj.TimedRobot; +import edu.wpi.first.wpilibj.XboxController; +import edu.wpi.first.wpilibj.controller.PIDController; +import edu.wpi.first.wpilibj.drive.DifferentialDrive; +import edu.wpi.first.wpilibj.util.Units; +import org.photonvision.PhotonCamera; +import org.photonvision.PhotonUtils; + +/** +* The VM is configured to automatically run this class, and to call the functions corresponding to +* each mode, as described in the TimedRobot documentation. If you change the name of this class or +* the package after creating this project, you must also update the build.gradle file in the +* project. +*/ +public class Robot extends TimedRobot { + // Constants such as camera and target height stored. Change per robot and goal! + final double CAMERA_HEIGHT_METERS = Units.inchesToMeters(24); + final double TARGET_HEIGHT_METERS = Units.feetToMeters(5); + + // Angle between horizontal and the camera. + final double CAMERA_PITCH_RADIANS = Units.degreesToRadians(0); + + // How far from the target we want to be + final double GOAL_RANGE_METERS = Units.feetToMeters(3); + + // Change this to match the name of your camera + PhotonCamera camera = new PhotonCamera("photonvision"); + + // PID constants should be tuned per robot + final double P_GAIN = 0.1; + final double D_GAIN = 0.0; + PIDController controller = new PIDController(P_GAIN, 0, D_GAIN); + + XboxController xboxController; + + // Drive motors + PWMVictorSPX leftMotor = new PWMVictorSPX(0); + PWMVictorSPX rightMotor = new PWMVictorSPX(1); + DifferentialDrive drive = new DifferentialDrive(leftMotor, rightMotor); + + @Override + public void robotInit() { + xboxController = new XboxController(0); + } + + @Override + public void teleopPeriodic() { + double forwardSpeed; + double rotationSpeed = xboxController.getX(GenericHID.Hand.kLeft); + + if (xboxController.getAButton()) { + // Vision-alignment mode + // Query the latest result from PhotonVision + var result = camera.getLatestResult(); + + if (result.hasTargets()) { + // First calculate range + double range = + PhotonUtils.calculateDistanceToTargetMeters( + CAMERA_HEIGHT_METERS, + TARGET_HEIGHT_METERS, + CAMERA_PITCH_RADIANS, + result.getBestTarget().getPitch()); + + // Use this range as the measurement we give to the PID controller. + forwardSpeed = controller.calculate(range, GOAL_RANGE_METERS); + } else { + // If we have no targets, stay still. + forwardSpeed = 0; + } + } else { + // Manual Driver Mode + forwardSpeed = xboxController.getY(GenericHID.Hand.kRight); + } + + // Use our forward/turn speeds to control the drivetrain + drive.arcadeDrive(forwardSpeed, rotationSpeed); + } +} diff --git a/settings.gradle b/settings.gradle index 1ccfe01f5..4e061e958 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1,2 +1,6 @@ +include 'photon-targeting' include 'photon-core' -include 'photon-server' \ No newline at end of file +include 'photon-server' +include 'photon-lib' +include 'photonlib-java-examples' +include 'photonlib-cpp-examples' diff --git a/versioningHelper.gradle b/versioningHelper.gradle new file mode 100644 index 000000000..d70670f73 --- /dev/null +++ b/versioningHelper.gradle @@ -0,0 +1,27 @@ +import java.time.LocalDateTime +import java.time.format.DateTimeFormatter +import java.nio.file.Path + +gradle.allprojects { + ext.getCurrentVersion = { -> + def stdout = new ByteArrayOutputStream() + String tagIsh + try { + exec { + commandLine 'git', 'describe', '--tags', '--exclude="Dev"' + standardOutput = stdout + } + tagIsh = stdout.toString().trim().toLowerCase() + } catch(Exception e) { + tagIsh = "dev-Unknown" + } + boolean isDev = tagIsh.matches(".*-[0-9]*-g[0-9a-f]*") + if(isDev) tagIsh = "dev-" + tagIsh + println("Picked up version: " + tagIsh) + return tagIsh + } + + if(!ext.has("versionString")) { + ext.versionString = getCurrentVersion() + } +}