mirror of
https://github.com/PhotonVision/photonvision
synced 2026-06-26 01:51:40 +00:00
Compare commits
32 Commits
v2025.0.0-
...
v2025.0.0-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e506ac6b28 | ||
|
|
88bc63cf82 | ||
|
|
a05542c06c | ||
|
|
ffc4e06ac6 | ||
|
|
81076375b8 | ||
|
|
66f369f3a9 | ||
|
|
7cba7b432d | ||
|
|
dd98d96d7e | ||
|
|
8ede892c14 | ||
|
|
08c62ab8cd | ||
|
|
e8efef476b | ||
|
|
c6403a65d2 | ||
|
|
6a8d638853 | ||
|
|
782929b006 | ||
|
|
4997ad9115 | ||
|
|
857a30d980 | ||
|
|
a40e69cca0 | ||
|
|
e069a79a32 | ||
|
|
d9dfe15bfe | ||
|
|
1dbd2e5990 | ||
|
|
7e9da4133d | ||
|
|
163b5c9c81 | ||
|
|
c6a3638a2f | ||
|
|
44f78cb03e | ||
|
|
61552ad6ca | ||
|
|
fa66ed866c | ||
|
|
08b4bd1f03 | ||
|
|
c536a1c312 | ||
|
|
adb18fe711 | ||
|
|
7d1e748b0e | ||
|
|
3a9d22c76b | ||
|
|
417e1a65b6 |
27
.github/workflows/build.yml
vendored
27
.github/workflows/build.yml
vendored
@@ -1,21 +1,14 @@
|
||||
name: Build
|
||||
|
||||
on:
|
||||
# Run on pushes to master and pushed tags, and on pull requests against master, but ignore the docs folder
|
||||
# Run on pushes to main and pushed tags, and on pull requests against main, but ignore the docs folder
|
||||
push:
|
||||
branches: [ master ]
|
||||
branches: [ main ]
|
||||
tags:
|
||||
- 'v*'
|
||||
paths:
|
||||
- '**'
|
||||
- '!docs/**'
|
||||
- '.github/**'
|
||||
pull_request:
|
||||
branches: [ master ]
|
||||
paths:
|
||||
- '**'
|
||||
- '!docs/**'
|
||||
- '.github/**'
|
||||
branches: [ main ]
|
||||
merge_group:
|
||||
|
||||
jobs:
|
||||
build-client:
|
||||
@@ -212,7 +205,7 @@ jobs:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- container: wpilib/roborio-cross-ubuntu:2024-22.04
|
||||
- container: wpilib/roborio-cross-ubuntu:2025-24.04
|
||||
artifact-name: Athena
|
||||
build-options: "-Ponlylinuxathena"
|
||||
- container: wpilib/raspbian-cross-ubuntu:bullseye-22.04
|
||||
@@ -478,6 +471,12 @@ jobs:
|
||||
image_url: https://github.com/PhotonVision/photon-image-modifier/releases/download/v2025.0.0-beta-6/photonvision_opi5max.img.xz
|
||||
cpu: cortex-a8
|
||||
image_additional_mb: 1024
|
||||
- os: ubuntu-22.04
|
||||
artifact-name: LinuxArm64
|
||||
image_suffix: rock5c
|
||||
image_url: https://github.com/PhotonVision/photon-image-modifier/releases/download/v2025.0.0-beta-7/photonvision_rock5c.img.xz
|
||||
cpu: cortex-a8
|
||||
image_additional_mb: 1024
|
||||
|
||||
runs-on: ${{ matrix.os }}
|
||||
name: "Build image - ${{ matrix.image_url }}"
|
||||
@@ -558,14 +557,14 @@ jobs:
|
||||
- uses: softprops/action-gh-release@v2.0.9
|
||||
with:
|
||||
files: |
|
||||
**/*orangepi5*.xz
|
||||
**/@(*orangepi5*|*rock5*).xz
|
||||
if: startsWith(github.ref, 'refs/tags/v')
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
- uses: softprops/action-gh-release@v2.0.9
|
||||
with:
|
||||
files: |
|
||||
**/!(*orangepi5*).xz
|
||||
**/!(*orangepi5*|*rock5*).xz
|
||||
**/*.jar
|
||||
**/photonlib*.json
|
||||
**/photonlib*.zip
|
||||
|
||||
21
.github/workflows/lint-format.yml
vendored
21
.github/workflows/lint-format.yml
vendored
@@ -1,21 +1,14 @@
|
||||
name: Lint and Format
|
||||
|
||||
on:
|
||||
# Run on pushes to master and pushed tags, and on pull requests against master, but ignore the docs folder
|
||||
# Run on pushes to main and pushed tags, and on pull requests against main, but ignore the docs folder
|
||||
push:
|
||||
branches: [ master ]
|
||||
branches: [ main ]
|
||||
tags:
|
||||
- 'v*'
|
||||
paths:
|
||||
- '**'
|
||||
- '!docs/**'
|
||||
- '.github/**'
|
||||
pull_request:
|
||||
branches: [ master ]
|
||||
paths:
|
||||
- '**'
|
||||
- '!docs/**'
|
||||
- '.github/**'
|
||||
branches: [ main ]
|
||||
merge_group:
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.head_ref || github.ref }}
|
||||
@@ -31,7 +24,7 @@ jobs:
|
||||
run: |
|
||||
git fetch --prune --unshallow
|
||||
git checkout -b pr
|
||||
git branch -f master origin/master
|
||||
git branch -f main origin/main
|
||||
- name: Set up Python 3.8
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
@@ -91,6 +84,6 @@ jobs:
|
||||
run: |
|
||||
git fetch --prune --unshallow
|
||||
git checkout -b pr
|
||||
git branch -f master origin/master
|
||||
git branch -f main origin/main
|
||||
- name: Check index.html not changed
|
||||
run: git --no-pager diff --exit-code origin/master photon-server/src/main/resources/web/index.html
|
||||
run: git --no-pager diff --exit-code origin/main photon-server/src/main/resources/web/index.html
|
||||
|
||||
17
.github/workflows/photon-code-docs.yml
vendored
17
.github/workflows/photon-code-docs.yml
vendored
@@ -1,21 +1,14 @@
|
||||
name: Photon Code Documentation
|
||||
|
||||
on:
|
||||
# Run on pushes to master and pushed tags, and on pull requests against master, but ignore the docs folder
|
||||
# Run on pushes to main and pushed tags, and on pull requests against main, but ignore the docs folder
|
||||
push:
|
||||
branches: [ master ]
|
||||
branches: [ main ]
|
||||
tags:
|
||||
- 'v*'
|
||||
paths:
|
||||
- '**'
|
||||
- '!docs/**'
|
||||
- '.github/**'
|
||||
pull_request:
|
||||
branches: [ master ]
|
||||
paths:
|
||||
- '**'
|
||||
- '!docs/**'
|
||||
- '.github/**'
|
||||
branches: [ main ]
|
||||
merge_group:
|
||||
|
||||
# Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages
|
||||
permissions:
|
||||
@@ -85,7 +78,7 @@ jobs:
|
||||
|
||||
- run: find .
|
||||
- name: copy file via ssh password
|
||||
if: github.ref == 'refs/heads/master'
|
||||
if: github.ref == 'refs/heads/main'
|
||||
uses: appleboy/scp-action@v0.1.7
|
||||
with:
|
||||
host: ${{ secrets.WEBMASTER_SSH_HOST }}
|
||||
|
||||
7
.github/workflows/photonvision-docs.yml
vendored
7
.github/workflows/photonvision-docs.yml
vendored
@@ -3,14 +3,9 @@ name: PhotonVision Sphinx Documentation Checks
|
||||
on:
|
||||
push:
|
||||
branches: [ master ]
|
||||
paths:
|
||||
- 'docs/**'
|
||||
- '.github/**'
|
||||
pull_request:
|
||||
branches: [ master ]
|
||||
paths:
|
||||
- 'docs/**'
|
||||
- '.github/**'
|
||||
merge_group:
|
||||
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
9
.github/workflows/python.yml
vendored
9
.github/workflows/python.yml
vendored
@@ -8,16 +8,9 @@ on:
|
||||
branches: [ master ]
|
||||
tags:
|
||||
- 'v*'
|
||||
paths:
|
||||
- '**'
|
||||
- '!docs/**'
|
||||
- '.github/**'
|
||||
pull_request:
|
||||
branches: [ master ]
|
||||
paths:
|
||||
- '**'
|
||||
- '!docs/**'
|
||||
- '.github/**'
|
||||
merge_group:
|
||||
|
||||
jobs:
|
||||
buildAndDeploy:
|
||||
|
||||
@@ -20,6 +20,8 @@ modifiableFileExclude {
|
||||
\.ico$
|
||||
\.rknn$
|
||||
gradlew
|
||||
photon-lib/py/photonlibpy/generated/
|
||||
photon-targeting/src/generated/
|
||||
}
|
||||
|
||||
includeProject {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# PhotonVision
|
||||
|
||||
[](https://github.com/PhotonVision/photonvision/actions?query=workflow%3ACI) [](https://codecov.io/gh/PhotonVision/photonvision) [](https://discord.gg/wYxTwym)
|
||||
[](https://github.com/PhotonVision/photonvision/actions?query=workflow%3ACI) [](https://codecov.io/gh/PhotonVision/photonvision) [](https://discord.gg/wYxTwym)
|
||||
|
||||
PhotonVision is the free, fast, and easy-to-use computer vision solution for the *FIRST* Robotics Competition. You can read an overview of our features [on our website](https://photonvision.org). You can find our comprehensive documentation [here](https://docs.photonvision.org).
|
||||
|
||||
@@ -17,7 +17,7 @@ If you are interested in contributing code or documentation to the project, plea
|
||||
## Documentation
|
||||
|
||||
- Our main documentation page: [docs.photonvision.org](https://docs.photonvision.org)
|
||||
- Photon UI demo: [demo.photonvision.org](https://demo.photonvision.org) (or [manual link](https://photonvision.github.io/photonvision/built-client/))
|
||||
- Photon UI demo: [http://photonvision.global/](http://photonvision.global/) (or [manual link](https://photonvision.github.io/photonvision/built-client/))
|
||||
- Javadocs: [javadocs.photonvision.org](https://javadocs.photonvision.org) (or [manual link](https://photonvision.github.io/photonvision/built-docs/javadoc/))
|
||||
- C++ Doxygen [cppdocs.photonvision.org](https://cppdocs.photonvision.org) (or [manual link](https://photonvision.github.io/photonvision/built-docs/doxygen/html/))
|
||||
|
||||
@@ -67,7 +67,7 @@ sudo apt install libcholmod3 liblapack3 libsuitesparseconfig5
|
||||
|
||||
PhotonVision was forked from [Chameleon Vision](https://github.com/Chameleon-Vision/chameleon-vision/). Thank you to everyone who worked on the original project.
|
||||
|
||||
* [WPILib](https://github.com/wpilibsuite) - Specifically [cscore](https://github.com/wpilibsuite/allwpilib/tree/master/cscore), [CameraServer](https://github.com/wpilibsuite/allwpilib/tree/master/cameraserver), [NTCore](https://github.com/wpilibsuite/allwpilib/tree/master/ntcore), and [OpenCV](https://github.com/wpilibsuite/thirdparty-opencv).
|
||||
* [WPILib](https://github.com/wpilibsuite) - Specifically [cscore](https://github.com/wpilibsuite/allwpilib/tree/main/cscore), [CameraServer](https://github.com/wpilibsuite/allwpilib/tree/main/cameraserver), [NTCore](https://github.com/wpilibsuite/allwpilib/tree/main/ntcore), and [OpenCV](https://github.com/wpilibsuite/thirdparty-opencv).
|
||||
|
||||
* [Apache Commons](https://commons.apache.org/) - Specifically [Commons Math](https://commons.apache.org/proper/commons-math/), and [Commons Lang](https://commons.apache.org/proper/commons-lang/)
|
||||
|
||||
|
||||
16
build.gradle
16
build.gradle
@@ -5,7 +5,7 @@ plugins {
|
||||
id "cpp"
|
||||
id "com.diffplug.spotless" version "6.24.0"
|
||||
id "edu.wpi.first.wpilib.repositories.WPILibRepositoriesPlugin" version "2020.2"
|
||||
id "edu.wpi.first.GradleRIO" version "2025.1.1-beta-1"
|
||||
id "edu.wpi.first.GradleRIO" version "2025.1.1-beta-3"
|
||||
id 'edu.wpi.first.WpilibTools' version '1.3.0'
|
||||
id 'com.google.protobuf' version '0.9.3' apply false
|
||||
id 'edu.wpi.first.GradleJni' version '1.1.0'
|
||||
@@ -30,16 +30,16 @@ ext.allOutputsFolder = file("$project.buildDir/outputs")
|
||||
apply from: "versioningHelper.gradle"
|
||||
|
||||
ext {
|
||||
wpilibVersion = "2025.1.1-beta-1"
|
||||
wpilibVersion = "2025.1.1-beta-3"
|
||||
wpimathVersion = wpilibVersion
|
||||
openCVYear = "2024"
|
||||
openCVversion = "4.8.0-4"
|
||||
openCVYear = "2025"
|
||||
openCVversion = "4.10.0-3"
|
||||
joglVersion = "2.4.0"
|
||||
javalinVersion = "5.6.2"
|
||||
libcameraDriverVersion = "dev-v2023.1.0-15-gc8988b3"
|
||||
rknnVersion = "dev-v2024.0.1-4-g0db16ac"
|
||||
libcameraDriverVersion = "v2025.0.0"
|
||||
rknnVersion = "v2025.0.0"
|
||||
frcYear = "2025"
|
||||
mrcalVersion = "dev-v2024.0.0-27-g41d7868";
|
||||
mrcalVersion = "v2025.0.0";
|
||||
|
||||
|
||||
pubVersion = versionString
|
||||
@@ -109,7 +109,7 @@ spotless {
|
||||
}
|
||||
|
||||
wrapper {
|
||||
gradleVersion '8.4'
|
||||
gradleVersion '8.11'
|
||||
}
|
||||
|
||||
ext.getCurrentArch = {
|
||||
|
||||
@@ -45,7 +45,7 @@ extensions = [
|
||||
|
||||
ogp_site_url = "https://docs.photonvision.org/en/latest/"
|
||||
ogp_site_name = "PhotonVision Documentation"
|
||||
ogp_image = "https://raw.githubusercontent.com/PhotonVision/photonvision-docs/master/source/assets/RectLogo.png"
|
||||
ogp_image = "https://raw.githubusercontent.com/PhotonVision/photonvision-docs/main/source/assets/RectLogo.png"
|
||||
|
||||
# Add any paths that contain templates here, relative to this directory.
|
||||
templates_path = ["_templates"]
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
# Installing Pre-Release Versions
|
||||
|
||||
Pre-release/development version of PhotonVision can be tested by installing/downloading artifacts from Github Actions (see below), which are built automatically on commits to open pull requests and to PhotonVision's `master` branch, or by {ref}`compiling PhotonVision locally <docs/contributing/building-photon:Build Instructions>`.
|
||||
Pre-release/development version of PhotonVision can be tested by installing/downloading artifacts from Github Actions (see below), which are built automatically on commits to open pull requests and to PhotonVision's `main` branch, or by {ref}`compiling PhotonVision locally <docs/contributing/building-photon:Build Instructions>`.
|
||||
|
||||
:::{warning}
|
||||
If testing a pre-release version of PhotonVision with a robot, PhotonLib must be updated to match the version downloaded! If not, packet schema definitions may not match and unexpected things will occur. To update PhotonLib, refer to {ref}`installing specific version of PhotonLib<docs/programming/photonlib/adding-vendordep:Install Specific Version - Java/C++>`.
|
||||
:::
|
||||
|
||||
GitHub Actions builds pre-release version of PhotonVision automatically on PRs and on each commit merged to master. To test a particular commit to master, navigate to the [PhotonVision commit list](https://github.com/PhotonVision/photonvision/commits/master/) and click on the check mark (below). Scroll to "Build / Build fat JAR - PLATFORM", click details, and then summary. From here, JAR and image files can be downloaded to be flashed or uploaded using "Offline Update".
|
||||
GitHub Actions builds pre-release version of PhotonVision automatically on PRs and on each commit merged to main. To test a particular commit to main, navigate to the [PhotonVision commit list](https://github.com/PhotonVision/photonvision/commits/main/) and click on the check mark (below). Scroll to "Build / Build fat JAR - PLATFORM", click details, and then summary. From here, JAR and image files can be downloaded to be flashed or uploaded using "Offline Update".
|
||||
|
||||
```{image} images/gh_actions_1.png
|
||||
:alt: Github Actions Badge
|
||||
|
||||
@@ -8,14 +8,14 @@ You do not need to install PhotonVision on a Windows PC in order to access the w
|
||||
|
||||
## Installing Java
|
||||
|
||||
PhotonVision requires a JDK installed and on the system path. JDK 11 is needed (different versions will not work). If you don't have JDK 11 already, run the following to install it:
|
||||
PhotonVision requires a JDK installed and on the system path. JDK 17 is needed (different versions will not work). If you don't have JDK 17 already, run the following to install it:
|
||||
|
||||
```
|
||||
$ sudo apt-get install openjdk-11-jdk
|
||||
$ sudo apt-get install openjdk-17-jdk
|
||||
```
|
||||
|
||||
:::{warning}
|
||||
Using a JDK other than JDK11 will cause issues when running PhotonVision and is not supported.
|
||||
Using a JDK other than JDK17 will cause issues when running PhotonVision and is not supported.
|
||||
:::
|
||||
|
||||
## Downloading the Latest Stable Release of PhotonVision
|
||||
|
||||
@@ -5,17 +5,17 @@ Due to current [cscore](https://github.com/wpilibsuite/allwpilib/tree/main/cscor
|
||||
:::
|
||||
|
||||
:::{note}
|
||||
You do not need to install PhotonVision on a Windows PC in order to access the webdashboard (assuming you are using an external coprocessor like a Raspberry Pi).
|
||||
You do not need to install PhotonVision on a Mac in order to access the webdashboard (assuming you are using an external coprocessor like a Raspberry Pi).
|
||||
:::
|
||||
|
||||
VERY Limited macOS support is available.
|
||||
|
||||
## Installing Java
|
||||
|
||||
PhotonVision requires a JDK installed and on the system path. JDK 11 is needed (different versions will not work). You may already have this if you have installed WPILib. If not, [download and install it from here](https://adoptium.net/temurin/releases?version=11).
|
||||
PhotonVision requires a JDK installed and on the system path. JDK 17 is needed (different versions will not work). You may already have this if you have installed WPILib 2025+. If not, [download and install it from here](https://adoptium.net/temurin/releases?version=17).
|
||||
|
||||
:::{warning}
|
||||
Using a JDK other than JDK11 will cause issues when running PhotonVision and is not supported.
|
||||
Using a JDK other than JDK17 will cause issues when running PhotonVision and is not supported.
|
||||
:::
|
||||
|
||||
## Downloading the Latest Stable Release of PhotonVision
|
||||
|
||||
@@ -12,10 +12,14 @@ Bonjour provides more stable networking when using Windows PCs. Install [Bonjour
|
||||
|
||||
## Installing Java
|
||||
|
||||
PhotonVision requires a JDK installed and on the system path. **JDK 11 is needed** (different versions will not work). You may already have this if you have installed WPILib, but ensure that running `java -version` shows JDK 11. If not, [download and install it from here](https://adoptium.net/temurin/releases?version=11) and ensure that the new JDK is being used.
|
||||
PhotonVision requires a JDK installed and on the system path. **JDK 17 is needed. Windows Users must use the JDK that ships with WPILib.** [Download and install it from here.](https://github.com/wpilibsuite/allwpilib/releases/tag/v2025.1.1-beta-3) Either ensure the only Java on your PATH is the WPILIB Java or specify it to gradle with `-Dorg.gradle.java.home=C:\Users\Public\wpilib\2025\jdk`:
|
||||
|
||||
```
|
||||
> ./gradlew run "-Dorg.gradle.java.home=C:\Users\Public\wpilib\2025\jdk"
|
||||
```
|
||||
|
||||
:::{warning}
|
||||
Using a JDK other than JDK11 will cause issues when running PhotonVision and is not supported.
|
||||
Using a JDK other than WPILIB's JDK17 will cause issues when running PhotonVision and is not supported.
|
||||
:::
|
||||
|
||||
## Downloading the Latest Stable Release of PhotonVision
|
||||
|
||||
@@ -21,7 +21,7 @@ AprilTag pipelines come with reasonable defaults to get you up and running with
|
||||
|
||||
### Target Family
|
||||
|
||||
Target families are defined by two numbers (before and after the h). The first number is the number of bits the tag is able to encode (which means more tags are available in the respective family) and the second is the hamming distance. Hamming distance describes the ability for error correction while identifying tag ids. A high hamming distance generally means that it will be easier for a tag to be identified even if there are errors. However, as hamming distance increases, the number of available tags decreases. The 2024 FRC game will be using 36h11 tags, which can be found [here](https://github.com/AprilRobotics/apriltag-imgs/tree/master/tag36h11).
|
||||
Target families are defined by two numbers (before and after the h). The first number is the number of bits the tag is able to encode (which means more tags are available in the respective family) and the second is the hamming distance. Hamming distance describes the ability for error correction while identifying tag ids. A high hamming distance generally means that it will be easier for a tag to be identified even if there are errors. However, as hamming distance increases, the number of available tags decreases. The 2024 FRC game will be using 36h11 tags, which can be found [here](https://github.com/AprilRobotics/apriltag-imgs/tree/main/tag36h11).
|
||||
|
||||
### Decimate
|
||||
|
||||
|
||||
@@ -83,7 +83,7 @@ More info on what these parameters mean can be found in [OpenCV's docs](https://
|
||||
|
||||
Below these outputs are the snapshots collected for calibration, along with a per-snapshot mean reprojection error. A snapshot with a larger reprojection error might indicate a bad snapshot, due to effects such as motion blur or misidentified chessboard corners.
|
||||
|
||||
Calibration images can also be extracted from the downloaded JSON file using [this Python script](https://raw.githubusercontent.com/PhotonVision/photonvision/master/devTools/calibrationUtils.py). This script will unpack calibration images, and also generate a VNL file for use [with mrcal](https://mrcal.secretsauce.net/).
|
||||
Calibration images can also be extracted from the downloaded JSON file using [this Python script](https://raw.githubusercontent.com/PhotonVision/photonvision/main/devTools/calibrationUtils.py). This script will unpack calibration images, and also generate a VNL file for use [with mrcal](https://mrcal.secretsauce.net/).
|
||||
|
||||
```
|
||||
python3 /path/to/calibrationUtils.py path/to/photon_calibration.json /path/to/output/folder
|
||||
|
||||
BIN
docs/source/docs/contributing/assets/vscode-gradle-args.png
Normal file
BIN
docs/source/docs/contributing/assets/vscode-gradle-args.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 87 KiB |
BIN
docs/source/docs/contributing/assets/vscode-gradle-tests.png
Normal file
BIN
docs/source/docs/contributing/assets/vscode-gradle-tests.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 42 KiB |
BIN
docs/source/docs/contributing/assets/vscode-runner-tests.png
Normal file
BIN
docs/source/docs/contributing/assets/vscode-runner-tests.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 334 KiB |
@@ -167,30 +167,31 @@ repositories {
|
||||
}
|
||||
```
|
||||
|
||||
### VSCode Test Runner Extension
|
||||
|
||||
With the VSCode [Extension Pack for Java](https://marketplace.visualstudio.com/items?itemName=vscjava.vscode-java-pack), you can get the Test Runner for Java and Gradle for Java extensions. This lets you easily run specific tests through the IDE:
|
||||
|
||||
```{image} assets/vscode-runner-tests.png
|
||||
:alt: An image showing how unit tests can be ran in VSCode through the Test Runner for Java extension.
|
||||
```
|
||||
|
||||
To correctly run PhotonVision tests this way, you must [delegate the tests to Gradle](https://code.visualstudio.com/docs/java/java-build#_delegate-tests-to-gradle). Debugging tests like this will [**not** currently](https://github.com/microsoft/build-server-for-gradle/issues/119) collect outputs.
|
||||
|
||||
### Debugging PhotonVision Running Locally
|
||||
|
||||
One way is by running the program using gradle with the {code}`--debug-jvm` flag. Run the program with {code}`./gradlew run --debug-jvm`, and attach to it with VSCode by adding the following to {code}`launch.json`. Note args can be passed with {code}`--args="foobar"`.
|
||||
Unit tests can instead be debugged through the ``test`` Gradle task for a specific subproject in VSCode, found in the Gradle tab:
|
||||
|
||||
```
|
||||
{
|
||||
// Use IntelliSense to learn about possible attributes.
|
||||
// Hover to view descriptions of existing attributes.
|
||||
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"type": "java",
|
||||
"name": "Attach to Remote Program",
|
||||
"request": "attach",
|
||||
"hostName": "localhost",
|
||||
"port": "5005",
|
||||
"projectName": "photon-core",
|
||||
}
|
||||
]
|
||||
}
|
||||
```{image} assets/vscode-gradle-tests.png
|
||||
:alt: An image showing how unit tests can be debugged in VSCode through the Gradle for Java extension.
|
||||
```
|
||||
|
||||
PhotonVision can also be run using the gradle tasks plugin with {code}`"args": "--debug-jvm"` added to launch.json.
|
||||
However, this will run all tests in a subproject.
|
||||
|
||||
Similarly, a local instance of PhotonVision can be debugged in the same way using the Gradle ``run`` task. In both cases, additional arguments can be specified:
|
||||
|
||||
```{image} assets/vscode-gradle-args.png
|
||||
:alt: An image showing how VSCode gradle tasks can specify additional arguments.
|
||||
```
|
||||
|
||||
### Debugging PhotonVision Running on a CoProcessor
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Combining Aiming and Getting in Range
|
||||
|
||||
The following example is from the PhotonLib example repository ([Java](https://github.com/PhotonVision/photonvision/tree/master/photonlib-java-examples/aimandrange)/[C++](https://github.com/PhotonVision/photonvision/tree/master/photonlib-cpp-examples/aimandrange)).
|
||||
The following example is from the PhotonLib example repository ([Java](https://github.com/PhotonVision/photonvision/tree/main/photonlib-java-examples/aimandrange)/[C++](https://github.com/PhotonVision/photonvision/tree/main/photonlib-cpp-examples/aimandrange)).
|
||||
|
||||
## Knowledge and Equipment Needed
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Aiming at a Target
|
||||
|
||||
The following example is from the PhotonLib example repository ([Java](https://github.com/PhotonVision/photonvision/tree/master/photonlib-java-examples/aimattarget)).
|
||||
The following example is from the PhotonLib example repository ([Java](https://github.com/PhotonVision/photonvision/tree/main/photonlib-java-examples/aimattarget)).
|
||||
|
||||
## Knowledge and Equipment Needed
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Using WPILib Pose Estimation, Simulation, and PhotonVision Together
|
||||
|
||||
The following example comes from the PhotonLib example repository ([Java](https://github.com/PhotonVision/photonvision/tree/master/photonlib-java-examples/poseest)/[C++](https://github.com/PhotonVision/photonvision/tree/master/photonlib-cpp-examples/poseest)/[Python](https://github.com/PhotonVision/photonvision/tree/master/photonlib-python-examples/poseest)). Full code is available at that links.
|
||||
The following example comes from the PhotonLib example repository ([Java](https://github.com/PhotonVision/photonvision/tree/main/photonlib-java-examples/poseest)/[C++](https://github.com/PhotonVision/photonvision/tree/main/photonlib-cpp-examples/poseest)/[Python](https://github.com/PhotonVision/photonvision/tree/main/photonlib-python-examples/poseest)). Full code is available at that links.
|
||||
|
||||
## Knowledge and Equipment Needed
|
||||
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
# Selecting Hardware
|
||||
|
||||
:::{note}
|
||||
It is highly recommended that you read the {ref}`quick start guide<docs/quick-start/common-setups:Common Hardware Setups>`, and use the hardware recommended there that
|
||||
is not touched on here.
|
||||
:::
|
||||
|
||||
In order to use PhotonVision, you need a coprocessor and a camera. Other than the recommended hardware found in the {ref}`quick start guide<docs/quick-start/common-setups:Common Hardware Setups>`, this page will help you select hardware that should work for photonvision even though it is not supported/recommended.
|
||||
|
||||
## Choosing a Coprocessor
|
||||
|
||||
@@ -47,6 +47,6 @@ Coming soon!
|
||||
PhotonVision currently ONLY supports YOLOv5 models trained and converted to `.rknn` format for RK3588 CPUs! Other models require different post-processing code and will NOT work. The model conversion process is also highly particular. Proceed with care.
|
||||
:::
|
||||
|
||||
Our [pre-trained NOTE model](https://github.com/PhotonVision/photonvision/blob/master/photon-server/src/main/resources/models/note-640-640-yolov5s.rknn) is automatically extracted from the JAR when PhotonVision starts, only if a file named “note-640-640-yolov5s.rknn” and "labels.txt" does not exist in the folder `photonvision_config/models/`. This technically allows power users to replace the model and label files with new ones without rebuilding Photon from source and uploading a new JAR.
|
||||
Our [pre-trained NOTE model](https://github.com/PhotonVision/photonvision/blob/main/photon-server/src/main/resources/models/note-640-640-yolov5s.rknn) is automatically extracted from the JAR when PhotonVision starts, only if a file named “note-640-640-yolov5s.rknn” and "labels.txt" does not exist in the folder `photonvision_config/models/`. This technically allows power users to replace the model and label files with new ones without rebuilding Photon from source and uploading a new JAR.
|
||||
|
||||
Use a program like WinSCP or FileZilla to access your coprocessor's filesystem, and copy the new `.rknn` model file into /home/pi. Next, SSH into the coprocessor and `sudo mv /path/to/new/model.rknn /opt/photonvision/photonvision_config/models/note-640-640-yolov5s.rknn`. Repeat this process with the labels file, which should contain one line per label the model outputs with no training newline. Next, restart PhotonVision via the web UI.
|
||||
|
||||
@@ -60,7 +60,7 @@ Use the `getLatestResult()`/`GetLatestResult()` (Java and C++ respectively) to o
|
||||
```
|
||||
|
||||
:::{note}
|
||||
Unlike other vision software solutions, using the latest result guarantees that all information is from the same timestamp. This is achievable because the PhotonVision backend sends a byte-packed string of data which is then deserialized by PhotonLib to get target data. For more information, check out the [PhotonLib source code](https://github.com/PhotonVision/photonvision/tree/master/photon-lib).
|
||||
Unlike other vision software solutions, using the latest result guarantees that all information is from the same timestamp. This is achievable because the PhotonVision backend sends a byte-packed string of data which is then deserialized by PhotonLib to get target data. For more information, check out the [PhotonLib source code](https://github.com/PhotonVision/photonvision/tree/main/photon-lib).
|
||||
:::
|
||||
|
||||
## Checking for Existence of Targets
|
||||
|
||||
@@ -8,7 +8,7 @@ If you’re not using cameras in 3D mode, calibration is optional, but it can st
|
||||
|
||||
## Print the Calibration Target
|
||||
|
||||
- Downloaded from our [demo site](https://demo.photonvision.org/#/cameras), or directly from your coprocessors cameras tab.
|
||||
- Downloaded from our [demo site](http://photonvision.global/#/cameras), or directly from your coprocessors cameras tab.
|
||||
- Use the Charuco calibration board:
|
||||
- Board Type: Charuco
|
||||
- Tag Family: 4x4
|
||||
|
||||
@@ -13,6 +13,11 @@ The Orange Pi 5 is the only currently supported device for object detection.
|
||||
|
||||
## SD Cards
|
||||
|
||||
:::{important}
|
||||
It is highly recommended that you use an industrial micro SD card, as they offer far greater protection against corruption from improper shutdowns, like most cards
|
||||
face every time the robot is turned off.
|
||||
:::
|
||||
|
||||
- 8GB or larger micro SD card
|
||||
- Many teams have found that an industrial micro sd card are much more stable in competition. One example is the SanDisk industrial 16GB micro SD card.
|
||||
|
||||
|
||||
@@ -88,4 +88,4 @@ The address in the code above (`photonvision.local`) is the hostname of the copr
|
||||
|
||||
## Camera Stream Ports
|
||||
|
||||
The camera streams start at they begin at 1181 with two ports for each camera (ex. 1181 and 1182 for camera one, 1183 and 1184 for camera two, etc.). The easiest way to identify the port of the camera that you want is by double clicking on the stream, which opens it in a separate page. The port will be listed below the stream.
|
||||
The camera streams start at 1181 with two ports for each camera (ex. 1181 and 1182 for camera one, 1183 and 1184 for camera two, etc.). The easiest way to identify the port of the camera that you want is by double clicking on the stream, which opens it in a separate page. The port will be listed below the stream.
|
||||
|
||||
@@ -8,7 +8,7 @@ In order for photonvision to connect to the roborio it needs to know your team n
|
||||
|
||||
### Camera Nickname
|
||||
|
||||
You **must** nickname your cameras in photonvision to ensure that every camera has a unique name. This is how we will identify cameras in robot code. The camera can be nickname using the edit button next to the camera name in the upper right of the Dashboard tab.
|
||||
You **must** nickname your cameras in PhotonVision to ensure that every camera has a unique name. This is how you will identify cameras in robot code. The camera can be nicknamed using the edit button next to the camera name in the upper right of the Dashboard tab.
|
||||
|
||||
```{image} images/editCameraName.png
|
||||
:align: center
|
||||
@@ -38,7 +38,7 @@ When detecting AprilTags, it's important to minimize 'motion blur' as much as po
|
||||
- Fixes
|
||||
- Lower your exposure as low as possible. Using gain and brightness to account for lack of brightness.
|
||||
- Other Options:
|
||||
- Don't use/rely vision measurements while moving.
|
||||
- Don't use/rely on vision measurements while moving.
|
||||
|
||||
```{image} images/motionblur.png
|
||||
:align: center
|
||||
@@ -51,7 +51,7 @@ When using an Orange Pi 5 with an OV9782 teams will usually change the following
|
||||
- Resolution:
|
||||
- Resolutions higher than 640x640 may not result in any higher detection accuracy and may lower {ref}`performance<docs/objectDetection/about-object-detection:Letterboxing>`.
|
||||
- Confidence:
|
||||
- 0.75 - 0.95 Lower values are fpr detecting warn game pieces or less ideal game pieces. Higher for less warn, more ideal game pieces.
|
||||
- 0.75 - 0.95 Lower values are for detecting worn game pieces or less ideal game pieces. Higher for less worn, more ideal game pieces.
|
||||
- White Balance Temperature:
|
||||
- Adjust this to achieve better color accuracy. This may be needed to increase confidence.
|
||||
- Set arducam specific camera type selector to OV9782
|
||||
|
||||
@@ -29,7 +29,7 @@ Limelights have a different installation processes. Simply connect the limelight
|
||||
Unless otherwise noted in release notes or if updating from the prior years version, to update PhotonVision after the initial installation, use the offline update option in the settings page with the downloaded jar file from the latest release.
|
||||
|
||||
:::{note}
|
||||
Limelight 2, 2+, and 3 will need a [custom hardware config file](https://github.com/PhotonVision/photonvision/tree/master/docs/source/docs/advanced-installation/sw_install/files) for lighting to work. Currently only limelight 2 and 2+ files are available.
|
||||
Limelight 2, 2+, and 3 will need a [custom hardware config file](https://github.com/PhotonVision/photonvision/tree/main/docs/source/docs/advanced-installation/sw_install/files) for lighting to work. Currently only limelight 2 and 2+ files are available.
|
||||
:::
|
||||
|
||||
:::{note}
|
||||
|
||||
@@ -6,3 +6,4 @@ org.gradle.jvmargs= \
|
||||
--add-exports jdk.compiler/com.sun.tools.javac.parser=ALL-UNNAMED \
|
||||
--add-exports jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED \
|
||||
--add-exports jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED
|
||||
org.ysb33r.gradle.doxygen.download.url=https://frcmaven.wpi.edu/artifactory/generic-release-mirror/doxygen
|
||||
|
||||
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
Binary file not shown.
2
gradle/wrapper/gradle-wrapper.properties
vendored
2
gradle/wrapper/gradle-wrapper.properties
vendored
@@ -1,6 +1,6 @@
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=permwrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.4-bin.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.11-bin.zip
|
||||
networkTimeout=10000
|
||||
validateDistributionUrl=true
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
|
||||
7
gradlew
vendored
7
gradlew
vendored
@@ -15,6 +15,8 @@
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
#
|
||||
|
||||
##############################################################################
|
||||
#
|
||||
@@ -55,7 +57,7 @@
|
||||
# Darwin, MinGW, and NonStop.
|
||||
#
|
||||
# (3) This script is generated from the Groovy template
|
||||
# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
|
||||
# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
|
||||
# within the Gradle project.
|
||||
#
|
||||
# You can find Gradle at https://github.com/gradle/gradle/.
|
||||
@@ -84,7 +86,8 @@ done
|
||||
# shellcheck disable=SC2034
|
||||
APP_BASE_NAME=${0##*/}
|
||||
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
|
||||
APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit
|
||||
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s
|
||||
' "$PWD" ) || exit
|
||||
|
||||
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
||||
MAX_FD=maximum
|
||||
|
||||
22
gradlew.bat
vendored
22
gradlew.bat
vendored
@@ -13,6 +13,8 @@
|
||||
@rem See the License for the specific language governing permissions and
|
||||
@rem limitations under the License.
|
||||
@rem
|
||||
@rem SPDX-License-Identifier: Apache-2.0
|
||||
@rem
|
||||
|
||||
@if "%DEBUG%"=="" @echo off
|
||||
@rem ##########################################################################
|
||||
@@ -43,11 +45,11 @@ set JAVA_EXE=java.exe
|
||||
%JAVA_EXE% -version >NUL 2>&1
|
||||
if %ERRORLEVEL% equ 0 goto execute
|
||||
|
||||
echo.
|
||||
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||
echo.
|
||||
echo Please set the JAVA_HOME variable in your environment to match the
|
||||
echo location of your Java installation.
|
||||
echo. 1>&2
|
||||
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
|
||||
echo. 1>&2
|
||||
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
|
||||
echo location of your Java installation. 1>&2
|
||||
|
||||
goto fail
|
||||
|
||||
@@ -57,11 +59,11 @@ set JAVA_EXE=%JAVA_HOME%/bin/java.exe
|
||||
|
||||
if exist "%JAVA_EXE%" goto execute
|
||||
|
||||
echo.
|
||||
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
|
||||
echo.
|
||||
echo Please set the JAVA_HOME variable in your environment to match the
|
||||
echo location of your Java installation.
|
||||
echo. 1>&2
|
||||
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
|
||||
echo. 1>&2
|
||||
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
|
||||
echo location of your Java installation. 1>&2
|
||||
|
||||
goto fail
|
||||
|
||||
|
||||
@@ -157,7 +157,10 @@ const downloadCalibBoard = () => {
|
||||
doc.save(`calibrationTarget-${CalibrationBoardTypes[boardType.value]}.pdf`);
|
||||
};
|
||||
|
||||
const isCalibrating = ref(false);
|
||||
const isCalibrating = computed(
|
||||
() => useCameraSettingsStore().currentCameraSettings.currentPipelineIndex === WebsocketPipelineType.Calib3d
|
||||
);
|
||||
|
||||
const startCalibration = () => {
|
||||
useCameraSettingsStore().startPnPCalibration({
|
||||
squareSizeIn: squareSizeIn.value,
|
||||
@@ -170,13 +173,15 @@ const startCalibration = () => {
|
||||
});
|
||||
// The Start PnP method already handles updating the backend so only a store update is required
|
||||
useCameraSettingsStore().currentCameraSettings.currentPipelineIndex = WebsocketPipelineType.Calib3d;
|
||||
isCalibrating.value = true;
|
||||
// isCalibrating.value = true;
|
||||
calibCanceled.value = false;
|
||||
};
|
||||
const showCalibEndDialog = ref(false);
|
||||
const calibCanceled = ref(false);
|
||||
const calibSuccess = ref<boolean | undefined>(undefined);
|
||||
const endCalibration = () => {
|
||||
calibSuccess.value = undefined;
|
||||
|
||||
if (!useStateStore().calibrationData.hasEnoughImages) {
|
||||
calibCanceled.value = true;
|
||||
}
|
||||
@@ -192,7 +197,8 @@ const endCalibration = () => {
|
||||
calibSuccess.value = false;
|
||||
})
|
||||
.finally(() => {
|
||||
isCalibrating.value = false;
|
||||
// isCalibrating.value = false;
|
||||
// backend deals with this for us
|
||||
});
|
||||
};
|
||||
|
||||
@@ -245,6 +251,7 @@ const setSelectedVideoFormat = (format: VideoFormat) => {
|
||||
<v-row style="display: flex; flex-direction: column" class="mt-4">
|
||||
<v-card-subtitle v-show="!isCalibrating" class="pl-3 pa-0 ma-0"> Configure New Calibration</v-card-subtitle>
|
||||
<v-form ref="form" v-model="settingsValid" class="pl-4 mb-10 pr-5">
|
||||
<!-- TODO: the default videoFormatIndex is 0, but the list of unique video mode indexes might not include 0. getUniqueVideoResolutionStrings indexing is also different from the normal video mode indexing -->
|
||||
<pv-select
|
||||
v-model="useStateStore().calibrationData.videoFormatIndex"
|
||||
label="Resolution"
|
||||
@@ -492,10 +499,12 @@ const setSelectedVideoFormat = (format: VideoFormat) => {
|
||||
process.</v-card-text
|
||||
>
|
||||
</template>
|
||||
<template v-else-if="isCalibrating">
|
||||
<!-- No result reported yet -->
|
||||
<template v-else-if="calibSuccess === undefined">
|
||||
<v-progress-circular indeterminate :size="70" :width="8" color="accent" />
|
||||
<v-card-text>Camera is being calibrated. This process may take several minutes...</v-card-text>
|
||||
</template>
|
||||
<!-- Got positive result -->
|
||||
<template v-else-if="calibSuccess">
|
||||
<v-icon color="green" size="70"> mdi-check-bold </v-icon>
|
||||
<v-card-text>
|
||||
|
||||
@@ -70,16 +70,12 @@ public class ConfigManager {
|
||||
if (INSTANCE == null) {
|
||||
Path rootFolder = PathManager.getInstance().getRootFolder();
|
||||
switch (m_saveStrat) {
|
||||
case SQL:
|
||||
INSTANCE = new ConfigManager(rootFolder, new SqlConfigProvider(rootFolder));
|
||||
break;
|
||||
case LEGACY:
|
||||
INSTANCE = new ConfigManager(rootFolder, new LegacyConfigProvider(rootFolder));
|
||||
break;
|
||||
case ATOMIC_ZIP:
|
||||
// not yet done, fall through
|
||||
default:
|
||||
break;
|
||||
case SQL -> INSTANCE = new ConfigManager(rootFolder, new SqlConfigProvider(rootFolder));
|
||||
case LEGACY ->
|
||||
INSTANCE = new ConfigManager(rootFolder, new LegacyConfigProvider(rootFolder));
|
||||
case ATOMIC_ZIP -> {
|
||||
// TODO: Not done yet
|
||||
}
|
||||
}
|
||||
}
|
||||
return INSTANCE;
|
||||
|
||||
@@ -21,12 +21,8 @@ import com.fasterxml.jackson.annotation.JsonAlias;
|
||||
import com.fasterxml.jackson.annotation.JsonCreator;
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import org.photonvision.common.hardware.Platform;
|
||||
import org.photonvision.common.networking.NetworkMode;
|
||||
import org.photonvision.common.util.file.JacksonUtils;
|
||||
|
||||
public class NetworkConfig {
|
||||
// Can be an integer team number, or an IP address
|
||||
@@ -89,15 +85,19 @@ public class NetworkConfig {
|
||||
setShouldManage(shouldManage);
|
||||
}
|
||||
|
||||
public Map<String, Object> toHashMap() {
|
||||
try {
|
||||
var ret = new ObjectMapper().convertValue(this, JacksonUtils.UIMap.class);
|
||||
ret.put("canManage", this.deviceCanManageNetwork());
|
||||
return ret;
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
return new HashMap<>();
|
||||
}
|
||||
public NetworkConfig(NetworkConfig config) {
|
||||
this(
|
||||
config.ntServerAddress,
|
||||
config.connectionType,
|
||||
config.staticIp,
|
||||
config.hostname,
|
||||
config.runNTServer,
|
||||
config.shouldManage,
|
||||
config.shouldPublishProto,
|
||||
config.networkManagerIface,
|
||||
config.setStaticCommand,
|
||||
config.setDHCPcommand,
|
||||
config.matchCamerasOnlyByPath);
|
||||
}
|
||||
|
||||
@JsonIgnore
|
||||
@@ -110,18 +110,12 @@ public class NetworkConfig {
|
||||
return "\"" + networkManagerIface + "\"";
|
||||
}
|
||||
|
||||
@JsonIgnore
|
||||
public boolean shouldManage() {
|
||||
return this.shouldManage;
|
||||
}
|
||||
|
||||
@JsonIgnore
|
||||
public void setShouldManage(boolean shouldManage) {
|
||||
this.shouldManage = shouldManage && this.deviceCanManageNetwork();
|
||||
}
|
||||
|
||||
@JsonIgnore
|
||||
private boolean deviceCanManageNetwork() {
|
||||
protected boolean deviceCanManageNetwork() {
|
||||
return Platform.isLinux();
|
||||
}
|
||||
|
||||
|
||||
@@ -205,13 +205,11 @@ public class NeuralNetworkModelManager {
|
||||
|
||||
try {
|
||||
switch (backend.get()) {
|
||||
case RKNN:
|
||||
case RKNN -> {
|
||||
models.get(backend.get()).add(new RknnModel(model, labels));
|
||||
logger.info(
|
||||
"Loaded model " + model.getName() + " for backend " + backend.get().toString());
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
} catch (IllegalArgumentException e) {
|
||||
logger.error("Failed to load model " + model.getName(), e);
|
||||
|
||||
@@ -21,19 +21,6 @@ import edu.wpi.first.apriltag.AprilTagFieldLayout;
|
||||
import java.util.Collection;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
import org.photonvision.PhotonVersion;
|
||||
import org.photonvision.common.hardware.Platform;
|
||||
import org.photonvision.common.networking.NetworkManager;
|
||||
import org.photonvision.common.networking.NetworkUtils;
|
||||
import org.photonvision.common.util.SerializationUtils;
|
||||
import org.photonvision.mrcal.MrCalJNILoader;
|
||||
import org.photonvision.raspi.LibCameraJNILoader;
|
||||
import org.photonvision.vision.calibration.UICameraCalibrationCoefficients;
|
||||
import org.photonvision.vision.camera.QuirkyCamera;
|
||||
import org.photonvision.vision.processes.VisionModule;
|
||||
import org.photonvision.vision.processes.VisionModuleManager;
|
||||
import org.photonvision.vision.processes.VisionSource;
|
||||
|
||||
public class PhotonConfiguration {
|
||||
@@ -124,81 +111,6 @@ public class PhotonConfiguration {
|
||||
return cameraConfigurations.remove(name) != null;
|
||||
}
|
||||
|
||||
public Map<String, Object> toHashMap() {
|
||||
Map<String, Object> map = new HashMap<>();
|
||||
var settingsSubmap = new HashMap<String, Object>();
|
||||
|
||||
// Hack active interfaces into networkSettings
|
||||
var netConfigMap = networkConfig.toHashMap();
|
||||
netConfigMap.put("networkInterfaceNames", NetworkUtils.getAllActiveWiredInterfaces());
|
||||
netConfigMap.put("networkingDisabled", NetworkManager.getInstance().networkingIsDisabled);
|
||||
|
||||
settingsSubmap.put("networkSettings", netConfigMap);
|
||||
|
||||
var lightingConfig = new UILightingConfig();
|
||||
lightingConfig.brightness = hardwareSettings.ledBrightnessPercentage;
|
||||
lightingConfig.supported = !hardwareConfig.ledPins.isEmpty();
|
||||
settingsSubmap.put("lighting", SerializationUtils.objectToHashMap(lightingConfig));
|
||||
// General Settings
|
||||
var generalSubmap = new HashMap<String, Object>();
|
||||
generalSubmap.put("version", PhotonVersion.versionString);
|
||||
generalSubmap.put(
|
||||
"gpuAcceleration",
|
||||
LibCameraJNILoader.isSupported()
|
||||
? "Zerocopy Libcamera Working"
|
||||
: ""); // TODO add support for other types of GPU accel
|
||||
generalSubmap.put("mrCalWorking", MrCalJNILoader.getInstance().isLoaded());
|
||||
generalSubmap.put("availableModels", NeuralNetworkModelManager.getInstance().getModels());
|
||||
generalSubmap.put(
|
||||
"supportedBackends", NeuralNetworkModelManager.getInstance().getSupportedBackends());
|
||||
generalSubmap.put(
|
||||
"hardwareModel",
|
||||
hardwareConfig.deviceName.isEmpty()
|
||||
? Platform.getHardwareModel()
|
||||
: hardwareConfig.deviceName);
|
||||
generalSubmap.put("hardwarePlatform", Platform.getPlatformName());
|
||||
settingsSubmap.put("general", generalSubmap);
|
||||
// AprilTagFieldLayout
|
||||
settingsSubmap.put("atfl", this.atfl);
|
||||
|
||||
map.put(
|
||||
"cameraSettings",
|
||||
VisionModuleManager.getInstance().getModules().stream()
|
||||
.map(VisionModule::toUICameraConfig)
|
||||
.map(SerializationUtils::objectToHashMap)
|
||||
.collect(Collectors.toList()));
|
||||
map.put("settings", settingsSubmap);
|
||||
|
||||
return map;
|
||||
}
|
||||
|
||||
public static class UILightingConfig {
|
||||
public int brightness = 0;
|
||||
public boolean supported = true;
|
||||
}
|
||||
|
||||
public static class UICameraConfiguration {
|
||||
@SuppressWarnings("unused")
|
||||
public double fov;
|
||||
|
||||
public String nickname;
|
||||
public String uniqueName;
|
||||
public HashMap<String, Object> currentPipelineSettings;
|
||||
public int currentPipelineIndex;
|
||||
public List<String> pipelineNicknames;
|
||||
public HashMap<Integer, HashMap<String, Object>> videoFormatList;
|
||||
public int outputStreamPort;
|
||||
public int inputStreamPort;
|
||||
public List<UICameraCalibrationCoefficients> calibrations;
|
||||
public boolean isFovConfigurable = true;
|
||||
public QuirkyCamera cameraQuirks;
|
||||
public boolean isCSICamera;
|
||||
public double minExposureRaw;
|
||||
public double maxExposureRaw;
|
||||
public double minWhiteBalanceTemp;
|
||||
public double maxWhiteBalanceTemp;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "PhotonConfiguration [\n hardwareConfig="
|
||||
|
||||
@@ -599,9 +599,9 @@ public class SqlConfigProvider extends ConfigProvider {
|
||||
result.getString(Columns.CAM_PIPELINE_JSONS), dummyList.getClass());
|
||||
|
||||
List<CVPipelineSettings> loadedSettings = new ArrayList<>();
|
||||
for (var str : pipelineSettings) {
|
||||
if (str instanceof String) {
|
||||
loadedSettings.add(JacksonUtils.deserialize((String) str, CVPipelineSettings.class));
|
||||
for (var setting : pipelineSettings) {
|
||||
if (setting instanceof String str) {
|
||||
loadedSettings.add(JacksonUtils.deserialize(str, CVPipelineSettings.class));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -32,6 +32,7 @@ import org.photonvision.common.configuration.ConfigManager;
|
||||
import org.photonvision.common.configuration.NetworkConfig;
|
||||
import org.photonvision.common.dataflow.DataChangeService;
|
||||
import org.photonvision.common.dataflow.events.OutgoingUIEvent;
|
||||
import org.photonvision.common.dataflow.websocket.UIPhotonConfiguration;
|
||||
import org.photonvision.common.hardware.HardwareManager;
|
||||
import org.photonvision.common.logging.LogGroup;
|
||||
import org.photonvision.common.logging.LogLevel;
|
||||
@@ -165,7 +166,8 @@ public class NetworkTablesManager {
|
||||
DataChangeService.getInstance()
|
||||
.publishEvent(
|
||||
new OutgoingUIEvent<>(
|
||||
"fullsettings", ConfigManager.getInstance().getConfig().toHashMap()));
|
||||
"fullsettings",
|
||||
UIPhotonConfiguration.programStateToUi(ConfigManager.getInstance().getConfig())));
|
||||
} catch (IOException e) {
|
||||
logger.error("Error deserializing atfl!");
|
||||
logger.error(atfl_json);
|
||||
|
||||
@@ -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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.photonvision.common.dataflow.websocket;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import org.photonvision.vision.calibration.UICameraCalibrationCoefficients;
|
||||
import org.photonvision.vision.camera.QuirkyCamera;
|
||||
|
||||
public class UICameraConfiguration {
|
||||
@SuppressWarnings("unused")
|
||||
public double fov;
|
||||
|
||||
public String nickname;
|
||||
public String uniqueName;
|
||||
public HashMap<String, Object> currentPipelineSettings;
|
||||
public int currentPipelineIndex;
|
||||
public List<String> pipelineNicknames;
|
||||
public HashMap<Integer, HashMap<String, Object>> videoFormatList;
|
||||
public int outputStreamPort;
|
||||
public int inputStreamPort;
|
||||
public List<UICameraCalibrationCoefficients> calibrations;
|
||||
public boolean isFovConfigurable = true;
|
||||
public QuirkyCamera cameraQuirks;
|
||||
public boolean isCSICamera;
|
||||
public double minExposureRaw;
|
||||
public double maxExposureRaw;
|
||||
public double minWhiteBalanceTemp;
|
||||
public double maxWhiteBalanceTemp;
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
/*
|
||||
* 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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.photonvision.common.dataflow.websocket;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
public class UIGeneralSettings {
|
||||
public UIGeneralSettings(
|
||||
String version,
|
||||
String gpuAcceleration,
|
||||
boolean mrCalWorking,
|
||||
Map<String, ArrayList<String>> availableModels,
|
||||
List<String> supportedBackends,
|
||||
String hardwareModel,
|
||||
String hardwarePlatform) {
|
||||
this.version = version;
|
||||
this.gpuAcceleration = gpuAcceleration;
|
||||
this.mrCalWorking = mrCalWorking;
|
||||
this.availableModels = availableModels;
|
||||
this.supportedBackends = supportedBackends;
|
||||
this.hardwareModel = hardwareModel;
|
||||
this.hardwarePlatform = hardwarePlatform;
|
||||
}
|
||||
|
||||
public String version;
|
||||
public String gpuAcceleration;
|
||||
public boolean mrCalWorking;
|
||||
public Map<String, ArrayList<String>> availableModels;
|
||||
public List<String> supportedBackends;
|
||||
public String hardwareModel;
|
||||
public String hardwarePlatform;
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
/*
|
||||
* 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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.photonvision.common.dataflow.websocket;
|
||||
|
||||
public class UILightingConfig {
|
||||
public UILightingConfig(int brightness, boolean supported) {
|
||||
this.brightness = brightness;
|
||||
this.supported = supported;
|
||||
}
|
||||
|
||||
public int brightness = 0;
|
||||
public boolean supported = true;
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
/*
|
||||
* 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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.photonvision.common.dataflow.websocket;
|
||||
|
||||
import java.util.List;
|
||||
import org.photonvision.common.configuration.NetworkConfig;
|
||||
import org.photonvision.common.networking.NetworkUtils.NMDeviceInfo;
|
||||
|
||||
public class UINetConfig extends NetworkConfig {
|
||||
public UINetConfig(
|
||||
NetworkConfig config, List<NMDeviceInfo> networkInterfaceNames, boolean networkingDisabled) {
|
||||
super(config);
|
||||
this.networkInterfaceNames = networkInterfaceNames;
|
||||
this.networkingDisabled = networkingDisabled;
|
||||
this.canManage = this.deviceCanManageNetwork();
|
||||
}
|
||||
|
||||
public List<NMDeviceInfo> networkInterfaceNames;
|
||||
public boolean networkingDisabled;
|
||||
public boolean canManage;
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
/*
|
||||
* 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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.photonvision.common.dataflow.websocket;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
import org.photonvision.PhotonVersion;
|
||||
import org.photonvision.common.configuration.NeuralNetworkModelManager;
|
||||
import org.photonvision.common.configuration.PhotonConfiguration;
|
||||
import org.photonvision.common.hardware.Platform;
|
||||
import org.photonvision.common.networking.NetworkManager;
|
||||
import org.photonvision.common.networking.NetworkUtils;
|
||||
import org.photonvision.mrcal.MrCalJNILoader;
|
||||
import org.photonvision.raspi.LibCameraJNILoader;
|
||||
import org.photonvision.vision.processes.VisionModule;
|
||||
import org.photonvision.vision.processes.VisionModuleManager;
|
||||
|
||||
public class UIPhotonConfiguration {
|
||||
public List<UICameraConfiguration> cameraSettings;
|
||||
public UIProgramSettings settings;
|
||||
|
||||
public UIPhotonConfiguration(
|
||||
UIProgramSettings settings, List<UICameraConfiguration> cameraSettings) {
|
||||
this.cameraSettings = cameraSettings;
|
||||
this.settings = settings;
|
||||
}
|
||||
|
||||
public static UIPhotonConfiguration programStateToUi(PhotonConfiguration c) {
|
||||
return new UIPhotonConfiguration(
|
||||
new UIProgramSettings(
|
||||
new UINetConfig(
|
||||
c.getNetworkConfig(),
|
||||
NetworkUtils.getAllActiveWiredInterfaces(),
|
||||
NetworkManager.getInstance().networkingIsDisabled),
|
||||
new UILightingConfig(
|
||||
c.getHardwareSettings().ledBrightnessPercentage,
|
||||
!c.getHardwareConfig().ledPins.isEmpty()),
|
||||
new UIGeneralSettings(
|
||||
PhotonVersion.versionString,
|
||||
// TODO add support for other types of GPU accel
|
||||
LibCameraJNILoader.isSupported() ? "Zerocopy Libcamera Working" : "",
|
||||
MrCalJNILoader.getInstance().isLoaded(),
|
||||
NeuralNetworkModelManager.getInstance().getModels(),
|
||||
NeuralNetworkModelManager.getInstance().getSupportedBackends(),
|
||||
c.getHardwareConfig().deviceName.isEmpty()
|
||||
? Platform.getHardwareModel()
|
||||
: c.getHardwareConfig().deviceName,
|
||||
Platform.getPlatformName()),
|
||||
c.getApriltagFieldLayout()),
|
||||
VisionModuleManager.getInstance().getModules().stream()
|
||||
.map(VisionModule::toUICameraConfig)
|
||||
.collect(Collectors.toList()));
|
||||
}
|
||||
}
|
||||
@@ -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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.photonvision.common.dataflow.websocket;
|
||||
|
||||
import edu.wpi.first.apriltag.AprilTagFieldLayout;
|
||||
|
||||
public class UIProgramSettings {
|
||||
public UIProgramSettings(
|
||||
UINetConfig networkSettings,
|
||||
UILightingConfig lighting,
|
||||
UIGeneralSettings general,
|
||||
AprilTagFieldLayout atfl) {
|
||||
this.networkSettings = networkSettings;
|
||||
this.lighting = lighting;
|
||||
this.general = general;
|
||||
this.atfl = atfl;
|
||||
}
|
||||
|
||||
public UINetConfig networkSettings;
|
||||
public UILightingConfig lighting;
|
||||
public UIGeneralSettings general;
|
||||
public AprilTagFieldLayout atfl;
|
||||
}
|
||||
@@ -242,22 +242,7 @@ public class PigpioSocket {
|
||||
waveSendOnce(waveformId);
|
||||
}
|
||||
} else {
|
||||
String error = "";
|
||||
switch (waveformId) {
|
||||
case PI_EMPTY_WAVEFORM:
|
||||
error = "Waveform empty";
|
||||
break;
|
||||
case PI_TOO_MANY_CBS:
|
||||
error = "Too many CBS";
|
||||
break;
|
||||
case PI_TOO_MANY_OOL:
|
||||
error = "Too many OOL";
|
||||
break;
|
||||
case PI_NO_WAVEFORM_ID:
|
||||
error = "No waveform ID";
|
||||
break;
|
||||
}
|
||||
logger.error("Failed to send wave: " + error);
|
||||
logger.error("Failed to send wave: " + getMessageForError(waveformId));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
/*
|
||||
* 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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.photonvision.common.hardware;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Optional;
|
||||
import org.photonvision.common.logging.LogGroup;
|
||||
import org.photonvision.common.logging.Logger;
|
||||
|
||||
/**
|
||||
* Our blessed images inject the current version via this build workflow:
|
||||
* https://github.com/PhotonVision/photon-image-modifier/blob/2e5ddb6b599df0be921c12c8dbe7b939ecd7f615/.github/workflows/main.yml#L67
|
||||
*
|
||||
* <p>This class provides a convienent abstraction around this
|
||||
*/
|
||||
public class OsImageVersion {
|
||||
private static final Logger logger = new Logger(OsImageVersion.class, LogGroup.General);
|
||||
|
||||
private static Path imageVersionFile = Path.of("/opt/photonvision/image-version");
|
||||
|
||||
public static final Optional<String> IMAGE_VERSION = getImageVersion();
|
||||
|
||||
private static Optional<String> getImageVersion() {
|
||||
if (!imageVersionFile.toFile().exists()) {
|
||||
logger.warn(
|
||||
"Photon cannot locate base OS image version metadata at " + imageVersionFile.toString());
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
try {
|
||||
return Optional.of(Files.readString(imageVersionFile).strip());
|
||||
} catch (IOException e) {
|
||||
logger.error("Couldn't read image-version file", e);
|
||||
}
|
||||
|
||||
return Optional.empty();
|
||||
}
|
||||
}
|
||||
@@ -134,25 +134,17 @@ public class VisionLED {
|
||||
var newLedModeRaw = (int) entryNotification.valueData.value.getInteger();
|
||||
logger.debug("Got LED mode " + newLedModeRaw);
|
||||
if (newLedModeRaw != currentLedMode.value) {
|
||||
VisionLEDMode newLedMode;
|
||||
switch (newLedModeRaw) {
|
||||
case -1:
|
||||
newLedMode = VisionLEDMode.kDefault;
|
||||
break;
|
||||
case 0:
|
||||
newLedMode = VisionLEDMode.kOff;
|
||||
break;
|
||||
case 1:
|
||||
newLedMode = VisionLEDMode.kOn;
|
||||
break;
|
||||
case 2:
|
||||
newLedMode = VisionLEDMode.kBlink;
|
||||
break;
|
||||
default:
|
||||
logger.warn("User supplied invalid LED mode, falling back to Default");
|
||||
newLedMode = VisionLEDMode.kDefault;
|
||||
break;
|
||||
}
|
||||
VisionLEDMode newLedMode =
|
||||
switch (newLedModeRaw) {
|
||||
case -1 -> newLedMode = VisionLEDMode.kDefault;
|
||||
case 0 -> newLedMode = VisionLEDMode.kOff;
|
||||
case 1 -> newLedMode = VisionLEDMode.kOn;
|
||||
case 2 -> newLedMode = VisionLEDMode.kBlink;
|
||||
default -> {
|
||||
logger.warn("User supplied invalid LED mode, falling back to Default");
|
||||
yield VisionLEDMode.kDefault;
|
||||
}
|
||||
};
|
||||
setInternal(newLedMode, true);
|
||||
|
||||
if (modeConsumer != null) modeConsumer.accept(newLedMode.value);
|
||||
@@ -164,18 +156,10 @@ public class VisionLED {
|
||||
|
||||
if (fromNT) {
|
||||
switch (newLedMode) {
|
||||
case kDefault:
|
||||
setStateImpl(pipelineModeSupplier.getAsBoolean());
|
||||
break;
|
||||
case kOff:
|
||||
setStateImpl(false);
|
||||
break;
|
||||
case kOn:
|
||||
setStateImpl(true);
|
||||
break;
|
||||
case kBlink:
|
||||
blinkImpl(85, -1);
|
||||
break;
|
||||
case kDefault -> setStateImpl(pipelineModeSupplier.getAsBoolean());
|
||||
case kOff -> setStateImpl(false);
|
||||
case kOn -> setStateImpl(true);
|
||||
case kBlink -> blinkImpl(85, -1);
|
||||
}
|
||||
currentLedMode = newLedMode;
|
||||
logger.info(
|
||||
@@ -183,18 +167,10 @@ public class VisionLED {
|
||||
} else {
|
||||
if (currentLedMode == VisionLEDMode.kDefault) {
|
||||
switch (newLedMode) {
|
||||
case kDefault:
|
||||
setStateImpl(pipelineModeSupplier.getAsBoolean());
|
||||
break;
|
||||
case kOff:
|
||||
setStateImpl(false);
|
||||
break;
|
||||
case kOn:
|
||||
setStateImpl(true);
|
||||
break;
|
||||
case kBlink:
|
||||
blinkImpl(85, -1);
|
||||
break;
|
||||
case kDefault -> setStateImpl(pipelineModeSupplier.getAsBoolean());
|
||||
case kOff -> setStateImpl(false);
|
||||
case kOn -> setStateImpl(true);
|
||||
case kBlink -> blinkImpl(85, -1);
|
||||
}
|
||||
}
|
||||
logger.info("Changing LED internal state to " + newLedMode.toString());
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
|
||||
package org.photonvision.common.logging;
|
||||
|
||||
import edu.wpi.first.util.RuntimeDetector;
|
||||
import org.photonvision.common.hardware.Platform;
|
||||
import org.photonvision.common.util.TimedTaskManager;
|
||||
import org.photonvision.jni.QueuedFileLogger;
|
||||
|
||||
@@ -39,7 +39,7 @@ public class KernelLogLogger {
|
||||
Logger logger = new Logger(KernelLogLogger.class, LogGroup.General);
|
||||
|
||||
public KernelLogLogger() {
|
||||
if (RuntimeDetector.isLinux()) {
|
||||
if (Platform.isLinux()) {
|
||||
listener = new QueuedFileLogger("/var/log/kern.log");
|
||||
} else {
|
||||
System.out.println("NOT for klogs");
|
||||
|
||||
@@ -113,7 +113,7 @@ public class NetworkManager {
|
||||
}
|
||||
|
||||
public void reinitialize() {
|
||||
initialize(ConfigManager.getInstance().getConfig().getNetworkConfig().shouldManage());
|
||||
initialize(ConfigManager.getInstance().getConfig().getNetworkConfig().shouldManage);
|
||||
|
||||
DataChangeService.getInstance()
|
||||
.publishEvent(
|
||||
|
||||
@@ -51,16 +51,9 @@ public abstract class NumberCouple<T extends Number> {
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
if (!(obj instanceof NumberCouple)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
var couple = (NumberCouple) obj;
|
||||
if (!couple.first.equals(first)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return couple.second.equals(second);
|
||||
return obj instanceof NumberCouple<?> couple
|
||||
&& couple.first.equals(first)
|
||||
&& couple.second.equals(second);
|
||||
}
|
||||
|
||||
@JsonIgnore
|
||||
|
||||
@@ -69,7 +69,8 @@ public abstract class PhotonJNICommon {
|
||||
logger.error("Couldn't load shared object " + libraryName, e);
|
||||
e.printStackTrace();
|
||||
// logger.error(System.getProperty("java.library.path"));
|
||||
break;
|
||||
instance.setLoaded(false);
|
||||
return;
|
||||
}
|
||||
}
|
||||
instance.setLoaded(true);
|
||||
|
||||
@@ -95,9 +95,11 @@ public class GenericUSBCameraSettables extends VisionSourceSettables {
|
||||
// first.
|
||||
var autoExpProp = findProperty("exposure_auto", "auto_exposure");
|
||||
|
||||
exposureAbsProp = expProp.get();
|
||||
this.minExposure = exposureAbsProp.getMin();
|
||||
this.maxExposure = exposureAbsProp.getMax();
|
||||
if (expProp.isPresent()) {
|
||||
exposureAbsProp = expProp.get();
|
||||
this.minExposure = exposureAbsProp.getMin();
|
||||
this.maxExposure = exposureAbsProp.getMax();
|
||||
}
|
||||
|
||||
if (autoExpProp.isPresent()) {
|
||||
autoExposureProp = autoExpProp.get();
|
||||
@@ -184,7 +186,7 @@ public class GenericUSBCameraSettables extends VisionSourceSettables {
|
||||
softSet("auto_exposure_bias", 12);
|
||||
softSet("iso_sensitivity_auto", 1);
|
||||
softSet("iso_sensitivity", 1); // Manual ISO adjustment by default
|
||||
autoExposureProp.set(PROP_AUTO_EXPOSURE_ENABLED);
|
||||
if (autoExposureProp != null) autoExposureProp.set(PROP_AUTO_EXPOSURE_ENABLED);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -22,9 +22,9 @@ import edu.wpi.first.cscore.CvSink;
|
||||
import edu.wpi.first.cscore.UsbCamera;
|
||||
import edu.wpi.first.cscore.VideoException;
|
||||
import edu.wpi.first.cscore.VideoProperty;
|
||||
import edu.wpi.first.util.RuntimeDetector;
|
||||
import java.util.*;
|
||||
import org.photonvision.common.configuration.CameraConfiguration;
|
||||
import org.photonvision.common.hardware.Platform;
|
||||
import org.photonvision.common.logging.LogGroup;
|
||||
import org.photonvision.common.logging.Logger;
|
||||
import org.photonvision.vision.camera.CameraQuirk;
|
||||
@@ -113,7 +113,7 @@ public class USBCameraSource extends VisionSource {
|
||||
GenericUSBCameraSettables settables;
|
||||
|
||||
if (quirks.hasQuirk(CameraQuirk.LifeCamControls)) {
|
||||
if (RuntimeDetector.isWindows()) {
|
||||
if (Platform.isWindows()) {
|
||||
logger.debug("Using Microsoft Lifecam 3000 Windows-Specific Settables");
|
||||
settables = new LifeCam3kWindowsCameraSettables(config, camera);
|
||||
} else {
|
||||
@@ -124,7 +124,7 @@ public class USBCameraSource extends VisionSource {
|
||||
logger.debug("Using PlayStation Eye Camera Settables");
|
||||
settables = new PsEyeCameraSettables(config, camera);
|
||||
} else if (quirks.hasQuirk(CameraQuirk.ArduOV2311Controls)) {
|
||||
if (RuntimeDetector.isWindows()) {
|
||||
if (Platform.isWindows()) {
|
||||
logger.debug("Using Arducam OV2311 Windows-Specific Settables");
|
||||
settables = new ArduOV2311WindowsCameraSettables(config, camera);
|
||||
} else {
|
||||
|
||||
@@ -135,20 +135,14 @@ public class MJPGFrameConsumer implements AutoCloseable {
|
||||
}
|
||||
|
||||
private static String pixelFormatToString(PixelFormat pixelFormat) {
|
||||
switch (pixelFormat) {
|
||||
case kMJPEG:
|
||||
return "MJPEG";
|
||||
case kYUYV:
|
||||
return "YUYV";
|
||||
case kRGB565:
|
||||
return "RGB565";
|
||||
case kBGR:
|
||||
return "BGR";
|
||||
case kGray:
|
||||
return "Gray";
|
||||
default:
|
||||
return "Unknown";
|
||||
}
|
||||
return switch (pixelFormat) {
|
||||
case kMJPEG -> "MJPEG";
|
||||
case kYUYV -> "YUYV";
|
||||
case kRGB565 -> "RGB565";
|
||||
case kBGR -> "BGR";
|
||||
case kGray -> "Gray";
|
||||
case kUYVY, kUnknown, kY16, kBGRA -> "Unknown";
|
||||
};
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -158,18 +158,19 @@ public class Contour implements Releasable {
|
||||
double massX = (x0A + x0B) / 2;
|
||||
double massY = (y0A + y0B) / 2;
|
||||
switch (intersectionDirection) {
|
||||
case Up:
|
||||
case None -> {}
|
||||
case Up -> {
|
||||
if (intersectionY < massY) isIntersecting = true;
|
||||
break;
|
||||
case Down:
|
||||
}
|
||||
case Down -> {
|
||||
if (intersectionY > massY) isIntersecting = true;
|
||||
break;
|
||||
case Left:
|
||||
}
|
||||
case Left -> {
|
||||
if (intersectionX < massX) isIntersecting = true;
|
||||
break;
|
||||
case Right:
|
||||
}
|
||||
case Right -> {
|
||||
if (intersectionX > massX) isIntersecting = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
intersectMatA.release();
|
||||
intersectMatB.release();
|
||||
|
||||
@@ -17,9 +17,6 @@
|
||||
|
||||
package org.photonvision.vision.opencv;
|
||||
|
||||
import java.util.EnumSet;
|
||||
import java.util.HashMap;
|
||||
|
||||
public enum ContourShape {
|
||||
Circle(0),
|
||||
Custom(-1),
|
||||
@@ -32,15 +29,12 @@ public enum ContourShape {
|
||||
this.sides = sides;
|
||||
}
|
||||
|
||||
private static final HashMap<Integer, ContourShape> sidesToValueMap = new HashMap<>();
|
||||
|
||||
static {
|
||||
for (var value : EnumSet.allOf(ContourShape.class)) {
|
||||
sidesToValueMap.put(value.sides, value);
|
||||
}
|
||||
}
|
||||
|
||||
public static ContourShape fromSides(int sides) {
|
||||
return sidesToValueMap.get(sides);
|
||||
return switch (sides) {
|
||||
case 0 -> Circle;
|
||||
case 3 -> Triangle;
|
||||
case 4 -> Quadrilateral;
|
||||
default -> Custom;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -48,13 +48,14 @@ public class Draw2dCrosshairPipe
|
||||
double scale = params.frameStaticProperties.imageWidth / (double) params.divisor.value / 32.0;
|
||||
|
||||
switch (params.robotOffsetPointMode) {
|
||||
case Single:
|
||||
case None -> {}
|
||||
case Single -> {
|
||||
if (params.singleOffsetPoint.x != 0 && params.singleOffsetPoint.y != 0) {
|
||||
x = params.singleOffsetPoint.x;
|
||||
y = params.singleOffsetPoint.y;
|
||||
}
|
||||
break;
|
||||
case Dual:
|
||||
}
|
||||
case Dual -> {
|
||||
if (!in.getRight().isEmpty()) {
|
||||
var target = in.getRight().get(0);
|
||||
if (target != null) {
|
||||
@@ -65,7 +66,7 @@ public class Draw2dCrosshairPipe
|
||||
y = offsetCrosshair.y;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
x /= (double) params.divisor.value;
|
||||
|
||||
@@ -50,23 +50,7 @@ public class FindPolygonPipe
|
||||
|
||||
private CVShape getShape(Contour in) {
|
||||
int corners = getCorners(in);
|
||||
|
||||
/*The contourShape enum has predefined shapes for Circles, Triangles, and Quads
|
||||
meaning any shape not fitting in those predefined shapes must be a custom shape.
|
||||
*/
|
||||
if (ContourShape.fromSides(corners) == null) {
|
||||
return new CVShape(in, ContourShape.Custom);
|
||||
}
|
||||
switch (ContourShape.fromSides(corners)) {
|
||||
case Circle:
|
||||
return new CVShape(in, ContourShape.Circle);
|
||||
case Triangle:
|
||||
return new CVShape(in, ContourShape.Triangle);
|
||||
case Quadrilateral:
|
||||
return new CVShape(in, ContourShape.Quadrilateral);
|
||||
}
|
||||
|
||||
return new CVShape(in, ContourShape.Custom);
|
||||
return new CVShape(in, ContourShape.fromSides(corners));
|
||||
}
|
||||
|
||||
private int getCorners(Contour contour) {
|
||||
|
||||
@@ -85,19 +85,29 @@ public class ArucoPipeline extends CVPipeline<CVPipelineResult, ArucoPipelineSet
|
||||
// 2023/other: best guess is 6in
|
||||
double tagWidth = Units.inchesToMeters(6);
|
||||
TargetModel tagModel = TargetModel.kAprilTag16h5;
|
||||
switch (settings.tagFamily) {
|
||||
case kTag36h11:
|
||||
// 2024 tag, 6.5in
|
||||
params.tagFamily = Objdetect.DICT_APRILTAG_36h11;
|
||||
tagWidth = Units.inchesToMeters(6.5);
|
||||
tagModel = TargetModel.kAprilTag36h11;
|
||||
break;
|
||||
case kTag25h9:
|
||||
params.tagFamily = Objdetect.DICT_APRILTAG_25h9;
|
||||
break;
|
||||
default:
|
||||
params.tagFamily = Objdetect.DICT_APRILTAG_16h5;
|
||||
}
|
||||
|
||||
params.tagFamily =
|
||||
switch (settings.tagFamily) {
|
||||
case kTag36h11 -> {
|
||||
// 2024 tag, 6.5in
|
||||
tagWidth = Units.inchesToMeters(6.5);
|
||||
tagModel = TargetModel.kAprilTag36h11;
|
||||
yield Objdetect.DICT_APRILTAG_36h11;
|
||||
}
|
||||
case kTag25h9 -> Objdetect.DICT_APRILTAG_25h9;
|
||||
// TODO: explicitly drop support for these
|
||||
case kTag16h5,
|
||||
kTagCircle21h7,
|
||||
kTagCircle49h12,
|
||||
kTagCustom48h11,
|
||||
kTagStandard41h12,
|
||||
kTagStandard52h13 -> {
|
||||
// 2024 tag, 6.5in
|
||||
tagWidth = Units.inchesToMeters(6.5);
|
||||
tagModel = TargetModel.kAprilTag36h11;
|
||||
yield Objdetect.DICT_APRILTAG_36h11;
|
||||
}
|
||||
};
|
||||
|
||||
int threshMinSize = Math.max(3, settings.threshWinSizes.getFirst());
|
||||
settings.threshWinSizes.setFirst(threshMinSize);
|
||||
|
||||
@@ -26,6 +26,7 @@ import org.photonvision.common.configuration.CameraConfiguration;
|
||||
import org.photonvision.common.configuration.ConfigManager;
|
||||
import org.photonvision.common.dataflow.DataChangeService;
|
||||
import org.photonvision.common.dataflow.events.OutgoingUIEvent;
|
||||
import org.photonvision.common.dataflow.websocket.UIPhotonConfiguration;
|
||||
import org.photonvision.common.logging.LogGroup;
|
||||
import org.photonvision.common.logging.Logger;
|
||||
import org.photonvision.vision.pipeline.*;
|
||||
@@ -231,7 +232,8 @@ public class PipelineManager {
|
||||
DataChangeService.getInstance()
|
||||
.publishEvent(
|
||||
new OutgoingUIEvent<>(
|
||||
"fullsettings", ConfigManager.getInstance().getConfig().toHashMap()));
|
||||
"fullsettings",
|
||||
UIPhotonConfiguration.programStateToUi(ConfigManager.getInstance().getConfig())));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -239,41 +241,38 @@ public class PipelineManager {
|
||||
* recreation after changing pipeline type
|
||||
*/
|
||||
private void recreateUserPipeline() {
|
||||
// Cleanup potential old native resources before swapping over from a user
|
||||
// pipeline
|
||||
// Cleanup potential old native resources before swapping over from a user pipeline
|
||||
if (currentUserPipeline != null && !(currentPipelineIndex < 0)) {
|
||||
currentUserPipeline.release();
|
||||
}
|
||||
|
||||
var desiredPipelineSettings = userPipelineSettings.get(currentPipelineIndex);
|
||||
switch (desiredPipelineSettings.pipelineType) {
|
||||
case Reflective:
|
||||
case Reflective -> {
|
||||
logger.debug("Creating Reflective pipeline");
|
||||
currentUserPipeline =
|
||||
new ReflectivePipeline((ReflectivePipelineSettings) desiredPipelineSettings);
|
||||
break;
|
||||
case ColoredShape:
|
||||
}
|
||||
case ColoredShape -> {
|
||||
logger.debug("Creating ColoredShape pipeline");
|
||||
currentUserPipeline =
|
||||
new ColoredShapePipeline((ColoredShapePipelineSettings) desiredPipelineSettings);
|
||||
break;
|
||||
case AprilTag:
|
||||
}
|
||||
case AprilTag -> {
|
||||
logger.debug("Creating AprilTag pipeline");
|
||||
currentUserPipeline =
|
||||
new AprilTagPipeline((AprilTagPipelineSettings) desiredPipelineSettings);
|
||||
break;
|
||||
|
||||
case Aruco:
|
||||
}
|
||||
case Aruco -> {
|
||||
logger.debug("Creating Aruco Pipeline");
|
||||
currentUserPipeline = new ArucoPipeline((ArucoPipelineSettings) desiredPipelineSettings);
|
||||
break;
|
||||
case ObjectDetection:
|
||||
}
|
||||
case ObjectDetection -> {
|
||||
logger.debug("Creating ObjectDetection Pipeline");
|
||||
currentUserPipeline =
|
||||
new ObjectDetectionPipeline((ObjectDetectionPipelineSettings) desiredPipelineSettings);
|
||||
default:
|
||||
// Can be calib3d or drivermode, both of which are special cases
|
||||
break;
|
||||
}
|
||||
case Calib3d, DriverMode -> {}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -340,44 +339,40 @@ public class PipelineManager {
|
||||
}
|
||||
|
||||
private CVPipelineSettings createSettingsForType(PipelineType type, String nickname) {
|
||||
CVPipelineSettings newSettings;
|
||||
switch (type) {
|
||||
case Reflective:
|
||||
{
|
||||
var added = new ReflectivePipelineSettings();
|
||||
added.pipelineNickname = nickname;
|
||||
return added;
|
||||
}
|
||||
case ColoredShape:
|
||||
{
|
||||
var added = new ColoredShapePipelineSettings();
|
||||
added.pipelineNickname = nickname;
|
||||
return added;
|
||||
}
|
||||
case AprilTag:
|
||||
{
|
||||
var added = new AprilTagPipelineSettings();
|
||||
added.pipelineNickname = nickname;
|
||||
return added;
|
||||
}
|
||||
case Aruco:
|
||||
{
|
||||
var added = new ArucoPipelineSettings();
|
||||
added.pipelineNickname = nickname;
|
||||
return added;
|
||||
}
|
||||
case ObjectDetection:
|
||||
{
|
||||
var added = new ObjectDetectionPipelineSettings();
|
||||
added.pipelineNickname = nickname;
|
||||
return added;
|
||||
}
|
||||
default:
|
||||
{
|
||||
logger.error("Got invalid pipeline type: " + type);
|
||||
return null;
|
||||
}
|
||||
case Reflective -> {
|
||||
var added = new ReflectivePipelineSettings();
|
||||
added.pipelineNickname = nickname;
|
||||
return added;
|
||||
}
|
||||
case ColoredShape -> {
|
||||
var added = new ColoredShapePipelineSettings();
|
||||
added.pipelineNickname = nickname;
|
||||
return added;
|
||||
}
|
||||
case AprilTag -> {
|
||||
var added = new AprilTagPipelineSettings();
|
||||
added.pipelineNickname = nickname;
|
||||
return added;
|
||||
}
|
||||
case Aruco -> {
|
||||
var added = new ArucoPipelineSettings();
|
||||
added.pipelineNickname = nickname;
|
||||
return added;
|
||||
}
|
||||
case ObjectDetection -> {
|
||||
var added = new ObjectDetectionPipelineSettings();
|
||||
added.pipelineNickname = nickname;
|
||||
return added;
|
||||
}
|
||||
case Calib3d, DriverMode -> {
|
||||
logger.error("Got invalid pipeline type: " + type);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// This can never happen, this is here to satisfy the compiler.
|
||||
throw new IllegalStateException("Got impossible pipeline type: " + type);
|
||||
}
|
||||
|
||||
private void addPipelineInternal(CVPipelineSettings settings) {
|
||||
|
||||
@@ -30,13 +30,14 @@ import java.util.stream.Collectors;
|
||||
import org.opencv.core.Size;
|
||||
import org.photonvision.common.configuration.CameraConfiguration;
|
||||
import org.photonvision.common.configuration.ConfigManager;
|
||||
import org.photonvision.common.configuration.PhotonConfiguration;
|
||||
import org.photonvision.common.dataflow.CVPipelineResultConsumer;
|
||||
import org.photonvision.common.dataflow.DataChangeService;
|
||||
import org.photonvision.common.dataflow.events.OutgoingUIEvent;
|
||||
import org.photonvision.common.dataflow.networktables.NTDataPublisher;
|
||||
import org.photonvision.common.dataflow.statusLEDs.StatusLEDConsumer;
|
||||
import org.photonvision.common.dataflow.websocket.UICameraConfiguration;
|
||||
import org.photonvision.common.dataflow.websocket.UIDataPublisher;
|
||||
import org.photonvision.common.dataflow.websocket.UIPhotonConfiguration;
|
||||
import org.photonvision.common.hardware.HardwareManager;
|
||||
import org.photonvision.common.logging.LogGroup;
|
||||
import org.photonvision.common.logging.Logger;
|
||||
@@ -489,7 +490,8 @@ public class VisionModule {
|
||||
DataChangeService.getInstance()
|
||||
.publishEvent(
|
||||
new OutgoingUIEvent<>(
|
||||
"fullsettings", ConfigManager.getInstance().getConfig().toHashMap()));
|
||||
"fullsettings",
|
||||
UIPhotonConfiguration.programStateToUi(ConfigManager.getInstance().getConfig())));
|
||||
}
|
||||
|
||||
void saveAndBroadcastSelective(WsContext originContext, String propertyName, Object value) {
|
||||
@@ -516,8 +518,8 @@ public class VisionModule {
|
||||
saveAndBroadcastAll();
|
||||
}
|
||||
|
||||
public PhotonConfiguration.UICameraConfiguration toUICameraConfig() {
|
||||
var ret = new PhotonConfiguration.UICameraConfiguration();
|
||||
public UICameraConfiguration toUICameraConfig() {
|
||||
var ret = new UICameraConfiguration();
|
||||
|
||||
ret.fov = visionSource.getSettables().getFOV();
|
||||
ret.isCSICamera = visionSource.getCameraConfiguration().cameraType == CameraType.ZeroCopyPicam;
|
||||
@@ -585,11 +587,9 @@ public class VisionModule {
|
||||
|
||||
// Pipelines like DriverMode and Calibrate3dPipeline have null output frames
|
||||
if (result.inputAndOutputFrame != null
|
||||
&& (pipelineManager.getCurrentPipelineSettings() instanceof AdvancedPipelineSettings)) {
|
||||
streamRunnable.updateData(
|
||||
result.inputAndOutputFrame,
|
||||
(AdvancedPipelineSettings) pipelineManager.getCurrentPipelineSettings(),
|
||||
result.targets);
|
||||
&& (pipelineManager.getCurrentPipelineSettings()
|
||||
instanceof AdvancedPipelineSettings settings)) {
|
||||
streamRunnable.updateData(result.inputAndOutputFrame, settings, result.targets);
|
||||
// The streamRunnable manages releasing in this case
|
||||
} else {
|
||||
consumeResults(result.inputAndOutputFrame, result.targets);
|
||||
@@ -613,9 +613,9 @@ public class VisionModule {
|
||||
}
|
||||
|
||||
public void setTargetModel(TargetModel targetModel) {
|
||||
var settings = pipelineManager.getCurrentPipeline().getSettings();
|
||||
if (settings instanceof ReflectivePipelineSettings) {
|
||||
((ReflectivePipelineSettings) settings).targetModel = targetModel;
|
||||
var pipelineSettings = pipelineManager.getCurrentPipeline().getSettings();
|
||||
if (pipelineSettings instanceof ReflectivePipelineSettings settings) {
|
||||
settings.targetModel = targetModel;
|
||||
saveAndBroadcastAll();
|
||||
} else {
|
||||
logger.error("Cannot set target model of non-reflective pipe! Ignoring...");
|
||||
|
||||
@@ -55,11 +55,8 @@ public class VisionModuleChangeSubscriber extends DataChangeSubscriber {
|
||||
|
||||
@Override
|
||||
public void onDataChangeEvent(DataChangeEvent<?> event) {
|
||||
if (event instanceof IncomingWebSocketEvent) {
|
||||
var wsEvent = (IncomingWebSocketEvent<?>) event;
|
||||
|
||||
// Camera index -1 means a "multicast event" (i.e. the event is received by all
|
||||
// cameras)
|
||||
if (event instanceof IncomingWebSocketEvent wsEvent) {
|
||||
// Camera index -1 means a "multicast event" (i.e. the event is received by all cameras)
|
||||
if (wsEvent.cameraIndex != null
|
||||
&& (wsEvent.cameraIndex == parentModule.moduleIndex || wsEvent.cameraIndex == -1)) {
|
||||
logger.trace("Got PSC event - propName: " + wsEvent.propertyName);
|
||||
@@ -93,120 +90,32 @@ public class VisionModuleChangeSubscriber extends DataChangeSubscriber {
|
||||
var currentSettings = change.getCurrentSettings();
|
||||
var originContext = change.getOriginContext();
|
||||
switch (propName) {
|
||||
case "pipelineName": // rename current pipeline
|
||||
logger.info("Changing nick to " + newPropValue);
|
||||
parentModule.pipelineManager.getCurrentPipelineSettings().pipelineNickname =
|
||||
(String) newPropValue;
|
||||
parentModule.saveAndBroadcastAll();
|
||||
continue;
|
||||
case "newPipelineInfo": // add new pipeline
|
||||
var typeName = (Pair<String, PipelineType>) newPropValue;
|
||||
var type = typeName.getRight();
|
||||
var name = typeName.getLeft();
|
||||
|
||||
logger.info("Adding a " + type + " pipeline with name " + name);
|
||||
|
||||
var addedSettings = parentModule.pipelineManager.addPipeline(type);
|
||||
addedSettings.pipelineNickname = name;
|
||||
parentModule.saveAndBroadcastAll();
|
||||
continue;
|
||||
case "deleteCurrPipeline":
|
||||
var indexToDelete = parentModule.pipelineManager.getRequestedIndex();
|
||||
logger.info("Deleting current pipe at index " + indexToDelete);
|
||||
int newIndex = parentModule.pipelineManager.removePipeline(indexToDelete);
|
||||
parentModule.setPipeline(newIndex);
|
||||
parentModule.saveAndBroadcastAll();
|
||||
continue;
|
||||
case "changePipeline": // change active pipeline
|
||||
var index = (Integer) newPropValue;
|
||||
if (index == parentModule.pipelineManager.getRequestedIndex()) {
|
||||
logger.debug("Skipping pipeline change, index " + index + " already active");
|
||||
continue;
|
||||
case "pipelineName" -> newPipelineNickname((String) newPropValue);
|
||||
case "newPipelineInfo" -> newPipelineInfo((Pair<String, PipelineType>) newPropValue);
|
||||
case "deleteCurrPipeline" -> deleteCurrPipeline();
|
||||
case "changePipeline" -> changePipeline((Integer) newPropValue);
|
||||
case "startCalibration" -> startCalibration((Map<String, Object>) newPropValue);
|
||||
case "saveInputSnapshot" -> parentModule.saveInputSnapshot();
|
||||
case "saveOutputSnapshot" -> parentModule.saveOutputSnapshot();
|
||||
case "takeCalSnapshot" -> parentModule.takeCalibrationSnapshot();
|
||||
case "duplicatePipeline" -> duplicatePipeline((Integer) newPropValue);
|
||||
case "calibrationUploaded" -> {
|
||||
if (newPropValue instanceof CameraCalibrationCoefficients newCal) {
|
||||
parentModule.addCalibrationToConfig(newCal);
|
||||
} else {
|
||||
logger.warn("Received invalid calibration data");
|
||||
}
|
||||
parentModule.setPipeline(index);
|
||||
parentModule.saveAndBroadcastAll();
|
||||
continue;
|
||||
case "startCalibration":
|
||||
try {
|
||||
var data =
|
||||
JacksonUtils.deserialize(
|
||||
(Map<String, Object>) newPropValue, UICalibrationData.class);
|
||||
parentModule.startCalibration(data);
|
||||
parentModule.saveAndBroadcastAll();
|
||||
} catch (Exception e) {
|
||||
logger.error("Error deserailizing start-cal request", e);
|
||||
}
|
||||
case "robotOffsetPoint" -> {
|
||||
if (currentSettings instanceof AdvancedPipelineSettings curAdvSettings) {
|
||||
robotOffsetPoint(curAdvSettings, (Integer) newPropValue);
|
||||
}
|
||||
continue;
|
||||
case "saveInputSnapshot":
|
||||
parentModule.saveInputSnapshot();
|
||||
continue;
|
||||
case "saveOutputSnapshot":
|
||||
parentModule.saveOutputSnapshot();
|
||||
continue;
|
||||
case "takeCalSnapshot":
|
||||
parentModule.takeCalibrationSnapshot();
|
||||
continue;
|
||||
case "duplicatePipeline":
|
||||
int idx = parentModule.pipelineManager.duplicatePipeline((Integer) newPropValue);
|
||||
parentModule.setPipeline(idx);
|
||||
parentModule.saveAndBroadcastAll();
|
||||
continue;
|
||||
case "calibrationUploaded":
|
||||
if (newPropValue instanceof CameraCalibrationCoefficients)
|
||||
parentModule.addCalibrationToConfig((CameraCalibrationCoefficients) newPropValue);
|
||||
continue;
|
||||
case "robotOffsetPoint":
|
||||
if (currentSettings instanceof AdvancedPipelineSettings) {
|
||||
var curAdvSettings = (AdvancedPipelineSettings) currentSettings;
|
||||
var offsetOperation = RobotOffsetPointOperation.fromIndex((int) newPropValue);
|
||||
var latestTarget = parentModule.lastPipelineResultBestTarget;
|
||||
|
||||
if (latestTarget != null) {
|
||||
var newPoint = latestTarget.getTargetOffsetPoint();
|
||||
|
||||
switch (curAdvSettings.offsetRobotOffsetMode) {
|
||||
case Single:
|
||||
if (offsetOperation == RobotOffsetPointOperation.ROPO_CLEAR) {
|
||||
curAdvSettings.offsetSinglePoint = new Point();
|
||||
} else if (offsetOperation == RobotOffsetPointOperation.ROPO_TAKESINGLE) {
|
||||
curAdvSettings.offsetSinglePoint = newPoint;
|
||||
}
|
||||
break;
|
||||
case Dual:
|
||||
if (offsetOperation == RobotOffsetPointOperation.ROPO_CLEAR) {
|
||||
curAdvSettings.offsetDualPointA = new Point();
|
||||
curAdvSettings.offsetDualPointAArea = 0;
|
||||
curAdvSettings.offsetDualPointB = new Point();
|
||||
curAdvSettings.offsetDualPointBArea = 0;
|
||||
} else {
|
||||
// update point and area
|
||||
switch (offsetOperation) {
|
||||
case ROPO_TAKEFIRSTDUAL:
|
||||
curAdvSettings.offsetDualPointA = newPoint;
|
||||
curAdvSettings.offsetDualPointAArea = latestTarget.getArea();
|
||||
break;
|
||||
case ROPO_TAKESECONDDUAL:
|
||||
curAdvSettings.offsetDualPointB = newPoint;
|
||||
curAdvSettings.offsetDualPointBArea = latestTarget.getArea();
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
continue;
|
||||
case "changePipelineType":
|
||||
}
|
||||
case "changePipelineType" -> {
|
||||
parentModule.changePipelineType((Integer) newPropValue);
|
||||
parentModule.saveAndBroadcastAll();
|
||||
continue;
|
||||
case "isDriverMode":
|
||||
parentModule.setDriverMode((Boolean) newPropValue);
|
||||
continue;
|
||||
}
|
||||
case "isDriverMode" -> parentModule.setDriverMode((Boolean) newPropValue);
|
||||
}
|
||||
|
||||
// special case for camera settables
|
||||
@@ -249,6 +158,104 @@ public class VisionModuleChangeSubscriber extends DataChangeSubscriber {
|
||||
}
|
||||
}
|
||||
|
||||
public void newPipelineNickname(String newNickname) {
|
||||
logger.info("Changing pipeline nickname to " + newNickname);
|
||||
parentModule.pipelineManager.getCurrentPipelineSettings().pipelineNickname = newNickname;
|
||||
parentModule.saveAndBroadcastAll();
|
||||
}
|
||||
|
||||
public void newPipelineInfo(Pair<String, PipelineType> typeName) {
|
||||
var type = typeName.getRight();
|
||||
var name = typeName.getLeft();
|
||||
|
||||
logger.info("Adding a " + type + " pipeline with name " + name);
|
||||
|
||||
var addedSettings = parentModule.pipelineManager.addPipeline(type);
|
||||
addedSettings.pipelineNickname = name;
|
||||
parentModule.saveAndBroadcastAll();
|
||||
}
|
||||
|
||||
public void deleteCurrPipeline() {
|
||||
var indexToDelete = parentModule.pipelineManager.getRequestedIndex();
|
||||
logger.info("Deleting current pipe at index " + indexToDelete);
|
||||
int newIndex = parentModule.pipelineManager.removePipeline(indexToDelete);
|
||||
parentModule.setPipeline(newIndex);
|
||||
parentModule.saveAndBroadcastAll();
|
||||
}
|
||||
|
||||
public void changePipeline(int index) {
|
||||
if (index == parentModule.pipelineManager.getRequestedIndex()) {
|
||||
logger.debug("Skipping pipeline change, index " + index + " already active");
|
||||
return;
|
||||
}
|
||||
parentModule.setPipeline(index);
|
||||
parentModule.saveAndBroadcastAll();
|
||||
}
|
||||
|
||||
public void startCalibration(Map<String, Object> data) {
|
||||
try {
|
||||
var deserialized = JacksonUtils.deserialize(data, UICalibrationData.class);
|
||||
parentModule.startCalibration(deserialized);
|
||||
parentModule.saveAndBroadcastAll();
|
||||
} catch (Exception e) {
|
||||
logger.error("Error deserailizing start-calibration request", e);
|
||||
}
|
||||
}
|
||||
|
||||
public void duplicatePipeline(int index) {
|
||||
var newIndex = parentModule.pipelineManager.duplicatePipeline(index);
|
||||
parentModule.setPipeline(newIndex);
|
||||
parentModule.saveAndBroadcastAll();
|
||||
}
|
||||
|
||||
public void robotOffsetPoint(AdvancedPipelineSettings curAdvSettings, int offsetIndex) {
|
||||
RobotOffsetPointOperation offsetOperation = RobotOffsetPointOperation.fromIndex(offsetIndex);
|
||||
|
||||
var latestTarget = parentModule.lastPipelineResultBestTarget;
|
||||
if (latestTarget == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
var newPoint = latestTarget.getTargetOffsetPoint();
|
||||
switch (curAdvSettings.offsetRobotOffsetMode) {
|
||||
case Single -> {
|
||||
switch (offsetOperation) {
|
||||
case CLEAR -> curAdvSettings.offsetSinglePoint = new Point();
|
||||
case TAKE_SINGLE -> curAdvSettings.offsetSinglePoint = newPoint;
|
||||
case TAKE_FIRST_DUAL, TAKE_SECOND_DUAL -> {
|
||||
logger.warn("Dual point operation in single point mode");
|
||||
}
|
||||
}
|
||||
}
|
||||
case Dual -> {
|
||||
switch (offsetOperation) {
|
||||
case CLEAR -> {
|
||||
curAdvSettings.offsetDualPointA = new Point();
|
||||
curAdvSettings.offsetDualPointAArea = 0;
|
||||
curAdvSettings.offsetDualPointB = new Point();
|
||||
curAdvSettings.offsetDualPointBArea = 0;
|
||||
}
|
||||
case TAKE_FIRST_DUAL -> {
|
||||
// update point and area
|
||||
curAdvSettings.offsetDualPointA = newPoint;
|
||||
curAdvSettings.offsetDualPointAArea = latestTarget.getArea();
|
||||
}
|
||||
case TAKE_SECOND_DUAL -> {
|
||||
// update point and area
|
||||
curAdvSettings.offsetDualPointB = newPoint;
|
||||
curAdvSettings.offsetDualPointBArea = latestTarget.getArea();
|
||||
}
|
||||
case TAKE_SINGLE -> {
|
||||
logger.warn("Single point operation in dual point mode");
|
||||
}
|
||||
}
|
||||
}
|
||||
case None -> {
|
||||
logger.warn("Robot offset point operation requested, but no offset mode set");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the value of a property in the given object using reflection. This method should not be
|
||||
* used generally and is only known to be correct in the context of `onDataChangeEvent`.
|
||||
@@ -281,8 +288,8 @@ public class VisionModuleChangeSubscriber extends DataChangeSubscriber {
|
||||
} else if (propType.equals(Integer.TYPE)) {
|
||||
propField.setInt(currentSettings, (Integer) newPropValue);
|
||||
} else if (propType.equals(Boolean.TYPE)) {
|
||||
if (newPropValue instanceof Integer) {
|
||||
propField.setBoolean(currentSettings, (Integer) newPropValue != 0);
|
||||
if (newPropValue instanceof Integer intValue) {
|
||||
propField.setBoolean(currentSettings, intValue != 0);
|
||||
} else {
|
||||
propField.setBoolean(currentSettings, (Boolean) newPropValue);
|
||||
}
|
||||
|
||||
@@ -84,8 +84,7 @@ public class VisionRunner {
|
||||
|
||||
frameSupplier.requestFrameThresholdType(wantedProcessType);
|
||||
var settings = pipeline.getSettings();
|
||||
if (settings instanceof AdvancedPipelineSettings) {
|
||||
var advanced = (AdvancedPipelineSettings) settings;
|
||||
if (settings instanceof AdvancedPipelineSettings advanced) {
|
||||
var hsvParams =
|
||||
new HSVPipe.HSVParams(
|
||||
advanced.hsvHue, advanced.hsvSaturation, advanced.hsvValue, advanced.hueInverted);
|
||||
|
||||
@@ -29,6 +29,7 @@ import org.photonvision.common.configuration.CameraConfiguration;
|
||||
import org.photonvision.common.configuration.ConfigManager;
|
||||
import org.photonvision.common.dataflow.DataChangeService;
|
||||
import org.photonvision.common.dataflow.events.OutgoingUIEvent;
|
||||
import org.photonvision.common.dataflow.websocket.UIPhotonConfiguration;
|
||||
import org.photonvision.common.hardware.Platform;
|
||||
import org.photonvision.common.hardware.Platform.OSType;
|
||||
import org.photonvision.common.logging.LogGroup;
|
||||
@@ -122,7 +123,8 @@ public class VisionSourceManager {
|
||||
DataChangeService.getInstance()
|
||||
.publishEvent(
|
||||
new OutgoingUIEvent<>(
|
||||
"fullsettings", ConfigManager.getInstance().getConfig().toHashMap()));
|
||||
"fullsettings",
|
||||
UIPhotonConfiguration.programStateToUi(ConfigManager.getInstance().getConfig())));
|
||||
}
|
||||
|
||||
protected List<VisionSource> tryMatchCamImpl() {
|
||||
|
||||
@@ -17,10 +17,10 @@
|
||||
package org.photonvision.vision.target;
|
||||
|
||||
public enum RobotOffsetPointOperation {
|
||||
ROPO_CLEAR(0),
|
||||
ROPO_TAKESINGLE(1),
|
||||
ROPO_TAKEFIRSTDUAL(2),
|
||||
ROPO_TAKESECONDDUAL(3);
|
||||
CLEAR(0),
|
||||
TAKE_SINGLE(1),
|
||||
TAKE_FIRST_DUAL(2),
|
||||
TAKE_SECOND_DUAL(3);
|
||||
|
||||
public final int index;
|
||||
|
||||
@@ -29,17 +29,12 @@ public enum RobotOffsetPointOperation {
|
||||
}
|
||||
|
||||
public static RobotOffsetPointOperation fromIndex(int index) {
|
||||
switch (index) {
|
||||
case 0:
|
||||
return ROPO_CLEAR;
|
||||
case 1:
|
||||
return ROPO_TAKESINGLE;
|
||||
case 2:
|
||||
return ROPO_TAKEFIRSTDUAL;
|
||||
case 3:
|
||||
return ROPO_TAKESECONDDUAL;
|
||||
default:
|
||||
return ROPO_CLEAR;
|
||||
}
|
||||
return switch (index) {
|
||||
case 0 -> CLEAR;
|
||||
case 1 -> TAKE_SINGLE;
|
||||
case 2 -> TAKE_FIRST_DUAL;
|
||||
case 3 -> TAKE_SECOND_DUAL;
|
||||
default -> CLEAR;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
plugins {
|
||||
id 'java'
|
||||
id "org.ysb33r.doxygen" version "0.7.0"
|
||||
id "org.ysb33r.doxygen" version "1.0.4"
|
||||
}
|
||||
|
||||
|
||||
@@ -36,15 +36,16 @@ doxygen {
|
||||
String arch = System.getProperty("os.arch");
|
||||
if (arch.equals("x86_64") || arch.equals("amd64")) {
|
||||
executables {
|
||||
doxygen version : '1.9.4',
|
||||
baseURI : 'https://frcmaven.wpi.edu/artifactory/generic-release-mirror/doxygen'
|
||||
doxygen {
|
||||
executableByVersion('1.12.0')
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
doxygen {
|
||||
generate_html true
|
||||
html_extra_stylesheet 'theme.css'
|
||||
option 'generate_html', true
|
||||
option 'html_extra_stylesheet', 'theme.css'
|
||||
|
||||
cppProjectZips.each {
|
||||
dependsOn it
|
||||
@@ -53,126 +54,37 @@ doxygen {
|
||||
cppIncludeRoots.add(it.absolutePath)
|
||||
}
|
||||
}
|
||||
cppIncludeRoots << '../ntcore/build/generated/main/native/include/'
|
||||
|
||||
if (project.hasProperty('docWarningsAsErrors')) {
|
||||
// Eigen
|
||||
exclude 'Eigen/**'
|
||||
exclude 'unsupported/**'
|
||||
|
||||
// LLVM
|
||||
exclude 'wpi/AlignOf.h'
|
||||
exclude 'wpi/Casting.h'
|
||||
exclude 'wpi/Chrono.h'
|
||||
exclude 'wpi/Compiler.h'
|
||||
exclude 'wpi/ConvertUTF.h'
|
||||
exclude 'wpi/DenseMap.h'
|
||||
exclude 'wpi/DenseMapInfo.h'
|
||||
exclude 'wpi/Endian.h'
|
||||
exclude 'wpi/EpochTracker.h'
|
||||
exclude 'wpi/Errc.h'
|
||||
exclude 'wpi/Errno.h'
|
||||
exclude 'wpi/ErrorHandling.h'
|
||||
exclude 'wpi/bit.h'
|
||||
exclude 'wpi/fs.h'
|
||||
exclude 'wpi/FunctionExtras.h'
|
||||
exclude 'wpi/function_ref.h'
|
||||
exclude 'wpi/Hashing.h'
|
||||
exclude 'wpi/iterator.h'
|
||||
exclude 'wpi/iterator_range.h'
|
||||
exclude 'wpi/ManagedStatic.h'
|
||||
exclude 'wpi/MapVector.h'
|
||||
exclude 'wpi/MathExtras.h'
|
||||
exclude 'wpi/MemAlloc.h'
|
||||
exclude 'wpi/PointerIntPair.h'
|
||||
exclude 'wpi/PointerLikeTypeTraits.h'
|
||||
exclude 'wpi/PointerUnion.h'
|
||||
exclude 'wpi/raw_os_ostream.h'
|
||||
exclude 'wpi/raw_ostream.h'
|
||||
exclude 'wpi/SmallPtrSet.h'
|
||||
exclude 'wpi/SmallSet.h'
|
||||
exclude 'wpi/SmallString.h'
|
||||
exclude 'wpi/SmallVector.h'
|
||||
exclude 'wpi/StringExtras.h'
|
||||
exclude 'wpi/StringMap.h'
|
||||
exclude 'wpi/SwapByteOrder.h'
|
||||
exclude 'wpi/type_traits.h'
|
||||
exclude 'wpi/VersionTuple.h'
|
||||
exclude 'wpi/WindowsError.h'
|
||||
|
||||
// fmtlib
|
||||
exclude 'fmt/**'
|
||||
|
||||
// libuv
|
||||
exclude 'uv.h'
|
||||
exclude 'uv/**'
|
||||
exclude 'wpinet/uv/**'
|
||||
|
||||
// json
|
||||
exclude 'wpi/adl_serializer.h'
|
||||
exclude 'wpi/byte_container_with_subtype.h'
|
||||
exclude 'wpi/detail/**'
|
||||
exclude 'wpi/json.h'
|
||||
exclude 'wpi/json_fwd.h'
|
||||
exclude 'wpi/ordered_map.h'
|
||||
exclude 'wpi/thirdparty/**'
|
||||
|
||||
// memory
|
||||
exclude 'wpi/memory/**'
|
||||
|
||||
// mpack
|
||||
exclude 'wpi/mpack.h'
|
||||
|
||||
// units
|
||||
exclude 'units/**'
|
||||
}
|
||||
|
||||
//TODO: building memory docs causes search to break
|
||||
exclude 'wpi/memory/**'
|
||||
|
||||
exclude '*.pb.h'
|
||||
|
||||
// Save space by excluding protobuf and eigen
|
||||
exclude 'Eigen/**'
|
||||
exclude 'google/protobuf/**'
|
||||
|
||||
aliases 'effects=\\par <i>Effects:</i>^^',
|
||||
'notes=\\par <i>Notes:</i>^^',
|
||||
'requires=\\par <i>Requires:</i>^^',
|
||||
'requiredbe=\\par <i>Required Behavior:</i>^^',
|
||||
'concept{2}=<a href=\"md_doc_concepts.html#\1\">\2</a>',
|
||||
'defaultbe=\\par <i>Default Behavior:</i>^^'
|
||||
case_sense_names false
|
||||
extension_mapping 'inc=C++', 'no_extension=C++'
|
||||
extract_all true
|
||||
extract_static true
|
||||
file_patterns '*'
|
||||
full_path_names true
|
||||
generate_html true
|
||||
generate_latex false
|
||||
generate_treeview true
|
||||
html_extra_stylesheet 'theme.css'
|
||||
html_timestamp true
|
||||
javadoc_autobrief true
|
||||
project_name 'PhotonVision C++'
|
||||
project_logo '../photon-client/src/assets/images/logoSmall.svg'
|
||||
project_number pubVersion
|
||||
quiet true
|
||||
recursive true
|
||||
strip_code_comments false
|
||||
strip_from_inc_path cppIncludeRoots as String[]
|
||||
strip_from_path cppIncludeRoots as String[]
|
||||
use_mathjax true
|
||||
warnings false
|
||||
warn_if_incomplete_doc true
|
||||
warn_if_undocumented false
|
||||
warn_no_paramdoc true
|
||||
option 'case_sense_names', false
|
||||
option 'extension_mapping', 'inc=C++ no_extension=C++'
|
||||
option 'extract_all', true
|
||||
option 'extract_static', true
|
||||
option 'file_patterns', '*'
|
||||
option 'full_path_names', true
|
||||
option 'generate_html', true
|
||||
option 'generate_latex', false
|
||||
option 'generate_treeview', true
|
||||
option 'html_extra_stylesheet', 'theme.css'
|
||||
option 'html_timestamp', true
|
||||
option 'javadoc_autobrief', true
|
||||
option 'project_name', 'PhotonVision C++'
|
||||
option 'project_logo', '../docs/source/assets/RoundLogo.png'
|
||||
option 'project_number', pubVersion
|
||||
option 'quiet', true
|
||||
option 'recursive', true
|
||||
option 'strip_code_comments', false
|
||||
option 'strip_from_inc_path', cppIncludeRoots
|
||||
option 'strip_from_path', cppIncludeRoots
|
||||
option 'use_mathjax', true
|
||||
option 'warnings', false
|
||||
option 'warn_if_incomplete_doc', true
|
||||
option 'warn_if_undocumented', false
|
||||
option 'warn_no_paramdoc', true
|
||||
|
||||
//enable doxygen preprocessor expansion of WPI_DEPRECATED to fix MotorController docs
|
||||
enable_preprocessing true
|
||||
macro_expansion true
|
||||
expand_only_predef true
|
||||
predefined "WPI_DEPRECATED(x)=[[deprecated(x)]]\"\\\n" +
|
||||
option 'enable_preprocessing', true
|
||||
option 'macro_expansion', true
|
||||
option 'expand_only_predef', true
|
||||
option 'predefined', "WPI_DEPRECATED(x)=[[deprecated(x)]]\"\\\n" +
|
||||
"\"__cplusplus\"\\\n" +
|
||||
"\"HAL_ENUM(name)=enum name : int32_t"
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,3 +1,4 @@
|
||||
import logging
|
||||
import math
|
||||
from typing import Any
|
||||
|
||||
@@ -11,6 +12,8 @@ from .rotTrlTransform3d import RotTrlTransform3d
|
||||
NWU_TO_EDN = Rotation3d(np.array([[0, -1, 0], [0, 0, -1], [1, 0, 0]]))
|
||||
EDN_TO_NWU = Rotation3d(np.array([[0, 0, 1], [-1, 0, 0], [0, -1, 0]]))
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class OpenCVHelp:
|
||||
@staticmethod
|
||||
@@ -243,7 +246,7 @@ class OpenCVHelp:
|
||||
|
||||
# solvePnP failed
|
||||
if reprojectionError is None or math.isnan(reprojectionError[0, 0]):
|
||||
print("SolvePNP_Square failed!")
|
||||
logger.error("SolvePNP_Square failed!")
|
||||
return None
|
||||
|
||||
if alt:
|
||||
@@ -303,6 +306,7 @@ class OpenCVHelp:
|
||||
)
|
||||
|
||||
if math.isnan(error):
|
||||
logger.error("SolvePNP_SQPNP failed!")
|
||||
return None
|
||||
|
||||
# We have no alternative so set it to best as well
|
||||
|
||||
@@ -42,9 +42,7 @@ class PhotonPipelineResultSerde:
|
||||
ret = Packet()
|
||||
|
||||
# metadata is of non-intrinsic type PhotonPipelineMetadata
|
||||
ret.encodeBytes(
|
||||
PhotonPipelineMetadata.photonStruct.pack(value.metadata).getData()
|
||||
)
|
||||
ret.encodeBytes(PhotonPipelineMetadata.photonStruct.pack(value.metadata).getData())
|
||||
|
||||
# targets is a custom VLA!
|
||||
ret.encodeList(value.targets, PhotonTrackedTarget.photonStruct)
|
||||
|
||||
@@ -17,10 +17,11 @@ class NTTopicSet:
|
||||
different for sim vs. real camera
|
||||
"""
|
||||
|
||||
def __init__(self, tableName: str, cameraName: str) -> None:
|
||||
instance = nt.NetworkTableInstance.getDefault()
|
||||
photonvision_root_table = instance.getTable(tableName)
|
||||
self.subTable = photonvision_root_table.getSubTable(cameraName)
|
||||
def __init__(
|
||||
self,
|
||||
ntSubTable: nt.NetworkTable,
|
||||
) -> None:
|
||||
self.subTable = ntSubTable
|
||||
|
||||
def updateEntries(self) -> None:
|
||||
options = nt.PubSubOptions()
|
||||
|
||||
@@ -298,43 +298,42 @@ class PhotonCamera:
|
||||
|
||||
# Check mdef UUID
|
||||
localUUID = PhotonPipelineResult.photonStruct.MESSAGE_VERSION
|
||||
remoteUUID = str(self._rawBytesEntry.getTopic().getProperty("message_uuid"))
|
||||
remoteUUID = self._rawBytesEntry.getTopic().getProperty("message_uuid")
|
||||
|
||||
if not remoteUUID:
|
||||
if remoteUUID is None:
|
||||
wpilib.reportWarning(
|
||||
f"PhotonVision coprocessor at path {self._path} has not reported a message interface UUID - is your coprocessor's camera started?",
|
||||
True,
|
||||
)
|
||||
else:
|
||||
# ntcore hands us a JSON string with leading/trailing quotes - remove those
|
||||
remoteUUID = str(remoteUUID).replace('"', "")
|
||||
|
||||
assert isinstance(remoteUUID, str)
|
||||
# ntcore hands us a JSON string with leading/trailing quotes - remove those
|
||||
remoteUUID = remoteUUID.replace('"', "")
|
||||
if localUUID != remoteUUID:
|
||||
# Verified version mismatch
|
||||
|
||||
if localUUID != remoteUUID:
|
||||
# Verified version mismatch
|
||||
bfw = """
|
||||
\n\n\n
|
||||
>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
|
||||
>>> !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
||||
>>>
|
||||
>>> You are running an incompatible version
|
||||
>>> of PhotonVision on your coprocessor!
|
||||
>>>
|
||||
>>> This is neither tested nor supported.
|
||||
>>> You MUST update PhotonVision,
|
||||
>>> PhotonLib, or both.
|
||||
>>>
|
||||
>>> Your code will now crash.
|
||||
>>> We hope your day gets better.
|
||||
>>>
|
||||
>>> !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
||||
>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
|
||||
\n\n
|
||||
"""
|
||||
|
||||
bfw = """
|
||||
\n\n\n
|
||||
>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
|
||||
>>> !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
||||
>>>
|
||||
>>> You are running an incompatible version
|
||||
>>> of PhotonVision on your coprocessor!
|
||||
>>>
|
||||
>>> This is neither tested nor supported.
|
||||
>>> You MUST update PhotonVision,
|
||||
>>> PhotonLib, or both.
|
||||
>>>
|
||||
>>> Your code will now crash.
|
||||
>>> We hope your day gets better.
|
||||
>>>
|
||||
>>> !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
||||
>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
|
||||
\n\n
|
||||
"""
|
||||
wpilib.reportWarning(bfw)
|
||||
|
||||
wpilib.reportWarning(bfw)
|
||||
|
||||
errText = f"Photonlibpy version {PHOTONLIB_VERSION} (With message UUID {localUUID}) does not match coprocessor version {versionString} (with message UUID {remoteUUID}). Please install photonlibpy version {versionString}, or update your coprocessor to {PHOTONLIB_VERSION}."
|
||||
wpilib.reportError(errText, True)
|
||||
raise Exception(errText)
|
||||
errText = f"Photonlibpy version {PHOTONLIB_VERSION} (With message UUID {localUUID}) does not match coprocessor version {versionString} (with message UUID {remoteUUID}). Please install photonlibpy version {versionString}, or update your coprocessor to {PHOTONLIB_VERSION}."
|
||||
wpilib.reportError(errText, True)
|
||||
raise Exception(errText)
|
||||
|
||||
1
photon-lib/py/photonlibpy/py.typed
Normal file
1
photon-lib/py/photonlibpy/py.typed
Normal file
@@ -0,0 +1 @@
|
||||
|
||||
@@ -60,11 +60,10 @@ class PhotonCameraSim:
|
||||
self.videoSimRawEnabled: bool = False
|
||||
self.videoSimWireframeEnabled: bool = False
|
||||
self.videoSimWireframeResolution: float = 0.1
|
||||
self.videoSimProcEnabled: bool = (
|
||||
False # TODO switch this back to default True when the functionality is enabled
|
||||
)
|
||||
# TODO switch this back to default True when the functionality is enabled
|
||||
self.videoSimProcEnabled: bool = False
|
||||
self.heartbeatCounter: int = 0
|
||||
self.nextNtEntryTime = int(wpilib.Timer.getFPGATimestamp() * 1e6)
|
||||
self.nextNtEntryTime = wpilib.Timer.getFPGATimestamp()
|
||||
self.tagLayout = AprilTagFieldLayout.loadField(AprilTagField.k2024Crescendo)
|
||||
|
||||
self.cam = camera
|
||||
@@ -95,7 +94,7 @@ class PhotonCameraSim:
|
||||
(self.prop.getResWidth(), self.prop.getResHeight())
|
||||
)
|
||||
|
||||
self.ts = NTTopicSet("photonvision", self.cam.getName())
|
||||
self.ts = NTTopicSet(self.cam._cameraTable)
|
||||
self.ts.updateEntries()
|
||||
|
||||
# Handle this last explicitly for this function signature because the other constructor is called in the initialiser list
|
||||
@@ -173,20 +172,20 @@ class PhotonCameraSim:
|
||||
def consumeNextEntryTime(self) -> float | None:
|
||||
"""Determine if this camera should process a new frame based on performance metrics and the time
|
||||
since the last update. This returns an Optional which is either empty if no update should occur
|
||||
or a Long of the timestamp in microseconds of when the frame which should be received by NT. If
|
||||
or a float of the timestamp in seconds of when the frame which should be received by NT. If
|
||||
a timestamp is returned, the last frame update time becomes that timestamp.
|
||||
|
||||
:returns: Optional long which is empty while blocked or the NT entry timestamp in microseconds if
|
||||
:returns: Optional float which is empty while blocked or the NT entry timestamp in seconds if
|
||||
ready
|
||||
"""
|
||||
# check if this camera is ready for another frame update
|
||||
now = int(wpilib.Timer.getFPGATimestamp() * 1e6)
|
||||
timestamp = 0
|
||||
now = wpilib.Timer.getFPGATimestamp()
|
||||
timestamp = 0.0
|
||||
iter = 0
|
||||
# prepare next latest update
|
||||
while now >= self.nextNtEntryTime:
|
||||
timestamp = int(self.nextNtEntryTime)
|
||||
frameTime = int(self.prop.estSecUntilNextFrame() * 1e6)
|
||||
timestamp = self.nextNtEntryTime
|
||||
frameTime = self.prop.estSecUntilNextFrame()
|
||||
self.nextNtEntryTime += frameTime
|
||||
|
||||
# if frame time is very small, avoid blocking
|
||||
@@ -432,7 +431,9 @@ class PhotonCameraSim:
|
||||
)
|
||||
|
||||
def submitProcessedFrame(
|
||||
self, result: PhotonPipelineResult, receiveTimestamp: float | None
|
||||
self,
|
||||
result: PhotonPipelineResult,
|
||||
receiveTimestamp_us: float | None = None,
|
||||
):
|
||||
"""Simulate one processed frame of vision data, putting one result to NT. Image capture timestamp
|
||||
overrides :meth:`.PhotonPipelineResult.getTimestampSeconds` for more
|
||||
@@ -441,44 +442,45 @@ class PhotonCameraSim:
|
||||
:param result: The pipeline result to submit
|
||||
:param receiveTimestamp: The (sim) timestamp when this result was read by NT in microseconds. If not passed image capture time is assumed be (current time - latency)
|
||||
"""
|
||||
if receiveTimestamp is None:
|
||||
receiveTimestamp = wpilib.Timer.getFPGATimestamp() * 1e6
|
||||
receiveTimestamp = int(receiveTimestamp)
|
||||
if receiveTimestamp_us is None:
|
||||
receiveTimestamp_us = wpilib.Timer.getFPGATimestamp() * 1e6
|
||||
receiveTimestamp_us = int(receiveTimestamp_us)
|
||||
|
||||
self.ts.latencyMillisEntry.set(result.getLatencyMillis(), receiveTimestamp)
|
||||
self.ts.latencyMillisEntry.set(result.getLatencyMillis(), receiveTimestamp_us)
|
||||
|
||||
newPacket = PhotonPipelineResult.photonStruct.pack(result)
|
||||
self.ts.rawBytesEntry.set(newPacket.getData(), receiveTimestamp)
|
||||
self.ts.rawBytesEntry.set(newPacket.getData(), receiveTimestamp_us)
|
||||
|
||||
hasTargets = result.hasTargets()
|
||||
self.ts.hasTargetEntry.set(hasTargets, receiveTimestamp)
|
||||
self.ts.hasTargetEntry.set(hasTargets, receiveTimestamp_us)
|
||||
if not hasTargets:
|
||||
self.ts.targetPitchEntry.set(0.0, receiveTimestamp)
|
||||
self.ts.targetYawEntry.set(0.0, receiveTimestamp)
|
||||
self.ts.targetAreaEntry.set(0.0, receiveTimestamp)
|
||||
self.ts.targetPoseEntry.set(Transform3d(), receiveTimestamp)
|
||||
self.ts.targetSkewEntry.set(0.0, receiveTimestamp)
|
||||
self.ts.targetPitchEntry.set(0.0, receiveTimestamp_us)
|
||||
self.ts.targetYawEntry.set(0.0, receiveTimestamp_us)
|
||||
self.ts.targetAreaEntry.set(0.0, receiveTimestamp_us)
|
||||
self.ts.targetPoseEntry.set(Transform3d(), receiveTimestamp_us)
|
||||
self.ts.targetSkewEntry.set(0.0, receiveTimestamp_us)
|
||||
else:
|
||||
bestTarget = result.getBestTarget()
|
||||
assert bestTarget
|
||||
|
||||
self.ts.targetPitchEntry.set(bestTarget.getPitch(), receiveTimestamp)
|
||||
self.ts.targetYawEntry.set(bestTarget.getYaw(), receiveTimestamp)
|
||||
self.ts.targetAreaEntry.set(bestTarget.getArea(), receiveTimestamp)
|
||||
self.ts.targetSkewEntry.set(bestTarget.getSkew(), receiveTimestamp)
|
||||
self.ts.targetPitchEntry.set(bestTarget.getPitch(), receiveTimestamp_us)
|
||||
self.ts.targetYawEntry.set(bestTarget.getYaw(), receiveTimestamp_us)
|
||||
self.ts.targetAreaEntry.set(bestTarget.getArea(), receiveTimestamp_us)
|
||||
self.ts.targetSkewEntry.set(bestTarget.getSkew(), receiveTimestamp_us)
|
||||
|
||||
self.ts.targetPoseEntry.set(
|
||||
bestTarget.getBestCameraToTarget(), receiveTimestamp
|
||||
bestTarget.getBestCameraToTarget(), receiveTimestamp_us
|
||||
)
|
||||
|
||||
intrinsics = self.prop.getIntrinsics()
|
||||
intrinsicsView = intrinsics.flatten().tolist()
|
||||
self.ts.cameraIntrinsicsPublisher.set(intrinsicsView, receiveTimestamp)
|
||||
intrinsics = self.prop.getIntrinsics()
|
||||
intrinsicsView = intrinsics.flatten().tolist()
|
||||
self.ts.cameraIntrinsicsPublisher.set(intrinsicsView, receiveTimestamp_us)
|
||||
|
||||
distortion = self.prop.getDistCoeffs()
|
||||
distortionView = distortion.flatten().tolist()
|
||||
self.ts.cameraDistortionPublisher.set(distortionView, receiveTimestamp)
|
||||
distortion = self.prop.getDistCoeffs()
|
||||
distortionView = distortion.flatten().tolist()
|
||||
self.ts.cameraDistortionPublisher.set(distortionView, receiveTimestamp_us)
|
||||
|
||||
self.ts.heartbeatPublisher.set(self.heartbeatCounter, receiveTimestamp)
|
||||
self.ts.heartbeatPublisher.set(self.heartbeatCounter, receiveTimestamp_us)
|
||||
self.heartbeatCounter += 1
|
||||
|
||||
self.ts.subTable.getInstance().flush()
|
||||
self.ts.subTable.getInstance().flush()
|
||||
|
||||
@@ -4,11 +4,14 @@ import typing
|
||||
|
||||
import cv2 as cv
|
||||
import numpy as np
|
||||
import numpy.typing as npt
|
||||
from wpimath.geometry import Rotation2d, Rotation3d, Translation3d
|
||||
from wpimath.units import hertz, seconds
|
||||
|
||||
from ..estimation import RotTrlTransform3d
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class SimCameraProperties:
|
||||
"""Calibration and performance values for this camera.
|
||||
@@ -29,8 +32,8 @@ class SimCameraProperties:
|
||||
"""Default constructor which is the same as {@link #PERFECT_90DEG}"""
|
||||
self.resWidth: int = -1
|
||||
self.resHeight: int = -1
|
||||
self.camIntrinsics: np.ndarray = np.zeros((3, 3)) # [3,3]
|
||||
self.distCoeffs: np.ndarray = np.zeros((8, 1)) # [8,1]
|
||||
self.camIntrinsics: npt.NDArray[np.floating] = np.zeros((3, 3)) # [3,3]
|
||||
self.distCoeffs: npt.NDArray[np.floating] = np.zeros((8, 1)) # [8,1]
|
||||
self.avgErrorPx: float = 0.0
|
||||
self.errorStdDevPx: float = 0.0
|
||||
self.frameSpeed: seconds = 0.0
|
||||
@@ -46,7 +49,7 @@ class SimCameraProperties:
|
||||
) -> None:
|
||||
if fovDiag.degrees() < 1.0 or fovDiag.degrees() > 179.0:
|
||||
fovDiag = Rotation2d.fromDegrees(max(min(fovDiag.degrees(), 179.0), 1.0))
|
||||
logging.error("Requested invalid FOV! Clamping between (1, 179) degrees...")
|
||||
logger.error("Requested invalid FOV! Clamping between (1, 179) degrees...")
|
||||
|
||||
resDiag = math.sqrt(width * width + height * height)
|
||||
diagRatio = math.tan(fovDiag.radians() / 2.0)
|
||||
@@ -78,7 +81,6 @@ class SimCameraProperties:
|
||||
newCamIntrinsics: np.ndarray,
|
||||
newDistCoeffs: np.ndarray,
|
||||
) -> None:
|
||||
|
||||
self.resWidth = width
|
||||
self.resHeight = height
|
||||
self.camIntrinsics = newCamIntrinsics
|
||||
@@ -171,10 +173,10 @@ class SimCameraProperties:
|
||||
def getAspectRatio(self) -> float:
|
||||
return 1.0 * self.resWidth / self.resHeight
|
||||
|
||||
def getIntrinsics(self) -> np.ndarray:
|
||||
def getIntrinsics(self) -> npt.NDArray[np.floating]:
|
||||
return self.camIntrinsics
|
||||
|
||||
def getDistCoeffs(self) -> np.ndarray:
|
||||
def getDistCoeffs(self) -> npt.NDArray[np.floating]:
|
||||
return self.distCoeffs
|
||||
|
||||
def getFPS(self) -> hertz:
|
||||
@@ -353,7 +355,6 @@ class SimCameraProperties:
|
||||
|
||||
# find intersections
|
||||
for i, normal in enumerate(self.viewplanes):
|
||||
|
||||
# // we want to know the value of t when the line intercepts this plane
|
||||
# // parametrized: v = t * ab + a, where v lies on the plane
|
||||
# // we can find the projection of a onto the plane normal
|
||||
@@ -465,7 +466,7 @@ class SimCameraProperties:
|
||||
|
||||
def estSecUntilNextFrame(self) -> seconds:
|
||||
"""
|
||||
:returns: Estimate how long until the next frame should be processed in milliseconds
|
||||
:returns: Estimate how long until the next frame should be processed in seconds
|
||||
"""
|
||||
# // exceptional processing latency blocks the next frame
|
||||
return self.frameSpeed + max(0.0, self.estLatency() - self.frameSpeed)
|
||||
|
||||
@@ -305,7 +305,7 @@ class VisionSystemSim:
|
||||
timestampNt = optTimestamp
|
||||
latency = camSim.prop.estLatency()
|
||||
# the image capture timestamp in seconds of this result
|
||||
timestampCapture = timestampNt * 1.0e-6 - latency
|
||||
timestampCapture = timestampNt - latency
|
||||
|
||||
# use camera pose from the image capture timestamp
|
||||
lateRobotPose = self.getRobotPose(timestampCapture)
|
||||
@@ -318,7 +318,8 @@ class VisionSystemSim:
|
||||
# process a PhotonPipelineResult with visible targets
|
||||
camResult = camSim.process(latency, lateCameraPose, allTargets)
|
||||
# publish this info to NT at estimated timestamp of receive
|
||||
camSim.submitProcessedFrame(camResult, timestampNt)
|
||||
# needs a timestamp in microseconds
|
||||
camSim.submitProcessedFrame(camResult, timestampNt * 1.0e6)
|
||||
# display debug results
|
||||
for tgt in camResult.getTargets():
|
||||
trf = tgt.getBestCameraToTarget()
|
||||
|
||||
@@ -29,7 +29,7 @@ if m:
|
||||
split = gitDescribeResult.split("-")
|
||||
if len(split) == 3:
|
||||
year, commits, sha = split
|
||||
# Chop off leading v from "v2024.1.2", and use "post" for commits to master since
|
||||
# Chop off leading v from "v2024.1.2", and use "post" for commits to main since
|
||||
versionString = f"{year[1:]}post{commits}"
|
||||
print("using dev release " + versionString)
|
||||
else:
|
||||
@@ -55,6 +55,7 @@ descriptionStr = f"Pure-python implementation of PhotonLib for interfacing with
|
||||
setup(
|
||||
name="photonlibpy",
|
||||
packages=find_packages(),
|
||||
package_data={"photonlibpy": ["py.typed"]},
|
||||
version=versionString,
|
||||
install_requires=[
|
||||
"numpy~=2.1",
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import math
|
||||
|
||||
import ntcore as nt
|
||||
import pytest
|
||||
from photonlibpy.estimation import TargetModel, VisionEstimation
|
||||
from photonlibpy.photonCamera import PhotonCamera, setVersionCheckEnabled
|
||||
from photonlibpy.photonCamera import PhotonCamera
|
||||
from photonlibpy.simulation import PhotonCameraSim, VisionSystemSim, VisionTargetSim
|
||||
from robotpy_apriltag import AprilTag, AprilTagFieldLayout
|
||||
from wpimath.geometry import (
|
||||
@@ -18,12 +17,6 @@ from wpimath.geometry import (
|
||||
from wpimath.units import feetToMeters, meters
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def setupCommon() -> None:
|
||||
nt.NetworkTableInstance.getDefault().startServer()
|
||||
setVersionCheckEnabled(False)
|
||||
|
||||
|
||||
def test_VisibilityCupidShuffle() -> None:
|
||||
targetPose = Pose3d(Translation3d(15.98, 0.0, 2.0), Rotation3d(0, 0, math.pi))
|
||||
|
||||
@@ -32,6 +25,8 @@ def test_VisibilityCupidShuffle() -> None:
|
||||
cameraSim = PhotonCameraSim(camera)
|
||||
visionSysSim.addCamera(cameraSim, Transform3d())
|
||||
|
||||
# Set massive FPS so timing isn't an issue
|
||||
cameraSim.prop.setFPS(1e6)
|
||||
cameraSim.prop.setCalibrationFromFOV(640, 480, fovDiag=Rotation2d.fromDegrees(80.0))
|
||||
|
||||
visionSysSim.addVisionTargets(
|
||||
@@ -93,6 +88,8 @@ def test_NotVisibleVert1() -> None:
|
||||
cameraSim = PhotonCameraSim(camera)
|
||||
visionSysSim.addCamera(cameraSim, Transform3d())
|
||||
|
||||
# Set massive FPS so timing isn't an issue
|
||||
cameraSim.prop.setFPS(1e6)
|
||||
cameraSim.prop.setCalibrationFromFOV(640, 480, fovDiag=Rotation2d.fromDegrees(80.0))
|
||||
|
||||
visionSysSim.addVisionTargets(
|
||||
@@ -128,6 +125,8 @@ def test_NotVisibleVert2() -> None:
|
||||
cameraSim = PhotonCameraSim(camera)
|
||||
visionSysSim.addCamera(cameraSim, robotToCamera)
|
||||
|
||||
# Set massive FPS so timing isn't an issue
|
||||
cameraSim.prop.setFPS(1e6)
|
||||
cameraSim.prop.setCalibrationFromFOV(
|
||||
4774, 4774, fovDiag=Rotation2d.fromDegrees(80.0)
|
||||
)
|
||||
@@ -156,6 +155,8 @@ def test_NotVisibleTargetSize() -> None:
|
||||
cameraSim = PhotonCameraSim(camera)
|
||||
visionSysSim.addCamera(cameraSim, Transform3d())
|
||||
|
||||
# Set massive FPS so timing isn't an issue
|
||||
cameraSim.prop.setFPS(1e6)
|
||||
cameraSim.prop.setCalibrationFromFOV(640, 480, fovDiag=Rotation2d.fromDegrees(80.0))
|
||||
cameraSim.setMinTargetAreaPixels(20.0)
|
||||
visionSysSim.addVisionTargets(
|
||||
@@ -183,6 +184,8 @@ def test_NotVisibleTooFarLeds() -> None:
|
||||
cameraSim = PhotonCameraSim(camera)
|
||||
visionSysSim.addCamera(cameraSim, Transform3d())
|
||||
|
||||
# Set massive FPS so timing isn't an issue
|
||||
cameraSim.prop.setFPS(1e6)
|
||||
cameraSim.prop.setCalibrationFromFOV(640, 480, fovDiag=Rotation2d.fromDegrees(80.0))
|
||||
cameraSim.setMinTargetAreaPixels(1.0)
|
||||
cameraSim.setMaxSightRange(10.0)
|
||||
@@ -216,6 +219,9 @@ def test_YawAngles(expected_yaw) -> None:
|
||||
cameraSim = PhotonCameraSim(camera)
|
||||
|
||||
visionSysSim.addCamera(cameraSim, Transform3d())
|
||||
|
||||
# Set massive FPS so timing isn't an issue
|
||||
cameraSim.prop.setFPS(1e6)
|
||||
cameraSim.prop.setCalibrationFromFOV(640, 480, fovDiag=Rotation2d.fromDegrees(80.0))
|
||||
cameraSim.setMinTargetAreaPixels(0.0)
|
||||
visionSysSim.addVisionTargets(
|
||||
@@ -250,6 +256,9 @@ def test_PitchAngles(expected_pitch) -> None:
|
||||
camera = PhotonCamera("camera")
|
||||
cameraSim = PhotonCameraSim(camera)
|
||||
visionSysSim.addCamera(cameraSim, Transform3d())
|
||||
|
||||
# Set massive FPS so timing isn't an issue
|
||||
cameraSim.prop.setFPS(1e6)
|
||||
cameraSim.prop.setCalibrationFromFOV(
|
||||
640, 480, fovDiag=Rotation2d.fromDegrees(120.0)
|
||||
)
|
||||
@@ -316,8 +325,10 @@ def test_distanceCalc(distParam, pitchParam, heightParam) -> None:
|
||||
)
|
||||
camera = PhotonCamera("camera")
|
||||
cameraSim = PhotonCameraSim(camera)
|
||||
|
||||
visionSysSim.addCamera(cameraSim, Transform3d())
|
||||
|
||||
# Set massive FPS so timing isn't an issue
|
||||
cameraSim.prop.setFPS(1e6)
|
||||
cameraSim.prop.setCalibrationFromFOV(
|
||||
640, 480, fovDiag=Rotation2d.fromDegrees(160.0)
|
||||
)
|
||||
@@ -354,6 +365,9 @@ def test_MultipleTargets() -> None:
|
||||
camera = PhotonCamera("camera")
|
||||
cameraSim = PhotonCameraSim(camera)
|
||||
visionSysSim.addCamera(cameraSim, Transform3d())
|
||||
|
||||
# Set massive FPS so timing isn't an issue
|
||||
cameraSim.prop.setFPS(1e6)
|
||||
cameraSim.prop.setCalibrationFromFOV(640, 480, fovDiag=Rotation2d.fromDegrees(80.0))
|
||||
cameraSim.setMinTargetAreaPixels(20.0)
|
||||
|
||||
@@ -451,6 +465,9 @@ def test_PoseEstimation() -> None:
|
||||
camera = PhotonCamera("camera")
|
||||
cameraSim = PhotonCameraSim(camera)
|
||||
visionSysSim.addCamera(cameraSim, Transform3d())
|
||||
|
||||
# Set massive FPS so timing isn't an issue
|
||||
cameraSim.prop.setFPS(1e6)
|
||||
cameraSim.prop.setCalibrationFromFOV(640, 480, fovDiag=Rotation2d.fromDegrees(90.0))
|
||||
cameraSim.setMinTargetAreaPixels(20.0)
|
||||
|
||||
@@ -525,6 +542,9 @@ def test_PoseEstimationRotated() -> None:
|
||||
camera = PhotonCamera("camera")
|
||||
cameraSim = PhotonCameraSim(camera)
|
||||
visionSysSim.addCamera(cameraSim, robotToCamera)
|
||||
|
||||
# Set massive FPS so timing isn't an issue
|
||||
cameraSim.prop.setFPS(1e6)
|
||||
cameraSim.prop.setCalibrationFromFOV(640, 480, fovDiag=Rotation2d.fromDegrees(90.0))
|
||||
cameraSim.setMinTargetAreaPixels(20.0)
|
||||
|
||||
|
||||
@@ -298,17 +298,12 @@ public class PhotonCamera implements AutoCloseable {
|
||||
*/
|
||||
public VisionLEDMode getLEDMode() {
|
||||
int value = (int) ledModeState.get(-1);
|
||||
switch (value) {
|
||||
case 0:
|
||||
return VisionLEDMode.kOff;
|
||||
case 1:
|
||||
return VisionLEDMode.kOn;
|
||||
case 2:
|
||||
return VisionLEDMode.kBlink;
|
||||
case -1:
|
||||
default:
|
||||
return VisionLEDMode.kDefault;
|
||||
}
|
||||
return switch (value) {
|
||||
case 0 -> VisionLEDMode.kOff;
|
||||
case 1 -> VisionLEDMode.kOn;
|
||||
case 2 -> VisionLEDMode.kBlink;
|
||||
default -> VisionLEDMode.kDefault;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -345,46 +345,35 @@ public class PhotonPoseEstimator {
|
||||
PhotonPipelineResult cameraResult,
|
||||
Optional<Matrix<N3, N3>> cameraMatrix,
|
||||
Optional<Matrix<N8, N1>> distCoeffs,
|
||||
PoseStrategy strat) {
|
||||
Optional<EstimatedRobotPose> estimatedPose = Optional.empty();
|
||||
switch (strat) {
|
||||
case LOWEST_AMBIGUITY:
|
||||
estimatedPose = lowestAmbiguityStrategy(cameraResult);
|
||||
break;
|
||||
case CLOSEST_TO_CAMERA_HEIGHT:
|
||||
estimatedPose = closestToCameraHeightStrategy(cameraResult);
|
||||
break;
|
||||
case CLOSEST_TO_REFERENCE_POSE:
|
||||
estimatedPose = closestToReferencePoseStrategy(cameraResult, referencePose);
|
||||
break;
|
||||
case CLOSEST_TO_LAST_POSE:
|
||||
setReferencePose(lastPose);
|
||||
estimatedPose = closestToReferencePoseStrategy(cameraResult, referencePose);
|
||||
break;
|
||||
case AVERAGE_BEST_TARGETS:
|
||||
estimatedPose = averageBestTargetsStrategy(cameraResult);
|
||||
break;
|
||||
case MULTI_TAG_PNP_ON_RIO:
|
||||
if (cameraMatrix.isEmpty()) {
|
||||
DriverStation.reportWarning(
|
||||
"Camera matrix is empty for multi-tag-on-rio",
|
||||
Thread.currentThread().getStackTrace());
|
||||
} else if (distCoeffs.isEmpty()) {
|
||||
DriverStation.reportWarning(
|
||||
"Camera matrix is empty for multi-tag-on-rio",
|
||||
Thread.currentThread().getStackTrace());
|
||||
} else {
|
||||
estimatedPose = multiTagOnRioStrategy(cameraResult, cameraMatrix, distCoeffs);
|
||||
}
|
||||
break;
|
||||
case MULTI_TAG_PNP_ON_COPROCESSOR:
|
||||
estimatedPose = multiTagOnCoprocStrategy(cameraResult);
|
||||
break;
|
||||
default:
|
||||
DriverStation.reportError(
|
||||
"[PhotonPoseEstimator] Unknown Position Estimation Strategy!", false);
|
||||
return Optional.empty();
|
||||
}
|
||||
PoseStrategy strategy) {
|
||||
Optional<EstimatedRobotPose> estimatedPose =
|
||||
switch (strategy) {
|
||||
case LOWEST_AMBIGUITY -> lowestAmbiguityStrategy(cameraResult);
|
||||
case CLOSEST_TO_CAMERA_HEIGHT -> closestToCameraHeightStrategy(cameraResult);
|
||||
case CLOSEST_TO_REFERENCE_POSE ->
|
||||
closestToReferencePoseStrategy(cameraResult, referencePose);
|
||||
case CLOSEST_TO_LAST_POSE -> {
|
||||
setReferencePose(lastPose);
|
||||
yield closestToReferencePoseStrategy(cameraResult, referencePose);
|
||||
}
|
||||
case AVERAGE_BEST_TARGETS -> averageBestTargetsStrategy(cameraResult);
|
||||
case MULTI_TAG_PNP_ON_RIO -> {
|
||||
if (cameraMatrix.isEmpty()) {
|
||||
DriverStation.reportWarning(
|
||||
"Camera matrix is empty for multi-tag-on-rio",
|
||||
Thread.currentThread().getStackTrace());
|
||||
yield Optional.empty();
|
||||
} else if (distCoeffs.isEmpty()) {
|
||||
DriverStation.reportWarning(
|
||||
"Camera matrix is empty for multi-tag-on-rio",
|
||||
Thread.currentThread().getStackTrace());
|
||||
yield Optional.empty();
|
||||
} else {
|
||||
yield multiTagOnRioStrategy(cameraResult, cameraMatrix, distCoeffs);
|
||||
}
|
||||
}
|
||||
case MULTI_TAG_PNP_ON_COPROCESSOR -> multiTagOnCoprocStrategy(cameraResult);
|
||||
};
|
||||
|
||||
if (estimatedPose.isPresent()) {
|
||||
lastPose = estimatedPose.get().estimatedPose;
|
||||
|
||||
@@ -84,11 +84,9 @@ public class VisionTargetSim {
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
if (this == obj) return true;
|
||||
if (obj instanceof VisionTargetSim) {
|
||||
var o = (VisionTargetSim) obj;
|
||||
return pose.equals(o.pose) && model.equals(o.model);
|
||||
}
|
||||
return false;
|
||||
return this == obj
|
||||
&& obj instanceof VisionTargetSim o
|
||||
&& pose.equals(o.pose)
|
||||
&& model.equals(o.model);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -305,6 +305,7 @@ void PhotonCamera::VerifyVersion() {
|
||||
FRC_ReportError(frc::warn::Warning,
|
||||
"Cannot find property message_uuid for PhotonCamera {}",
|
||||
path);
|
||||
return;
|
||||
}
|
||||
std::string remote_uuid{remote_uuid_json};
|
||||
|
||||
|
||||
@@ -6,6 +6,8 @@ bool:
|
||||
cpp_type: bool
|
||||
java_decode_method: decodeBoolean
|
||||
java_encode_shim: encodeBoolean
|
||||
python_decode_shim: decodeBoolean
|
||||
python_encode_shim: encodeBoolean
|
||||
int16:
|
||||
len: 2
|
||||
java_type: short
|
||||
@@ -13,27 +15,37 @@ int16:
|
||||
java_decode_method: decodeShort
|
||||
java_list_decode_method: decodeShortList
|
||||
java_encode_shim: encodeShort
|
||||
python_decode_shim: decodeShort
|
||||
python_encode_shim: encodeShort
|
||||
int32:
|
||||
len: 4
|
||||
java_type: int
|
||||
cpp_type: int32_t
|
||||
java_decode_method: decodeInt
|
||||
java_encode_shim: encodeInt
|
||||
python_decode_shim: decodeInt
|
||||
python_encode_shim: encodeInt
|
||||
int64:
|
||||
len: 8
|
||||
java_type: long
|
||||
cpp_type: int64_t
|
||||
java_decode_method: decodeLong
|
||||
java_encode_shim: encodeLong
|
||||
python_decode_shim: decodeLong
|
||||
python_encode_shim: encodeLong
|
||||
float32:
|
||||
len: 4
|
||||
java_type: float
|
||||
cpp_type: float
|
||||
java_decode_method: decodeFloat
|
||||
java_encode_shim: encodeFloat
|
||||
python_decode_shim: decodeFloat
|
||||
python_encode_shim: encodeFloat
|
||||
float64:
|
||||
len: 8
|
||||
java_type: double
|
||||
cpp_type: double
|
||||
java_decode_method: decodeDouble
|
||||
java_encode_shim: encodeDouble
|
||||
python_decode_shim: decodeDouble
|
||||
python_encode_shim: encodeDouble
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
java_encode_shim: PacketUtils.packTransform3d
|
||||
cpp_type: frc::Transform3d
|
||||
cpp_include: "<frc/geometry/Transform3d.h>"
|
||||
python_decode_shim: packet.decodeTransform
|
||||
python_decode_shim: decodeTransform
|
||||
python_encode_shim: encodeTransform
|
||||
java_import: edu.wpi.first.math.geometry.Transform3d
|
||||
# shim since we expect fields to at least exist
|
||||
|
||||
@@ -54,26 +54,26 @@ public class {{ name }}Serde implements PacketSerde<{{name}}> {
|
||||
@Override
|
||||
public void pack(Packet packet, {{ name }} value) {
|
||||
{%- for field in fields -%}
|
||||
{%- if field.type | is_shimmed %}
|
||||
{%- if field.type | is_shimmed %}
|
||||
{{ get_message_by_name(field.type).java_encode_shim }}(packet, value.{{ field.name }});
|
||||
{%- elif field.optional == True %}
|
||||
{%- elif field.optional == True %}
|
||||
// {{ field.name }} is optional! it better not be a VLA too
|
||||
packet.encodeOptional(value.{{ field.name }});
|
||||
{%- elif field.vla == True and field.type | is_intrinsic %}
|
||||
{%- elif field.vla == True and field.type | is_intrinsic %}
|
||||
// {{ field.name }} is a intrinsic VLA!
|
||||
packet.encode(value.{{ field.name }});
|
||||
{%- elif field.vla == True %}
|
||||
{%- elif field.vla == True %}
|
||||
// {{ field.name }} is a custom VLA!
|
||||
packet.encodeList(value.{{ field.name }});
|
||||
{%- elif field.type | is_intrinsic %}
|
||||
{%- elif field.type | is_intrinsic %}
|
||||
// field {{ field.name }} is of intrinsic type {{ field.type }}
|
||||
packet.encode(({{ type_map[field.type].java_type }}) value.{{ field.name }});
|
||||
{%- else %}
|
||||
{%- else %}
|
||||
// field {{ field.name }} is of non-intrinsic type {{ field.type }}
|
||||
{{ field.type }}.photonStruct.pack(packet, value.{{ field.name }});
|
||||
{%- endif %}
|
||||
{%- if not loop.last %}
|
||||
{% endif -%}
|
||||
{%- endif %}
|
||||
{%- if not loop.last %}
|
||||
{% endif -%}
|
||||
{% endfor%}
|
||||
}
|
||||
|
||||
@@ -81,26 +81,26 @@ public class {{ name }}Serde implements PacketSerde<{{name}}> {
|
||||
public {{ name }} unpack(Packet packet) {
|
||||
var ret = new {{ name }}();
|
||||
{% for field in fields -%}
|
||||
{%- if field.type | is_shimmed %}
|
||||
{%- if field.type | is_shimmed %}
|
||||
ret.{{ field.name }} = {{ get_message_by_name(field.type).java_decode_shim }}(packet);
|
||||
{%- elif field.optional == True %}
|
||||
{%- elif field.optional == True %}
|
||||
// {{ field.name }} is optional! it better not be a VLA too
|
||||
ret.{{ field.name }} = packet.decodeOptional({{ field.type }}.photonStruct);
|
||||
{%- elif field.vla == True and not field.type | is_intrinsic %}
|
||||
{%- elif field.vla == True and not field.type | is_intrinsic %}
|
||||
// {{ field.name }} is a custom VLA!
|
||||
ret.{{ field.name }} = packet.decodeList({{ field.type }}.photonStruct);
|
||||
{%- elif field.vla == True and field.type | is_intrinsic %}
|
||||
{%- elif field.vla == True and field.type | is_intrinsic %}
|
||||
// {{ field.name }} is a custom VLA!
|
||||
ret.{{ field.name }} = packet.decode{{ type_map[field.type].java_type.title() }}List();
|
||||
{%- elif field.type | is_intrinsic %}
|
||||
{%- elif field.type | is_intrinsic %}
|
||||
// {{ field.name }} is of intrinsic type {{ field.type }}
|
||||
ret.{{field.name}} = packet.{{ type_map[field.type].java_decode_method }}();
|
||||
{%- else %}
|
||||
{%- else %}
|
||||
// {{ field.name }} is of non-intrinsic type {{ field.type }}
|
||||
ret.{{field.name}} = {{ field.type }}.photonStruct.unpack(packet);
|
||||
{%- endif %}
|
||||
{%- if not loop.last %}
|
||||
{% endif -%}
|
||||
{%- endif %}
|
||||
{%- if not loop.last %}
|
||||
{% endif -%}
|
||||
{% endfor%}
|
||||
|
||||
return ret;
|
||||
@@ -125,4 +125,4 @@ public class {{ name }}Serde implements PacketSerde<{{name}}> {
|
||||
{%- endfor%}
|
||||
};
|
||||
}
|
||||
}
|
||||
}{{'\n'}}
|
||||
|
||||
@@ -24,21 +24,21 @@ namespace photon {
|
||||
using StructType = SerdeType<{{ name }}>;
|
||||
|
||||
void StructType::Pack(Packet& packet, const {{ name }}& value) {
|
||||
{% for field in fields -%}
|
||||
packet.Pack<{{ field | get_qualified_name }}>(value.{{ field.name }});
|
||||
{%- if not loop.last %}
|
||||
{% endif -%}
|
||||
{% endfor %}
|
||||
{% for field in fields -%}
|
||||
packet.Pack<{{ field | get_qualified_name }}>(value.{{ field.name }});
|
||||
{%- if not loop.last %}
|
||||
{% endif -%}
|
||||
{% endfor %}
|
||||
}
|
||||
|
||||
{{ name }} StructType::Unpack(Packet& packet) {
|
||||
return {{ name }}{ {{ name }}_PhotonStruct{
|
||||
{% for field in fields -%}
|
||||
.{{ field.name}} = packet.Unpack<{{ field | get_qualified_name }}>(),
|
||||
{%- if not loop.last %}
|
||||
{% endif -%}
|
||||
{% endfor %}
|
||||
}};
|
||||
return {{ name }}{ {{ name }}_PhotonStruct{
|
||||
{% for field in fields -%}
|
||||
.{{ field.name}} = packet.Unpack<{{ field | get_qualified_name }}>(),
|
||||
{%- if not loop.last %}
|
||||
{% endif -%}
|
||||
{% endfor %}
|
||||
}};
|
||||
}
|
||||
|
||||
} // namespace photon
|
||||
} // namespace photon{{'\n'}}
|
||||
|
||||
@@ -48,4 +48,4 @@ struct WPILIB_DLLEXPORT SerdeType<{{ name }}> {
|
||||
|
||||
static_assert(photon::PhotonStructSerializable<photon::{{ name }}>);
|
||||
|
||||
} // namespace photon
|
||||
} // namespace photon{{'\n'}}
|
||||
|
||||
@@ -44,7 +44,7 @@ class {{ name }}Serde:
|
||||
MESSAGE_FORMAT = "{{ message_fmt }}"
|
||||
|
||||
@staticmethod
|
||||
def pack(value: '{{ name }}' ) -> 'Packet':
|
||||
def pack(value: "{{ name }}") -> "Packet":
|
||||
ret = Packet()
|
||||
{% for field in fields -%}
|
||||
{%- if field.type | is_shimmed %}
|
||||
@@ -60,7 +60,7 @@ class {{ name }}Serde:
|
||||
ret.encode{{ type_map[field.type].java_type.title() }}List(value.{{ field.name }})
|
||||
{%- elif field.type | is_intrinsic %}
|
||||
# {{ field.name }} is of intrinsic type {{ field.type }}
|
||||
ret.{{ type_map[field.type].java_encode_shim }}(value.{{field.name}})
|
||||
ret.{{ type_map[field.type].python_encode_shim }}(value.{{field.name}})
|
||||
{%- else %}
|
||||
# {{ field.name }} is of non-intrinsic type {{ field.type }}
|
||||
ret.encodeBytes({{ field.type }}.photonStruct.pack(value.{{field.name}}).getData())
|
||||
@@ -70,13 +70,12 @@ class {{ name }}Serde:
|
||||
{% endfor%}
|
||||
return ret
|
||||
|
||||
|
||||
@staticmethod
|
||||
def unpack(packet: 'Packet') -> '{{ name }}':
|
||||
def unpack(packet: "Packet") -> "{{ name }}":
|
||||
ret = {{ name }}()
|
||||
{% for field in fields -%}
|
||||
{%- if field.type | is_shimmed %}
|
||||
ret.{{ field.name }} = {{ get_message_by_name(field.type).python_decode_shim }}()
|
||||
ret.{{ field.name }} = packet.{{ get_message_by_name(field.type).python_decode_shim }}()
|
||||
{%- elif field.optional == True %}
|
||||
# {{ field.name }} is optional! it better not be a VLA too
|
||||
ret.{{ field.name }} = packet.decodeOptional({{ field.type }}.photonStruct)
|
||||
@@ -88,7 +87,7 @@ class {{ name }}Serde:
|
||||
ret.{{ field.name }} = packet.decode{{ type_map[field.type].java_type.title() }}List()
|
||||
{%- elif field.type | is_intrinsic %}
|
||||
# {{ field.name }} is of intrinsic type {{ field.type }}
|
||||
ret.{{field.name}} = packet.{{ type_map[field.type].java_decode_method }}()
|
||||
ret.{{field.name}} = packet.{{ type_map[field.type].python_decode_shim }}()
|
||||
{%- else %}
|
||||
# {{ field.name }} is of non-intrinsic type {{ field.type }}
|
||||
ret.{{field.name}} = {{ field.type }}.photonStruct.unpack(packet)
|
||||
@@ -101,4 +100,4 @@ class {{ name }}Serde:
|
||||
|
||||
|
||||
# Hack ourselves into the base class
|
||||
{{ name }}.photonStruct = {{ name }}Serde()
|
||||
{{ name }}.photonStruct = {{ name }}Serde(){{'\n'}}
|
||||
|
||||
@@ -36,4 +36,4 @@ struct {{ name }}_PhotonStruct {
|
||||
friend bool operator==({{ name }}_PhotonStruct const&, {{ name }}_PhotonStruct const&) = default;
|
||||
};
|
||||
|
||||
} // namespace photon
|
||||
} // namespace photon{{'\n'}}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
plugins {
|
||||
id "application"
|
||||
id 'com.github.johnrengelman.shadow' version '8.1.1'
|
||||
id 'com.gradleup.shadow' version '8.3.4'
|
||||
id "com.github.node-gradle.node" version "7.0.1"
|
||||
id "org.hidetake.ssh" version "2.11.2"
|
||||
id 'edu.wpi.first.WpilibTools' version '1.3.0'
|
||||
|
||||
@@ -31,6 +31,7 @@ import org.photonvision.common.configuration.ConfigManager;
|
||||
import org.photonvision.common.configuration.NeuralNetworkModelManager;
|
||||
import org.photonvision.common.dataflow.networktables.NetworkTablesManager;
|
||||
import org.photonvision.common.hardware.HardwareManager;
|
||||
import org.photonvision.common.hardware.OsImageVersion;
|
||||
import org.photonvision.common.hardware.PiVersion;
|
||||
import org.photonvision.common.hardware.Platform;
|
||||
import org.photonvision.common.logging.KernelLogLogger;
|
||||
@@ -353,10 +354,14 @@ public class Main {
|
||||
logger.info(
|
||||
"Starting PhotonVision version "
|
||||
+ PhotonVersion.versionString
|
||||
+ " on "
|
||||
+ " on platform "
|
||||
+ Platform.getPlatformName()
|
||||
+ (Platform.isRaspberryPi() ? (" (Pi " + PiVersion.getPiVersion() + ")") : ""));
|
||||
|
||||
if (OsImageVersion.IMAGE_VERSION.isPresent()) {
|
||||
logger.info("PhotonVision image version: " + OsImageVersion.IMAGE_VERSION.get());
|
||||
}
|
||||
|
||||
try {
|
||||
if (!handleArgs(args)) {
|
||||
System.exit(1);
|
||||
|
||||
@@ -99,10 +99,9 @@ public class DataSocketHandler {
|
||||
objectMapper.readValue(context.data(), new TypeReference<>() {});
|
||||
|
||||
// Special case the current camera index
|
||||
var camIndexValue = deserializedData.get("cameraIndex");
|
||||
Integer cameraIndex = null;
|
||||
if (camIndexValue instanceof Integer) {
|
||||
cameraIndex = (Integer) camIndexValue;
|
||||
if (deserializedData.get("cameraIndex") instanceof Integer camIndexValue) {
|
||||
cameraIndex = camIndexValue;
|
||||
deserializedData.remove("cameraIndex");
|
||||
}
|
||||
|
||||
@@ -128,216 +127,182 @@ public class DataSocketHandler {
|
||||
}
|
||||
|
||||
switch (socketMessageType) {
|
||||
case SMT_DRIVERMODE:
|
||||
{
|
||||
// TODO: what is this event?
|
||||
var data = (Boolean) entryValue;
|
||||
var dmIsDriverEvent =
|
||||
new IncomingWebSocketEvent<Boolean>(
|
||||
DataChangeDestination.DCD_ACTIVEMODULE,
|
||||
"isDriverMode",
|
||||
data,
|
||||
cameraIndex,
|
||||
context);
|
||||
case SMT_DRIVERMODE -> {
|
||||
// TODO: what is this event?
|
||||
var data = (Boolean) entryValue;
|
||||
var dmIsDriverEvent =
|
||||
new IncomingWebSocketEvent<Boolean>(
|
||||
DataChangeDestination.DCD_ACTIVEMODULE,
|
||||
"isDriverMode",
|
||||
data,
|
||||
cameraIndex,
|
||||
context);
|
||||
|
||||
dcService.publishEvents(dmIsDriverEvent);
|
||||
break;
|
||||
}
|
||||
case SMT_CHANGECAMERANAME:
|
||||
{
|
||||
var ccnEvent =
|
||||
new IncomingWebSocketEvent<>(
|
||||
DataChangeDestination.DCD_ACTIVEMODULE,
|
||||
"cameraNickname",
|
||||
(String) entryValue,
|
||||
cameraIndex,
|
||||
context);
|
||||
dcService.publishEvent(ccnEvent);
|
||||
break;
|
||||
}
|
||||
case SMT_CHANGEPIPELINENAME:
|
||||
{
|
||||
var cpnEvent =
|
||||
new IncomingWebSocketEvent<>(
|
||||
DataChangeDestination.DCD_ACTIVEMODULE,
|
||||
"pipelineName",
|
||||
(String) entryValue,
|
||||
cameraIndex,
|
||||
context);
|
||||
dcService.publishEvent(cpnEvent);
|
||||
break;
|
||||
}
|
||||
case SMT_ADDNEWPIPELINE:
|
||||
{
|
||||
// HashMap<String, Object> data = (HashMap<String,
|
||||
// Object>) entryValue;
|
||||
// var type = (PipelineType)
|
||||
// data.get("pipelineType");
|
||||
// var name = (String) data.get("pipelineName");
|
||||
var arr = (ArrayList<Object>) entryValue;
|
||||
var name = (String) arr.get(0);
|
||||
var type = PipelineType.values()[(Integer) arr.get(1) + 2];
|
||||
dcService.publishEvents(dmIsDriverEvent);
|
||||
}
|
||||
case SMT_CHANGECAMERANAME -> {
|
||||
var ccnEvent =
|
||||
new IncomingWebSocketEvent<>(
|
||||
DataChangeDestination.DCD_ACTIVEMODULE,
|
||||
"cameraNickname",
|
||||
(String) entryValue,
|
||||
cameraIndex,
|
||||
context);
|
||||
dcService.publishEvent(ccnEvent);
|
||||
}
|
||||
case SMT_CHANGEPIPELINENAME -> {
|
||||
var cpnEvent =
|
||||
new IncomingWebSocketEvent<>(
|
||||
DataChangeDestination.DCD_ACTIVEMODULE,
|
||||
"pipelineName",
|
||||
(String) entryValue,
|
||||
cameraIndex,
|
||||
context);
|
||||
dcService.publishEvent(cpnEvent);
|
||||
}
|
||||
case SMT_ADDNEWPIPELINE -> {
|
||||
// HashMap<String, Object> data = (HashMap<String, Object>) entryValue;
|
||||
// var type = (PipelineType) data.get("pipelineType");
|
||||
// var name = (String) data.get("pipelineName");
|
||||
var arr = (ArrayList<Object>) entryValue;
|
||||
var name = (String) arr.get(0);
|
||||
var type = PipelineType.values()[(Integer) arr.get(1) + 2];
|
||||
|
||||
var newPipelineEvent =
|
||||
new IncomingWebSocketEvent<>(
|
||||
DataChangeDestination.DCD_ACTIVEMODULE,
|
||||
"newPipelineInfo",
|
||||
Pair.of(name, type),
|
||||
cameraIndex,
|
||||
context);
|
||||
dcService.publishEvent(newPipelineEvent);
|
||||
break;
|
||||
}
|
||||
case SMT_CHANGEBRIGHTNESS:
|
||||
{
|
||||
HardwareManager.getInstance()
|
||||
.setBrightnessPercent(Integer.parseInt(entryValue.toString()));
|
||||
break;
|
||||
}
|
||||
case SMT_DUPLICATEPIPELINE:
|
||||
{
|
||||
var pipeIndex = (Integer) entryValue;
|
||||
var newPipelineEvent =
|
||||
new IncomingWebSocketEvent<>(
|
||||
DataChangeDestination.DCD_ACTIVEMODULE,
|
||||
"newPipelineInfo",
|
||||
Pair.of(name, type),
|
||||
cameraIndex,
|
||||
context);
|
||||
dcService.publishEvent(newPipelineEvent);
|
||||
}
|
||||
case SMT_CHANGEBRIGHTNESS -> {
|
||||
HardwareManager.getInstance()
|
||||
.setBrightnessPercent(Integer.parseInt(entryValue.toString()));
|
||||
}
|
||||
case SMT_DUPLICATEPIPELINE -> {
|
||||
var pipeIndex = (Integer) entryValue;
|
||||
|
||||
logger.info("Duplicating pipe@index" + pipeIndex + " for camera " + cameraIndex);
|
||||
logger.info("Duplicating pipe@index" + pipeIndex + " for camera " + cameraIndex);
|
||||
|
||||
var newPipelineEvent =
|
||||
new IncomingWebSocketEvent<>(
|
||||
DataChangeDestination.DCD_ACTIVEMODULE,
|
||||
"duplicatePipeline",
|
||||
pipeIndex,
|
||||
cameraIndex,
|
||||
context);
|
||||
dcService.publishEvent(newPipelineEvent);
|
||||
break;
|
||||
}
|
||||
case SMT_DELETECURRENTPIPELINE:
|
||||
{
|
||||
var deleteCurrentPipelineEvent =
|
||||
new IncomingWebSocketEvent<>(
|
||||
DataChangeDestination.DCD_ACTIVEMODULE,
|
||||
"deleteCurrPipeline",
|
||||
0,
|
||||
cameraIndex,
|
||||
context);
|
||||
dcService.publishEvent(deleteCurrentPipelineEvent);
|
||||
break;
|
||||
}
|
||||
case SMT_ROBOTOFFSETPOINT:
|
||||
{
|
||||
var robotOffsetPointEvent =
|
||||
new IncomingWebSocketEvent<>(
|
||||
DataChangeDestination.DCD_ACTIVEMODULE,
|
||||
"robotOffsetPoint",
|
||||
(Integer) entryValue,
|
||||
cameraIndex,
|
||||
null);
|
||||
dcService.publishEvent(robotOffsetPointEvent);
|
||||
break;
|
||||
}
|
||||
case SMT_CURRENTCAMERA:
|
||||
{
|
||||
var changeCurrentCameraEvent =
|
||||
new IncomingWebSocketEvent<>(
|
||||
DataChangeDestination.DCD_OTHER, "changeUICamera", (Integer) entryValue);
|
||||
dcService.publishEvent(changeCurrentCameraEvent);
|
||||
break;
|
||||
}
|
||||
case SMT_CURRENTPIPELINE:
|
||||
{
|
||||
var changePipelineEvent =
|
||||
new IncomingWebSocketEvent<>(
|
||||
DataChangeDestination.DCD_ACTIVEMODULE,
|
||||
"changePipeline",
|
||||
(Integer) entryValue,
|
||||
cameraIndex,
|
||||
context);
|
||||
dcService.publishEvent(changePipelineEvent);
|
||||
break;
|
||||
}
|
||||
case SMT_STARTPNPCALIBRATION:
|
||||
{
|
||||
var changePipelineEvent =
|
||||
new IncomingWebSocketEvent<>(
|
||||
DataChangeDestination.DCD_ACTIVEMODULE,
|
||||
"startCalibration",
|
||||
(Map) entryValue,
|
||||
cameraIndex,
|
||||
context);
|
||||
dcService.publishEvent(changePipelineEvent);
|
||||
break;
|
||||
}
|
||||
case SMT_SAVEINPUTSNAPSHOT:
|
||||
{
|
||||
var takeInputSnapshotEvent =
|
||||
new IncomingWebSocketEvent<>(
|
||||
DataChangeDestination.DCD_ACTIVEMODULE,
|
||||
"saveInputSnapshot",
|
||||
0,
|
||||
cameraIndex,
|
||||
context);
|
||||
dcService.publishEvent(takeInputSnapshotEvent);
|
||||
break;
|
||||
}
|
||||
case SMT_SAVEOUTPUTSNAPSHOT:
|
||||
{
|
||||
var takeOutputSnapshotEvent =
|
||||
new IncomingWebSocketEvent<>(
|
||||
DataChangeDestination.DCD_ACTIVEMODULE,
|
||||
"saveOutputSnapshot",
|
||||
0,
|
||||
cameraIndex,
|
||||
context);
|
||||
dcService.publishEvent(takeOutputSnapshotEvent);
|
||||
break;
|
||||
}
|
||||
case SMT_TAKECALIBRATIONSNAPSHOT:
|
||||
{
|
||||
var takeCalSnapshotEvent =
|
||||
new IncomingWebSocketEvent<>(
|
||||
DataChangeDestination.DCD_ACTIVEMODULE,
|
||||
"takeCalSnapshot",
|
||||
0,
|
||||
cameraIndex,
|
||||
context);
|
||||
dcService.publishEvent(takeCalSnapshotEvent);
|
||||
break;
|
||||
}
|
||||
case SMT_PIPELINESETTINGCHANGE:
|
||||
{
|
||||
HashMap<String, Object> data = (HashMap<String, Object>) entryValue;
|
||||
var newPipelineEvent =
|
||||
new IncomingWebSocketEvent<>(
|
||||
DataChangeDestination.DCD_ACTIVEMODULE,
|
||||
"duplicatePipeline",
|
||||
pipeIndex,
|
||||
cameraIndex,
|
||||
context);
|
||||
dcService.publishEvent(newPipelineEvent);
|
||||
}
|
||||
case SMT_DELETECURRENTPIPELINE -> {
|
||||
var deleteCurrentPipelineEvent =
|
||||
new IncomingWebSocketEvent<>(
|
||||
DataChangeDestination.DCD_ACTIVEMODULE,
|
||||
"deleteCurrPipeline",
|
||||
0,
|
||||
cameraIndex,
|
||||
context);
|
||||
dcService.publishEvent(deleteCurrentPipelineEvent);
|
||||
}
|
||||
case SMT_ROBOTOFFSETPOINT -> {
|
||||
var robotOffsetPointEvent =
|
||||
new IncomingWebSocketEvent<>(
|
||||
DataChangeDestination.DCD_ACTIVEMODULE,
|
||||
"robotOffsetPoint",
|
||||
(Integer) entryValue,
|
||||
cameraIndex,
|
||||
null);
|
||||
dcService.publishEvent(robotOffsetPointEvent);
|
||||
}
|
||||
case SMT_CURRENTCAMERA -> {
|
||||
var changeCurrentCameraEvent =
|
||||
new IncomingWebSocketEvent<>(
|
||||
DataChangeDestination.DCD_OTHER, "changeUICamera", (Integer) entryValue);
|
||||
dcService.publishEvent(changeCurrentCameraEvent);
|
||||
}
|
||||
case SMT_CURRENTPIPELINE -> {
|
||||
var changePipelineEvent =
|
||||
new IncomingWebSocketEvent<>(
|
||||
DataChangeDestination.DCD_ACTIVEMODULE,
|
||||
"changePipeline",
|
||||
(Integer) entryValue,
|
||||
cameraIndex,
|
||||
context);
|
||||
dcService.publishEvent(changePipelineEvent);
|
||||
}
|
||||
case SMT_STARTPNPCALIBRATION -> {
|
||||
var changePipelineEvent =
|
||||
new IncomingWebSocketEvent<>(
|
||||
DataChangeDestination.DCD_ACTIVEMODULE,
|
||||
"startCalibration",
|
||||
(Map) entryValue,
|
||||
cameraIndex,
|
||||
context);
|
||||
dcService.publishEvent(changePipelineEvent);
|
||||
}
|
||||
case SMT_SAVEINPUTSNAPSHOT -> {
|
||||
var takeInputSnapshotEvent =
|
||||
new IncomingWebSocketEvent<>(
|
||||
DataChangeDestination.DCD_ACTIVEMODULE,
|
||||
"saveInputSnapshot",
|
||||
0,
|
||||
cameraIndex,
|
||||
context);
|
||||
dcService.publishEvent(takeInputSnapshotEvent);
|
||||
}
|
||||
case SMT_SAVEOUTPUTSNAPSHOT -> {
|
||||
var takeOutputSnapshotEvent =
|
||||
new IncomingWebSocketEvent<>(
|
||||
DataChangeDestination.DCD_ACTIVEMODULE,
|
||||
"saveOutputSnapshot",
|
||||
0,
|
||||
cameraIndex,
|
||||
context);
|
||||
dcService.publishEvent(takeOutputSnapshotEvent);
|
||||
}
|
||||
case SMT_TAKECALIBRATIONSNAPSHOT -> {
|
||||
var takeCalSnapshotEvent =
|
||||
new IncomingWebSocketEvent<>(
|
||||
DataChangeDestination.DCD_ACTIVEMODULE,
|
||||
"takeCalSnapshot",
|
||||
0,
|
||||
cameraIndex,
|
||||
context);
|
||||
dcService.publishEvent(takeCalSnapshotEvent);
|
||||
}
|
||||
case SMT_PIPELINESETTINGCHANGE -> {
|
||||
HashMap<String, Object> data = (HashMap<String, Object>) entryValue;
|
||||
|
||||
if (data.size() >= 2) {
|
||||
var cameraIndex2 = (int) data.get("cameraIndex");
|
||||
for (var dataEntry : data.entrySet()) {
|
||||
if (dataEntry.getKey().equals("cameraIndex")) {
|
||||
continue;
|
||||
}
|
||||
var pipelineSettingChangeEvent =
|
||||
new IncomingWebSocketEvent(
|
||||
DataChangeDestination.DCD_ACTIVEPIPELINESETTINGS,
|
||||
dataEntry.getKey(),
|
||||
dataEntry.getValue(),
|
||||
cameraIndex2,
|
||||
context);
|
||||
dcService.publishEvent(pipelineSettingChangeEvent);
|
||||
if (data.size() >= 2) {
|
||||
var cameraIndex2 = (int) data.get("cameraIndex");
|
||||
for (var dataEntry : data.entrySet()) {
|
||||
if (dataEntry.getKey().equals("cameraIndex")) {
|
||||
continue;
|
||||
}
|
||||
} else {
|
||||
logger.warn("Unknown message for PSC: " + data.keySet().iterator().next());
|
||||
var pipelineSettingChangeEvent =
|
||||
new IncomingWebSocketEvent(
|
||||
DataChangeDestination.DCD_ACTIVEPIPELINESETTINGS,
|
||||
dataEntry.getKey(),
|
||||
dataEntry.getValue(),
|
||||
cameraIndex2,
|
||||
context);
|
||||
dcService.publishEvent(pipelineSettingChangeEvent);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case SMT_CHANGEPIPELINETYPE:
|
||||
{
|
||||
var changePipelineEvent =
|
||||
new IncomingWebSocketEvent<>(
|
||||
DataChangeDestination.DCD_ACTIVEMODULE,
|
||||
"changePipelineType",
|
||||
(Integer) entryValue,
|
||||
cameraIndex,
|
||||
context);
|
||||
dcService.publishEvent(changePipelineEvent);
|
||||
break;
|
||||
} else {
|
||||
logger.warn("Unknown message for PSC: " + data.keySet().iterator().next());
|
||||
}
|
||||
}
|
||||
case SMT_CHANGEPIPELINETYPE -> {
|
||||
var changePipelineEvent =
|
||||
new IncomingWebSocketEvent<>(
|
||||
DataChangeDestination.DCD_ACTIVEMODULE,
|
||||
"changePipelineType",
|
||||
(Integer) entryValue,
|
||||
cameraIndex,
|
||||
context);
|
||||
dcService.publishEvent(changePipelineEvent);
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
logger.error("Failed to parse message!", e);
|
||||
|
||||
@@ -27,6 +27,7 @@ import org.photonvision.common.dataflow.events.DataChangeEvent;
|
||||
import org.photonvision.common.dataflow.events.IncomingWebSocketEvent;
|
||||
import org.photonvision.common.dataflow.events.OutgoingUIEvent;
|
||||
import org.photonvision.common.dataflow.networktables.NetworkTablesManager;
|
||||
import org.photonvision.common.dataflow.websocket.UIPhotonConfiguration;
|
||||
import org.photonvision.common.logging.Logger;
|
||||
|
||||
public class UIInboundSubscriber extends DataChangeSubscriber {
|
||||
@@ -38,12 +39,12 @@ public class UIInboundSubscriber extends DataChangeSubscriber {
|
||||
|
||||
@Override
|
||||
public void onDataChangeEvent(DataChangeEvent<?> event) {
|
||||
if (event instanceof IncomingWebSocketEvent) {
|
||||
var incomingWSEvent = (IncomingWebSocketEvent<?>) event;
|
||||
if (event instanceof IncomingWebSocketEvent incomingWSEvent) {
|
||||
if (incomingWSEvent.propertyName.equals("userConnected")
|
||||
|| incomingWSEvent.propertyName.equals("sendFullSettings")) {
|
||||
// Send full settings
|
||||
var settings = ConfigManager.getInstance().getConfig().toHashMap();
|
||||
var settings =
|
||||
UIPhotonConfiguration.programStateToUi(ConfigManager.getInstance().getConfig());
|
||||
var message =
|
||||
new OutgoingUIEvent<>("fullsettings", settings, incomingWSEvent.originContext);
|
||||
DataChangeService.getInstance().publishEvent(message);
|
||||
|
||||
@@ -44,11 +44,9 @@ class UIOutboundSubscriber extends DataChangeSubscriber {
|
||||
|
||||
@Override
|
||||
public void onDataChangeEvent(DataChangeEvent event) {
|
||||
if (event instanceof OutgoingUIEvent) {
|
||||
var thisEvent = (OutgoingUIEvent) event;
|
||||
if (event instanceof OutgoingUIEvent thisEvent) {
|
||||
try {
|
||||
if (event.data instanceof HashMap) {
|
||||
var data = (HashMap) event.data;
|
||||
if (event.data instanceof HashMap data) {
|
||||
socketHandler.broadcastMessage(data, thisEvent.originContext);
|
||||
} else {
|
||||
socketHandler.broadcastMessage(event.data, thisEvent.originContext);
|
||||
|
||||
@@ -25,13 +25,7 @@ import edu.wpi.first.apriltag.jni.AprilTagJNI;
|
||||
import edu.wpi.first.cscore.CameraServerJNI;
|
||||
import edu.wpi.first.cscore.OpenCvLoader;
|
||||
import edu.wpi.first.hal.JNIWrapper;
|
||||
import edu.wpi.first.math.jni.ArmFeedforwardJNI;
|
||||
import edu.wpi.first.math.jni.DAREJNI;
|
||||
import edu.wpi.first.math.jni.EigenJNI;
|
||||
import edu.wpi.first.math.jni.Ellipse2dJNI;
|
||||
import edu.wpi.first.math.jni.Pose3dJNI;
|
||||
import edu.wpi.first.math.jni.StateSpaceUtilJNI;
|
||||
import edu.wpi.first.math.jni.TrajectoryUtilJNI;
|
||||
import edu.wpi.first.math.jni.WPIMathJNI;
|
||||
import edu.wpi.first.net.WPINetJNI;
|
||||
import edu.wpi.first.networktables.NetworkTablesJNI;
|
||||
import edu.wpi.first.util.CombinedRuntimeLoader;
|
||||
@@ -48,18 +42,8 @@ public class WpilibLoader {
|
||||
OpenCvLoader.Helper.setExtractOnStaticLoad(false);
|
||||
JNIWrapper.Helper.setExtractOnStaticLoad(false);
|
||||
WPINetJNI.Helper.setExtractOnStaticLoad(false);
|
||||
WPIMathJNI.Helper.setExtractOnStaticLoad(false);
|
||||
AprilTagJNI.Helper.setExtractOnStaticLoad(false);
|
||||
|
||||
// wpimathjni is a bit odd, it's all in the wpimathjni shared lib, but the java side stuff has
|
||||
// been split.
|
||||
ArmFeedforwardJNI.Helper.setExtractOnStaticLoad(false);
|
||||
DAREJNI.Helper.setExtractOnStaticLoad(false);
|
||||
EigenJNI.Helper.setExtractOnStaticLoad(false);
|
||||
Ellipse2dJNI.Helper.setExtractOnStaticLoad(false);
|
||||
Pose3dJNI.Helper.setExtractOnStaticLoad(false);
|
||||
StateSpaceUtilJNI.Helper.setExtractOnStaticLoad(false);
|
||||
TrajectoryUtilJNI.Helper.setExtractOnStaticLoad(false);
|
||||
|
||||
try {
|
||||
CombinedRuntimeLoader.loadLibraries(
|
||||
WpilibLoader.class,
|
||||
|
||||
@@ -34,6 +34,7 @@ import edu.wpi.first.util.struct.Struct;
|
||||
* Auto-generated serialization/deserialization helper for MultiTargetPNPResult
|
||||
*/
|
||||
public class MultiTargetPNPResultSerde implements PacketSerde<MultiTargetPNPResult> {
|
||||
|
||||
@Override
|
||||
public final String getInterfaceUUID() { return "541096947e9f3ca2d3f425ff7b04aa7b"; }
|
||||
@Override
|
||||
@@ -79,6 +80,7 @@ public class MultiTargetPNPResultSerde implements PacketSerde<MultiTargetPNPResu
|
||||
@Override
|
||||
public Struct<?>[] getNestedWpilibMessages() {
|
||||
return new Struct<?>[] {
|
||||
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,6 +34,7 @@ import edu.wpi.first.util.struct.Struct;
|
||||
* Auto-generated serialization/deserialization helper for PhotonPipelineMetadata
|
||||
*/
|
||||
public class PhotonPipelineMetadataSerde implements PacketSerde<PhotonPipelineMetadata> {
|
||||
|
||||
@Override
|
||||
public final String getInterfaceUUID() { return "ac0a45f686457856fb30af77699ea356"; }
|
||||
@Override
|
||||
@@ -84,12 +85,14 @@ public class PhotonPipelineMetadataSerde implements PacketSerde<PhotonPipelineMe
|
||||
@Override
|
||||
public PacketSerde<?>[] getNestedPhotonMessages() {
|
||||
return new PacketSerde<?>[] {
|
||||
|
||||
};
|
||||
}
|
||||
|
||||
@Override
|
||||
public Struct<?>[] getNestedWpilibMessages() {
|
||||
return new Struct<?>[] {
|
||||
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user