mirror of
https://github.com/PhotonVision/photonvision
synced 2026-06-19 00:41:41 +00:00
Compare commits
106 Commits
v2026.0.0-
...
py-docs
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f022130bfa | ||
|
|
5457db947e | ||
|
|
8c7ca1697e | ||
|
|
a7329c48a3 | ||
|
|
ce0b00ee03 | ||
|
|
a8d825919e | ||
|
|
63593b873a | ||
|
|
aa64bfe82e | ||
|
|
d27b3d0775 | ||
|
|
77e5545eef | ||
|
|
618072c3dd | ||
|
|
7d2c69dbdb | ||
|
|
a2b19c080e | ||
|
|
def3b9faa8 | ||
|
|
25c355ebc2 | ||
|
|
dad7f0a82d | ||
|
|
6f2603f0cb | ||
|
|
f499e4fb50 | ||
|
|
e5c8859c57 | ||
|
|
2cde701cff | ||
|
|
d649a9cb9e | ||
|
|
695742bfcf | ||
|
|
5df9137256 | ||
|
|
36b437323f | ||
|
|
5d39ef5b62 | ||
|
|
2bb59f8437 | ||
|
|
cd502a22c7 | ||
|
|
f16ffe3cd2 | ||
|
|
45236b872a | ||
|
|
6b20dc3c1b | ||
|
|
9f6d8caf48 | ||
|
|
3cbac8117e | ||
|
|
8e88a9a780 | ||
|
|
99ca8228a1 | ||
|
|
7cb3b7a37b | ||
|
|
054ed8b6a1 | ||
|
|
d44480ddad | ||
|
|
c71921c41e | ||
|
|
ee4501f1d6 | ||
|
|
d9b86a718e | ||
|
|
1ac185c247 | ||
|
|
7170c29efe | ||
|
|
4f549ba579 | ||
|
|
b531fe6b81 | ||
|
|
373ed2ff05 | ||
|
|
115bc09f2e | ||
|
|
46e71703ef | ||
|
|
6fbb41fb76 | ||
|
|
05fcf876cd | ||
|
|
1637be6044 | ||
|
|
6f2fd19351 | ||
|
|
892e240b18 | ||
|
|
326c77fa38 | ||
|
|
8cf48bee57 | ||
|
|
26f08a6fdf | ||
|
|
abb8ccf4e9 | ||
|
|
50adef1672 | ||
|
|
cf68403182 | ||
|
|
dc0985dfb5 | ||
|
|
8fb29ff5c4 | ||
|
|
476cd6df8b | ||
|
|
783ed82d50 | ||
|
|
416e2f7607 | ||
|
|
ebd1071553 | ||
|
|
fa8b60fe27 | ||
|
|
c2581f3e99 | ||
|
|
96b0938dc0 | ||
|
|
697e52f886 | ||
|
|
87b219d9be | ||
|
|
abcd6b8f50 | ||
|
|
b22371d7c0 | ||
|
|
3eea79f0d4 | ||
|
|
0147a44100 | ||
|
|
0bec1f239c | ||
|
|
44b46cf117 | ||
|
|
ffdda9ddfa | ||
|
|
a5bc63878d | ||
|
|
a5b1cc0ded | ||
|
|
aacbdf5010 | ||
|
|
3547d0584b | ||
|
|
40815020de | ||
|
|
e522642a48 | ||
|
|
3cdda8a84e | ||
|
|
228caf47f2 | ||
|
|
d1761d07e9 | ||
|
|
331f4f0218 | ||
|
|
eb85834180 | ||
|
|
871ca61c8d | ||
|
|
358f5747ab | ||
|
|
e334d26459 | ||
|
|
77d5388a35 | ||
|
|
88a1e789ad | ||
|
|
abc67bdd95 | ||
|
|
05309b1e25 | ||
|
|
ddacff7079 | ||
|
|
32e4f0029b | ||
|
|
34057f223d | ||
|
|
55303ccd9c | ||
|
|
3c73b68ba3 | ||
|
|
6d816b5053 | ||
|
|
7c6bab1dfa | ||
|
|
462a2aa629 | ||
|
|
47b799a0ce | ||
|
|
35c72e8446 | ||
|
|
c440ce57ce | ||
|
|
8452527589 |
@@ -68,19 +68,26 @@ ForEachMacros:
|
||||
- BOOST_FOREACH
|
||||
IncludeBlocks: Regroup
|
||||
IncludeCategories:
|
||||
- Regex: '^<ext/.*\.h>'
|
||||
Priority: 2
|
||||
SortPriority: 0
|
||||
- Regex: '^<.*\.h>'
|
||||
Priority: 1
|
||||
SortPriority: 0
|
||||
- Regex: '^<.*'
|
||||
Priority: 2
|
||||
SortPriority: 0
|
||||
- Regex: '.*'
|
||||
Priority: 3
|
||||
SortPriority: 0
|
||||
IncludeIsMainRegex: '([-_](test|unittest))?$'
|
||||
# C standard library headers
|
||||
#
|
||||
# https://en.cppreference.com/w/cpp/header:
|
||||
# * C compatibility headers
|
||||
# * Special C compatibility headers
|
||||
# * Empty C headers
|
||||
# * Meaningless C headers
|
||||
# * Unsupported C headers
|
||||
- Regex: '^<(assert\.h|ctype\.h|errno\.h|fenv\.h|float\.h|inttypes\.h|limits\.h|locale\.h|math\.h|setjmp\.h|signal\.h|stdarg\.h|stddef\.h|stdint\.h|stdio\.h|stdlib\.h|string\.h|time\.h|uchar\.h|wchar\.h|wctype\.h|stdatomic\.h|ccomplex|complex\.h|ctgmath|tgmath\.h|ciso646|cstdalign|cstdbool|iso646\.h|stdalign\.h|stdbool\.h|stdatomic\.h|stdnoreturn\.h|threads\.h)>'
|
||||
Priority: 1
|
||||
# C++ standard library headers (lowercase and underscores with no .h suffix)
|
||||
- Regex: '^<[a-z_]+>'
|
||||
Priority: 2
|
||||
# Other library headers (angle brackets)
|
||||
- Regex: '^<.*'
|
||||
Priority: 3
|
||||
# Project headers (double quotes)
|
||||
- Regex: '^".*'
|
||||
Priority: 4
|
||||
IncludeIsMainRegex: '(Test|_test)?$'
|
||||
IncludeIsMainSourceRegex: ''
|
||||
IndentCaseLabels: true
|
||||
IndentGotoLabels: true
|
||||
@@ -136,7 +143,7 @@ RawStringFormats:
|
||||
CanonicalDelimiter: ''
|
||||
BasedOnStyle: google
|
||||
ReflowComments: true
|
||||
SortIncludes: false
|
||||
SortIncludes: true
|
||||
SortUsingDeclarations: true
|
||||
SpaceAfterCStyleCast: false
|
||||
SpaceAfterLogicalNot: false
|
||||
|
||||
1
.github/pull_request_template.md
vendored
1
.github/pull_request_template.md
vendored
@@ -11,6 +11,7 @@
|
||||
Merge checklist:
|
||||
- [ ] Pull Request title is [short, imperative summary](https://cbea.ms/git-commit/) of proposed changes
|
||||
- [ ] The description documents the _what_ and _why_
|
||||
- [ ] This PR has been [linted](https://docs.photonvision.org/en/latest/docs/contributing/linting.html).
|
||||
- [ ] If this PR changes behavior or adds a feature, user documentation is updated
|
||||
- [ ] If this PR touches photon-serde, all messages have been regenerated and hashes have not changed unexpectedly
|
||||
- [ ] If this PR touches configuration, this is backwards compatible with settings back to v2025.3.2
|
||||
|
||||
20
.github/workflows/build.yml
vendored
20
.github/workflows/build.yml
vendored
@@ -10,7 +10,7 @@ concurrency:
|
||||
cancel-in-progress: true
|
||||
|
||||
env:
|
||||
IMAGE_VERSION: v2026.0.3
|
||||
IMAGE_VERSION: v2026.0.4
|
||||
|
||||
jobs:
|
||||
|
||||
@@ -88,10 +88,8 @@ jobs:
|
||||
run: sudo apt-get update && sudo apt-get install -y libcholmod3 liblapack3 libsuitesparseconfig5
|
||||
- name: Gradle Build
|
||||
run: ./gradlew photon-targeting:build photon-core:build photon-server:build -x check
|
||||
- name: Gradle Tests
|
||||
run: ./gradlew testHeadless --stacktrace
|
||||
- name: Gradle Coverage
|
||||
run: ./gradlew jacocoTestReport
|
||||
- name: Gradle Tests and Coverage
|
||||
run: ./gradlew test jacocoTestReport --stacktrace
|
||||
build-offline-docs:
|
||||
name: "Build Offline Docs"
|
||||
runs-on: ubuntu-22.04
|
||||
@@ -445,6 +443,12 @@ jobs:
|
||||
image_url: https://github.com/PhotonVision/photon-image-modifier/releases/download/$IMAGE_VERSION/photonvision_limelight4.img.xz
|
||||
cpu: cortex-a76
|
||||
image_additional_mb: 0
|
||||
- os: ubuntu-24.04
|
||||
artifact-name: LinuxArm64
|
||||
image_suffix: luma_p1
|
||||
image_url: https://github.com/PhotonVision/photon-image-modifier/releases/download/$IMAGE_VERSION/photonvision_luma_p1.img.xz
|
||||
cpu: cortex-a76
|
||||
image_additional_mb: 0
|
||||
- os: ubuntu-24.04
|
||||
artifact-name: LinuxArm64
|
||||
image_suffix: orangepi5
|
||||
@@ -538,6 +542,12 @@ jobs:
|
||||
wget https://raw.githubusercontent.com/PhotonVision/photon-image-modifier/refs/tags/$IMAGE_VERSION/mount_rubikpi3.sh
|
||||
chmod +x mount_rubikpi3.sh
|
||||
./mount_rubikpi3.sh https://github.com/PhotonVision/photon-image-modifier/releases/download/$IMAGE_VERSION/photonvision_rubikpi3.tar.xz /tmp/build/scripts/armrunner.sh
|
||||
- name: Compress image
|
||||
run: |
|
||||
new_jar=$(realpath $(find . -name photonvision\*-linuxarm64.jar))
|
||||
new_image_name=$(basename "${new_jar/.jar/_rubikpi3.img}")
|
||||
mv photonvision_rubikpi3 $new_image_name
|
||||
tar -I 'xz -T0' -cf ${new_image_name}.tar.xz $new_image_name --checkpoint=10000 --checkpoint-action=echo='%T'
|
||||
- uses: actions/upload-artifact@v4
|
||||
name: Upload image
|
||||
with:
|
||||
|
||||
2
.github/workflows/lint-format.yml
vendored
2
.github/workflows/lint-format.yml
vendored
@@ -31,7 +31,7 @@ jobs:
|
||||
with:
|
||||
python-version: 3.11
|
||||
- name: Install wpiformat
|
||||
run: pip3 install wpiformat==2025.34
|
||||
run: pip3 install wpiformat==2025.75
|
||||
- name: Run
|
||||
run: wpiformat
|
||||
- name: Check output
|
||||
|
||||
38
.github/workflows/photon-api-docs.yml
vendored
38
.github/workflows/photon-api-docs.yml
vendored
@@ -46,7 +46,7 @@ jobs:
|
||||
run: pnpm run build-demo
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: built-demo
|
||||
name: demo
|
||||
path: photon-client/dist/
|
||||
|
||||
run_java_cpp_docs:
|
||||
@@ -74,9 +74,39 @@ jobs:
|
||||
name: docs-java-cpp
|
||||
path: photon-docs/build/docs
|
||||
|
||||
run_py_docs:
|
||||
name: Build Python API Docs
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.10'
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip install -r photon-lib/py/docs/requirements.txt
|
||||
|
||||
- name: Build Sphinx site
|
||||
run: |
|
||||
sphinx-apidoc -o docs/source photonlibpy
|
||||
make -C docs html
|
||||
working-directory: photon-lib/py
|
||||
|
||||
- name: Upload built site as artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: docs-python
|
||||
path: photon-lib/py/docs/build/html
|
||||
|
||||
publish_api_docs:
|
||||
name: Publish API Docs
|
||||
needs: [run_java_cpp_docs]
|
||||
needs: [ run_java_cpp_docs, run_py_docs ]
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
# Download docs artifact
|
||||
@@ -85,7 +115,7 @@ jobs:
|
||||
pattern: docs-*
|
||||
- run: find .
|
||||
- name: Publish Docs To Development
|
||||
if: github.ref == 'refs/heads/main'
|
||||
# if: github.ref == 'refs/heads/main'
|
||||
uses: up9cloud/action-rsync@v1.4
|
||||
env:
|
||||
HOST: ${{ secrets.WEBMASTER_SSH_HOST }}
|
||||
@@ -108,7 +138,7 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: built-demo
|
||||
name: demo
|
||||
- run: find .
|
||||
- name: Publish demo
|
||||
if: github.ref == 'refs/heads/main'
|
||||
|
||||
2
.github/workflows/python.yml
vendored
2
.github/workflows/python.yml
vendored
@@ -29,7 +29,7 @@ jobs:
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip install setuptools wheel pytest mypy
|
||||
pip install setuptools wheel pytest mypy mkdocs mkdocs-gen-files
|
||||
|
||||
- name: Build wheel
|
||||
working-directory: ./photon-lib/py
|
||||
|
||||
4
.gitignore
vendored
4
.gitignore
vendored
@@ -142,9 +142,13 @@ venv
|
||||
.venv/*
|
||||
.venv
|
||||
networktables.json
|
||||
|
||||
# Web stuff
|
||||
photon-server/src/main/resources/web/*
|
||||
node_modules
|
||||
dist
|
||||
components.d.ts
|
||||
|
||||
# Py docs stuff
|
||||
photon-lib/py/docs/build
|
||||
photon-server/src/main/resources/web/index.html
|
||||
|
||||
@@ -1,41 +1,25 @@
|
||||
cppHeaderFileInclude {
|
||||
\.h$
|
||||
\.hpp$
|
||||
\.inc$
|
||||
\.inl$
|
||||
}
|
||||
|
||||
cppSrcFileInclude {
|
||||
\.cpp$
|
||||
}
|
||||
|
||||
modifiableFileExclude {
|
||||
\.jpg$
|
||||
\.jpeg$
|
||||
\.png$
|
||||
\.gif$
|
||||
\.so$
|
||||
\.dll$
|
||||
\.webp$
|
||||
\.gif$
|
||||
\.ico$
|
||||
\.rknn$
|
||||
\.tflite$
|
||||
\.jpeg$
|
||||
\.jpg$
|
||||
\.mp4$
|
||||
\.pdf$
|
||||
\.png$
|
||||
\.rknn$
|
||||
\.so$
|
||||
\.svg$
|
||||
\.tflite$
|
||||
\.ttf$
|
||||
\.webp$
|
||||
\.woff2$
|
||||
gradlew
|
||||
photon-lib/py/photonlibpy/generated/
|
||||
photon-targeting/src/main/native/cpp/photon/constrained_solvepnp/generate/
|
||||
photon-targeting/src/generated/
|
||||
}
|
||||
|
||||
includeProject {
|
||||
^photonLib/
|
||||
}
|
||||
|
||||
includeOtherLibs {
|
||||
^frc/
|
||||
^networktables/
|
||||
^units/
|
||||
^wpi/
|
||||
photon-targeting/src/main/native/cpp/photon/constrained_solvepnp/generate/
|
||||
}
|
||||
@@ -20,6 +20,7 @@ If you are interested in contributing code or documentation to the project, plea
|
||||
- Photon UI demo: [http://photonvision.global/](http://photonvision.global/)
|
||||
- Javadocs: [javadocs.photonvision.org](https://javadocs.photonvision.org)
|
||||
- C++ Doxygen [cppdocs.photonvision.org](https://cppdocs.photonvision.org)
|
||||
- Python Documentation [pydocs.photonvision.org](https://pydocs.photonvision.org)
|
||||
|
||||
## Building
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ plugins {
|
||||
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'
|
||||
id "org.ysb33r.doxygen" version "1.0.4" apply false
|
||||
id "org.ysb33r.doxygen" version "2.0.0" apply false
|
||||
id 'com.gradleup.shadow' version '8.3.4' apply false
|
||||
id "com.github.node-gradle.node" version "7.0.1" apply false
|
||||
}
|
||||
@@ -39,7 +39,7 @@ ext {
|
||||
javalinVersion = "6.7.0"
|
||||
libcameraDriverVersion = "v2025.0.4"
|
||||
rknnVersion = "dev-v2025.0.0-5-g666c0c6"
|
||||
rubikVersion = "dev-v2025.1.0-8-g067a316"
|
||||
rubikVersion = "dev-v2025.1.0-6-g4a5e508"
|
||||
frcYear = "2025"
|
||||
mrcalVersion = "v2025.0.0";
|
||||
|
||||
@@ -92,7 +92,7 @@ spotless {
|
||||
format 'misc', {
|
||||
target fileTree('.') {
|
||||
include '**/*.md', '**/.gitignore'
|
||||
exclude '**/build/**', '**/build-*/**'
|
||||
exclude '**/build/**', '**/build-*/**', '**/node_modules/**'
|
||||
}
|
||||
trimTrailingWhitespace()
|
||||
indentWithSpaces(2)
|
||||
@@ -101,7 +101,7 @@ spotless {
|
||||
}
|
||||
|
||||
wrapper {
|
||||
gradleVersion '8.14.3'
|
||||
gradleVersion = '8.14.3'
|
||||
}
|
||||
|
||||
ext.getCurrentArch = {
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
|
||||
modifiableFileExclude {
|
||||
\.jpg$
|
||||
\.jpeg$
|
||||
\.png$
|
||||
\.gif$
|
||||
\.so$
|
||||
\.pdf$
|
||||
\.mp4$
|
||||
\.dll$
|
||||
\.webp$
|
||||
\.ico$
|
||||
\.rknn$
|
||||
\.tflite$
|
||||
\.svg$
|
||||
\.woff2$
|
||||
gradlew
|
||||
}
|
||||
@@ -21,6 +21,29 @@ project = "PhotonVision"
|
||||
copyright = "2024, PhotonVision"
|
||||
author = "Banks Troutman, Matt Morley"
|
||||
|
||||
# -- Git configuration -----------------------------------------------------
|
||||
import subprocess
|
||||
|
||||
try:
|
||||
# Use closest tag
|
||||
git_tag_ref = (
|
||||
subprocess.check_output(
|
||||
[
|
||||
"git",
|
||||
"describe",
|
||||
"--tags",
|
||||
],
|
||||
stderr=subprocess.DEVNULL,
|
||||
)
|
||||
.strip()
|
||||
.decode()
|
||||
)
|
||||
except subprocess.CalledProcessError:
|
||||
# Couldn't find closest tag, fallback to main
|
||||
git_tag_ref = "main"
|
||||
|
||||
myst_substitutions = {"git_tag_ref": git_tag_ref}
|
||||
|
||||
# -- General configuration ---------------------------------------------------
|
||||
|
||||
# Add any Sphinx extension module names here, as strings. They can be
|
||||
@@ -158,4 +181,4 @@ if token:
|
||||
linkcheck_auth = [(R"https://github.com/.+", token)]
|
||||
|
||||
# MyST configuration (https://myst-parser.readthedocs.io/en/latest/configuration.html)
|
||||
myst_enable_extensions = ["colon_fence"]
|
||||
myst_enable_extensions = ["colon_fence", "substitution"]
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
## Tracking AprilTags
|
||||
|
||||
Before you get started tracking AprilTags, ensure that you have followed the previous sections on installation, wiring and networking. Next, open the Web UI, go to the top right card, and switch to the "AprilTag" or "Aruco" type. You should see a screen similar to the one below.
|
||||
Before you get started tracking AprilTags, ensure that you have followed the previous sections on installation, wiring and networking. Next, open the Web UI, go to the top right card, and switch to the "AprilTag" or "ArUco" type. You should see a screen similar to the one below.
|
||||
|
||||
```{image} images/apriltag.png
|
||||
:align: center
|
||||
@@ -12,7 +12,7 @@ You are now able to detect and track AprilTags in 2D (yaw, pitch, roll, etc.). I
|
||||
|
||||
## Tuning AprilTags
|
||||
|
||||
AprilTag pipelines come with reasonable defaults to get you up and running with tracking. However, in order to optimize your performance and accuracy, you must tune your AprilTag pipeline using the settings below. Note that the settings below are different between the AprilTag and Aruco detectors but the concepts are the same.
|
||||
AprilTag pipelines come with reasonable defaults to get you up and running with tracking. However, in order to optimize your performance and accuracy, you must tune your AprilTag pipeline using the settings below. Note that the settings below are different between the AprilTag and ArUco detectors but the concepts are the same.
|
||||
|
||||
```{image} images/apriltag-tune.png
|
||||
:align: center
|
||||
|
||||
@@ -8,8 +8,8 @@ Note that both of these pipeline types detect AprilTag markers and are just two
|
||||
|
||||
## AprilTag
|
||||
|
||||
The AprilTag pipeline type is based on the [AprilTag](https://april.eecs.umich.edu/software/apriltag.html) library from the University of Michigan and we recommend it for most use cases. It is (to our understanding) most accurate pipeline type, but is also ~2x slower than AruCo. This was the pipeline type used by teams in the 2023 season and is well tested.
|
||||
The AprilTag pipeline type is based on the [AprilTag](https://april.eecs.umich.edu/software/apriltag.html) library from the University of Michigan and we recommend it for most use cases. It is (to our understanding) most accurate pipeline type, but is also ~2x slower than ArUco. This was the pipeline type used by teams in the 2023 season and is well tested.
|
||||
|
||||
## AruCo
|
||||
## ArUco
|
||||
|
||||
The AruCo pipeline is based on the [AruCo](https://docs.opencv.org/4.8.0/d9/d6a/group__aruco.html) library implementation from OpenCV. It is ~2x higher fps and ~2x lower latency than the AprilTag pipeline type, but is less accurate. We recommend this pipeline type for teams that need to run at a higher framerate or have a lower powered device. This pipeline type was new for the 2024 season.
|
||||
The ArUco pipeline is based on the [ArUco](https://docs.opencv.org/4.8.0/d9/d6a/group__aruco.html) library implementation from OpenCV. It is ~2x higher fps and ~2x lower latency than the AprilTag pipeline type, but is less accurate. We recommend this pipeline type for teams that need to run at a higher framerate or have a lower powered device. This pipeline type was new for the 2024 season.
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
In order to detect AprilTags and use 3D mode, your camera must be calibrated at the desired resolution! Inaccurate calibration will lead to poor performance.
|
||||
:::
|
||||
|
||||
To calibrate a camera, images of a Charuco board (or chessboard) are taken. By comparing where the grid corners should be in object space (for example, a corner once every inch in an 8x6 grid) with where they appear in the camera image, we can find a least-squares estimate for intrinsic camera properties like focal lengths, center point, and distortion coefficients. For more on camera calibration, please review the [OpenCV documentation](https://docs.opencv.org/4.x/dc/dbb/tutorial_py_calibration.html).
|
||||
To calibrate a camera, images of a ChArUco board (or chessboard) are taken. By comparing where the grid corners should be in object space (for example, a corner once every inch in an 8x6 grid) with where they appear in the camera image, we can find a least-squares estimate for intrinsic camera properties like focal lengths, center point, and distortion coefficients. For more on camera calibration, please review the [OpenCV documentation](https://docs.opencv.org/4.x/dc/dbb/tutorial_py_calibration.html).
|
||||
|
||||
:::{warning}
|
||||
While any resolution can be calibrated, higher resolutions may be too performance-intensive for some coprocessors to handle. Therefore, we recommend experimenting to see what works best for your coprocessor.
|
||||
@@ -16,6 +16,10 @@ The calibration data collected during calibration is specific to each physical c
|
||||
|
||||
## Calibration Tips
|
||||
|
||||
:::{warning}
|
||||
The usage of chessboards can result in bad calibration results if multiple similar images are taken. We strongly recommend that teams use ChArUco boards instead!
|
||||
:::
|
||||
|
||||
Accurate camera calibration is required in order to get accurate pose measurements when using AprilTags and 3D mode. The tips below should help ensure success:
|
||||
|
||||
01. Ensure the images you take have the target in different positions and angles, with as big of a difference between angles as possible. It is important to make sure the target overlay still lines up with the board while doing this. Tilt no more than 45 degrees.
|
||||
@@ -34,11 +38,11 @@ Following the ideas above should help in getting an accurate calibration.
|
||||
|
||||
### 1. Navigate to the calibration section in the UI.
|
||||
|
||||
The Cameras tab of the UI houses PhotonVision's camera calibration tooling. It assists users with calibrating their cameras, as well as allows them to view previously calibrated resolutions. We support both charuco and chessboard calibrations.
|
||||
The Cameras tab of the UI houses PhotonVision's camera calibration tooling. It assists users with calibrating their cameras, as well as allows them to view previously calibrated resolutions. We support both ChArUco and chessboard calibrations.
|
||||
|
||||
### 2. Print out the calibration target.
|
||||
|
||||
In the Camera Calibration tab, we'll print out the calibration target using the "Download" button. This should be printed on 8.5x11 printer paper. This page shows using an 8x8 charuco board (or chessboard depending on the selected calibration type).
|
||||
In the Camera Calibration tab, we'll print out the calibration target using the "Download" button. This should be printed on 8.5x11 printer paper. This page shows using an 8x8 ChArUco board (or chessboard depending on the selected calibration type).
|
||||
|
||||
:::{warning}
|
||||
Ensure that there is no scaling applied during printing (it should be at 100%) and that the PDF is printed as is on regular printer paper. Check the square size with calipers or an accurate measuring device after printing to ensure squares are sized properly, and enter the true size of the square in the UI text box. For optimal results, various resources are available online to calibrate your specific printer if needed.
|
||||
@@ -46,13 +50,13 @@ Ensure that there is no scaling applied during printing (it should be at 100%) a
|
||||
|
||||
### 3. Select calibration resolution and fill in appropriate target data.
|
||||
|
||||
We'll next select a resolution to calibrate and populate our pattern spacing, marker size, and board size. The provided chessboard and charuco board are an 8x8 grid of 1 inch square. The provided charuco board uses the 4x4 dictionary with a marker size of 0.75 inches (this board does not need the old OpenCV pattern selector selected). Printers are not perfect, and you need to measure your calibration target and enter the correct marker size (size of the aruco marker) and pattern spacing (aka size of the black square) using calipers or similar. Finally, once our entered data is correct, we'll click "start calibration."
|
||||
We'll next select a resolution to calibrate and populate our pattern spacing, marker size, and board size. The provided chessboard and ChArUco board are an 8x8 grid of 1 inch square. The provided ChArUco board uses the 4x4 dictionary with a marker size of 0.75 inches (this board does not need the old OpenCV pattern selector selected). Printers are not perfect, and you need to measure your calibration target and enter the correct marker size (size of the ArUco marker) and pattern spacing (aka size of the black square) using calipers or similar. Finally, once our entered data is correct, we'll click "start calibration."
|
||||
|
||||
:::{warning} Old OpenCV Pattern selector. This should be used in the case that the calibration image is generated from a version of OpenCV before version 4.6.0. This would include targets created by calib.io. If this selector is not set correctly the calibration will be completely invalid. For more info view [this GitHub issue](https://github.com/opencv/opencv_contrib/issues/3291).
|
||||
:::
|
||||
|
||||
:::{note}
|
||||
If you have a [calib.io](https://calib.io/) CharuCo Target you will have to enter the paramaters of your target. For example if your target says "9x12 | Checker Size: 30 mm | Marker Size: 22 mm | Dictionary: AruCo DICT 5x5", you would have to set the board type to Dict_5x5_1000, the pattern spacing to 1.1811 in (30 mm converted to inches), the marker size 0.866142 in (22 mm converted to inches), the board width to 12 and the board height to 9. If you chose the wrong tag family the board wont be detected during calibration. If you swap the width and height your calibration will have a very high error.
|
||||
If you have a [calib.io](https://calib.io/) ChArUco Target you will have to enter the paramaters of your target. For example if your target says "9x12 | Checker Size: 30 mm | Marker Size: 22 mm | Dictionary: ArUco DICT 5x5", you would have to set the board type to Dict_5x5_1000, the pattern spacing to 1.1811 in (30 mm converted to inches), the marker size 0.866142 in (22 mm converted to inches), the board width to 12 and the board height to 9. If you chose the wrong tag family the board wont be detected during calibration. If you swap the width and height your calibration will have a very high error.
|
||||
:::
|
||||
|
||||
### 4. Take at calibration images from various angles.
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
# Arducam Cameras
|
||||
|
||||
:::{warning}
|
||||
Arducam Pivariety cameras are **incompatible** with PhotonVision as they require a custom camera library not compatible with PhotonVision.
|
||||
:::
|
||||
|
||||
Arducam cameras are supported for setups with multiple devices. This is possible because Arducam provides software that allows you to assign truly different device names to each camera. This feature is particularly useful in complex setups where multiple cameras are used simultaneously.
|
||||
|
||||
## Setting Up Arducam Cameras
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Camera-Specifc Configuration
|
||||
# Camera-Specific Configuration
|
||||
|
||||
```{toctree}
|
||||
:maxdepth: 2
|
||||
|
||||
@@ -18,6 +18,10 @@ This section contains the build instructions from the source code available at [
|
||||
|
||||
[pnpm](https://pnpm.io/) is the package manager used to download dependencies for the UI. To install pnpm, follow [the instructions on the official pnpm website](https://pnpm.io/installation).
|
||||
|
||||
**Cross-Compilation Toolchains (Optional):**
|
||||
|
||||
If you plan to deploy PhotonVision to a coprocessor like a Raspberry Pi, you will need to install the appropriate cross-compilation toolchain for your platform. For `linuxarm64` devices, this can be accomplished by running `./gradlew installArm64Toolchain` in the root folder of the project.
|
||||
|
||||
## Compiling Instructions
|
||||
|
||||
### Getting the Source Code
|
||||
@@ -173,6 +177,29 @@ With the VSCode [Extension Pack for Java](https://marketplace.visualstudio.com/i
|
||||
|
||||
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.
|
||||
|
||||
### Running Tests With UI
|
||||
|
||||
By default, tests are run with UI disabled so they are not obtrusive during a build. All tests should be useful when the UI is disabled. However, if a particular test would benefit from having UI access (i.e. for debugging info), the UI can be enabled by passing the `enableTestUi` project property to Gradle. This will run all tests by default, but the Gradle `--tests` option can be used to [filter for specific tests](https://docs.gradle.org/current/userguide/java_testing.html#test_filtering).
|
||||
|
||||
```{eval-rst}
|
||||
.. tab-set::
|
||||
|
||||
.. tab-item:: Linux
|
||||
:sync: linux
|
||||
|
||||
``./gradlew test -PenableTestUi``
|
||||
|
||||
.. tab-item:: macOS
|
||||
:sync: macos
|
||||
|
||||
``./gradlew test -PenableTestUi``
|
||||
|
||||
.. tab-item:: Windows (cmd)
|
||||
:sync: windows
|
||||
|
||||
``gradlew test -PenableTestUi``
|
||||
```
|
||||
|
||||
### Debugging PhotonVision Running Locally
|
||||
|
||||
Unit tests can instead be debugged through the ``test`` Gradle task for a specific subproject in VSCode, found in the Gradle tab:
|
||||
@@ -275,3 +302,9 @@ Using the [GitHub CLI](https://cli.github.com/), we can download artifacts from
|
||||
MacOS builds are not published to releases as MacOS is not an officially
|
||||
supported platform. However, MacOS builds are still available from the MacOS
|
||||
build action, which can be found [here](https://github.com/PhotonVision/photonvision/actions/workflows/build.yml).
|
||||
|
||||
#### Forcing Object Detection in the UI
|
||||
|
||||
In order to force the Object Detection interface to be visible, it's necessary to hardcode the platform that `Platform.java` returns. This can be done by changing the function that detects the RK3588S/QCS6490 platform to always return true, and changing the `getCurrentPlatform()` function to always return the RK3588S/QCS6490 architecture.
|
||||
Alternatively, it's possible to modify the frontend code by changing all instances of `useSettingsStore().general.supportedBackends.length > 0` to `true`, which will force the card to render.
|
||||
Make sure to revert these changes before submitting a Pull Request.
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
```{toctree}
|
||||
building-photon
|
||||
building-docs
|
||||
linting
|
||||
developer-docs/index
|
||||
design-descriptions/index
|
||||
```
|
||||
|
||||
43
docs/source/docs/contributing/linting.md
Normal file
43
docs/source/docs/contributing/linting.md
Normal file
@@ -0,0 +1,43 @@
|
||||
# Linting the PhotonVision Codebase
|
||||
|
||||
## Versions
|
||||
|
||||
:::{note}
|
||||
If you work on other projects that use different versions of the same linters as PhotonVision, you may find it beneficial to use a [venv](https://docs.python.org/3/library/venv.html) instead of installing the linters globally. This will allow you to have different versions of the same linter installed for different projects.
|
||||
:::
|
||||
|
||||
The correct versions for each linter can be found under the linting workflow located [here](https://github.com/PhotonVision/photonvision/tree/main/.github/workflows). For *doc8*, the version can be found in `docs/requirements.txt`. If you've linted, and are still unable to pass CI, please check the versions of your linters.
|
||||
|
||||
## Frontend
|
||||
|
||||
### Linting the frontend
|
||||
|
||||
In order to lint the frontend, run `pnpm -C photon-client lint && pnpm -C photon-client format`. This should be done from the base level of the repo.
|
||||
|
||||
## Backend
|
||||
|
||||
### wpiformat installation
|
||||
|
||||
To lint the backend, PhotonVision uses *wpiformat* and *spotless*. Spotless is included with gradle, which means installation is not needed. To install wpiformat, run `pipx install wpiformat`. To install a specific version, run `pipx install wpiformat==<version>`.
|
||||
|
||||
### Linting the backend
|
||||
|
||||
To lint, run `./gradlew spotlessApply` and `wpiformat`.
|
||||
|
||||
## Documentation
|
||||
|
||||
### doc8 installation
|
||||
|
||||
To install *doc8*, the python tool we use to lint our documentation, run `pipx install doc8`. To install a specific version, run `pipx install doc8==<version>`.
|
||||
|
||||
### Linting the documentation
|
||||
|
||||
To lint the documentation, run `doc8 docs` from the root level of the docs.
|
||||
|
||||
## Alias
|
||||
|
||||
The following [alias](https://www.computerworld.com/article/1373210/how-to-use-aliases-in-linux-shell-commands.html) can be added to your shell config, which will allow you to lint the entirety of the PhotonVision project by running `pvLint`. The alias will work on Linux, macOS, Git Bash on Windows, and WSL.
|
||||
|
||||
```sh
|
||||
alias pvLint='wpiformat -v && ./gradlew spotlessApply && pnpm -C photon-client lint && pnpm -C photon-client format && doc8 docs'
|
||||
```
|
||||
@@ -6,7 +6,7 @@ The following example is from the PhotonLib example repository ([Java](https://g
|
||||
|
||||
- A Robot
|
||||
- A camera mounted rigidly to the robot's frame, centered and pointed forward.
|
||||
- A coprocessor running PhotonVision with an AprilTag or Aruco 2D Pipeline.
|
||||
- A coprocessor running PhotonVision with an AprilTag or ArUco 2D Pipeline.
|
||||
- [A printout of AprilTag 7](https://firstfrc.blob.core.windows.net/frc2025/FieldAssets/Apriltag_Images_and_User_Guide.pdf), mounted on a rigid and flat surface.
|
||||
|
||||
## Code
|
||||
|
||||
@@ -14,6 +14,6 @@ PhotonVision currently ONLY supports 640x640 Ultralytics YOLOv5, YOLOv8, and YOL
|
||||
Only quantized models are supported, so take care when exporting to select the option for quantization.
|
||||
:::
|
||||
|
||||
PhotonVision now ships with a [Python Notebook](https://github.com/PhotonVision/photonvision/blob/main/scripts/rknn-convert-tool/rknn_conversion.ipynb) that you can use in [Google Colab](https://colab.research.google.com) or in a local environment. In Google Colab, you can simply paste the PhotonVision GitHub URL into the "GitHub" tab and select the `rknn_conversion.ipynb` notebook without needing to manually download anything.
|
||||
PhotonVision now ships with a {{ '[Python Notebook](https://github.com/PhotonVision/photonvision/blob/{}/scripts/rknn_conversion.ipynb)'.format(git_tag_ref) }} that you can use in [Google Colab](https://colab.research.google.com) or in a local **Linux** environment (since `rknn-toolkit2` only supports Linux). In Google Colab, you can simply paste the PhotonVision GitHub URL into the "GitHub" tab and select the `rknn_conversion.ipynb` notebook without needing to manually download anything.
|
||||
|
||||
Please ensure that the model you are attempting to convert is among the {ref}`supported models <docs/objectDetection/opi:Supported Models>` and using the PyTorch format.
|
||||
|
||||
@@ -14,7 +14,7 @@ PhotonVision currently ONLY supports 640x640 Ultralytics YOLOv8 and YOLOv11 mode
|
||||
Only quantized models are supported, so take care when exporting to select the option for quantization.
|
||||
:::
|
||||
|
||||
PhotonVision now ships with a [Python Notebook](https://github.com/PhotonVision/photonvision/blob/main/scripts/rubik_conversion.ipynb) that you can use in [Google Colab](https://colab.research.google.com) or in a local environment. In Google Colab, you can simply paste the PhotonVision GitHub URL into the "GitHub" tab and select the `rubik_conversion.ipynb` notebook without needing to manually download anything.
|
||||
PhotonVision now ships with a {{ '[Python Notebook](https://github.com/PhotonVision/photonvision/blob/{}/scripts/rubik_conversion.ipynb)'.format(git_tag_ref) }} that you can use in [Google Colab](https://colab.research.google.com), [Kaggle](https://kaggle.com/code), or in a local environment. In Google Colab, you can simply paste the PhotonVision GitHub URL into the "GitHub" tab and select the `rubik_conversion.ipynb` notebook without needing to manually download anything.
|
||||
|
||||
Please ensure that the model you are attempting to convert is among the {ref}`supported models <docs/objectDetection/rubik:Supported Models>` and using the PyTorch format.
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ A vision pipeline represents a series of steps that are used to acquire an image
|
||||
|
||||
## Types of Pipelines
|
||||
|
||||
### AprilTag / AruCo
|
||||
### AprilTag / ArUco
|
||||
|
||||
This pipeline type is based on detecting AprilTag fiducial markers. More information about AprilTags can be found in the [WPILib documentation](https://docs.wpilib.org/en/stable/docs/software/vision-processing/apriltag/apriltag-intro.html). This pipeline provides easy to use 3D pose information which allows localization.
|
||||
|
||||
@@ -56,7 +56,7 @@ Each pipeline has a set of tabs that are used to configure the pipeline. All pip
|
||||
|
||||
Pipielines also have additional tabs that are specific to the pipeline type. Listed below are the tabs for each pipeline type.
|
||||
|
||||
### AprilTag / AruCo Pipelines
|
||||
### AprilTag / ArUco Pipelines
|
||||
|
||||
- AprilTag: This tab includes AprilTag specific tuning parameters, such as decimate, blur, threads, pose iterations, and more.
|
||||
|
||||
|
||||
@@ -9,8 +9,8 @@ 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](http://photonvision.global/#/cameras), or directly from your coprocessors cameras tab.
|
||||
- Use the Charuco calibration board:
|
||||
- Board Type: Charuco
|
||||
- Use the ChArUco calibration board:
|
||||
- Board Type: ChAruCo
|
||||
- Tag Family: 4x4
|
||||
- Pattern Spacing: 1.00in
|
||||
- Marker Size: 0.75in
|
||||
|
||||
19
docs/source/docs/quick-start/camera-focusing.md
Normal file
19
docs/source/docs/quick-start/camera-focusing.md
Normal file
@@ -0,0 +1,19 @@
|
||||
# Camera Focusing
|
||||
|
||||
## Prepare Camera
|
||||
:::{warning}
|
||||
Refocusing your camera **will** make your calibration inaccurate, make sure to recalibrate after focusing.
|
||||
:::
|
||||
To ensure that your camera is focused properly, mount it to a secure surface and ensure it does not move drastically. Point your camera at a detailed surface like a calibration board, and make sure that it not too close to the camera.
|
||||
|
||||
## Using Focus Mode
|
||||
:::{important}
|
||||
When you enable Focus Mode, it will assign a *Score* to the current focus, this score depends on your environment and the lighting. This score cannot be compared to a focus score collected from other environments.
|
||||
:::
|
||||
- In the Cameras tab, turn on Focus Mode.
|
||||
- Rotate the lens on your camera to try and get the focus score as high as possible.
|
||||
- Once you cannot get a higher score, this indicates that your camera is fully focused and can be set in place using glue if desired.
|
||||
|
||||
```{image} images/focusModeExample.png
|
||||
:scale: 50%
|
||||
```
|
||||
BIN
docs/source/docs/quick-start/images/focusModeExample.png
Normal file
BIN
docs/source/docs/quick-start/images/focusModeExample.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 313 KiB |
@@ -9,5 +9,6 @@ wiring
|
||||
networking
|
||||
camera-matching
|
||||
camera-calibration
|
||||
camera-focusing
|
||||
quick-configure
|
||||
```
|
||||
|
||||
@@ -34,14 +34,17 @@ Balena Etcher can also be used, but historically has had issues such as bootloop
|
||||
|
||||
## Limelight Installation
|
||||
|
||||
:::{note}
|
||||
In order to mount the Limelight 4 on your computer, it's necessary to use `rpiboot`. To do this, follow the instructions [here](https://docs.limelightvision.io/docs/docs-limelight/getting-started/limelight-4#4-updating-limelightos).
|
||||
:::
|
||||
In order to flash your Limelight you should follow the instructions on the Limelight documentation for the relevant version. Make sure to replace the Limelight OS image with the relevant PhotonVision image.
|
||||
|
||||
Limelights have a different installation processes. Simply connect the limelight to your computer using the proper usb cable. Select the compute module in the [Raspberry Pi Imager](https://www.raspberrypi.com/software/). If it doesn’t show up after 30s try using another USB port, initialization may take a while. If prompted, install the recommended missing drivers. Select the image, and flash.
|
||||
| Limelight Version | Limelight Documentation | PhotonVision Image | |
|
||||
| ----------------- | ------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------- | --- |
|
||||
| 2 | [Updating Limelight 2 OS](https://docs.limelightvision.io/docs/docs-limelight/getting-started/limelight-2#4-updating-limelightos) | photonvision-{version}-linuxarm64_limelight2.img.xz | |
|
||||
| 3 | [Updating Limelight 3 OS](https://docs.limelightvision.io/docs/docs-limelight/getting-started/limelight-3#4-updating-limelightos) | photonvision-{version}-linuxarm64_limelight3.img.xz | |
|
||||
| 3G | [Updating Limelight 3G OS](https://docs.limelightvision.io/docs/docs-limelight/getting-started/limelight-3g#4-updating-limelightos) | photonvision-{version}-linuxarm64_limelight3g.img.xz | |
|
||||
| 4 | [Updating Limelight 4 OS](https://docs.limelightvision.io/docs/docs-limelight/getting-started/limelight-4#4-updating-limelightos) | photonvision-{version}-linuxarm64_limelight4.img.xz | |
|
||||
|
||||
:::{note}
|
||||
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.
|
||||
Limelight models will need a [custom hardware config file](https://github.com/PhotonVision/photonvision/tree/main/docs/source/docs/advanced-installation/sw_install/files) for LEDs or other hardware features to work.
|
||||
:::
|
||||
|
||||
## Rubik Pi 3 Installation
|
||||
|
||||
@@ -138,5 +138,7 @@ docs/contributing/index
|
||||
|
||||
Java <https://javadocs.photonvision.org>
|
||||
|
||||
C++ <https://cppdocs.photonvision.org/>
|
||||
C++ <https://cppdocs.photonvision.org>
|
||||
|
||||
Python <https://pydocs.photonvision.org>
|
||||
```
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
"build-demo": "vite build --mode demo",
|
||||
"lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix",
|
||||
"format": "prettier --write src/",
|
||||
"lint-ci": "eslint . --max-warnings 0 --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix",
|
||||
"lint-ci": "eslint . --max-warnings 0 --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts",
|
||||
"format-ci": "prettier --check src/"
|
||||
},
|
||||
"dependencies": {
|
||||
|
||||
@@ -105,18 +105,21 @@ onBeforeUnmount(() => {
|
||||
/>
|
||||
<div class="stream-overlay" :style="overlayStyle">
|
||||
<pv-icon
|
||||
color="primary"
|
||||
icon-name="mdi-camera-image"
|
||||
tooltip="Capture and save a frame of this stream"
|
||||
class="ma-1 mr-2"
|
||||
@click="handleCaptureClick"
|
||||
/>
|
||||
<pv-icon
|
||||
color="primary"
|
||||
icon-name="mdi-fullscreen"
|
||||
tooltip="Open this stream in fullscreen"
|
||||
class="ma-1 mr-2"
|
||||
@click="handleFullscreenRequest"
|
||||
/>
|
||||
<pv-icon
|
||||
color="primary"
|
||||
icon-name="mdi-open-in-new"
|
||||
tooltip="Open this stream in a new window"
|
||||
class="ma-1 mr-2"
|
||||
|
||||
@@ -13,5 +13,15 @@ import { useStateStore } from "@/stores/StateStore";
|
||||
<p style="padding: 0; margin: 0; text-align: center">
|
||||
{{ useStateStore().snackbarData.message }}
|
||||
</p>
|
||||
<v-progress-linear
|
||||
v-if="useStateStore().snackbarData.progressBar != -1"
|
||||
v-model="useStateStore().snackbarData.progressBar"
|
||||
height="15"
|
||||
:color="useStateStore().snackbarData.progressBarColor"
|
||||
>
|
||||
<template #default="{ value }">
|
||||
<strong> {{ Math.ceil(value) }}% </strong>
|
||||
</template>
|
||||
</v-progress-linear>
|
||||
</v-snackbar>
|
||||
</template>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, ref } from "vue";
|
||||
import { computed, ref, watchEffect } from "vue";
|
||||
import { useCameraSettingsStore } from "@/stores/settings/CameraSettingsStore";
|
||||
import { CalibrationBoardTypes, CalibrationTagFamilies, type VideoFormat } from "@/types/SettingTypes";
|
||||
import MonoLogo from "@/assets/images/logoMono.png";
|
||||
@@ -79,6 +79,18 @@ const calibrationDivisors = computed(() =>
|
||||
})
|
||||
);
|
||||
|
||||
const uniqueVideoResolutionString = ref("");
|
||||
|
||||
// Use a watchEffect so the value is populated/reacts when the stores become available or update.
|
||||
// This avoids trying to index into an array that may be empty during page reload.
|
||||
watchEffect(() => {
|
||||
const currentIndex = useCameraSettingsStore().currentVideoFormat.index ?? 0;
|
||||
useStateStore().calibrationData.videoFormatIndex = currentIndex;
|
||||
const names = useCameraSettingsStore().currentCameraSettings.validVideoFormats.map((f) =>
|
||||
getResolutionString(f.resolution)
|
||||
);
|
||||
uniqueVideoResolutionString.value = names[currentIndex] ?? names[0] ?? "";
|
||||
});
|
||||
const squareSizeIn = ref(1);
|
||||
const markerSizeIn = ref(0.75);
|
||||
const patternWidth = ref(8);
|
||||
@@ -129,7 +141,7 @@ const downloadCalibBoard = async () => {
|
||||
break;
|
||||
|
||||
case CalibrationBoardTypes.Charuco:
|
||||
// Add pregenerated charuco
|
||||
// Add pregenerated ChArUco
|
||||
const charucoImage = new Image();
|
||||
charucoImage.src = CharucoImage;
|
||||
doc.addImage(charucoImage, "PNG", 0.25, 1.5, 8, 8);
|
||||
@@ -279,23 +291,36 @@ const setSelectedVideoFormat = (format: VideoFormat) => {
|
||||
: 'MrCal failed to load, check journalctl logs for details.'
|
||||
"
|
||||
/>
|
||||
<!-- 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"
|
||||
v-model="uniqueVideoResolutionString"
|
||||
label="Resolution"
|
||||
:select-cols="8"
|
||||
:disabled="isCalibrating"
|
||||
tooltip="Resolution to calibrate at (you will have to calibrate every resolution you use 3D mode on)"
|
||||
:items="getUniqueVideoResolutionStrings()"
|
||||
@update:model-value="
|
||||
useStateStore().calibrationData.videoFormatIndex =
|
||||
getUniqueVideoResolutionStrings().find((v) => v.value === $event)?.value || 0
|
||||
"
|
||||
/>
|
||||
<pv-select
|
||||
v-model="boardType"
|
||||
label="Board Type"
|
||||
tooltip="Calibration board pattern to use"
|
||||
:select-cols="8"
|
||||
:items="['Chessboard', 'Charuco']"
|
||||
:items="['Chessboard', 'ChArUco']"
|
||||
:disabled="isCalibrating"
|
||||
/>
|
||||
<v-alert
|
||||
v-if="boardType !== CalibrationBoardTypes.Charuco"
|
||||
closable
|
||||
density="compact"
|
||||
variant="tonal"
|
||||
color="warning"
|
||||
icon="mdi-alert-box"
|
||||
text="The usage of chessboards can result in bad calibration results if multiple
|
||||
similar images are taken. We strongly recommend that teams use ChArUco boards instead!"
|
||||
/>
|
||||
<pv-select
|
||||
v-if="boardType !== CalibrationBoardTypes.Charuco"
|
||||
v-model="useCameraSettingsStore().currentPipelineSettings.streamingFrameDivisor"
|
||||
@@ -311,7 +336,7 @@ const setSelectedVideoFormat = (format: VideoFormat) => {
|
||||
v-if="boardType === CalibrationBoardTypes.Charuco"
|
||||
v-model="tagFamily"
|
||||
label="Tag Family"
|
||||
tooltip="Dictionary of aruco markers on the charuco board"
|
||||
tooltip="Dictionary of ArUco markers on the ChArUco board"
|
||||
:select-cols="8"
|
||||
:items="['Dict_4X4_1000', 'Dict_5X5_1000', 'Dict_6X6_1000', 'Dict_7X7_1000']"
|
||||
:disabled="isCalibrating"
|
||||
@@ -527,9 +552,9 @@ const setSelectedVideoFormat = (format: VideoFormat) => {
|
||||
<v-card-text>
|
||||
Camera has been successfully calibrated for
|
||||
{{
|
||||
getUniqueVideoResolutionStrings().find(
|
||||
(v) => v.value === useStateStore().calibrationData.videoFormatIndex
|
||||
)?.name
|
||||
useCameraSettingsStore().currentCameraSettings.validVideoFormats.map((f) =>
|
||||
getResolutionString(f.resolution)
|
||||
)[useStateStore().calibrationData.videoFormatIndex]
|
||||
}}!
|
||||
</v-card-text>
|
||||
</template>
|
||||
|
||||
@@ -3,8 +3,9 @@ import type { CameraCalibrationResult, VideoFormat } from "@/types/SettingTypes"
|
||||
import { useCameraSettingsStore } from "@/stores/settings/CameraSettingsStore";
|
||||
import { useStateStore } from "@/stores/StateStore";
|
||||
import { computed, inject, ref } from "vue";
|
||||
import { getResolutionString, parseJsonFile } from "@/lib/PhotonUtils";
|
||||
import { axiosPost, getResolutionString, parseJsonFile } from "@/lib/PhotonUtils";
|
||||
import { useTheme } from "vuetify";
|
||||
import PvDeleteModal from "@/components/common/pv-delete-modal.vue";
|
||||
|
||||
const theme = useTheme();
|
||||
|
||||
@@ -12,6 +13,16 @@ const props = defineProps<{
|
||||
videoFormat: VideoFormat;
|
||||
}>();
|
||||
|
||||
const confirmRemoveDialog = ref({ show: false, vf: props.videoFormat as VideoFormat });
|
||||
|
||||
const removeCalibration = (vf: VideoFormat) => {
|
||||
axiosPost("/calibration/remove", "delete a camera calibration", {
|
||||
cameraUniqueName: useCameraSettingsStore().currentCameraSettings.uniqueName,
|
||||
width: vf.resolution.width,
|
||||
height: vf.resolution.height
|
||||
});
|
||||
};
|
||||
|
||||
const exportCalibration = ref();
|
||||
const openExportCalibrationPrompt = () => {
|
||||
exportCalibration.value.click();
|
||||
@@ -93,18 +104,19 @@ const calibrationImageURL = (index: number) =>
|
||||
</script>
|
||||
<template>
|
||||
<v-card color="surface" dark>
|
||||
<div class="d-flex flex-wrap pt-2 pl-2 pr-2">
|
||||
<div class="d-flex flex-wrap pt-2 pl-2 pr-2 align-center">
|
||||
<v-col cols="12" md="6">
|
||||
<v-card-title class="pa-0"> Calibration Details </v-card-title>
|
||||
</v-col>
|
||||
<v-col cols="6" md="3" class="d-flex align-center pt-0 pt-md-3 pl-6 pl-md-3">
|
||||
<v-col cols="12" md="6" class="d-flex align-center pt-0 pt-md-3">
|
||||
<v-btn
|
||||
color="buttonPassive"
|
||||
style="width: 100%"
|
||||
class="mr-2"
|
||||
style="flex: 1"
|
||||
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
|
||||
@click="openUploadPhotonCalibJsonPrompt"
|
||||
>
|
||||
<v-icon start size="large"> mdi-import</v-icon>
|
||||
<v-icon start size="large">mdi-import</v-icon>
|
||||
<span>Import</span>
|
||||
</v-btn>
|
||||
<input
|
||||
@@ -114,12 +126,11 @@ const calibrationImageURL = (index: number) =>
|
||||
style="display: none"
|
||||
@change="importCalibration"
|
||||
/>
|
||||
</v-col>
|
||||
<v-col cols="6" md="3" class="d-flex align-center pt-0 pt-md-3 pr-6 pr-md-3">
|
||||
<v-btn
|
||||
color="buttonPassive"
|
||||
class="mr-2"
|
||||
:disabled="!currentCalibrationCoeffs"
|
||||
style="width: 100%"
|
||||
style="flex: 1"
|
||||
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
|
||||
@click="openExportCalibrationPrompt"
|
||||
>
|
||||
@@ -132,6 +143,16 @@ const calibrationImageURL = (index: number) =>
|
||||
:href="exportCalibrationURL"
|
||||
target="_blank"
|
||||
/>
|
||||
<v-btn
|
||||
color="error"
|
||||
:disabled="!currentCalibrationCoeffs"
|
||||
style="flex: 1"
|
||||
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
|
||||
@click="() => (confirmRemoveDialog = { show: true, vf: props.videoFormat })"
|
||||
>
|
||||
<v-icon start size="large">mdi-delete</v-icon>
|
||||
<span>Delete</span>
|
||||
</v-btn>
|
||||
</v-col>
|
||||
</div>
|
||||
<v-card-title class="pt-0 pb-0"
|
||||
@@ -289,6 +310,14 @@ const calibrationImageURL = (index: number) =>
|
||||
</v-data-table>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
|
||||
<pv-delete-modal
|
||||
v-model="confirmRemoveDialog.show"
|
||||
:width="500"
|
||||
:title="'Delete Calibration'"
|
||||
:description="`Are you sure you want to delete the calibration for '${confirmRemoveDialog.vf.resolution.width}x${confirmRemoveDialog.vf.resolution.height}'? This action cannot be undone.`"
|
||||
:on-confirm="() => removeCalibration(confirmRemoveDialog.vf)"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
<script setup lang="ts">
|
||||
import PvSelect, { type SelectItem } from "@/components/common/pv-select.vue";
|
||||
import PvInput from "@/components/common/pv-input.vue";
|
||||
import PvDeleteModal from "@/components/common/pv-delete-modal.vue";
|
||||
import PvNumberInput from "@/components/common/pv-number-input.vue";
|
||||
import PvSwitch from "@/components/common/pv-switch.vue";
|
||||
import { useCameraSettingsStore } from "@/stores/settings/CameraSettingsStore";
|
||||
import { useStateStore } from "@/stores/StateStore";
|
||||
import { computed, ref, watchEffect } from "vue";
|
||||
import { type CameraSettingsChangeRequest, ValidQuirks } from "@/types/SettingTypes";
|
||||
import axios from "axios";
|
||||
import { useTheme } from "vuetify";
|
||||
import { axiosPost } from "@/lib/PhotonUtils";
|
||||
|
||||
const theme = useTheme();
|
||||
|
||||
@@ -15,7 +16,14 @@ const tempSettingsStruct = ref<CameraSettingsChangeRequest>({
|
||||
fov: useCameraSettingsStore().currentCameraSettings.fov.value,
|
||||
quirksToChange: Object.assign({}, useCameraSettingsStore().currentCameraSettings.cameraQuirks.quirks)
|
||||
});
|
||||
|
||||
const focusMode = computed<boolean>({
|
||||
get: () => useCameraSettingsStore().isFocusMode,
|
||||
set: (v) =>
|
||||
useCameraSettingsStore().changeCurrentPipelineIndex(
|
||||
v ? -3 : useCameraSettingsStore().currentCameraSettings.lastPipelineIndex || 0,
|
||||
true
|
||||
)
|
||||
});
|
||||
const arducamSelectWrapper = computed<number>({
|
||||
get: () => {
|
||||
if (tempSettingsStruct.value.quirksToChange.ArduOV9281Controls) return 1;
|
||||
@@ -112,44 +120,10 @@ watchEffect(() => {
|
||||
});
|
||||
|
||||
const showDeleteCamera = ref(false);
|
||||
const yesDeleteMySettingsText = ref("");
|
||||
const deletingCamera = ref(false);
|
||||
const deleteThisCamera = () => {
|
||||
if (deletingCamera.value) return;
|
||||
deletingCamera.value = true;
|
||||
|
||||
const payload = { cameraUniqueName: useStateStore().currentCameraUniqueName };
|
||||
|
||||
axios
|
||||
.post("/utils/nukeOneCamera", payload)
|
||||
.then(() => {
|
||||
useStateStore().showSnackbarMessage({
|
||||
message: "Successfully dispatched the delete command. Waiting for backend to start back up",
|
||||
color: "success"
|
||||
});
|
||||
})
|
||||
.catch((error) => {
|
||||
if (error.response) {
|
||||
useStateStore().showSnackbarMessage({
|
||||
message: "The backend is unable to fulfil the request to delete this camera.",
|
||||
color: "error"
|
||||
});
|
||||
} else if (error.request) {
|
||||
useStateStore().showSnackbarMessage({
|
||||
message: "Error while trying to process the request! The backend didn't respond.",
|
||||
color: "error"
|
||||
});
|
||||
} else {
|
||||
useStateStore().showSnackbarMessage({
|
||||
message: "An error occurred while trying to process the request.",
|
||||
color: "error"
|
||||
});
|
||||
}
|
||||
})
|
||||
.finally(() => {
|
||||
deletingCamera.value = false;
|
||||
showDeleteCamera.value = false;
|
||||
});
|
||||
axiosPost("/utils/nukeOneCamera", "delete this camera", {
|
||||
cameraUniqueName: useStateStore().currentCameraUniqueName
|
||||
});
|
||||
};
|
||||
const wrappedCameras = computed<SelectItem[]>(() =>
|
||||
Object.keys(useCameraSettingsStore().cameras).map((cameraUniqueName) => ({
|
||||
@@ -192,6 +166,11 @@ const wrappedCameras = computed<SelectItem[]>(() =>
|
||||
]"
|
||||
:select-cols="8"
|
||||
/>
|
||||
<pv-switch
|
||||
v-model="focusMode"
|
||||
tooltip="Enable Focus Mode to start focusing the lens on your camera"
|
||||
label="Focus Mode"
|
||||
></pv-switch>
|
||||
</v-card-text>
|
||||
<v-card-text class="d-flex pt-0">
|
||||
<v-col cols="6" class="pa-0 pr-2">
|
||||
@@ -221,45 +200,13 @@ const wrappedCameras = computed<SelectItem[]>(() =>
|
||||
</v-col>
|
||||
</v-card-text>
|
||||
|
||||
<v-dialog v-model="showDeleteCamera" width="800">
|
||||
<v-card color="surface" flat>
|
||||
<v-card-title> Delete {{ useCameraSettingsStore().currentCameraSettings.nickname }}? </v-card-title>
|
||||
<v-card-text class="pt-0 pb-10px">
|
||||
Are you sure you want to delete "{{ useCameraSettingsStore().currentCameraSettings.nickname }}"? This cannot
|
||||
be undone.
|
||||
</v-card-text>
|
||||
<v-card-text class="pt-0 pb-10px">
|
||||
<pv-input
|
||||
v-model="yesDeleteMySettingsText"
|
||||
:label="'Type "' + useCameraSettingsStore().currentCameraName + '":'"
|
||||
:label-cols="6"
|
||||
:input-cols="6"
|
||||
/>
|
||||
</v-card-text>
|
||||
<v-card-actions class="pa-5 pt-0">
|
||||
<v-btn
|
||||
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
|
||||
color="primary"
|
||||
class="text-black"
|
||||
@click="showDeleteCamera = false"
|
||||
>
|
||||
Cancel
|
||||
</v-btn>
|
||||
<v-btn
|
||||
color="error"
|
||||
:disabled="
|
||||
yesDeleteMySettingsText.toLowerCase() !== useCameraSettingsStore().currentCameraName.toLowerCase()
|
||||
"
|
||||
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
|
||||
:loading="deletingCamera"
|
||||
@click="deleteThisCamera"
|
||||
>
|
||||
<v-icon start class="open-icon" size="large"> mdi-trash-can-outline </v-icon>
|
||||
<span class="open-label">Delete</span>
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
<pv-delete-modal
|
||||
v-model="showDeleteCamera"
|
||||
title="Delete Camera"
|
||||
:description="`Are you sure you want to delete the camera '${useCameraSettingsStore().currentCameraSettings.nickname}'? This action cannot be undone.`"
|
||||
:expected-confirmation-text="useCameraSettingsStore().currentCameraSettings.nickname"
|
||||
:on-confirm="deleteThisCamera"
|
||||
/>
|
||||
</v-card>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -58,6 +58,15 @@ const fpsTooLow = computed<boolean>(() => {
|
||||
<v-chip v-else label color="red" variant="text" style="font-size: 1rem; padding: 0; margin: 0">
|
||||
<span class="pr-1">Camera not connected</span>
|
||||
</v-chip>
|
||||
<v-chip
|
||||
v-if="useCameraSettingsStore().isFocusMode"
|
||||
label
|
||||
color="primary"
|
||||
variant="text"
|
||||
style="font-size: 1rem; padding: 0; margin: auto"
|
||||
>
|
||||
<span class="pr-1"> Focus: {{ Math.round(useStateStore().currentPipelineResults?.focus || 0) }} </span>
|
||||
</v-chip>
|
||||
<v-switch
|
||||
v-model="driverMode"
|
||||
:disabled="useCameraSettingsStore().isCalibrationMode || useCameraSettingsStore().pipelineNames.length === 0"
|
||||
@@ -95,7 +104,11 @@ const fpsTooLow = computed<boolean>(() => {
|
||||
color="buttonPassive"
|
||||
class="fill"
|
||||
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
|
||||
:disabled="useCameraSettingsStore().isDriverMode || useCameraSettingsStore().isCalibrationMode"
|
||||
:disabled="
|
||||
useCameraSettingsStore().isDriverMode ||
|
||||
useCameraSettingsStore().isCalibrationMode ||
|
||||
useCameraSettingsStore().isFocusMode
|
||||
"
|
||||
>
|
||||
<v-icon start class="mode-btn-icon" size="large">mdi-import</v-icon>
|
||||
<span class="mode-btn-label">Raw</span>
|
||||
@@ -104,7 +117,11 @@ const fpsTooLow = computed<boolean>(() => {
|
||||
color="buttonPassive"
|
||||
class="fill"
|
||||
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
|
||||
:disabled="useCameraSettingsStore().isDriverMode || useCameraSettingsStore().isCalibrationMode"
|
||||
:disabled="
|
||||
useCameraSettingsStore().isDriverMode ||
|
||||
useCameraSettingsStore().isCalibrationMode ||
|
||||
useCameraSettingsStore().isFocusMode
|
||||
"
|
||||
>
|
||||
<v-icon start class="mode-btn-icon" size="large">mdi-export</v-icon>
|
||||
<span class="mode-btn-label">Processed</span>
|
||||
|
||||
85
photon-client/src/components/common/pv-delete-modal.vue
Normal file
85
photon-client/src/components/common/pv-delete-modal.vue
Normal file
@@ -0,0 +1,85 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from "vue";
|
||||
import { useTheme } from "vuetify";
|
||||
import pvInput from "./pv-input.vue";
|
||||
|
||||
const theme = useTheme();
|
||||
|
||||
const value = defineModel<boolean | undefined>({ required: true });
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
expectedConfirmationText?: string;
|
||||
onBackup?: () => void;
|
||||
onConfirm: () => void;
|
||||
title: string;
|
||||
description?: string;
|
||||
deleteText?: string;
|
||||
width?: number;
|
||||
}>(),
|
||||
{
|
||||
width: 700
|
||||
}
|
||||
);
|
||||
|
||||
const confirmationText = ref("");
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<v-dialog v-model="value" :width="props.width" dark>
|
||||
<v-card color="surface" flat>
|
||||
<v-card-title style="display: flex; justify-content: center">
|
||||
{{ title }}
|
||||
</v-card-title>
|
||||
<v-card-text class="pt-0 pb-10px">
|
||||
<span> {{ description }} </span>
|
||||
</v-card-text>
|
||||
<v-card-text v-if="expectedConfirmationText" class="pt-0 pb-0">
|
||||
<pv-input
|
||||
v-model="confirmationText"
|
||||
:label="'Type "' + expectedConfirmationText + '":'"
|
||||
:label-cols="6"
|
||||
:input-cols="6"
|
||||
/>
|
||||
</v-card-text>
|
||||
<v-card-text class="pt-10px">
|
||||
<v-row class="align-center text-white">
|
||||
<v-col v-if="onBackup" cols="6">
|
||||
<v-btn
|
||||
color="buttonActive"
|
||||
style="float: right"
|
||||
width="100%"
|
||||
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
|
||||
@click="onBackup"
|
||||
>
|
||||
<v-icon start class="open-icon" size="large"> mdi-export </v-icon>
|
||||
<span class="open-label">Backup Data</span>
|
||||
</v-btn>
|
||||
</v-col>
|
||||
<v-col v-if="description" :cols="onBackup ? '6' : '12'">
|
||||
<v-btn
|
||||
color="error"
|
||||
width="100%"
|
||||
:disabled="
|
||||
expectedConfirmationText
|
||||
? confirmationText.toLowerCase() !== expectedConfirmationText.toLowerCase()
|
||||
: false
|
||||
"
|
||||
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
|
||||
@click="
|
||||
onConfirm();
|
||||
confirmationText = '';
|
||||
value = false;
|
||||
"
|
||||
>
|
||||
<v-icon start class="open-icon" size="large"> mdi-trash-can-outline </v-icon>
|
||||
<span class="open-label">
|
||||
{{ deleteText ?? title }}
|
||||
</span>
|
||||
</v-btn>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
</template>
|
||||
@@ -9,6 +9,7 @@ import PvInput from "@/components/common/pv-input.vue";
|
||||
import { PipelineType } from "@/types/PipelineTypes";
|
||||
import { useSettingsStore } from "@/stores/settings/GeneralSettingsStore";
|
||||
import { useTheme } from "vuetify";
|
||||
import PvDeleteModal from "@/components/common/pv-delete-modal.vue";
|
||||
|
||||
const theme = useTheme();
|
||||
|
||||
@@ -92,6 +93,9 @@ const pipelineNamesWrapper = computed<SelectItem[]>(() => {
|
||||
if (useCameraSettingsStore().isDriverMode) {
|
||||
pipelineNames.push({ name: "Driver Mode", value: WebsocketPipelineType.DriverMode });
|
||||
}
|
||||
if (useCameraSettingsStore().isFocusMode) {
|
||||
pipelineNames.push({ name: "Focus Mode", value: WebsocketPipelineType.FocusCamera });
|
||||
}
|
||||
if (useCameraSettingsStore().isCalibrationMode) {
|
||||
pipelineNames.push({ name: "3D Calibration Mode", value: WebsocketPipelineType.Calib3d });
|
||||
}
|
||||
@@ -130,7 +134,7 @@ const validNewPipelineTypes = computed(() => {
|
||||
{ name: "Reflective", value: WebsocketPipelineType.Reflective },
|
||||
{ name: "Colored Shape", value: WebsocketPipelineType.ColoredShape },
|
||||
{ name: "AprilTag", value: WebsocketPipelineType.AprilTag },
|
||||
{ name: "Aruco", value: WebsocketPipelineType.Aruco }
|
||||
{ name: "ArUco", value: WebsocketPipelineType.Aruco }
|
||||
];
|
||||
if (useSettingsStore().general.supportedBackends.length > 0) {
|
||||
pipelineTypes.push({ name: "Object Detection", value: WebsocketPipelineType.ObjectDetection });
|
||||
@@ -168,7 +172,7 @@ const pipelineTypesWrapper = computed<{ name: string; value: number }[]>(() => {
|
||||
{ name: "Reflective", value: WebsocketPipelineType.Reflective },
|
||||
{ name: "Colored Shape", value: WebsocketPipelineType.ColoredShape },
|
||||
{ name: "AprilTag", value: WebsocketPipelineType.AprilTag },
|
||||
{ name: "Aruco", value: WebsocketPipelineType.Aruco }
|
||||
{ name: "ArUco", value: WebsocketPipelineType.Aruco }
|
||||
];
|
||||
if (useSettingsStore().general.supportedBackends.length > 0) {
|
||||
pipelineTypes.push({ name: "Object Detection", value: WebsocketPipelineType.ObjectDetection });
|
||||
@@ -177,6 +181,9 @@ const pipelineTypesWrapper = computed<{ name: string; value: number }[]>(() => {
|
||||
if (useCameraSettingsStore().isDriverMode) {
|
||||
pipelineTypes.push({ name: "Driver Mode", value: WebsocketPipelineType.DriverMode });
|
||||
}
|
||||
if (useCameraSettingsStore().isFocusMode) {
|
||||
pipelineTypes.push({ name: "Focus Mode", value: WebsocketPipelineType.FocusCamera });
|
||||
}
|
||||
if (useCameraSettingsStore().isCalibrationMode) {
|
||||
pipelineTypes.push({ name: "3D Calibration Mode", value: WebsocketPipelineType.Calib3d });
|
||||
}
|
||||
@@ -187,6 +194,7 @@ const pipelineType = ref<WebsocketPipelineType>(useCameraSettingsStore().current
|
||||
const currentPipelineType = computed<WebsocketPipelineType>({
|
||||
get: () => {
|
||||
if (useCameraSettingsStore().isDriverMode) return WebsocketPipelineType.DriverMode;
|
||||
if (useCameraSettingsStore().isFocusMode) return WebsocketPipelineType.FocusCamera;
|
||||
if (useCameraSettingsStore().isCalibrationMode) return WebsocketPipelineType.Calib3d;
|
||||
return pipelineType.value;
|
||||
},
|
||||
@@ -290,6 +298,7 @@ const wrappedCameras = computed<SelectItem[]>(() =>
|
||||
tooltip="Each pipeline runs on a camera output and stores a unique set of processing settings"
|
||||
:disabled="
|
||||
useCameraSettingsStore().isDriverMode ||
|
||||
useCameraSettingsStore().isFocusMode ||
|
||||
useCameraSettingsStore().isCalibrationMode ||
|
||||
!useCameraSettingsStore().hasConnected
|
||||
"
|
||||
@@ -366,6 +375,7 @@ const wrappedCameras = computed<SelectItem[]>(() =>
|
||||
tooltip="Changes the pipeline type, which changes the type of processing that will happen on input frames"
|
||||
:disabled="
|
||||
useCameraSettingsStore().isDriverMode ||
|
||||
useCameraSettingsStore().isFocusMode ||
|
||||
useCameraSettingsStore().isCalibrationMode ||
|
||||
!useCameraSettingsStore().hasConnected
|
||||
"
|
||||
@@ -413,33 +423,13 @@ const wrappedCameras = computed<SelectItem[]>(() =>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
<v-dialog v-model="showPipelineDeletionConfirmationDialog" width="500">
|
||||
<v-card color="surface">
|
||||
<v-card-title class="pb-0">Delete Pipeline</v-card-title>
|
||||
<v-card-text>
|
||||
Are you sure you want to delete
|
||||
<span style="color: white">"{{ useCameraSettingsStore().currentPipelineSettings.pipelineNickname }}"</span>?
|
||||
This cannot be undone.
|
||||
</v-card-text>
|
||||
<v-card-actions class="pa-5 pt-0">
|
||||
<v-btn
|
||||
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
|
||||
color="primary"
|
||||
class="text-black"
|
||||
@click="showPipelineDeletionConfirmationDialog = false"
|
||||
>
|
||||
Cancel
|
||||
</v-btn>
|
||||
<v-btn
|
||||
color="error"
|
||||
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
|
||||
@click="confirmDeleteCurrentPipeline"
|
||||
>
|
||||
Delete
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
<pv-delete-modal
|
||||
v-model="showPipelineDeletionConfirmationDialog"
|
||||
:width="500"
|
||||
title="Delete Pipeline"
|
||||
description="Are you sure you want to delete the current pipeline? This action cannot be undone."
|
||||
:on-confirm="confirmDeleteCurrentPipeline"
|
||||
/>
|
||||
<v-dialog v-model="showPipelineTypeChangeDialog" persistent width="600">
|
||||
<v-card color="surface" dark>
|
||||
<v-card-title class="pb-0">Change Pipeline Type</v-card-title>
|
||||
|
||||
@@ -29,7 +29,7 @@ const allTabs = Object.freeze({
|
||||
thresholdTab: { tabName: "Threshold", component: ThresholdTab },
|
||||
contoursTab: { tabName: "Contours", component: ContoursTab },
|
||||
apriltagTab: { tabName: "AprilTag", component: AprilTagTab },
|
||||
arucoTab: { tabName: "Aruco", component: ArucoTab },
|
||||
arucoTab: { tabName: "ArUco", component: ArucoTab },
|
||||
objectDetectionTab: { tabName: "Object Detection", component: ObjectDetectionTab },
|
||||
outputTab: { tabName: "Output", component: OutputTab },
|
||||
targetsTab: { tabName: "Targets", component: TargetsTab },
|
||||
@@ -99,8 +99,8 @@ const tabGroups = computed<ConfigOption[][]>(() => {
|
||||
!((isAprilTag || isAruco || isObjectDetection) && tabConfig.tabName === "Threshold") && //Filter out threshold tab if we're doing AprilTags
|
||||
!((isAprilTag || isAruco || isObjectDetection) && tabConfig.tabName === "Contours") && //Filter out contours if we're doing AprilTags
|
||||
!(!isAprilTag && tabConfig.tabName === "AprilTag") && //Filter out apriltag unless we actually are doing AprilTags
|
||||
!(!isAruco && tabConfig.tabName === "Aruco") &&
|
||||
!(!isObjectDetection && tabConfig.tabName === "Object Detection") //Filter out aruco unless we actually are doing Aruco
|
||||
!(!isAruco && tabConfig.tabName === "ArUco") &&
|
||||
!(!isObjectDetection && tabConfig.tabName === "Object Detection") //Filter out ArUco unless we actually are doing ArUco
|
||||
)
|
||||
)
|
||||
.filter((it) => it.length); // Remove empty tab groups
|
||||
|
||||
@@ -21,7 +21,9 @@ const processingMode = computed<number>({
|
||||
|
||||
<template>
|
||||
<v-card
|
||||
:disabled="useCameraSettingsStore().isDriverMode || useStateStore().colorPickingMode"
|
||||
:disabled="
|
||||
useCameraSettingsStore().isDriverMode || useCameraSettingsStore().isFocusMode || useStateStore().colorPickingMode
|
||||
"
|
||||
class="mt-3 rounded-12"
|
||||
color="surface"
|
||||
style="flex-grow: 1; display: flex; flex-direction: column"
|
||||
|
||||
@@ -2,60 +2,17 @@
|
||||
import { inject, ref } from "vue";
|
||||
import { useStateStore } from "@/stores/StateStore";
|
||||
import PvSelect from "@/components/common/pv-select.vue";
|
||||
import PvInput from "@/components/common/pv-input.vue";
|
||||
import axios from "axios";
|
||||
import PvDeleteModal from "@/components/common/pv-delete-modal.vue";
|
||||
import { useTheme } from "vuetify";
|
||||
import { axiosPost } from "@/lib/PhotonUtils";
|
||||
|
||||
const theme = useTheme();
|
||||
|
||||
const restartProgram = () => {
|
||||
axios
|
||||
.post("/utils/restartProgram")
|
||||
.then(() => {
|
||||
useStateStore().showSnackbarMessage({ message: "Successfully sent program restart request", color: "success" });
|
||||
})
|
||||
.catch((error) => {
|
||||
// This endpoint always return 204 regardless of outcome
|
||||
if (error.request) {
|
||||
useStateStore().showSnackbarMessage({
|
||||
message: "Error while trying to process the request! The backend didn't respond.",
|
||||
color: "error"
|
||||
});
|
||||
} else {
|
||||
useStateStore().showSnackbarMessage({
|
||||
message: "An error occurred while trying to process the request.",
|
||||
color: "error"
|
||||
});
|
||||
}
|
||||
});
|
||||
axiosPost("/utils/restartProgram", "restart PhotonVision");
|
||||
};
|
||||
const restartDevice = () => {
|
||||
axios
|
||||
.post("/utils/restartDevice")
|
||||
.then(() => {
|
||||
useStateStore().showSnackbarMessage({
|
||||
message: "Successfully dispatched the restart command. It isn't confirmed if a device restart will occur.",
|
||||
color: "success"
|
||||
});
|
||||
})
|
||||
.catch((error) => {
|
||||
if (error.response) {
|
||||
useStateStore().showSnackbarMessage({
|
||||
message: "The backend is unable to fulfil the request to restart the device.",
|
||||
color: "error"
|
||||
});
|
||||
} else if (error.request) {
|
||||
useStateStore().showSnackbarMessage({
|
||||
message: "Error while trying to process the request! The backend didn't respond.",
|
||||
color: "error"
|
||||
});
|
||||
} else {
|
||||
useStateStore().showSnackbarMessage({
|
||||
message: "An error occurred while trying to process the request.",
|
||||
color: "error"
|
||||
});
|
||||
}
|
||||
});
|
||||
axiosPost("/utils/restartDevice", "restart the device");
|
||||
};
|
||||
|
||||
const address = inject<string>("backendHost");
|
||||
@@ -77,47 +34,27 @@ const handleOfflineUpdate = () => {
|
||||
timeout: -1
|
||||
});
|
||||
|
||||
axios
|
||||
.post("/utils/offlineUpdate", formData, {
|
||||
headers: { "Content-Type": "multipart/form-data" },
|
||||
onUploadProgress: ({ progress }) => {
|
||||
const uploadPercentage = (progress || 0) * 100.0;
|
||||
if (uploadPercentage < 99.5) {
|
||||
useStateStore().showSnackbarMessage({
|
||||
message: "New Software Upload in Process, " + uploadPercentage.toFixed(2) + "% complete",
|
||||
color: "secondary",
|
||||
timeout: -1
|
||||
});
|
||||
} else {
|
||||
useStateStore().showSnackbarMessage({
|
||||
message: "Installing uploaded software...",
|
||||
color: "secondary",
|
||||
timeout: -1
|
||||
});
|
||||
}
|
||||
}
|
||||
})
|
||||
.then((response) => {
|
||||
useStateStore().showSnackbarMessage({ message: response.data.text || response.data, color: "success" });
|
||||
})
|
||||
.catch((error) => {
|
||||
if (error.response) {
|
||||
axiosPost("/utils/offlineUpdate", "upload new software", formData, {
|
||||
headers: { "Content-Type": "multipart/form-data" },
|
||||
onUploadProgress: ({ progress }) => {
|
||||
const uploadPercentage = (progress || 0) * 100.0;
|
||||
if (uploadPercentage < 99.5) {
|
||||
useStateStore().showSnackbarMessage({
|
||||
color: "error",
|
||||
message: error.response.data.text || error.response.data
|
||||
});
|
||||
} else if (error.request) {
|
||||
useStateStore().showSnackbarMessage({
|
||||
color: "error",
|
||||
message: "Error while trying to process the request! The backend didn't respond."
|
||||
message: "New Software Upload in Progress",
|
||||
color: "secondary",
|
||||
timeout: -1,
|
||||
progressBar: uploadPercentage,
|
||||
progressBarColor: "primary"
|
||||
});
|
||||
} else {
|
||||
useStateStore().showSnackbarMessage({
|
||||
color: "error",
|
||||
message: "An error occurred while trying to process the request."
|
||||
message: "Installing uploaded software...",
|
||||
color: "secondary",
|
||||
timeout: -1
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const exportLogFile = ref();
|
||||
@@ -166,29 +103,9 @@ const handleSettingsImport = () => {
|
||||
break;
|
||||
}
|
||||
|
||||
axios
|
||||
.post(`/settings${settingsEndpoint}`, formData, { headers: { "Content-Type": "multipart/form-data" } })
|
||||
.then((response) => {
|
||||
useStateStore().showSnackbarMessage({ message: response.data.text || response.data, color: "success" });
|
||||
})
|
||||
.catch((error) => {
|
||||
if (error.response) {
|
||||
useStateStore().showSnackbarMessage({
|
||||
color: "error",
|
||||
message: error.response.data.text || error.response.data
|
||||
});
|
||||
} else if (error.request) {
|
||||
useStateStore().showSnackbarMessage({
|
||||
color: "error",
|
||||
message: "Error while trying to process the request! The backend didn't respond."
|
||||
});
|
||||
} else {
|
||||
useStateStore().showSnackbarMessage({
|
||||
color: "error",
|
||||
message: "An error occurred while trying to process the request."
|
||||
});
|
||||
}
|
||||
});
|
||||
axiosPost(`/settings${settingsEndpoint}`, "import settings", formData, {
|
||||
headers: { "Content-Type": "multipart/form-data" }
|
||||
});
|
||||
|
||||
showImportDialog.value = false;
|
||||
importType.value = undefined;
|
||||
@@ -196,36 +113,8 @@ const handleSettingsImport = () => {
|
||||
};
|
||||
|
||||
const showFactoryReset = ref(false);
|
||||
const expected = "Delete Everything";
|
||||
const yesDeleteMySettingsText = ref("");
|
||||
const nukePhotonConfigDirectory = () => {
|
||||
axios
|
||||
.post("/utils/nukeConfigDirectory")
|
||||
.then(() => {
|
||||
useStateStore().showSnackbarMessage({
|
||||
message: "Successfully dispatched the reset command. Waiting for backend to start back up",
|
||||
color: "success"
|
||||
});
|
||||
})
|
||||
.catch((error) => {
|
||||
if (error.response) {
|
||||
useStateStore().showSnackbarMessage({
|
||||
message: "The backend is unable to fulfill the request to reset the device.",
|
||||
color: "error"
|
||||
});
|
||||
} else if (error.request) {
|
||||
useStateStore().showSnackbarMessage({
|
||||
message: "Error while trying to process the request! The backend didn't respond.",
|
||||
color: "error"
|
||||
});
|
||||
} else {
|
||||
useStateStore().showSnackbarMessage({
|
||||
message: "An error occurred while trying to process the request.",
|
||||
color: "error"
|
||||
});
|
||||
}
|
||||
});
|
||||
showFactoryReset.value = false;
|
||||
axiosPost("/utils/nukeConfigDirectory", "delete the config directory");
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -387,63 +276,15 @@ const nukePhotonConfigDirectory = () => {
|
||||
</v-col>
|
||||
</v-row>
|
||||
</div>
|
||||
<v-dialog v-model="showFactoryReset" width="800" dark>
|
||||
<v-card color="surface" flat>
|
||||
<v-card-title style="display: flex; justify-content: center">
|
||||
<span class="open-label">
|
||||
<v-icon end color="red" class="open-icon ma-1" size="large">mdi-alert-outline</v-icon>
|
||||
Factory Reset PhotonVision
|
||||
<v-icon end color="red" class="open-icon ma-1" size="large">mdi-alert-outline</v-icon>
|
||||
</span>
|
||||
</v-card-title>
|
||||
<v-card-text class="pt-0 pb-10px">
|
||||
<v-row class="align-center text-white">
|
||||
<v-col cols="12" md="6">
|
||||
<span> This will delete ALL OF YOUR SETTINGS and restart PhotonVision. </span>
|
||||
</v-col>
|
||||
<v-col cols="12" md="6">
|
||||
<v-btn
|
||||
color="primary"
|
||||
style="float: right"
|
||||
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
|
||||
@click="openExportSettingsPrompt"
|
||||
>
|
||||
<v-icon start class="open-icon" size="large"> mdi-export </v-icon>
|
||||
<span class="open-label">Backup Settings</span>
|
||||
<a
|
||||
ref="exportSettings"
|
||||
style="color: black; text-decoration: none; display: none"
|
||||
:href="`http://${address}/api/settings/photonvision_config.zip`"
|
||||
download="photonvision-settings.zip"
|
||||
target="_blank"
|
||||
/>
|
||||
</v-btn>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-card-text>
|
||||
<v-card-text class="pt-0 pb-0">
|
||||
<pv-input
|
||||
v-model="yesDeleteMySettingsText"
|
||||
:label="'Type "' + expected + '":'"
|
||||
:label-cols="6"
|
||||
:input-cols="6"
|
||||
/>
|
||||
</v-card-text>
|
||||
<v-card-text class="pt-10px">
|
||||
<v-btn
|
||||
color="error"
|
||||
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
|
||||
:disabled="yesDeleteMySettingsText.toLowerCase() !== expected.toLowerCase()"
|
||||
@click="nukePhotonConfigDirectory"
|
||||
>
|
||||
<v-icon start class="open-icon" size="large"> mdi-trash-can-outline </v-icon>
|
||||
<span class="open-label">
|
||||
{{ $vuetify.display.mdAndUp ? "Delete everything, I have backed up what I need" : "Delete Everything" }}
|
||||
</span>
|
||||
</v-btn>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
<pv-delete-modal
|
||||
v-model="showFactoryReset"
|
||||
title="Factory Reset PhotonVision"
|
||||
description="This will delete all settings and configurations stored on this device, including network settings. This action cannot be undone."
|
||||
expected-confirmation-text="Delete Everything"
|
||||
:on-confirm="nukePhotonConfigDirectory"
|
||||
:on-backup="openExportSettingsPrompt"
|
||||
delete-text="Factory reset"
|
||||
/>
|
||||
</v-card>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -315,23 +315,23 @@ watchEffect(() => {
|
||||
<v-col class="text-center">
|
||||
Background
|
||||
<v-color-picker
|
||||
v-model:model-value="backgroundColor"
|
||||
class="ma-auto pt-3"
|
||||
elevation="0"
|
||||
mode="hex"
|
||||
:modes="['hex']"
|
||||
v-model:model-value="backgroundColor"
|
||||
v-on:update:model-value="(hex) => setThemeColor(theme, 'background', hex)"
|
||||
@update:model-value="(hex) => setThemeColor(theme, 'background', hex)"
|
||||
></v-color-picker>
|
||||
</v-col>
|
||||
<v-col class="text-center">
|
||||
Surface
|
||||
<v-color-picker
|
||||
v-model:model-value="surfaceColor"
|
||||
class="ma-auto pt-3"
|
||||
elevation="0"
|
||||
mode="hex"
|
||||
:modes="['hex']"
|
||||
v-model:model-value="surfaceColor"
|
||||
v-on:update:model-value="(hex) => setThemeColor(theme, 'surface', hex)"
|
||||
@update:model-value="(hex) => setThemeColor(theme, 'surface', hex)"
|
||||
></v-color-picker>
|
||||
</v-col>
|
||||
</v-row>
|
||||
@@ -339,23 +339,23 @@ watchEffect(() => {
|
||||
<v-col class="text-center">
|
||||
Primary
|
||||
<v-color-picker
|
||||
v-model:model-value="primaryColor"
|
||||
class="ma-auto pt-3"
|
||||
elevation="0"
|
||||
mode="hex"
|
||||
:modes="['hex']"
|
||||
v-model:model-value="primaryColor"
|
||||
v-on:update:model-value="(hex) => setThemeColor(theme, 'primary', hex)"
|
||||
@update:model-value="(hex) => setThemeColor(theme, 'primary', hex)"
|
||||
></v-color-picker>
|
||||
</v-col>
|
||||
<v-col class="text-center">
|
||||
Secondary
|
||||
<v-color-picker
|
||||
v-model:model-value="secondaryColor"
|
||||
class="ma-auto pt-3"
|
||||
elevation="0"
|
||||
mode="hex"
|
||||
:modes="['hex']"
|
||||
v-model:model-value="secondaryColor"
|
||||
v-on:update:model-value="(hex) => setThemeColor(theme, 'secondary', hex)"
|
||||
@update:model-value="(hex) => setThemeColor(theme, 'secondary', hex)"
|
||||
></v-color-picker>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, inject } from "vue";
|
||||
import axios from "axios";
|
||||
import { useStateStore } from "@/stores/StateStore";
|
||||
import { useSettingsStore } from "@/stores/settings/GeneralSettingsStore";
|
||||
import type { ObjectDetectionModelProperties } from "@/types/SettingTypes";
|
||||
import pvInput from "@/components/common/pv-input.vue";
|
||||
import { type ObjectDetectionModelProperties } from "@/types/SettingTypes";
|
||||
import PvDeleteModal from "@/components/common/pv-delete-modal.vue";
|
||||
import { useTheme } from "vuetify";
|
||||
import { axiosPost } from "@/lib/PhotonUtils";
|
||||
|
||||
const theme = useTheme();
|
||||
const showImportDialog = ref(false);
|
||||
@@ -43,34 +43,25 @@ const handleImport = async () => {
|
||||
timeout: -1
|
||||
});
|
||||
|
||||
axios
|
||||
.post("/objectdetection/import", formData, {
|
||||
headers: { "Content-Type": "multipart/form-data" }
|
||||
})
|
||||
.then((response) => {
|
||||
useStateStore().showSnackbarMessage({
|
||||
message: response.data.text || response.data,
|
||||
color: "success"
|
||||
});
|
||||
})
|
||||
.catch((error) => {
|
||||
if (error.response) {
|
||||
axiosPost("/objectdetection/import", "import an object detection model", formData, {
|
||||
headers: { "Content-Type": "multipart/form-data" },
|
||||
onUploadProgress: ({ progress }) => {
|
||||
const uploadPercentage = (progress || 0) * 100.0;
|
||||
if (uploadPercentage < 99.5) {
|
||||
useStateStore().showSnackbarMessage({
|
||||
color: "error",
|
||||
message: error.response.data.text || error.response.data
|
||||
});
|
||||
} else if (error.request) {
|
||||
useStateStore().showSnackbarMessage({
|
||||
color: "error",
|
||||
message: "Error while trying to process the request! The backend didn't respond."
|
||||
message: "Object Detection Model Upload in Process, " + uploadPercentage.toFixed(2) + "% complete",
|
||||
color: "secondary",
|
||||
timeout: -1
|
||||
});
|
||||
} else {
|
||||
useStateStore().showSnackbarMessage({
|
||||
color: "error",
|
||||
message: "An error occurred while trying to process the request."
|
||||
message: "Processing uploaded Object Detection Model...",
|
||||
color: "secondary",
|
||||
timeout: -1
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
showImportDialog.value = false;
|
||||
|
||||
@@ -82,41 +73,9 @@ const handleImport = async () => {
|
||||
};
|
||||
|
||||
const deleteModel = async (model: ObjectDetectionModelProperties) => {
|
||||
useStateStore().showSnackbarMessage({
|
||||
message: "Deleting Object Detection Model...",
|
||||
color: "secondary",
|
||||
timeout: -1
|
||||
axiosPost("/objectdetection/delete", "delete an object detection model", {
|
||||
modelPath: model.modelPath
|
||||
});
|
||||
|
||||
axios
|
||||
.post("/objectdetection/delete", {
|
||||
modelPath: model.modelPath
|
||||
})
|
||||
.then((response) => {
|
||||
useStateStore().showSnackbarMessage({
|
||||
message: response.data.text || response.data,
|
||||
color: "success"
|
||||
});
|
||||
})
|
||||
.catch((error) => {
|
||||
if (error.response) {
|
||||
useStateStore().showSnackbarMessage({
|
||||
color: "error",
|
||||
message: error.response.data.text || error.response.data
|
||||
});
|
||||
} else if (error.request) {
|
||||
useStateStore().showSnackbarMessage({
|
||||
color: "error",
|
||||
message: "Error while trying to process the request! The backend didn't respond."
|
||||
});
|
||||
} else {
|
||||
useStateStore().showSnackbarMessage({
|
||||
color: "error",
|
||||
message: "An error occurred while trying to process the request."
|
||||
});
|
||||
}
|
||||
});
|
||||
confirmDeleteDialog.value.show = false;
|
||||
};
|
||||
|
||||
const renameModel = async (model: ObjectDetectionModelProperties, newName: string) => {
|
||||
@@ -126,35 +85,10 @@ const renameModel = async (model: ObjectDetectionModelProperties, newName: strin
|
||||
timeout: -1
|
||||
});
|
||||
|
||||
axios
|
||||
.post("/objectdetection/rename", {
|
||||
modelPath: model.modelPath.replace("file:", ""),
|
||||
newName: newName
|
||||
})
|
||||
.then((response) => {
|
||||
useStateStore().showSnackbarMessage({
|
||||
message: response.data.text || response.data,
|
||||
color: "success"
|
||||
});
|
||||
})
|
||||
.catch((error) => {
|
||||
if (error.response) {
|
||||
useStateStore().showSnackbarMessage({
|
||||
color: "error",
|
||||
message: error.response.data.text || error.response.data
|
||||
});
|
||||
} else if (error.request) {
|
||||
useStateStore().showSnackbarMessage({
|
||||
color: "error",
|
||||
message: "Error while trying to process the request! The backend didn't respond."
|
||||
});
|
||||
} else {
|
||||
useStateStore().showSnackbarMessage({
|
||||
color: "error",
|
||||
message: "An error occurred while trying to process the request."
|
||||
});
|
||||
}
|
||||
});
|
||||
axiosPost("/objectdetection/rename", "rename an object detection model", {
|
||||
modelPath: model.modelPath.replace("file:", ""),
|
||||
newName: newName
|
||||
});
|
||||
showRenameDialog.value.show = false;
|
||||
};
|
||||
|
||||
@@ -181,36 +115,8 @@ const openExportIndividualModelPrompt = () => {
|
||||
};
|
||||
|
||||
const showNukeDialog = ref(false);
|
||||
const expected = "Delete Models";
|
||||
const yesDeleteMyModelsText = ref("");
|
||||
const nukeModels = () => {
|
||||
axios
|
||||
.post("/objectdetection/nuke")
|
||||
.then(() => {
|
||||
useStateStore().showSnackbarMessage({
|
||||
message: "Successfully dispatched the clear models command.",
|
||||
color: "success"
|
||||
});
|
||||
})
|
||||
.catch((error) => {
|
||||
if (error.response) {
|
||||
useStateStore().showSnackbarMessage({
|
||||
message: "The backend is unable to fulfill the request to clear the models.",
|
||||
color: "error"
|
||||
});
|
||||
} else if (error.request) {
|
||||
useStateStore().showSnackbarMessage({
|
||||
message: "Error while trying to process the request! The backend didn't respond.",
|
||||
color: "error"
|
||||
});
|
||||
} else {
|
||||
useStateStore().showSnackbarMessage({
|
||||
message: "An error occurred while trying to process the request.",
|
||||
color: "error"
|
||||
});
|
||||
}
|
||||
});
|
||||
showNukeDialog.value = false;
|
||||
axiosPost("/objectdetection/nuke", "clear and reset object detection models");
|
||||
};
|
||||
|
||||
const showBulkImportDialog = ref(false);
|
||||
@@ -221,51 +127,27 @@ const handleBulkImport = () => {
|
||||
const formData = new FormData();
|
||||
formData.append("data", importFile.value);
|
||||
|
||||
axios
|
||||
.post("/objectdetection/bulkimport", formData, {
|
||||
headers: { "Content-Type": "multipart/form-data" },
|
||||
onUploadProgress: ({ progress }) => {
|
||||
const uploadPercentage = (progress || 0) * 100.0;
|
||||
if (uploadPercentage < 99.5) {
|
||||
useStateStore().showSnackbarMessage({
|
||||
message: "Object Detection Models Upload in Process, " + uploadPercentage.toFixed(2) + "% complete",
|
||||
color: "secondary",
|
||||
timeout: -1
|
||||
});
|
||||
} else {
|
||||
useStateStore().showSnackbarMessage({
|
||||
message: "Importing New Object Detection Models...",
|
||||
color: "secondary",
|
||||
timeout: -1
|
||||
});
|
||||
}
|
||||
}
|
||||
})
|
||||
.then((response) => {
|
||||
useStateStore().showSnackbarMessage({
|
||||
message: response.data.text || response.data,
|
||||
color: "success"
|
||||
});
|
||||
})
|
||||
.catch((error) => {
|
||||
if (error.response) {
|
||||
axiosPost("/objectdetection/bulkimport", "import object detection models", formData, {
|
||||
headers: { "Content-Type": "multipart/form-data" },
|
||||
onUploadProgress: ({ progress }) => {
|
||||
const uploadPercentage = (progress || 0) * 100.0;
|
||||
if (uploadPercentage < 99.5) {
|
||||
useStateStore().showSnackbarMessage({
|
||||
color: "error",
|
||||
message: error.response.data.text || error.response.data
|
||||
});
|
||||
} else if (error.request) {
|
||||
useStateStore().showSnackbarMessage({
|
||||
color: "error",
|
||||
message: "Error while trying to process the request! The backend didn't respond."
|
||||
message: "Object Detection Models Upload in Progress",
|
||||
color: "secondary",
|
||||
timeout: -1,
|
||||
progressBar: uploadPercentage,
|
||||
progressBarColor: "primary"
|
||||
});
|
||||
} else {
|
||||
useStateStore().showSnackbarMessage({
|
||||
color: "error",
|
||||
message: "An error occurred while trying to process the request."
|
||||
message: "Importing New Object Detection Models...",
|
||||
color: "secondary",
|
||||
timeout: -1
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
});
|
||||
showImportDialog.value = false;
|
||||
importFile.value = null;
|
||||
};
|
||||
@@ -485,35 +367,20 @@ const handleBulkImport = () => {
|
||||
</tbody>
|
||||
</v-table>
|
||||
|
||||
<v-dialog v-model="confirmDeleteDialog.show" width="600">
|
||||
<v-card color="surface" dark>
|
||||
<v-card-title>Delete Object Detection Model</v-card-title>
|
||||
<v-card-text class="pt-0">
|
||||
Are you sure you want to delete the model {{ confirmDeleteDialog.model.nickname }}?
|
||||
<v-card-actions class="pt-5 pb-0 pr-0" style="justify-content: flex-end">
|
||||
<v-btn
|
||||
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
|
||||
color="buttonPassive"
|
||||
@click="confirmDeleteDialog.show = false"
|
||||
>
|
||||
Cancel
|
||||
</v-btn>
|
||||
<v-btn
|
||||
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
|
||||
color="error"
|
||||
@click="deleteModel(confirmDeleteDialog.model)"
|
||||
>
|
||||
Delete
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
<pv-delete-modal
|
||||
v-model="confirmDeleteDialog.show"
|
||||
:width="500"
|
||||
:on-confirm="() => deleteModel(confirmDeleteDialog.model)"
|
||||
title="Delete Object Detection Model"
|
||||
:description="`Are you sure you want to delete the model ${confirmDeleteDialog.model.nickname}?`"
|
||||
delete-text="Delete model"
|
||||
/>
|
||||
|
||||
<v-dialog v-model="showRenameDialog.show" width="600">
|
||||
<v-card color="surface" dark>
|
||||
<v-card-title>Rename Object Detection Model</v-card-title>
|
||||
<v-card-text class="pt-0">
|
||||
Enter a new name for the model {{ showRenameDialog.model.nickname }}:
|
||||
Enter a new name for the model "{{ showRenameDialog.model.nickname }}":
|
||||
<div class="pa-5 pb-0">
|
||||
<v-text-field v-model="showRenameDialog.newName" hide-details label="New Name" variant="underlined" />
|
||||
</div>
|
||||
@@ -569,64 +436,15 @@ const handleBulkImport = () => {
|
||||
</v-row>
|
||||
</div>
|
||||
|
||||
<v-dialog v-model="showNukeDialog" width="800" dark>
|
||||
<v-card color="surface" flat>
|
||||
<v-card-title style="display: flex; justify-content: center">
|
||||
<span class="open-label">
|
||||
<v-icon end color="error" class="open-icon ma-1" size="large">mdi-alert-outline</v-icon>
|
||||
Clear and Reset Object Detection Models
|
||||
<v-icon end color="error" class="open-icon ma-1" size="large">mdi-alert-outline</v-icon>
|
||||
</span>
|
||||
</v-card-title>
|
||||
<v-card-text class="pt-0 pb-10px">
|
||||
<v-row class="align-center text-white">
|
||||
<v-col cols="12" md="6">
|
||||
<span> This will delete ALL OF YOUR MODELS and re-extract the default models. </span>
|
||||
</v-col>
|
||||
<v-col cols="12" md="6">
|
||||
<v-btn
|
||||
color="buttonActive"
|
||||
style="float: right"
|
||||
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
|
||||
@click="openExportPrompt"
|
||||
>
|
||||
<v-icon start class="open-icon" size="large"> mdi-export </v-icon>
|
||||
<span class="open-label">Backup Models</span>
|
||||
<a
|
||||
ref="exportModels"
|
||||
style="color: black; text-decoration: none; display: none"
|
||||
:href="`http://${address}/api/objectdetection/export`"
|
||||
download="photonvision-object-detection-models-export.zip"
|
||||
target="_blank"
|
||||
/>
|
||||
</v-btn>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-card-text>
|
||||
<v-card-text class="pt-0 pb-0">
|
||||
<pv-input
|
||||
v-model="yesDeleteMyModelsText"
|
||||
:label="'Type "' + expected + '":'"
|
||||
:label-cols="6"
|
||||
:input-cols="6"
|
||||
/>
|
||||
</v-card-text>
|
||||
<v-card-text class="pt-10px">
|
||||
<v-btn
|
||||
color="error"
|
||||
width="100%"
|
||||
:disabled="yesDeleteMyModelsText.toLowerCase() !== expected.toLowerCase()"
|
||||
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
|
||||
@click="nukeModels"
|
||||
>
|
||||
<v-icon start class="open-icon" size="large"> mdi-trash-can-outline </v-icon>
|
||||
<span class="open-label">
|
||||
{{ $vuetify.display.mdAndUp ? "Delete models, I have backed up what I need" : "Delete Models" }}
|
||||
</span>
|
||||
</v-btn>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
<pv-delete-modal
|
||||
v-model="showNukeDialog"
|
||||
:on-backup="openExportPrompt"
|
||||
:on-confirm="nukeModels"
|
||||
title="Delete and Reset All Object Detection Models"
|
||||
:description="'This will delete ALL object detection models and re-extract the default object detection models. This action cannot be undone.'"
|
||||
:expected-confirmation-text="'Delete Models'"
|
||||
delete-text="Delete all models"
|
||||
/>
|
||||
</v-card>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import { useStateStore } from "@/stores/StateStore";
|
||||
import type { Resolution } from "@/types/SettingTypes";
|
||||
import axios from "axios";
|
||||
|
||||
export const resolutionsAreEqual = (a: Resolution, b: Resolution) => {
|
||||
return a.height === b.height && a.width === b.width;
|
||||
@@ -18,3 +20,41 @@ export const parseJsonFile = async <T extends Record<string, any>>(file: File):
|
||||
fileReader.readAsText(file);
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* A helper function to make POST requests using axios with standardized success and error handling.
|
||||
*
|
||||
* @param url The endpoint URL to which the POST request is sent
|
||||
* @param description A brief description of the request for users, e.g., "import object detection models".
|
||||
* @param data Payload to be sent in the POST request
|
||||
* @param config Optional axios request configuration
|
||||
* @returns A promise that resolves when the POST request is complete
|
||||
*/
|
||||
export const axiosPost = (url: string, description: string, data?: any, config?: any): Promise<void> => {
|
||||
return axios
|
||||
.post(url, data, config)
|
||||
.then(() => {
|
||||
useStateStore().showSnackbarMessage({
|
||||
message: "Successfully dispatched the request to " + description + ". Waiting for backend to respond",
|
||||
color: "success"
|
||||
});
|
||||
})
|
||||
.catch((error) => {
|
||||
if (error.response) {
|
||||
useStateStore().showSnackbarMessage({
|
||||
message: "The backend is unable to fulfill the request to " + description + ".",
|
||||
color: "error"
|
||||
});
|
||||
} else if (error.request) {
|
||||
useStateStore().showSnackbarMessage({
|
||||
message: "Error while trying to process the request to " + description + "! The backend didn't respond.",
|
||||
color: "error"
|
||||
});
|
||||
} else {
|
||||
useStateStore().showSnackbarMessage({
|
||||
message: "An error occurred while trying to process the request to " + description + ".",
|
||||
color: "error"
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
@@ -39,6 +39,8 @@ interface StateStore {
|
||||
|
||||
snackbarData: {
|
||||
show: boolean;
|
||||
progressBar: number;
|
||||
progressBarColor: string;
|
||||
message: string;
|
||||
color: string;
|
||||
timeout: number;
|
||||
@@ -86,6 +88,8 @@ export const useStateStore = defineStore("state", {
|
||||
|
||||
snackbarData: {
|
||||
show: false,
|
||||
progressBar: -1,
|
||||
progressBarColor: "info",
|
||||
message: "No Message",
|
||||
color: "info",
|
||||
timeout: 2000
|
||||
@@ -158,11 +162,19 @@ export const useStateStore = defineStore("state", {
|
||||
updateDiscoveredCameras(data: VsmState) {
|
||||
this.vsmState = data;
|
||||
},
|
||||
showSnackbarMessage(data: { message: string; color: string; timeout?: number }) {
|
||||
showSnackbarMessage(data: {
|
||||
message: string;
|
||||
color: string;
|
||||
timeout?: number;
|
||||
progressBar?: number;
|
||||
progressBarColor?: string;
|
||||
}) {
|
||||
this.snackbarData = {
|
||||
show: true,
|
||||
progressBar: data.progressBar || -1,
|
||||
message: data.message,
|
||||
color: data.color,
|
||||
progressBarColor: data.progressBarColor || "",
|
||||
timeout: data.timeout || 2000
|
||||
};
|
||||
}
|
||||
|
||||
@@ -45,7 +45,7 @@ export const useCameraSettingsStore = defineStore("cameraSettings", {
|
||||
},
|
||||
// This method only exists due to just how lazy I am and my dislike of consolidating the pipeline type enums (which mind you, suck as is)
|
||||
currentWebsocketPipelineType(): WebsocketPipelineType {
|
||||
return this.currentPipelineType - 2;
|
||||
return this.currentPipelineType - 3;
|
||||
},
|
||||
currentVideoFormat(): VideoFormat {
|
||||
return this.currentCameraSettings.validVideoFormats[this.currentPipelineSettings.cameraVideoModeIndex];
|
||||
@@ -76,6 +76,9 @@ export const useCameraSettingsStore = defineStore("cameraSettings", {
|
||||
isCalibrationMode(): boolean {
|
||||
return this.currentCameraSettings.currentPipelineIndex == WebsocketPipelineType.Calib3d;
|
||||
},
|
||||
isFocusMode(): boolean {
|
||||
return this.currentCameraSettings.currentPipelineIndex == WebsocketPipelineType.FocusCamera;
|
||||
},
|
||||
isCSICamera(): boolean {
|
||||
return this.currentCameraSettings.isCSICamera;
|
||||
},
|
||||
@@ -142,7 +145,8 @@ export const useCameraSettingsStore = defineStore("cameraSettings", {
|
||||
maxWhiteBalanceTemp: d.maxWhiteBalanceTemp,
|
||||
matchedCameraInfo: d.matchedCameraInfo,
|
||||
isConnected: d.isConnected,
|
||||
hasConnected: d.hasConnected
|
||||
hasConnected: d.hasConnected,
|
||||
mismatch: d.mismatch
|
||||
};
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
@@ -70,6 +70,8 @@ export interface PipelineResult {
|
||||
sequenceID: number;
|
||||
fps: number;
|
||||
latency: number;
|
||||
// Focus pipeline
|
||||
focus?: number;
|
||||
targets: PhotonTarget[];
|
||||
// undefined if multitag failed or non-tag pipeline
|
||||
multitagResult?: MultitagResult;
|
||||
|
||||
@@ -1,13 +1,16 @@
|
||||
import type { WebsocketNumberPair } from "@/types/WebsocketDataTypes";
|
||||
import type { ObjectDetectionModelProperties } from "@/types/SettingTypes";
|
||||
|
||||
/**
|
||||
* The on-wire form of PipelineType.java (the enum is serialized with `ordinal()`)
|
||||
*/
|
||||
export enum PipelineType {
|
||||
DriverMode = 1,
|
||||
Reflective = 2,
|
||||
ColoredShape = 3,
|
||||
AprilTag = 4,
|
||||
Aruco = 5,
|
||||
ObjectDetection = 6
|
||||
DriverMode = 2,
|
||||
Reflective = 3,
|
||||
ColoredShape = 4,
|
||||
AprilTag = 5,
|
||||
Aruco = 6,
|
||||
ObjectDetection = 7
|
||||
}
|
||||
|
||||
export enum AprilTagFamily {
|
||||
|
||||
@@ -266,6 +266,7 @@ export interface UiCameraConfiguration {
|
||||
matchedCameraInfo: PVCameraInfo;
|
||||
isConnected: boolean;
|
||||
hasConnected: boolean;
|
||||
mismatch: boolean;
|
||||
}
|
||||
|
||||
export interface CameraSettingsChangeRequest {
|
||||
@@ -388,7 +389,8 @@ export const PlaceholderCameraSettings: UiCameraConfiguration = {
|
||||
PVUsbCameraInfo: undefined
|
||||
},
|
||||
isConnected: true,
|
||||
hasConnected: true
|
||||
hasConnected: true,
|
||||
mismatch: false
|
||||
};
|
||||
|
||||
export enum CalibrationBoardTypes {
|
||||
|
||||
@@ -69,6 +69,7 @@ export interface WebsocketCameraSettingsUpdate {
|
||||
matchedCameraInfo: PVCameraInfo;
|
||||
isConnected: boolean;
|
||||
hasConnected: boolean;
|
||||
mismatch: boolean;
|
||||
}
|
||||
export interface WebsocketNTUpdate {
|
||||
connected: boolean;
|
||||
@@ -109,6 +110,7 @@ export interface IncomingWebsocketData {
|
||||
}
|
||||
|
||||
export enum WebsocketPipelineType {
|
||||
FocusCamera = -3,
|
||||
Calib3d = -2,
|
||||
DriverMode = -1,
|
||||
Reflective = 0,
|
||||
|
||||
@@ -7,16 +7,13 @@ import {
|
||||
PVCameraInfo,
|
||||
type PVCSICameraInfo,
|
||||
type PVFileCameraInfo,
|
||||
type PVUsbCameraInfo,
|
||||
type UiCameraConfiguration
|
||||
type PVUsbCameraInfo
|
||||
} from "@/types/SettingTypes";
|
||||
import { getResolutionString } from "@/lib/PhotonUtils";
|
||||
import { axiosPost, getResolutionString } from "@/lib/PhotonUtils";
|
||||
import PhotonCameraStream from "@/components/app/photon-camera-stream.vue";
|
||||
import PvInput from "@/components/common/pv-input.vue";
|
||||
import PvDeleteModal from "@/components/common/pv-delete-modal.vue";
|
||||
import PvCameraInfoCard from "@/components/common/pv-camera-info-card.vue";
|
||||
import axios from "axios";
|
||||
import PvCameraMatchCard from "@/components/common/pv-camera-match-card.vue";
|
||||
import type { WebsocketCameraSettingsUpdate } from "@/types/WebsocketDataTypes";
|
||||
import { useTheme } from "vuetify";
|
||||
|
||||
const theme = useTheme();
|
||||
@@ -28,33 +25,9 @@ const activateModule = (moduleUniqueName: string) => {
|
||||
if (activatingModule.value) return;
|
||||
activatingModule.value = true;
|
||||
|
||||
axios
|
||||
.post("/utils/activateMatchedCamera", { cameraUniqueName: moduleUniqueName })
|
||||
.then(() => {
|
||||
useStateStore().showSnackbarMessage({
|
||||
message: "Camera activated successfully",
|
||||
color: "success"
|
||||
});
|
||||
})
|
||||
.catch((error) => {
|
||||
if (error.response) {
|
||||
useStateStore().showSnackbarMessage({
|
||||
message: "The backend is unable to fulfil the request to activate this camera.",
|
||||
color: "error"
|
||||
});
|
||||
} else if (error.request) {
|
||||
useStateStore().showSnackbarMessage({
|
||||
message: "Error while trying to process the request! The backend didn't respond.",
|
||||
color: "error"
|
||||
});
|
||||
} else {
|
||||
useStateStore().showSnackbarMessage({
|
||||
message: "An error occurred while trying to process the request.",
|
||||
color: "error"
|
||||
});
|
||||
}
|
||||
})
|
||||
.finally(() => (activatingModule.value = false));
|
||||
axiosPost("/utils/activateMatchedCamera", "activate a matched camera", {
|
||||
cameraUniqueName: moduleUniqueName
|
||||
}).finally(() => (activatingModule.value = false));
|
||||
};
|
||||
|
||||
const assigningCamera = ref(false);
|
||||
@@ -66,166 +39,32 @@ const assignCamera = (cameraInfo: PVCameraInfo) => {
|
||||
cameraInfo: cameraInfo
|
||||
};
|
||||
|
||||
axios
|
||||
.post("/utils/assignUnmatchedCamera", payload)
|
||||
.then(() => {
|
||||
useStateStore().showSnackbarMessage({
|
||||
message: "Unmatched camera assigned successfully",
|
||||
color: "success"
|
||||
});
|
||||
})
|
||||
.catch((error) => {
|
||||
if (error.response) {
|
||||
useStateStore().showSnackbarMessage({
|
||||
message: "The backend is unable to fulfil the request to assign this unmatched camera.",
|
||||
color: "error"
|
||||
});
|
||||
} else if (error.request) {
|
||||
useStateStore().showSnackbarMessage({
|
||||
message: "Error while trying to process the request! The backend didn't respond.",
|
||||
color: "error"
|
||||
});
|
||||
} else {
|
||||
useStateStore().showSnackbarMessage({
|
||||
message: "An error occurred while trying to process the request.",
|
||||
color: "error"
|
||||
});
|
||||
}
|
||||
})
|
||||
.finally(() => (assigningCamera.value = false));
|
||||
axiosPost("/utils/assignUnmatchedCamera", "assign an unmatched camera", payload).finally(
|
||||
() => (assigningCamera.value = false)
|
||||
);
|
||||
};
|
||||
|
||||
const deactivatingModule = ref(false);
|
||||
const deactivateModule = (cameraUniqueName: string) => {
|
||||
if (deactivatingModule.value) return;
|
||||
deactivatingModule.value = true;
|
||||
axios
|
||||
.post("/utils/unassignCamera", { cameraUniqueName: cameraUniqueName })
|
||||
.then(() => {
|
||||
useStateStore().showSnackbarMessage({
|
||||
message: "Camera deactivated successfully",
|
||||
color: "success"
|
||||
});
|
||||
})
|
||||
.catch((error) => {
|
||||
if (error.response) {
|
||||
useStateStore().showSnackbarMessage({
|
||||
message: "The backend is unable to fulfil the request to deactivate this camera.",
|
||||
color: "error"
|
||||
});
|
||||
} else if (error.request) {
|
||||
useStateStore().showSnackbarMessage({
|
||||
message: "Error while trying to process the request! The backend didn't respond.",
|
||||
color: "error"
|
||||
});
|
||||
} else {
|
||||
useStateStore().showSnackbarMessage({
|
||||
message: "An error occurred while trying to process the request.",
|
||||
color: "error"
|
||||
});
|
||||
}
|
||||
})
|
||||
.finally(() => (deactivatingModule.value = false));
|
||||
};
|
||||
|
||||
const deletingCamera = ref(false);
|
||||
const deleteThisCamera = (cameraName: string) => {
|
||||
if (deletingCamera.value) return;
|
||||
deletingCamera.value = true;
|
||||
const payload = {
|
||||
cameraUniqueName: cameraName
|
||||
};
|
||||
|
||||
axios
|
||||
.post("/utils/nukeOneCamera", payload)
|
||||
.then(() => {
|
||||
useStateStore().showSnackbarMessage({
|
||||
message: "Camera deleted successfully",
|
||||
color: "success"
|
||||
});
|
||||
})
|
||||
.catch((error) => {
|
||||
if (error.response) {
|
||||
useStateStore().showSnackbarMessage({
|
||||
message: "The backend is unable to fulfil the request to delete this camera.",
|
||||
color: "error"
|
||||
});
|
||||
} else if (error.request) {
|
||||
useStateStore().showSnackbarMessage({
|
||||
message: "Error while trying to process the request! The backend didn't respond.",
|
||||
color: "error"
|
||||
});
|
||||
} else {
|
||||
useStateStore().showSnackbarMessage({
|
||||
message: "An error occurred while trying to process the request.",
|
||||
color: "error"
|
||||
});
|
||||
}
|
||||
})
|
||||
.finally(() => {
|
||||
setCameraDeleting(null);
|
||||
deletingCamera.value = false;
|
||||
});
|
||||
};
|
||||
|
||||
const camerasMatch = (camera1: PVCameraInfo, camera2: PVCameraInfo) => {
|
||||
if (camera1.PVUsbCameraInfo && camera2.PVUsbCameraInfo)
|
||||
return (
|
||||
camera1.PVUsbCameraInfo.name === camera2.PVUsbCameraInfo.name &&
|
||||
camera1.PVUsbCameraInfo.vendorId === camera2.PVUsbCameraInfo.vendorId &&
|
||||
camera1.PVUsbCameraInfo.productId === camera2.PVUsbCameraInfo.productId &&
|
||||
camera1.PVUsbCameraInfo.uniquePath === camera2.PVUsbCameraInfo.uniquePath
|
||||
);
|
||||
else if (camera1.PVCSICameraInfo && camera2.PVCSICameraInfo)
|
||||
return (
|
||||
camera1.PVCSICameraInfo.uniquePath === camera2.PVCSICameraInfo.uniquePath &&
|
||||
camera1.PVCSICameraInfo.baseName === camera2.PVCSICameraInfo.baseName
|
||||
);
|
||||
else if (camera1.PVFileCameraInfo && camera2.PVFileCameraInfo)
|
||||
return (
|
||||
camera1.PVFileCameraInfo.uniquePath === camera2.PVFileCameraInfo.uniquePath &&
|
||||
camera1.PVFileCameraInfo.name === camera2.PVFileCameraInfo.name
|
||||
);
|
||||
else return false;
|
||||
};
|
||||
|
||||
const cameraInfoFor = (camera: PVCameraInfo | null): PVUsbCameraInfo | PVCSICameraInfo | PVFileCameraInfo | any => {
|
||||
if (!camera) return null;
|
||||
if (camera.PVUsbCameraInfo) {
|
||||
return camera.PVUsbCameraInfo;
|
||||
}
|
||||
if (camera.PVCSICameraInfo) {
|
||||
return camera.PVCSICameraInfo;
|
||||
}
|
||||
if (camera.PVFileCameraInfo) {
|
||||
return camera.PVFileCameraInfo;
|
||||
}
|
||||
return {};
|
||||
};
|
||||
|
||||
/**
|
||||
* Find the PVCameraInfo currently occupying the same uniquepath as the the given module
|
||||
*/
|
||||
const getMatchedDevice = (info: PVCameraInfo | undefined): PVCameraInfo => {
|
||||
if (!info) {
|
||||
return {
|
||||
PVFileCameraInfo: undefined,
|
||||
PVCSICameraInfo: undefined,
|
||||
PVUsbCameraInfo: undefined
|
||||
};
|
||||
}
|
||||
return (
|
||||
useStateStore().vsmState.allConnectedCameras.find(
|
||||
(it) => cameraInfoFor(it).uniquePath === cameraInfoFor(info).uniquePath
|
||||
) || {
|
||||
PVFileCameraInfo: undefined,
|
||||
PVCSICameraInfo: undefined,
|
||||
PVUsbCameraInfo: undefined
|
||||
}
|
||||
axiosPost("/utils/unassignCamera", "unassign a camera", { cameraUniqueName: cameraUniqueName }).finally(
|
||||
() => (deactivatingModule.value = false)
|
||||
);
|
||||
};
|
||||
|
||||
const cameraCononected = (uniquePath: string): boolean => {
|
||||
const confirmDeleteDialog = ref({ show: false, nickname: "", cameraUniqueName: "" });
|
||||
const deletingCamera = ref<string | null>(null);
|
||||
|
||||
const deleteThisCamera = (cameraUniqueName: string) => {
|
||||
if (deletingCamera.value) return;
|
||||
deletingCamera.value = cameraUniqueName;
|
||||
axiosPost("/utils/nukeOneCamera", "delete a camera", { cameraUniqueName: cameraUniqueName }).finally(() => {
|
||||
deletingCamera.value = null;
|
||||
});
|
||||
};
|
||||
|
||||
const cameraConnected = (uniquePath: string): boolean => {
|
||||
return (
|
||||
useStateStore().vsmState.allConnectedCameras.find((it) => cameraInfoFor(it).uniquePath === uniquePath) !== undefined
|
||||
);
|
||||
@@ -252,8 +91,8 @@ const activeVisionModules = computed(() =>
|
||||
// Display connected cameras first
|
||||
.sort(
|
||||
(first, second) =>
|
||||
(cameraCononected(cameraInfoFor(second.matchedCameraInfo).uniquePath) ? 1 : 0) -
|
||||
(cameraCononected(cameraInfoFor(first.matchedCameraInfo).uniquePath) ? 1 : 0)
|
||||
(cameraConnected(cameraInfoFor(second.matchedCameraInfo).uniquePath) ? 1 : 0) -
|
||||
(cameraConnected(cameraInfoFor(first.matchedCameraInfo).uniquePath) ? 1 : 0)
|
||||
)
|
||||
);
|
||||
|
||||
@@ -266,14 +105,44 @@ const setCameraView = (camera: PVCameraInfo | null, isConnected: boolean | null)
|
||||
viewingCamera.value = [camera, isConnected];
|
||||
};
|
||||
|
||||
const viewingDeleteCamera = ref(false);
|
||||
const cameraToDelete = ref<UiCameraConfiguration | WebsocketCameraSettingsUpdate | null>(null);
|
||||
const setCameraDeleting = (camera: UiCameraConfiguration | WebsocketCameraSettingsUpdate | null) => {
|
||||
yesDeleteMySettingsText.value = "";
|
||||
viewingDeleteCamera.value = camera !== null;
|
||||
cameraToDelete.value = camera;
|
||||
/**
|
||||
* Get the connection-type-specific camera info from the given PVCameraInfo object.
|
||||
*/
|
||||
const cameraInfoFor = (camera: PVCameraInfo | null): PVUsbCameraInfo | PVCSICameraInfo | PVFileCameraInfo | any => {
|
||||
if (!camera) return null;
|
||||
if (camera.PVUsbCameraInfo) {
|
||||
return camera.PVUsbCameraInfo;
|
||||
}
|
||||
if (camera.PVCSICameraInfo) {
|
||||
return camera.PVCSICameraInfo;
|
||||
}
|
||||
if (camera.PVFileCameraInfo) {
|
||||
return camera.PVFileCameraInfo;
|
||||
}
|
||||
return {};
|
||||
};
|
||||
|
||||
/**
|
||||
* Find the PVCameraInfo currently occupying the same uniquePath as the the given module
|
||||
*/
|
||||
const getMatchedDevice = (info: PVCameraInfo | undefined): PVCameraInfo => {
|
||||
if (!info) {
|
||||
return {
|
||||
PVFileCameraInfo: undefined,
|
||||
PVCSICameraInfo: undefined,
|
||||
PVUsbCameraInfo: undefined
|
||||
};
|
||||
}
|
||||
return (
|
||||
useStateStore().vsmState.allConnectedCameras.find(
|
||||
(it) => cameraInfoFor(it).uniquePath === cameraInfoFor(info).uniquePath
|
||||
) || {
|
||||
PVFileCameraInfo: undefined,
|
||||
PVCSICameraInfo: undefined,
|
||||
PVUsbCameraInfo: undefined
|
||||
}
|
||||
);
|
||||
};
|
||||
const yesDeleteMySettingsText = ref("");
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -290,14 +159,11 @@ const yesDeleteMySettingsText = ref("");
|
||||
>
|
||||
<v-card color="surface" class="rounded-12">
|
||||
<v-card-title>{{ cameraInfoFor(module.matchedCameraInfo).name }}</v-card-title>
|
||||
<v-card-subtitle v-if="!cameraCononected(cameraInfoFor(module.matchedCameraInfo).uniquePath)"
|
||||
<v-card-subtitle v-if="!cameraConnected(cameraInfoFor(module.matchedCameraInfo).uniquePath)"
|
||||
>Status: <span class="inactive-status">Disconnected</span></v-card-subtitle
|
||||
>
|
||||
<v-card-subtitle
|
||||
v-else-if="
|
||||
cameraCononected(cameraInfoFor(module.matchedCameraInfo).uniquePath) &&
|
||||
camerasMatch(getMatchedDevice(module.matchedCameraInfo), module.matchedCameraInfo)
|
||||
"
|
||||
v-else-if="cameraConnected(cameraInfoFor(module.matchedCameraInfo).uniquePath) && !module.mismatch"
|
||||
>Status: <span class="active-status">Active</span></v-card-subtitle
|
||||
>
|
||||
<v-card-subtitle v-else>Status: <span class="mismatch-status">Mismatch</span></v-card-subtitle>
|
||||
@@ -306,7 +172,7 @@ const yesDeleteMySettingsText = ref("");
|
||||
<tbody>
|
||||
<tr
|
||||
v-if="
|
||||
cameraCononected(cameraInfoFor(module.matchedCameraInfo).uniquePath) &&
|
||||
cameraConnected(cameraInfoFor(module.matchedCameraInfo).uniquePath) &&
|
||||
useStateStore().backendResults[module.uniqueName]
|
||||
"
|
||||
>
|
||||
@@ -348,7 +214,7 @@ const yesDeleteMySettingsText = ref("");
|
||||
</tbody>
|
||||
</v-table>
|
||||
<div
|
||||
v-if="cameraCononected(cameraInfoFor(module.matchedCameraInfo).uniquePath)"
|
||||
v-if="cameraConnected(cameraInfoFor(module.matchedCameraInfo).uniquePath)"
|
||||
:id="`stream-container-${index}`"
|
||||
class="d-flex flex-column justify-center align-center mt-3"
|
||||
style="height: 250px"
|
||||
@@ -370,7 +236,7 @@ const yesDeleteMySettingsText = ref("");
|
||||
@click="
|
||||
setCameraView(
|
||||
module.matchedCameraInfo,
|
||||
cameraCononected(cameraInfoFor(module.matchedCameraInfo).uniquePath)
|
||||
cameraConnected(cameraInfoFor(module.matchedCameraInfo).uniquePath)
|
||||
)
|
||||
"
|
||||
>
|
||||
@@ -394,8 +260,16 @@ const yesDeleteMySettingsText = ref("");
|
||||
class="pa-0"
|
||||
color="error"
|
||||
style="width: 100%"
|
||||
:loading="module.uniqueName === deletingCamera"
|
||||
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
|
||||
@click="setCameraDeleting(module)"
|
||||
@click="
|
||||
() =>
|
||||
(confirmDeleteDialog = {
|
||||
show: true,
|
||||
nickname: module.nickname,
|
||||
cameraUniqueName: module.uniqueName
|
||||
})
|
||||
"
|
||||
>
|
||||
<v-icon size="x-large">mdi-trash-can-outline</v-icon>
|
||||
</v-btn>
|
||||
@@ -441,7 +315,7 @@ const yesDeleteMySettingsText = ref("");
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Connected</td>
|
||||
<td>{{ cameraCononected(cameraInfoFor(module.matchedCameraInfo).uniquePath) }}</td>
|
||||
<td>{{ cameraConnected(cameraInfoFor(module.matchedCameraInfo).uniquePath) }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</v-table>
|
||||
@@ -456,7 +330,7 @@ const yesDeleteMySettingsText = ref("");
|
||||
@click="
|
||||
setCameraView(
|
||||
module.matchedCameraInfo,
|
||||
cameraCononected(cameraInfoFor(module.matchedCameraInfo).uniquePath)
|
||||
cameraConnected(cameraInfoFor(module.matchedCameraInfo).uniquePath)
|
||||
)
|
||||
"
|
||||
>
|
||||
@@ -480,8 +354,16 @@ const yesDeleteMySettingsText = ref("");
|
||||
class="pa-0"
|
||||
color="error"
|
||||
style="width: 100%"
|
||||
:loading="module.uniqueName === deletingCamera"
|
||||
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
|
||||
@click="setCameraDeleting(module)"
|
||||
@click="
|
||||
() =>
|
||||
(confirmDeleteDialog = {
|
||||
show: true,
|
||||
nickname: module.nickname,
|
||||
cameraUniqueName: module.uniqueName
|
||||
})
|
||||
"
|
||||
>
|
||||
<v-icon size="x-large">mdi-trash-can-outline</v-icon>
|
||||
</v-btn>
|
||||
@@ -562,7 +444,13 @@ const yesDeleteMySettingsText = ref("");
|
||||
<v-card-text v-if="!viewingCamera[1]">
|
||||
<PvCameraInfoCard :camera="viewingCamera[0]" />
|
||||
</v-card-text>
|
||||
<v-card-text v-else-if="!camerasMatch(getMatchedDevice(viewingCamera[0]), viewingCamera[0])">
|
||||
<v-card-text
|
||||
v-else-if="
|
||||
activeVisionModules.find(
|
||||
(it) => cameraInfoFor(it.matchedCameraInfo).uniquePath === cameraInfoFor(viewingCamera[0]).uniquePath
|
||||
)?.mismatch
|
||||
"
|
||||
>
|
||||
<v-alert
|
||||
class="mb-3"
|
||||
color="buttonActive"
|
||||
@@ -579,43 +467,13 @@ const yesDeleteMySettingsText = ref("");
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
|
||||
<!-- Camera delete modal -->
|
||||
<v-dialog v-model="viewingDeleteCamera" width="800">
|
||||
<v-card v-if="cameraToDelete !== null" class="dialog-container" color="surface" flat>
|
||||
<v-card-title> Delete {{ cameraToDelete.nickname }}? </v-card-title>
|
||||
<v-card-text class="pb-10px">
|
||||
Are you sure you want to delete "{{ cameraToDelete.nickname }}"? This cannot be undone.
|
||||
</v-card-text>
|
||||
<v-card-text class="pt-0 pb-10px">
|
||||
<pv-input
|
||||
v-model="yesDeleteMySettingsText"
|
||||
:label="'Type "' + cameraToDelete.nickname + '":'"
|
||||
:label-cols="6"
|
||||
:input-cols="6"
|
||||
/>
|
||||
</v-card-text>
|
||||
<v-card-actions class="pa-5 pt-0">
|
||||
<v-btn
|
||||
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
|
||||
color="primary"
|
||||
class="text-black"
|
||||
@click="cameraToDelete = null"
|
||||
>
|
||||
Cancel
|
||||
</v-btn>
|
||||
<v-btn
|
||||
color="error"
|
||||
:disabled="yesDeleteMySettingsText.toLowerCase() !== cameraToDelete.nickname.toLowerCase()"
|
||||
:loading="deletingCamera"
|
||||
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
|
||||
@click="deleteThisCamera(cameraToDelete.uniqueName)"
|
||||
>
|
||||
<v-icon start class="open-icon" size="large"> mdi-trash-can-outline </v-icon>
|
||||
<span class="open-label">Delete</span>
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
<pv-delete-modal
|
||||
v-model="confirmDeleteDialog.show"
|
||||
title="Delete Camera"
|
||||
:description="`Are you sure you want to delete the camera '${useCameraSettingsStore().currentCameraSettings.nickname}'? This action cannot be undone.`"
|
||||
:expected-confirmation-text="confirmDeleteDialog.nickname"
|
||||
:on-confirm="() => deleteThisCamera(confirmDeleteDialog.cameraUniqueName)"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -12,8 +12,13 @@ const cameraViewType = computed<number[]>({
|
||||
// Only show the input stream in Color Picking Mode
|
||||
if (useStateStore().colorPickingMode) return [0];
|
||||
|
||||
// Only show the output stream in Driver Mode or Calibration Mode
|
||||
if (useCameraSettingsStore().isDriverMode || useCameraSettingsStore().isCalibrationMode) return [1];
|
||||
// Only show the output stream in Driver Mode or Calibration Mode or Focus Mode
|
||||
if (
|
||||
useCameraSettingsStore().isDriverMode ||
|
||||
useCameraSettingsStore().isCalibrationMode ||
|
||||
useCameraSettingsStore().isFocusMode
|
||||
)
|
||||
return [1];
|
||||
|
||||
const ret: number[] = [];
|
||||
if (useCameraSettingsStore().currentPipelineSettings.inputShouldShow) {
|
||||
|
||||
@@ -10,14 +10,20 @@ import { useSettingsStore } from "@/stores/settings/GeneralSettingsStore";
|
||||
import { useTheme } from "vuetify";
|
||||
|
||||
const theme = useTheme();
|
||||
import { PlaceholderCameraSettings } from "@/types/SettingTypes";
|
||||
|
||||
const cameraViewType = computed<number[]>({
|
||||
get: (): number[] => {
|
||||
// Only show the input stream in Color Picking Mode
|
||||
if (useStateStore().colorPickingMode) return [0];
|
||||
|
||||
// Only show the output stream in Driver Mode or Calibration Mode
|
||||
if (useCameraSettingsStore().isDriverMode || useCameraSettingsStore().isCalibrationMode) return [1];
|
||||
// Only show the output stream in Driver Mode or Calibration Mode or Focus Mode
|
||||
if (
|
||||
useCameraSettingsStore().isDriverMode ||
|
||||
useCameraSettingsStore().isCalibrationMode ||
|
||||
useCameraSettingsStore().isFocusMode
|
||||
)
|
||||
return [1];
|
||||
|
||||
const ret: number[] = [];
|
||||
if (useCameraSettingsStore().currentPipelineSettings.inputShouldShow) {
|
||||
@@ -54,6 +60,17 @@ const arducamWarningShown = computed<boolean>(() => {
|
||||
);
|
||||
});
|
||||
|
||||
const cameraMismatchWarningShown = computed<boolean>(() => {
|
||||
return (
|
||||
Object.values(useCameraSettingsStore().cameras)
|
||||
// Ignore placeholder camera
|
||||
.filter((camera) => JSON.stringify(camera) !== JSON.stringify(PlaceholderCameraSettings))
|
||||
.some((camera) => {
|
||||
return camera.mismatch;
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
const conflictingHostnameShown = computed<boolean>(() => {
|
||||
return useSettingsStore().general.conflictingHostname;
|
||||
});
|
||||
@@ -104,6 +121,21 @@ const showCameraSetupDialog = ref(useCameraSettingsStore().needsCameraConfigurat
|
||||
{{ useSettingsStore().general.conflictingCameras }}!
|
||||
</span>
|
||||
</v-alert>
|
||||
<v-banner
|
||||
v-if="cameraMismatchWarningShown"
|
||||
v-model="cameraMismatchWarningShown"
|
||||
rounded
|
||||
color="error"
|
||||
dark
|
||||
class="mb-3"
|
||||
icon="mdi-alert-circle-outline"
|
||||
>
|
||||
<span
|
||||
>Camera Mismatch Detected! Visit the <a href="#/cameraConfigs">Camera Matching</a> page for more information.
|
||||
Note: Camera matching is done by USB port. Ensure cameras are plugged into the same USB ports as when they were
|
||||
activated.
|
||||
</span>
|
||||
</v-banner>
|
||||
<v-row no-gutters>
|
||||
<v-col cols="12" class="pb-3 pr-lg-3" lg="8" align-self="stretch">
|
||||
<CamerasCard v-model="cameraViewType" />
|
||||
|
||||
@@ -8,24 +8,26 @@ apply from: "${rootDir}/shared/common.gradle"
|
||||
wpilibTools.deps.wpilibVersion = wpi.versions.wpilibVersion.get()
|
||||
|
||||
def nativeConfigName = 'wpilibNatives'
|
||||
def nativeConfig = configurations.create(nativeConfigName)
|
||||
|
||||
configurations {
|
||||
wpilibNatives
|
||||
}
|
||||
def nativeTasks = wpilibTools.createExtractionTasks {
|
||||
configurationName = nativeConfigName
|
||||
}
|
||||
|
||||
nativeTasks.addToSourceSetResources(sourceSets.main)
|
||||
|
||||
nativeConfig.dependencies.add wpilibTools.deps.wpilib("wpimath")
|
||||
nativeConfig.dependencies.add wpilibTools.deps.wpilib("wpinet")
|
||||
nativeConfig.dependencies.add wpilibTools.deps.wpilib("wpiutil")
|
||||
nativeConfig.dependencies.add wpilibTools.deps.wpilib("ntcore")
|
||||
nativeConfig.dependencies.add wpilibTools.deps.wpilib("cscore")
|
||||
nativeConfig.dependencies.add wpilibTools.deps.wpilib("apriltag")
|
||||
nativeConfig.dependencies.add wpilibTools.deps.wpilib("hal")
|
||||
nativeConfig.dependencies.add wpilibTools.deps.wpilibOpenCv("frc" + openCVYear, wpi.versions.opencvVersion.get())
|
||||
|
||||
dependencies {
|
||||
wpilibNatives project(path: ':photon-targeting', configuration: 'wpilibNatives')
|
||||
wpilibNatives wpilibTools.deps.wpilib("wpimath")
|
||||
wpilibNatives wpilibTools.deps.wpilib("wpinet")
|
||||
wpilibNatives wpilibTools.deps.wpilib("wpiutil")
|
||||
wpilibNatives wpilibTools.deps.wpilib("ntcore")
|
||||
wpilibNatives wpilibTools.deps.wpilib("cscore")
|
||||
wpilibNatives wpilibTools.deps.wpilib("apriltag")
|
||||
wpilibNatives wpilibTools.deps.wpilib("hal")
|
||||
wpilibNatives wpilibTools.deps.wpilibOpenCv("frc" + openCVYear, wpi.versions.opencvVersion.get())
|
||||
|
||||
// Zip
|
||||
implementation 'org.zeroturnaround:zt-zip:1.14'
|
||||
|
||||
|
||||
@@ -24,6 +24,7 @@ import edu.wpi.first.cscore.UsbCameraInfo;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
import org.opencv.core.Size;
|
||||
import org.photonvision.common.dataflow.websocket.UICameraConfiguration;
|
||||
import org.photonvision.common.logging.LogGroup;
|
||||
import org.photonvision.common.logging.Logger;
|
||||
@@ -189,6 +190,23 @@ public class CameraConfiguration {
|
||||
calibrations.add(calibration);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a calibration from our list.
|
||||
*
|
||||
* @param calibration The calibration to remove
|
||||
*/
|
||||
public void removeCalibration(Size unrotatedImageSize) {
|
||||
logger.info("deleting calibration " + unrotatedImageSize);
|
||||
calibrations.stream()
|
||||
.filter(it -> it.unrotatedImageSize.equals(unrotatedImageSize))
|
||||
.findAny()
|
||||
.ifPresent(
|
||||
(it) -> {
|
||||
it.release();
|
||||
calibrations.remove(it);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* cscore will auto-reconnect to the camera path we give it. v4l does not guarantee that if i swap
|
||||
* cameras around, the same /dev/videoN ID will be assigned to that camera. So instead default to
|
||||
|
||||
@@ -21,57 +21,102 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||
import java.util.ArrayList;
|
||||
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
public record HardwareConfig(
|
||||
String deviceName,
|
||||
String deviceLogoPath,
|
||||
String supportURL,
|
||||
// LED control
|
||||
public class HardwareConfig {
|
||||
public final String deviceName;
|
||||
public final String deviceLogoPath;
|
||||
public final String supportURL;
|
||||
|
||||
ArrayList<Integer> ledPins,
|
||||
String ledSetCommand,
|
||||
boolean ledsCanDim,
|
||||
ArrayList<Integer> ledBrightnessRange,
|
||||
String ledDimCommand,
|
||||
String ledBlinkCommand,
|
||||
ArrayList<Integer> statusRGBPins,
|
||||
// Metrics
|
||||
// LED control
|
||||
public final ArrayList<Integer> ledPins;
|
||||
public final String ledSetCommand;
|
||||
public final boolean ledsCanDim;
|
||||
public final ArrayList<Integer> ledBrightnessRange;
|
||||
public final String ledDimCommand;
|
||||
public final String ledBlinkCommand;
|
||||
public final ArrayList<Integer> statusRGBPins;
|
||||
|
||||
String cpuTempCommand,
|
||||
String cpuMemoryCommand,
|
||||
String cpuUtilCommand,
|
||||
String cpuThrottleReasonCmd,
|
||||
String cpuUptimeCommand,
|
||||
String gpuMemoryCommand,
|
||||
String ramUtilCommand,
|
||||
String gpuMemUsageCommand,
|
||||
String diskUsageCommand,
|
||||
// Device stuff
|
||||
String restartHardwareCommand,
|
||||
double vendorFOV) { // -1 for unmanaged
|
||||
// Metrics
|
||||
public final String cpuTempCommand;
|
||||
public final String cpuMemoryCommand;
|
||||
public final String cpuUtilCommand;
|
||||
public final String cpuThrottleReasonCmd;
|
||||
public final String cpuUptimeCommand;
|
||||
public final String gpuMemoryCommand;
|
||||
public final String ramUtilCommand;
|
||||
public final String gpuMemUsageCommand;
|
||||
public final String diskUsageCommand;
|
||||
|
||||
// Device stuff
|
||||
public final String restartHardwareCommand;
|
||||
public final double vendorFOV; // -1 for unmanaged
|
||||
|
||||
public HardwareConfig(
|
||||
String deviceName,
|
||||
String deviceLogoPath,
|
||||
String supportURL,
|
||||
ArrayList<Integer> ledPins,
|
||||
String ledSetCommand,
|
||||
boolean ledsCanDim,
|
||||
ArrayList<Integer> ledBrightnessRange,
|
||||
String ledDimCommand,
|
||||
String ledBlinkCommand,
|
||||
ArrayList<Integer> statusRGBPins,
|
||||
String cpuTempCommand,
|
||||
String cpuMemoryCommand,
|
||||
String cpuUtilCommand,
|
||||
String cpuThrottleReasonCmd,
|
||||
String cpuUptimeCommand,
|
||||
String gpuMemoryCommand,
|
||||
String ramUtilCommand,
|
||||
String gpuMemUsageCommand,
|
||||
String diskUsageCommand,
|
||||
String restartHardwareCommand,
|
||||
double vendorFOV) {
|
||||
this.deviceName = deviceName;
|
||||
this.deviceLogoPath = deviceLogoPath;
|
||||
this.supportURL = supportURL;
|
||||
this.ledPins = ledPins;
|
||||
this.ledSetCommand = ledSetCommand;
|
||||
this.ledsCanDim = ledsCanDim;
|
||||
this.ledBrightnessRange = ledBrightnessRange;
|
||||
this.ledDimCommand = ledDimCommand;
|
||||
this.ledBlinkCommand = ledBlinkCommand;
|
||||
this.statusRGBPins = statusRGBPins;
|
||||
this.cpuTempCommand = cpuTempCommand;
|
||||
this.cpuMemoryCommand = cpuMemoryCommand;
|
||||
this.cpuUtilCommand = cpuUtilCommand;
|
||||
this.cpuThrottleReasonCmd = cpuThrottleReasonCmd;
|
||||
this.cpuUptimeCommand = cpuUptimeCommand;
|
||||
this.gpuMemoryCommand = gpuMemoryCommand;
|
||||
this.ramUtilCommand = ramUtilCommand;
|
||||
this.gpuMemUsageCommand = gpuMemUsageCommand;
|
||||
this.diskUsageCommand = diskUsageCommand;
|
||||
this.restartHardwareCommand = restartHardwareCommand;
|
||||
this.vendorFOV = vendorFOV;
|
||||
}
|
||||
|
||||
public HardwareConfig() {
|
||||
this(
|
||||
"", // deviceName
|
||||
"", // deviceLogoPath
|
||||
"", // supportURL
|
||||
new ArrayList<>(), // ledPins
|
||||
"", // ledSetCommand
|
||||
false, // ledsCanDim
|
||||
new ArrayList<>(), // ledBrightnessRange
|
||||
"", // ledDimCommand
|
||||
"", // ledBlinkCommand
|
||||
new ArrayList<>(), // statusRGBPins
|
||||
"", // cpuTempCommand
|
||||
"", // cpuMemoryCommand
|
||||
"", // cpuUtilCommand
|
||||
"", // cpuThrottleReasonCmd
|
||||
"", // cpuUptimeCommand
|
||||
"", // gpuMemoryCommand
|
||||
"", // ramUtilCommand
|
||||
"", // gpuMemUsageCommand
|
||||
"", // diskUsageCommand
|
||||
"", // restartHardwareCommand
|
||||
-1); // vendorFOV
|
||||
deviceName = "";
|
||||
deviceLogoPath = "";
|
||||
supportURL = "";
|
||||
ledPins = new ArrayList<>();
|
||||
ledSetCommand = "";
|
||||
ledsCanDim = false;
|
||||
ledBrightnessRange = new ArrayList<>();
|
||||
ledDimCommand = "";
|
||||
ledBlinkCommand = "";
|
||||
statusRGBPins = new ArrayList<>();
|
||||
cpuTempCommand = "";
|
||||
cpuMemoryCommand = "";
|
||||
cpuUtilCommand = "";
|
||||
cpuThrottleReasonCmd = "";
|
||||
cpuUptimeCommand = "";
|
||||
gpuMemoryCommand = "";
|
||||
ramUtilCommand = "";
|
||||
gpuMemUsageCommand = "";
|
||||
diskUsageCommand = "";
|
||||
restartHardwareCommand = "";
|
||||
vendorFOV = -1;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -96,4 +141,51 @@ public record HardwareConfig(
|
||||
|| gpuMemUsageCommand != ""
|
||||
|| diskUsageCommand != "";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "HardwareConfig[deviceName="
|
||||
+ deviceName
|
||||
+ ", deviceLogoPath="
|
||||
+ deviceLogoPath
|
||||
+ ", supportURL="
|
||||
+ supportURL
|
||||
+ ", ledPins="
|
||||
+ ledPins
|
||||
+ ", ledSetCommand="
|
||||
+ ledSetCommand
|
||||
+ ", ledsCanDim="
|
||||
+ ledsCanDim
|
||||
+ ", ledBrightnessRange="
|
||||
+ ledBrightnessRange
|
||||
+ ", ledDimCommand="
|
||||
+ ledDimCommand
|
||||
+ ", ledBlinkCommand="
|
||||
+ ledBlinkCommand
|
||||
+ ", statusRGBPins="
|
||||
+ statusRGBPins
|
||||
+ ", cpuTempCommand="
|
||||
+ cpuTempCommand
|
||||
+ ", cpuMemoryCommand="
|
||||
+ cpuMemoryCommand
|
||||
+ ", cpuUtilCommand="
|
||||
+ cpuUtilCommand
|
||||
+ ", cpuThrottleReasonCmd="
|
||||
+ cpuThrottleReasonCmd
|
||||
+ ", cpuUptimeCommand="
|
||||
+ cpuUptimeCommand
|
||||
+ ", gpuMemoryCommand="
|
||||
+ gpuMemoryCommand
|
||||
+ ", ramUtilCommand="
|
||||
+ ramUtilCommand
|
||||
+ ", gpuMemUsageCommand="
|
||||
+ gpuMemUsageCommand
|
||||
+ ", diskUsageCommand="
|
||||
+ diskUsageCommand
|
||||
+ ", restartHardwareCommand="
|
||||
+ restartHardwareCommand
|
||||
+ ", vendorFOV="
|
||||
+ vendorFOV
|
||||
+ "]";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -70,6 +70,8 @@ public class NetworkTablesManager {
|
||||
// Creating the alert up here since it should be persistent
|
||||
private final Alert conflictAlert = new Alert("PhotonAlerts", "", AlertType.kWarning);
|
||||
|
||||
private final Alert mismatchAlert = new Alert("PhotonAlerts", "", AlertType.kWarning);
|
||||
|
||||
public boolean conflictingHostname = false;
|
||||
public String conflictingCameras = "";
|
||||
private String currentMacAddress;
|
||||
@@ -95,6 +97,7 @@ public class NetworkTablesManager {
|
||||
|
||||
// This should start as false, since we don't know if there's a conflict yet
|
||||
conflictAlert.set(false);
|
||||
mismatchAlert.set(false);
|
||||
|
||||
// Get the UI state in sync with the backend. NT should fire a callback when it
|
||||
// first connects to the robot
|
||||
@@ -115,6 +118,14 @@ public class NetworkTablesManager {
|
||||
return INSTANCE;
|
||||
}
|
||||
|
||||
public void setMismatchAlert(boolean on, String message) {
|
||||
if (mismatchAlert != null) {
|
||||
mismatchAlert.set(on);
|
||||
mismatchAlert.setText(message);
|
||||
SmartDashboard.updateValues();
|
||||
}
|
||||
}
|
||||
|
||||
private void logNtMessage(NetworkTableEvent event) {
|
||||
String levelmsg = "DEBUG";
|
||||
LogLevel pvlevel = LogLevel.DEBUG;
|
||||
|
||||
@@ -25,7 +25,6 @@ import org.photonvision.common.configuration.NetworkConfig;
|
||||
import org.photonvision.common.logging.LogGroup;
|
||||
import org.photonvision.common.logging.Logger;
|
||||
import org.photonvision.common.util.TimedTaskManager;
|
||||
import org.photonvision.jni.PhotonTargetingJniLoader;
|
||||
import org.photonvision.jni.TimeSyncClient;
|
||||
import org.photonvision.jni.TimeSyncServer;
|
||||
|
||||
@@ -43,10 +42,6 @@ public class TimeSyncManager {
|
||||
IntegerPublisher m_lastPongTimePub;
|
||||
|
||||
public TimeSyncManager(NetworkTable kRootTable) {
|
||||
if (!PhotonTargetingJniLoader.isWorking) {
|
||||
logger.error("PhotonTargetingJNI was not loaded! Cannot do time-sync");
|
||||
}
|
||||
|
||||
this.ntInstance = kRootTable.getInstance();
|
||||
|
||||
// Need this subtable to be unique per coprocessor. TODO: consider using MAC address or
|
||||
@@ -65,18 +60,10 @@ public class TimeSyncManager {
|
||||
|
||||
// Since we're spinning off tasks in a new thread, be careful and start it seperately
|
||||
public void start() {
|
||||
if (!PhotonTargetingJniLoader.isWorking) {
|
||||
logger.error("PhotonTargetingJNI was not loaded! Cannot start");
|
||||
}
|
||||
|
||||
TimedTaskManager.getInstance().addTask("TimeSyncManager::tick", this::tick, 1000);
|
||||
}
|
||||
|
||||
public synchronized long getOffset() {
|
||||
if (!PhotonTargetingJniLoader.isWorking) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
// if we're a client, return the offset to server time
|
||||
if (m_client != null) return m_client.getOffset();
|
||||
// if we're a server, our time (nt::Now) is the same as network time
|
||||
@@ -88,10 +75,6 @@ public class TimeSyncManager {
|
||||
}
|
||||
|
||||
synchronized void setConfig(NetworkConfig config) {
|
||||
if (!PhotonTargetingJniLoader.isWorking) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (m_client == null && m_server == null) {
|
||||
throw new RuntimeException("Neither client nor server are null?");
|
||||
}
|
||||
|
||||
@@ -52,6 +52,7 @@ public class UICameraConfiguration {
|
||||
public double minWhiteBalanceTemp;
|
||||
public double maxWhiteBalanceTemp;
|
||||
public PVCameraInfo matchedCameraInfo;
|
||||
public boolean mismatch;
|
||||
|
||||
// Status for if the underlying device is present and such
|
||||
public boolean isConnected;
|
||||
|
||||
@@ -27,6 +27,7 @@ import org.photonvision.common.logging.Logger;
|
||||
import org.photonvision.common.util.SerializationUtils;
|
||||
import org.photonvision.vision.pipeline.result.CVPipelineResult;
|
||||
import org.photonvision.vision.pipeline.result.CalibrationPipelineResult;
|
||||
import org.photonvision.vision.pipeline.result.FocusPipelineResult;
|
||||
|
||||
public class UIDataPublisher implements CVPipelineResultConsumer {
|
||||
private static final Logger logger = new Logger(UIDataPublisher.class, LogGroup.VisionModule);
|
||||
@@ -77,6 +78,10 @@ public class UIDataPublisher implements CVPipelineResultConsumer {
|
||||
var uiMap = new HashMap<String, HashMap<String, Object>>();
|
||||
uiMap.put(uniqueName, dataMap);
|
||||
|
||||
if (result instanceof FocusPipelineResult focusResult) {
|
||||
dataMap.put("focus", focusResult.focus);
|
||||
}
|
||||
|
||||
DataChangeService.getInstance()
|
||||
.publishEvent(OutgoingUIEvent.wrappedOf("updatePipelineResult", uiMap));
|
||||
lastUIResultUpdateTime = now;
|
||||
|
||||
@@ -49,7 +49,7 @@ public class UIPhotonConfiguration {
|
||||
NetworkManager.getInstance().networkingIsDisabled),
|
||||
new UILightingConfig(
|
||||
c.getHardwareSettings().ledBrightnessPercentage,
|
||||
!c.getHardwareConfig().ledPins().isEmpty()),
|
||||
!c.getHardwareConfig().ledPins.isEmpty()),
|
||||
new UIGeneralSettings(
|
||||
PhotonVersion.versionString,
|
||||
// TODO add support for other types of GPU accel
|
||||
@@ -57,9 +57,9 @@ public class UIPhotonConfiguration {
|
||||
MrCalJNILoader.getInstance().isLoaded(),
|
||||
c.neuralNetworkPropertyManager().getModels(),
|
||||
NeuralNetworkModelManager.getInstance().getSupportedBackends(),
|
||||
c.getHardwareConfig().deviceName().isEmpty()
|
||||
c.getHardwareConfig().deviceName.isEmpty()
|
||||
? Platform.getHardwareModel()
|
||||
: c.getHardwareConfig().deviceName(),
|
||||
: c.getHardwareConfig().deviceName,
|
||||
Platform.getPlatformName(),
|
||||
NetworkTablesManager.getInstance().conflictingHostname,
|
||||
NetworkTablesManager.getInstance().conflictingCameras),
|
||||
|
||||
@@ -92,8 +92,8 @@ public class CustomGPIO extends GPIOBase {
|
||||
|
||||
public static void setConfig(HardwareConfig config) {
|
||||
if (Platform.isRaspberryPi()) return;
|
||||
commands.replace("setState", config.ledSetCommand());
|
||||
commands.replace("dim", config.ledDimCommand());
|
||||
commands.replace("blink", config.ledBlinkCommand());
|
||||
commands.replace("setState", config.ledSetCommand);
|
||||
commands.replace("dim", config.ledDimCommand);
|
||||
commands.replace("blink", config.ledBlinkCommand);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -97,22 +97,22 @@ public class HardwareManager {
|
||||
}
|
||||
|
||||
statusLED =
|
||||
hardwareConfig.statusRGBPins().size() == 3
|
||||
? new StatusLED(hardwareConfig.statusRGBPins())
|
||||
hardwareConfig.statusRGBPins.size() == 3
|
||||
? new StatusLED(hardwareConfig.statusRGBPins)
|
||||
: null;
|
||||
|
||||
if (statusLED != null) {
|
||||
TimedTaskManager.getInstance().addTask("StatusLEDUpdate", this::statusLEDUpdate, 150);
|
||||
}
|
||||
|
||||
var hasBrightnessRange = hardwareConfig.ledBrightnessRange().size() == 2;
|
||||
var hasBrightnessRange = hardwareConfig.ledBrightnessRange.size() == 2;
|
||||
visionLED =
|
||||
hardwareConfig.ledPins().isEmpty()
|
||||
hardwareConfig.ledPins.isEmpty()
|
||||
? null
|
||||
: new VisionLED(
|
||||
hardwareConfig.ledPins(),
|
||||
hasBrightnessRange ? hardwareConfig.ledBrightnessRange().get(0) : 0,
|
||||
hasBrightnessRange ? hardwareConfig.ledBrightnessRange().get(1) : 100,
|
||||
hardwareConfig.ledPins,
|
||||
hasBrightnessRange ? hardwareConfig.ledBrightnessRange.get(0) : 0,
|
||||
hasBrightnessRange ? hardwareConfig.ledBrightnessRange.get(1) : 100,
|
||||
pigpioSocket,
|
||||
ledModeState::set);
|
||||
|
||||
@@ -161,7 +161,7 @@ public class HardwareManager {
|
||||
}
|
||||
}
|
||||
try {
|
||||
return shellExec.executeBashCommand(hardwareConfig.restartHardwareCommand()) == 0;
|
||||
return shellExec.executeBashCommand(hardwareConfig.restartHardwareCommand) == 0;
|
||||
} catch (IOException e) {
|
||||
logger.error("Could not restart device!", e);
|
||||
return false;
|
||||
|
||||
@@ -22,18 +22,18 @@ import org.photonvision.common.configuration.HardwareConfig;
|
||||
public class FileCmds extends CmdBase {
|
||||
@Override
|
||||
public void initCmds(HardwareConfig config) {
|
||||
cpuTemperatureCommand = config.cpuTempCommand();
|
||||
cpuUtilizationCommand = config.cpuUtilCommand();
|
||||
cpuThrottleReasonCmd = config.cpuThrottleReasonCmd();
|
||||
cpuTemperatureCommand = config.cpuTempCommand;
|
||||
cpuUtilizationCommand = config.cpuUtilCommand;
|
||||
cpuThrottleReasonCmd = config.cpuThrottleReasonCmd;
|
||||
|
||||
ramMemCommand = config.cpuMemoryCommand();
|
||||
ramUtilCommand = config.ramUtilCommand();
|
||||
ramMemCommand = config.cpuMemoryCommand;
|
||||
ramUtilCommand = config.ramUtilCommand;
|
||||
|
||||
gpuMemCommand = config.gpuMemoryCommand();
|
||||
gpuMemUtilCommand = config.gpuMemUsageCommand();
|
||||
gpuMemCommand = config.gpuMemoryCommand;
|
||||
gpuMemUtilCommand = config.gpuMemUsageCommand;
|
||||
|
||||
diskUsageCommand = config.diskUsageCommand();
|
||||
diskUsageCommand = config.diskUsageCommand;
|
||||
|
||||
uptimeCommand = config.cpuUptimeCommand();
|
||||
uptimeCommand = config.cpuUptimeCommand;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,14 +26,14 @@ import java.io.IOException;
|
||||
import java.nio.file.Path;
|
||||
import org.opencv.core.Mat;
|
||||
import org.opencv.highgui.HighGui;
|
||||
import org.photonvision.jni.WpilibLoader;
|
||||
import org.photonvision.jni.LibraryLoader;
|
||||
import org.photonvision.vision.calibration.CameraCalibrationCoefficients;
|
||||
import org.photonvision.vision.pipeline.result.CVPipelineResult;
|
||||
import org.photonvision.vision.target.TrackedTarget;
|
||||
|
||||
public class TestUtils {
|
||||
public static boolean loadLibraries() {
|
||||
return WpilibLoader.loadLibraries();
|
||||
return LibraryLoader.loadWpiLibraries() && LibraryLoader.loadTargeting();
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
|
||||
@@ -90,7 +90,7 @@ public class PhotonArucoDetector implements Releasable {
|
||||
// each detection has a Mat of corners
|
||||
Mat cornerMat = cornerMats.get(i);
|
||||
|
||||
// Aruco detection returns corners (BR, BL, TL, TR).
|
||||
// ArUco detection returns corners (BR, BL, TL, TR).
|
||||
// For parity with AprilTags and photonlib, we want (BL, BR, TR, TL).
|
||||
double[] xCorners = {
|
||||
cornerMat.get(0, 1)[0],
|
||||
|
||||
@@ -26,6 +26,7 @@ import com.fasterxml.jackson.annotation.JsonTypeInfo;
|
||||
import com.fasterxml.jackson.annotation.JsonTypeName;
|
||||
import edu.wpi.first.cscore.UsbCameraInfo;
|
||||
import java.util.Arrays;
|
||||
import java.util.Objects;
|
||||
|
||||
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.WRAPPER_OBJECT)
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
@@ -70,8 +71,15 @@ public sealed interface PVCameraInfo {
|
||||
|
||||
CameraType type();
|
||||
|
||||
/**
|
||||
* Default equals implementation that delegates to the implementing class's equals method. This
|
||||
* method checks type compatibility first, then delegates to the actual implementation.
|
||||
*/
|
||||
default boolean equals(PVCameraInfo other) {
|
||||
return uniquePath().equals(other.uniquePath());
|
||||
if (other == null) return false;
|
||||
if (this.type() != other.type()) return false;
|
||||
// Delegate to the actual equals(Object) implementation of this instance
|
||||
return this.equals((Object) other);
|
||||
}
|
||||
|
||||
@JsonTypeName("PVUsbCameraInfo")
|
||||
@@ -125,7 +133,17 @@ public sealed interface PVCameraInfo {
|
||||
public boolean equals(Object obj) {
|
||||
if (this == obj) return true;
|
||||
if (obj == null) return false;
|
||||
return obj instanceof PVCameraInfo info && equals(info);
|
||||
if (!(obj instanceof PVUsbCameraInfo info)) return false;
|
||||
|
||||
return super.name.equals(info.name)
|
||||
&& super.vendorId == info.vendorId
|
||||
&& super.productId == info.productId
|
||||
&& uniquePath().equals(info.uniquePath());
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(super.name, super.vendorId, super.productId, uniquePath());
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -191,7 +209,14 @@ public sealed interface PVCameraInfo {
|
||||
public boolean equals(Object obj) {
|
||||
if (this == obj) return true;
|
||||
if (obj == null) return false;
|
||||
return obj instanceof PVCameraInfo info && equals(info);
|
||||
if (!(obj instanceof PVCSICameraInfo info)) return false;
|
||||
|
||||
return baseName.equals(info.baseName) && path.equals(info.path);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(baseName, path);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -248,7 +273,14 @@ public sealed interface PVCameraInfo {
|
||||
public boolean equals(Object obj) {
|
||||
if (this == obj) return true;
|
||||
if (obj == null) return false;
|
||||
return obj instanceof PVFileCameraInfo info && equals(info);
|
||||
if (!(obj instanceof PVFileCameraInfo info)) return false;
|
||||
|
||||
return name.equals(info.name) && path.equals(info.path);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(name, path);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -77,10 +77,9 @@ public class LibcameraGpuSettables extends VisionSourceSettables {
|
||||
videoModes.put(5, new FPSRatedVideoMode(PixelFormat.kUnknown, 3280 / 2, 2464 / 2, 15, 20, 1));
|
||||
videoModes.put(6, new FPSRatedVideoMode(PixelFormat.kUnknown, 3280 / 4, 2464 / 4, 15, 20, 1));
|
||||
} else if (sensorModel == LibCameraJNI.SensorModel.OV9281) {
|
||||
videoModes.put(0, new FPSRatedVideoMode(PixelFormat.kUnknown, 320, 240, 30, 30, .39));
|
||||
videoModes.put(1, new FPSRatedVideoMode(PixelFormat.kUnknown, 1280 / 2, 800 / 2, 60, 60, 1));
|
||||
videoModes.put(2, new FPSRatedVideoMode(PixelFormat.kUnknown, 640, 480, 65, 90, .39));
|
||||
videoModes.put(3, new FPSRatedVideoMode(PixelFormat.kUnknown, 1280, 800, 60, 60, 1));
|
||||
// Taken from https://www.ovt.com/wp-content/uploads/2022/01/OV9281-OV9282-PB-v1.3-WEB.pdf
|
||||
videoModes.put(0, new FPSRatedVideoMode(PixelFormat.kUnknown, 640, 400, 120, 240, 1));
|
||||
videoModes.put(1, new FPSRatedVideoMode(PixelFormat.kUnknown, 1280, 800, 120, 120, 1));
|
||||
|
||||
} else {
|
||||
if (sensorModel == LibCameraJNI.SensorModel.IMX477) {
|
||||
|
||||
@@ -100,7 +100,7 @@ public class LibcameraGpuSource extends VisionSource {
|
||||
|
||||
@Override
|
||||
public boolean hasLEDs() {
|
||||
return (ConfigManager.getInstance().getConfig().getHardwareConfig().ledPins().size() > 0);
|
||||
return (ConfigManager.getInstance().getConfig().getHardwareConfig().ledPins.size() > 0);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -280,14 +280,14 @@ public class FindBoardCornersPipe
|
||||
}
|
||||
board.matchImagePoints(detectedCornersList, detectedIds, objPoints, imgPoints);
|
||||
|
||||
// draw the charuco board
|
||||
// Draw the ChArUco board
|
||||
Objdetect.drawDetectedCornersCharuco(
|
||||
outFrame, detectedCorners, detectedIds, new Scalar(0, 0, 255)); // Red Text
|
||||
|
||||
imgPoints.copyTo(outBoardCorners);
|
||||
objPoints.copyTo(objPts);
|
||||
|
||||
// Since charuco can still detect without the whole board we need to send "fake" (all
|
||||
// Since ChaArUco can still detect without the whole board we need to send "fake" (all
|
||||
// values less than zero) points and then tell it to ignore that corner by setting the
|
||||
// corresponding level to -1. Calibrate3dPipe deals with piping this into the correct format
|
||||
// for each backend
|
||||
@@ -321,7 +321,7 @@ public class FindBoardCornersPipe
|
||||
detectedCorners.release();
|
||||
detectedIds.release();
|
||||
|
||||
} else { // If not Charuco then do chessboard
|
||||
} else { // If not ChArUco then do chessboard
|
||||
// Reduce the image size to be much more manageable
|
||||
// Note that opencv will copy the frame if no resize is requested; we can skip
|
||||
// this since we
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
/*
|
||||
* 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.vision.pipe.impl;
|
||||
|
||||
import org.opencv.core.Core;
|
||||
import org.opencv.core.CvType;
|
||||
import org.opencv.core.Mat;
|
||||
import org.opencv.core.MatOfDouble;
|
||||
import org.opencv.imgproc.Imgproc;
|
||||
import org.photonvision.vision.pipe.CVPipe;
|
||||
|
||||
public class FocusPipe extends CVPipe<Mat, FocusPipe.FocusResult, FocusPipe.FocusParams> {
|
||||
private double maxVariance = 0.0;
|
||||
|
||||
@Override
|
||||
protected FocusResult process(Mat in) {
|
||||
var outputMat = new Mat();
|
||||
|
||||
Imgproc.Laplacian(in, outputMat, CvType.CV_64F, 3);
|
||||
|
||||
var mean = new MatOfDouble();
|
||||
var stddev = new MatOfDouble();
|
||||
Core.meanStdDev(outputMat, mean, stddev);
|
||||
var sd = stddev.get(0, 0)[0];
|
||||
var variance = sd * sd;
|
||||
|
||||
return new FocusResult(outputMat, variance);
|
||||
}
|
||||
|
||||
public static class FocusResult {
|
||||
public final Mat frame;
|
||||
public final double variance;
|
||||
|
||||
public FocusResult(Mat frame, double variance) {
|
||||
this.frame = frame;
|
||||
this.variance = variance;
|
||||
}
|
||||
}
|
||||
|
||||
public static class FocusParams {}
|
||||
}
|
||||
@@ -73,13 +73,13 @@ public class AprilTagPipeline extends CVPipeline<CVPipelineResult, AprilTagPipel
|
||||
settings.threads = Math.max(1, settings.threads);
|
||||
|
||||
// for now, hard code tag width based on enum value
|
||||
// 2023/other: best guess is 6in
|
||||
double tagWidth = Units.inchesToMeters(6);
|
||||
TargetModel tagModel = TargetModel.kAprilTag16h5;
|
||||
if (settings.tagFamily == AprilTagFamily.kTag36h11) {
|
||||
// 2024 tag, 6.5in
|
||||
tagWidth = Units.inchesToMeters(6.5);
|
||||
tagModel = TargetModel.kAprilTag36h11;
|
||||
// From 2024 best guess is 6.5
|
||||
double tagWidth = Units.inchesToMeters(6.5);
|
||||
TargetModel tagModel = TargetModel.kAprilTag36h11;
|
||||
if (settings.tagFamily == AprilTagFamily.kTag16h5) {
|
||||
// 2023 tag, 6in
|
||||
tagWidth = Units.inchesToMeters(6);
|
||||
tagModel = TargetModel.kAprilTag16h5;
|
||||
}
|
||||
|
||||
var config = new AprilTagDetector.Config();
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
/*
|
||||
* 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.vision.pipeline;
|
||||
|
||||
import org.opencv.core.Mat;
|
||||
import org.photonvision.common.util.math.MathUtils;
|
||||
import org.photonvision.vision.frame.Frame;
|
||||
import org.photonvision.vision.frame.FrameThresholdType;
|
||||
import org.photonvision.vision.opencv.CVMat;
|
||||
import org.photonvision.vision.pipe.impl.CalculateFPSPipe;
|
||||
import org.photonvision.vision.pipe.impl.FocusPipe;
|
||||
import org.photonvision.vision.pipe.impl.ResizeImagePipe;
|
||||
import org.photonvision.vision.pipeline.result.FocusPipelineResult;
|
||||
|
||||
public class FocusPipeline extends CVPipeline<FocusPipelineResult, FocusPipelineSettings> {
|
||||
private final FocusPipe focusPipe = new FocusPipe();
|
||||
private final CalculateFPSPipe calculateFPSPipe = new CalculateFPSPipe();
|
||||
private final ResizeImagePipe resizeImagePipe = new ResizeImagePipe();
|
||||
|
||||
private static final FrameThresholdType PROCESSING_TYPE = FrameThresholdType.NONE;
|
||||
|
||||
public FocusPipeline() {
|
||||
super(PROCESSING_TYPE);
|
||||
settings = new FocusPipelineSettings();
|
||||
}
|
||||
|
||||
public FocusPipeline(FocusPipelineSettings settings) {
|
||||
super(PROCESSING_TYPE);
|
||||
this.settings = settings;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void setPipeParamsImpl() {
|
||||
resizeImagePipe.setParams(
|
||||
new ResizeImagePipe.ResizeImageParams(settings.streamingFrameDivisor));
|
||||
}
|
||||
|
||||
@Override
|
||||
public FocusPipelineResult process(Frame frame, FocusPipelineSettings settings) {
|
||||
long totalNanos = 0;
|
||||
|
||||
var inputMat = frame.colorImage.getMat();
|
||||
boolean emptyIn = inputMat.empty();
|
||||
Mat displayMat = new Mat();
|
||||
double variance = 0.0;
|
||||
|
||||
if (!emptyIn) {
|
||||
totalNanos += resizeImagePipe.run(inputMat).nanosElapsed;
|
||||
|
||||
var focusResult = focusPipe.run(inputMat);
|
||||
totalNanos += focusResult.nanosElapsed;
|
||||
variance = focusResult.output.variance;
|
||||
displayMat = focusResult.output.frame;
|
||||
}
|
||||
|
||||
var fpsResult = calculateFPSPipe.run(null);
|
||||
var fps = fpsResult.output;
|
||||
|
||||
var processedCVMat = new CVMat(displayMat);
|
||||
|
||||
return new FocusPipelineResult(
|
||||
frame.sequenceID,
|
||||
MathUtils.nanosToMillis(totalNanos),
|
||||
fps,
|
||||
new Frame(
|
||||
frame.sequenceID,
|
||||
frame.colorImage,
|
||||
processedCVMat,
|
||||
frame.type,
|
||||
frame.frameStaticProperties),
|
||||
variance);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void release() {
|
||||
// we never actually need to give resources up since pipelinemanager only makes
|
||||
// one of us
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
/*
|
||||
* Copyright (C) Photon Vision.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.photonvision.vision.pipeline;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonTypeName;
|
||||
import org.photonvision.vision.processes.PipelineManager;
|
||||
|
||||
@JsonTypeName("FocusPipelineSettings")
|
||||
public class FocusPipelineSettings extends CVPipelineSettings {
|
||||
public FocusPipelineSettings() {
|
||||
super();
|
||||
pipelineNickname = "Focus Camera";
|
||||
pipelineIndex = PipelineManager.FOCUS_INDEX;
|
||||
pipelineType = PipelineType.FocusCamera;
|
||||
inputShouldShow = true;
|
||||
cameraAutoExposure = true;
|
||||
}
|
||||
}
|
||||
@@ -19,6 +19,7 @@ package org.photonvision.vision.pipeline;
|
||||
|
||||
@SuppressWarnings("rawtypes")
|
||||
public enum PipelineType {
|
||||
FocusCamera(-3, FocusPipeline.class),
|
||||
Calib3d(-2, Calibrate3dPipeline.class),
|
||||
DriverMode(-1, DriverModePipeline.class),
|
||||
Reflective(0, ReflectivePipeline.class),
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
/*
|
||||
* 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.vision.pipeline.result;
|
||||
|
||||
import java.util.List;
|
||||
import org.photonvision.vision.frame.Frame;
|
||||
|
||||
public class FocusPipelineResult extends CVPipelineResult {
|
||||
public final double focus;
|
||||
|
||||
public FocusPipelineResult(
|
||||
long seq, double latencyNanos, double fps, Frame outputFrame, double focus) {
|
||||
super(seq, latencyNanos, fps, List.of(), outputFrame);
|
||||
this.focus = focus;
|
||||
}
|
||||
}
|
||||
@@ -36,10 +36,12 @@ public class PipelineManager {
|
||||
private static final Logger logger = new Logger(PipelineManager.class, LogGroup.VisionModule);
|
||||
|
||||
public static final int DRIVERMODE_INDEX = -1;
|
||||
public static final int FOCUS_INDEX = -3;
|
||||
public static final int CAL_3D_INDEX = -2;
|
||||
|
||||
protected final List<CVPipelineSettings> userPipelineSettings;
|
||||
protected final Calibrate3dPipeline calibration3dPipeline;
|
||||
protected final FocusPipeline focusPipeline = new FocusPipeline();
|
||||
protected final DriverModePipeline driverModePipeline = new DriverModePipeline();
|
||||
|
||||
/** Index of the currently active pipeline. Defaults to 0. */
|
||||
@@ -93,6 +95,7 @@ public class PipelineManager {
|
||||
return switch (index) {
|
||||
case DRIVERMODE_INDEX -> driverModePipeline.getSettings();
|
||||
case CAL_3D_INDEX -> calibration3dPipeline.getSettings();
|
||||
case FOCUS_INDEX -> focusPipeline.getSettings();
|
||||
default -> {
|
||||
for (var setting : userPipelineSettings) {
|
||||
if (setting.pipelineIndex == index) yield setting;
|
||||
@@ -112,6 +115,7 @@ public class PipelineManager {
|
||||
return switch (index) {
|
||||
case DRIVERMODE_INDEX -> driverModePipeline.getSettings().pipelineNickname;
|
||||
case CAL_3D_INDEX -> calibration3dPipeline.getSettings().pipelineNickname;
|
||||
case FOCUS_INDEX -> focusPipeline.getSettings().pipelineNickname;
|
||||
default -> {
|
||||
for (var setting : userPipelineSettings) {
|
||||
if (setting.pipelineIndex == index) yield setting.pipelineNickname;
|
||||
@@ -153,6 +157,7 @@ public class PipelineManager {
|
||||
return switch (currentPipelineIndex) {
|
||||
case CAL_3D_INDEX -> calibration3dPipeline;
|
||||
case DRIVERMODE_INDEX -> driverModePipeline;
|
||||
case FOCUS_INDEX -> focusPipeline;
|
||||
// Just return the current user pipeline, we're not on a built-in one
|
||||
default -> currentUserPipeline;
|
||||
};
|
||||
@@ -253,7 +258,7 @@ public class PipelineManager {
|
||||
new AprilTagPipeline((AprilTagPipelineSettings) desiredPipelineSettings);
|
||||
}
|
||||
case Aruco -> {
|
||||
logger.debug("Creating Aruco Pipeline");
|
||||
logger.debug("Creating ArUco Pipeline");
|
||||
currentUserPipeline = new ArucoPipeline((ArucoPipelineSettings) desiredPipelineSettings);
|
||||
}
|
||||
case ObjectDetection -> {
|
||||
@@ -261,7 +266,7 @@ public class PipelineManager {
|
||||
currentUserPipeline =
|
||||
new ObjectDetectionPipeline((ObjectDetectionPipelineSettings) desiredPipelineSettings);
|
||||
}
|
||||
case Calib3d, DriverMode -> {}
|
||||
case Calib3d, DriverMode, FocusCamera -> {}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -335,7 +340,7 @@ public class PipelineManager {
|
||||
case AprilTag -> new AprilTagPipelineSettings();
|
||||
case Aruco -> new ArucoPipelineSettings();
|
||||
case ObjectDetection -> new ObjectDetectionPipelineSettings();
|
||||
case Calib3d, DriverMode -> {
|
||||
case Calib3d, DriverMode, FocusCamera -> {
|
||||
logger.error("Got invalid pipeline type: " + type);
|
||||
yield null;
|
||||
}
|
||||
|
||||
@@ -93,6 +93,8 @@ public class VisionModule {
|
||||
MJPGFrameConsumer inputVideoStreamer;
|
||||
MJPGFrameConsumer outputVideoStreamer;
|
||||
|
||||
boolean mismatch;
|
||||
|
||||
public VisionModule(PipelineManager pipelineManager, VisionSource visionSource) {
|
||||
logger =
|
||||
new Logger(
|
||||
@@ -100,6 +102,8 @@ public class VisionModule {
|
||||
visionSource.getSettables().getConfiguration().nickname,
|
||||
LogGroup.VisionModule);
|
||||
|
||||
mismatch = false;
|
||||
|
||||
cameraQuirks = visionSource.getCameraConfiguration().cameraQuirks;
|
||||
|
||||
if (visionSource.getCameraConfiguration().cameraQuirks == null)
|
||||
@@ -160,7 +164,7 @@ public class VisionModule {
|
||||
|
||||
// Set vendor FOV
|
||||
if (isVendorCamera()) {
|
||||
var fov = ConfigManager.getInstance().getConfig().getHardwareConfig().vendorFOV();
|
||||
var fov = ConfigManager.getInstance().getConfig().getHardwareConfig().vendorFOV;
|
||||
logger.info("Setting FOV of vendor camera to " + fov);
|
||||
visionSource.getSettables().setFOV(fov);
|
||||
}
|
||||
@@ -568,6 +572,8 @@ public class VisionModule {
|
||||
|
||||
ret.deactivated = config.deactivated;
|
||||
|
||||
ret.mismatch = this.mismatch;
|
||||
|
||||
// TODO refactor into helper method
|
||||
var temp = new HashMap<Integer, HashMap<String, Object>>();
|
||||
var videoModes = visionSource.getSettables().getAllVideoModes();
|
||||
@@ -674,6 +680,16 @@ public class VisionModule {
|
||||
saveAndBroadcastAll();
|
||||
}
|
||||
|
||||
public void removeCalibrationFromConfig(Size unrotatedImageSize) {
|
||||
if (unrotatedImageSize != null) {
|
||||
visionSource.getSettables().removeCalibration(unrotatedImageSize);
|
||||
} else {
|
||||
logger.error("Got null size?");
|
||||
}
|
||||
|
||||
saveAndBroadcastAll();
|
||||
}
|
||||
|
||||
/**
|
||||
* Add/remove quirks from the camera we're controlling
|
||||
*
|
||||
|
||||
@@ -31,6 +31,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.networktables.NetworkTablesManager;
|
||||
import org.photonvision.common.dataflow.websocket.UICameraConfiguration;
|
||||
import org.photonvision.common.dataflow.websocket.UIPhotonConfiguration;
|
||||
import org.photonvision.common.hardware.Platform;
|
||||
@@ -311,16 +312,118 @@ public class VisionSourceManager {
|
||||
.forEach(cameraInfos::add);
|
||||
}
|
||||
|
||||
// FileVisionSources are a bit quirky. They aren't enumerated by the above, but i still want my
|
||||
// FileVisionSources are a bit quirky. They aren't enumerated by the above, but I still want my
|
||||
// UI to look like it ought to work
|
||||
vmm.getModules().stream()
|
||||
.map(it -> it.getCameraConfiguration().matchedCameraInfo)
|
||||
.filter(info -> info instanceof PVCameraInfo.PVFileCameraInfo)
|
||||
.forEach(cameraInfos::add);
|
||||
|
||||
checkMismatches(cameraInfos);
|
||||
|
||||
return cameraInfos;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check for mismatches between connected cameras and saved camera configurations.
|
||||
*
|
||||
* <p>Note that if the information for a camera spontaneously changes without it being
|
||||
* disconnected/unplugged and reconnected/replugged, we may experience unexpected behavior.
|
||||
*
|
||||
* @param cameraInfos List of currently connected camera infos, checked against saved configs
|
||||
*/
|
||||
protected void checkMismatches(List<PVCameraInfo> cameraInfos) {
|
||||
// from the listed physical camera infos, match them to the camera configs and check for
|
||||
// mismatches
|
||||
for (VisionModule module : vmm.getModules()) {
|
||||
PVCameraInfo matchedCameraInfo = module.getCameraConfiguration().matchedCameraInfo;
|
||||
// We use unique paths to determine if the module has a camera in the port. If no unique path
|
||||
// is found that matches the module, it's removed from the mismatched set as a disconnected
|
||||
// camera cannot be mismatched.
|
||||
if (!cameraInfos.stream()
|
||||
.map(PVCameraInfo::uniquePath)
|
||||
.toList()
|
||||
.contains(matchedCameraInfo.uniquePath())) {
|
||||
module.mismatch = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
for (PVCameraInfo info : cameraInfos) {
|
||||
// if the unique path doesn't match, skip cause it's not in the same port
|
||||
if (!matchedCameraInfo.uniquePath().equals(info.uniquePath())) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// If the camera info doesn't match, log an error
|
||||
if (!matchedCameraInfo.equals(info) && !module.mismatch) {
|
||||
logger.error("Camera mismatch error!");
|
||||
logger.error("Camera config mismatch for " + matchedCameraInfo.name());
|
||||
logCameraInfoDiff(matchedCameraInfo, info);
|
||||
module.mismatch = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Set the NetworkTables mismatch alert
|
||||
if (vmm.getModules().stream().anyMatch(m -> m.mismatch)) {
|
||||
NetworkTablesManager.getInstance()
|
||||
.setMismatchAlert(
|
||||
true,
|
||||
"Camera mismatch error! See logs for details. ("
|
||||
+ vmm.getModules().stream()
|
||||
.filter(m -> m.mismatch)
|
||||
.map(m -> m.getCameraConfiguration().nickname)
|
||||
.toList()
|
||||
.toString()
|
||||
.replaceAll("[\\[\\]()]", "")
|
||||
+ " affected)");
|
||||
} else {
|
||||
NetworkTablesManager.getInstance().setMismatchAlert(false, "");
|
||||
}
|
||||
}
|
||||
|
||||
/** Log the differences between two PVCameraInfo objects. */
|
||||
private static void logCameraInfoDiff(PVCameraInfo saved, PVCameraInfo current) {
|
||||
String expected = "Expected: Name: " + saved.name();
|
||||
String actual = "Actual: Name: " + current.name();
|
||||
if (saved instanceof PVCameraInfo.PVCSICameraInfo savedCsi
|
||||
&& current instanceof PVCameraInfo.PVCSICameraInfo currentCsi) {
|
||||
expected += " Base Name: " + savedCsi.baseName;
|
||||
actual += " Base Name: " + currentCsi.baseName;
|
||||
}
|
||||
|
||||
expected += " Type: " + saved.type().toString();
|
||||
actual += " Type: " + current.type().toString();
|
||||
|
||||
if (saved instanceof PVCameraInfo.PVUsbCameraInfo savedUsb
|
||||
&& current instanceof PVCameraInfo.PVUsbCameraInfo currentUsb) {
|
||||
expected +=
|
||||
" Device Number: "
|
||||
+ savedUsb.dev
|
||||
+ " Vendor ID: "
|
||||
+ savedUsb.vendorId
|
||||
+ " Product ID: "
|
||||
+ savedUsb.productId;
|
||||
actual +=
|
||||
" Device Number: "
|
||||
+ currentUsb.dev
|
||||
+ " Vendor ID: "
|
||||
+ currentUsb.vendorId
|
||||
+ " Product ID: "
|
||||
+ currentUsb.productId;
|
||||
}
|
||||
|
||||
expected += " Path: " + saved.path();
|
||||
actual += " Path: " + current.path();
|
||||
expected += " Unique Path: " + saved.uniquePath();
|
||||
actual += " Unique Path: " + current.uniquePath();
|
||||
expected += " Other Paths: " + Arrays.toString(saved.otherPaths());
|
||||
actual += " Other Paths: " + Arrays.toString(current.otherPaths());
|
||||
|
||||
logger.error(expected);
|
||||
logger.error(actual);
|
||||
}
|
||||
|
||||
private static List<PVCameraInfo> filterAllowedDevices(List<PVCameraInfo> allDevices) {
|
||||
Platform platform = Platform.getCurrentPlatform();
|
||||
ArrayList<PVCameraInfo> filteredDevices = new ArrayList<>();
|
||||
|
||||
@@ -19,6 +19,7 @@ package org.photonvision.vision.processes;
|
||||
|
||||
import edu.wpi.first.cscore.VideoMode;
|
||||
import java.util.HashMap;
|
||||
import org.opencv.core.Size;
|
||||
import org.photonvision.common.configuration.CameraConfiguration;
|
||||
import org.photonvision.common.logging.LogGroup;
|
||||
import org.photonvision.common.logging.Logger;
|
||||
@@ -120,6 +121,11 @@ public abstract class VisionSourceSettables {
|
||||
calculateFrameStaticProps();
|
||||
}
|
||||
|
||||
public void removeCalibration(Size unrotatedImageSize) {
|
||||
configuration.removeCalibration(unrotatedImageSize);
|
||||
calculateFrameStaticProps();
|
||||
}
|
||||
|
||||
protected void calculateFrameStaticProps() {
|
||||
var videoMode = getCurrentVideoMode();
|
||||
this.frameStaticProperties =
|
||||
|
||||
@@ -25,6 +25,7 @@ import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Path;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.photonvision.common.util.TestUtils;
|
||||
|
||||
public class NetworkConfigTest {
|
||||
@Test
|
||||
@@ -39,13 +40,13 @@ public class NetworkConfigTest {
|
||||
@Test
|
||||
public void testDeserializeTeamNumberOrNtServerAddress() {
|
||||
{
|
||||
var folder = Path.of("test-resources/network-old-team-number");
|
||||
var folder = TestUtils.getResourcesFolderPath(true).resolve("network-old-team-number");
|
||||
var configMgr = new ConfigManager(folder, new LegacyConfigProvider(folder));
|
||||
configMgr.load();
|
||||
assertEquals("9999", configMgr.getConfig().getNetworkConfig().ntServerAddress);
|
||||
}
|
||||
{
|
||||
var folder = Path.of("test-resources/network-new-team-number");
|
||||
var folder = TestUtils.getResourcesFolderPath(true).resolve("network-new-team-number");
|
||||
var configMgr = new ConfigManager(folder, new LegacyConfigProvider(folder));
|
||||
configMgr.load();
|
||||
assertEquals("9999", configMgr.getConfig().getNetworkConfig().ntServerAddress);
|
||||
|
||||
@@ -34,10 +34,13 @@ public class HardwareConfigTest {
|
||||
System.out.println("Loading Hardware configs...");
|
||||
var config =
|
||||
new ObjectMapper().readValue(TestUtils.getHardwareConfigJson(), HardwareConfig.class);
|
||||
assertEquals(config.deviceName(), "PhotonVision");
|
||||
assertEquals(config.deviceLogoPath(), "photonvision.png");
|
||||
assertEquals(config.supportURL(), "https://support.photonvision.com");
|
||||
assertArrayEquals(config.ledPins().stream().mapToInt(i -> i).toArray(), new int[] {2, 13});
|
||||
assertEquals(config.deviceName, "PhotonVision");
|
||||
assertEquals(config.deviceLogoPath, "photonvision.png");
|
||||
assertEquals(config.supportURL, "https://support.photonvision.com");
|
||||
// Ensure defaults are not null
|
||||
assertEquals(config.cpuThrottleReasonCmd, "");
|
||||
assertEquals(config.diskUsageCommand, "");
|
||||
assertArrayEquals(config.ledPins.stream().mapToInt(i -> i).toArray(), new int[] {2, 13});
|
||||
CustomGPIO.setConfig(config);
|
||||
|
||||
} catch (IOException e) {
|
||||
|
||||
@@ -20,7 +20,6 @@ package org.photonvision.hardware;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
import java.io.IOException;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.photonvision.common.hardware.GPIO.CustomGPIO;
|
||||
import org.photonvision.common.hardware.GPIO.GPIOBase;
|
||||
@@ -28,17 +27,11 @@ import org.photonvision.common.hardware.GPIO.pi.PigpioPin;
|
||||
import org.photonvision.common.hardware.Platform;
|
||||
import org.photonvision.common.hardware.metrics.MetricsManager;
|
||||
import org.photonvision.common.util.TestUtils;
|
||||
import org.photonvision.jni.PhotonTargetingJniLoader;
|
||||
|
||||
public class HardwareTest {
|
||||
@Test
|
||||
public void testHardware() {
|
||||
try {
|
||||
TestUtils.loadLibraries();
|
||||
PhotonTargetingJniLoader.load();
|
||||
} catch (UnsatisfiedLinkError | IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
TestUtils.loadLibraries();
|
||||
MetricsManager mm = new MetricsManager();
|
||||
|
||||
if (!Platform.isRaspberryPi()) return;
|
||||
|
||||
@@ -41,24 +41,20 @@ import org.junitpioneer.jupiter.cartesian.CartesianTest.Values;
|
||||
import org.photonvision.common.configuration.ConfigManager;
|
||||
import org.photonvision.common.dataflow.networktables.NetworkTablesManager;
|
||||
import org.photonvision.common.util.TestUtils;
|
||||
import org.photonvision.jni.PhotonTargetingJniLoader;
|
||||
import org.photonvision.jni.WpilibLoader;
|
||||
import org.photonvision.jni.LibraryLoader;
|
||||
import org.photonvision.vision.frame.provider.FileFrameProvider;
|
||||
|
||||
public class FileSaveFrameConsumerTest {
|
||||
NetworkTableInstance inst = null;
|
||||
|
||||
@BeforeAll
|
||||
public static void init() throws UnsatisfiedLinkError, IOException {
|
||||
if (!WpilibLoader.loadLibraries()) {
|
||||
public static void init() throws IOException {
|
||||
if (!LibraryLoader.loadWpiLibraries()) {
|
||||
fail();
|
||||
}
|
||||
|
||||
try {
|
||||
if (!PhotonTargetingJniLoader.load()) fail();
|
||||
} catch (UnsatisfiedLinkError | IOException e) {
|
||||
e.printStackTrace();
|
||||
fail(e);
|
||||
if (!LibraryLoader.loadTargeting()) {
|
||||
fail();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -109,7 +109,6 @@ public class AprilTagTest {
|
||||
pipeline.getSettings().solvePNPEnabled = true;
|
||||
pipeline.getSettings().cornerDetectionAccuracyPercentage = 4;
|
||||
pipeline.getSettings().cornerDetectionUseConvexHulls = true;
|
||||
pipeline.getSettings().targetModel = TargetModel.kAprilTag6p5in_36h11;
|
||||
pipeline.getSettings().tagFamily = AprilTagFamily.kTag16h5;
|
||||
|
||||
var frameProvider =
|
||||
|
||||
@@ -22,7 +22,6 @@ import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import static org.junit.jupiter.api.Assertions.fail;
|
||||
|
||||
import edu.wpi.first.cscore.VideoMode;
|
||||
import java.io.IOException;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
@@ -32,7 +31,7 @@ import org.photonvision.common.configuration.CameraConfiguration;
|
||||
import org.photonvision.common.configuration.ConfigManager;
|
||||
import org.photonvision.common.dataflow.CVPipelineResultConsumer;
|
||||
import org.photonvision.common.util.TestUtils;
|
||||
import org.photonvision.jni.PhotonTargetingJniLoader;
|
||||
import org.photonvision.jni.LibraryLoader;
|
||||
import org.photonvision.vision.camera.PVCameraInfo;
|
||||
import org.photonvision.vision.camera.QuirkyCamera;
|
||||
import org.photonvision.vision.camera.USBCameras.USBCameraSource;
|
||||
@@ -48,12 +47,7 @@ public class VisionModuleManagerTest {
|
||||
System.out.print(classpathStr);
|
||||
|
||||
TestUtils.loadLibraries();
|
||||
try {
|
||||
if (!PhotonTargetingJniLoader.load()) fail();
|
||||
} catch (UnsatisfiedLinkError | IOException e) {
|
||||
e.printStackTrace();
|
||||
fail(e);
|
||||
}
|
||||
if (!LibraryLoader.loadTargeting()) fail();
|
||||
}
|
||||
|
||||
private static class TestSource extends VisionSource {
|
||||
|
||||
@@ -17,8 +17,8 @@
|
||||
|
||||
package org.photonvision.vision.processes;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
import edu.wpi.first.cscore.UsbCameraInfo;
|
||||
@@ -33,7 +33,6 @@ import org.photonvision.common.configuration.CameraConfiguration;
|
||||
import org.photonvision.common.configuration.ConfigManager;
|
||||
import org.photonvision.common.util.TestUtils;
|
||||
import org.photonvision.common.util.file.JacksonUtils;
|
||||
import org.photonvision.jni.PhotonTargetingJniLoader;
|
||||
import org.photonvision.vision.camera.PVCameraInfo;
|
||||
|
||||
public class VisionSourceManagerTest {
|
||||
@@ -59,9 +58,7 @@ public class VisionSourceManagerTest {
|
||||
|
||||
@BeforeAll
|
||||
public static void loadLibraries() {
|
||||
TestUtils.loadLibraries();
|
||||
assertDoesNotThrow(PhotonTargetingJniLoader::load);
|
||||
assertTrue(PhotonTargetingJniLoader.isWorking);
|
||||
assertTrue(TestUtils.loadLibraries());
|
||||
|
||||
// Broadcast all still calls into configmanager (ew) so set that up here
|
||||
ConfigManager.getInstance().load();
|
||||
@@ -277,4 +274,55 @@ public class VisionSourceManagerTest {
|
||||
assertEquals(2, vsm.getVsmState().disabledConfigs.size());
|
||||
assertEquals(1, vsm.vmm.getModules().size());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testMismatch() throws InterruptedException {
|
||||
var vsm = new TestVsm();
|
||||
|
||||
// Create a saved camera configuration that expects a device at /dev/video0 with a name
|
||||
PVCameraInfo savedInfo =
|
||||
PVCameraInfo.fromUsbCameraInfo(
|
||||
new UsbCameraInfo(
|
||||
0, "/dev/video0", "CamA", new String[] {"/dev/v4l/by-path/1"}, 111, 222));
|
||||
CameraConfiguration savedConf = new CameraConfiguration(savedInfo);
|
||||
savedConf.deactivated = false;
|
||||
savedConf.nickname = "SavedCam";
|
||||
|
||||
// Register the saved config so VSM creates a VisionModule
|
||||
vsm.registerLoadedConfigs(List.of(savedConf));
|
||||
|
||||
// Now simulate a connected camera at same uniquePath but with a different name (mismatch)
|
||||
List<PVCameraInfo> currentInfo =
|
||||
List.of(
|
||||
PVCameraInfo.fromUsbCameraInfo(
|
||||
new UsbCameraInfo(
|
||||
0,
|
||||
"/dev/video0",
|
||||
"CamDifferent",
|
||||
new String[] {"/dev/v4l/by-path/1"},
|
||||
111,
|
||||
222)));
|
||||
|
||||
// Trigger state evaluation
|
||||
vsm.checkMismatches(currentInfo);
|
||||
|
||||
// The module should have detected a mismatch
|
||||
assertTrue(vsm.getVisionModules().stream().anyMatch(m -> m.mismatch));
|
||||
|
||||
// Now simulate the device being disconnected
|
||||
currentInfo = List.of();
|
||||
vsm.checkMismatches(currentInfo);
|
||||
|
||||
// Mismatch should be cleared when device is disconnected
|
||||
assertFalse(vsm.getVisionModules().stream().anyMatch(m -> m.mismatch));
|
||||
|
||||
// Test with a matching camera info
|
||||
currentInfo = List.of(savedInfo);
|
||||
vsm.checkMismatches(currentInfo);
|
||||
|
||||
// The mismatch should be cleared
|
||||
assertFalse(vsm.getVisionModules().stream().anyMatch(m -> m.mismatch));
|
||||
|
||||
vsm.teardown();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,13 +39,10 @@ doxygen {
|
||||
}
|
||||
}
|
||||
|
||||
doxygen {
|
||||
option 'generate_html', true
|
||||
option 'html_extra_stylesheet', 'theme.css'
|
||||
|
||||
doxygen.sourceSets.main {
|
||||
cppProjectZips.each {
|
||||
dependsOn it
|
||||
source it.source
|
||||
doxygenDox.dependsOn it
|
||||
sources it.source
|
||||
it.ext.includeDirs.each {
|
||||
cppIncludeRoots.add(it.absolutePath)
|
||||
}
|
||||
@@ -100,7 +97,7 @@ tasks.register("zipCppDocs", Zip) {
|
||||
// Java
|
||||
configurations {
|
||||
javaSource {
|
||||
transitive false
|
||||
transitive = false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -158,22 +155,22 @@ publishing {
|
||||
artifact zipJavaDocs
|
||||
|
||||
artifactId = "${baseArtifactIdJava}"
|
||||
groupId artifactGroupIdJava
|
||||
version pubVersion
|
||||
groupId = artifactGroupIdJava
|
||||
version = pubVersion
|
||||
}
|
||||
cpp(MavenPublication) {
|
||||
artifact zipCppDocs
|
||||
|
||||
artifactId = "${baseArtifactIdCpp}"
|
||||
groupId artifactGroupIdCpp
|
||||
version pubVersion
|
||||
groupId = artifactGroupIdCpp
|
||||
version = pubVersion
|
||||
}
|
||||
}
|
||||
|
||||
repositories {
|
||||
maven {
|
||||
// Just throw everything into build/maven
|
||||
url(localMavenURL)
|
||||
url = localMavenURL
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -282,7 +282,7 @@ if (!project.hasProperty('copyOfflineArtifacts')) {
|
||||
|
||||
artifactId = "${nativeName}-json"
|
||||
groupId = "org.photonvision"
|
||||
version "1.0"
|
||||
version = "1.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -344,8 +344,8 @@ publishing {
|
||||
artifact combinedHeadersZip
|
||||
|
||||
artifactId = "${nativeName}-combinedcpp"
|
||||
groupId artifactGroupId
|
||||
version pubVersion
|
||||
groupId = artifactGroupId
|
||||
version = pubVersion
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -363,6 +363,9 @@ def nativeTasks = wpilibTools.createExtractionTasks {
|
||||
|
||||
nativeTasks.addToSourceSetResources(sourceSets.test)
|
||||
|
||||
dependencies {
|
||||
wpilibNatives project(":photon-targeting")
|
||||
}
|
||||
nativeConfig.dependencies.add wpilibTools.deps.wpilib("wpimath")
|
||||
nativeConfig.dependencies.add wpilibTools.deps.wpilib("wpinet")
|
||||
nativeConfig.dependencies.add wpilibTools.deps.wpilib("wpiutil")
|
||||
|
||||
20
photon-lib/py/docs/Makefile
Normal file
20
photon-lib/py/docs/Makefile
Normal file
@@ -0,0 +1,20 @@
|
||||
# Minimal makefile for Sphinx documentation
|
||||
#
|
||||
|
||||
# You can set these variables from the command line, and also
|
||||
# from the environment for the first two.
|
||||
SPHINXOPTS ?=
|
||||
SPHINXBUILD ?= sphinx-build
|
||||
SOURCEDIR = source
|
||||
BUILDDIR = build
|
||||
|
||||
# Put it first so that "make" without argument is like "make help".
|
||||
help:
|
||||
@$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
|
||||
|
||||
.PHONY: help Makefile
|
||||
|
||||
# Catch-all target: route all unknown targets to Sphinx using the new
|
||||
# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS).
|
||||
%: Makefile
|
||||
@$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
|
||||
5
photon-lib/py/docs/_stubs/wpimath/__init__.py
Normal file
5
photon-lib/py/docs/_stubs/wpimath/__init__.py
Normal file
@@ -0,0 +1,5 @@
|
||||
# Minimal wpimath stub for Sphinx docs build
|
||||
from . import geometry
|
||||
from . import units
|
||||
|
||||
__all__ = ["geometry", "units"]
|
||||
4
photon-lib/py/docs/_stubs/wpimath/_init__wpimath.py
Normal file
4
photon-lib/py/docs/_stubs/wpimath/_init__wpimath.py
Normal file
@@ -0,0 +1,4 @@
|
||||
# Stub module to match wpimath compiled module names
|
||||
# This file exists so imports like `wpimath._init__wpimath` succeed during docs build.
|
||||
|
||||
# no-op
|
||||
130
photon-lib/py/docs/_stubs/wpimath/geometry.py
Normal file
130
photon-lib/py/docs/_stubs/wpimath/geometry.py
Normal file
@@ -0,0 +1,130 @@
|
||||
# Minimal geometry stubs for Sphinx documentation
|
||||
|
||||
class Rotation3d:
|
||||
def __init__(self, roll=0.0, pitch=0.0, yaw=0.0):
|
||||
# store yaw as the primary rotation for simple stubs
|
||||
self.roll = roll
|
||||
self.pitch = pitch
|
||||
self.yaw = yaw
|
||||
|
||||
def toRotation2d(self):
|
||||
# convert yaw to a Rotation2d for simple compatibility in docs build
|
||||
return Rotation2d(self.yaw)
|
||||
|
||||
class Translation3d:
|
||||
def __init__(self, x=0.0, y=0.0, z=0.0):
|
||||
# Support both (x, y, z) and (distance, Rotation3d) forms used by the real wpimath
|
||||
# If y is a Rotation3d, compute a point at 'distance' along its yaw/pitch
|
||||
try:
|
||||
from math import cos, sin
|
||||
except Exception:
|
||||
def cos(x):
|
||||
return x
|
||||
def sin(x):
|
||||
return x
|
||||
|
||||
if hasattr(y, "yaw") and hasattr(y, "pitch"):
|
||||
# interpret constructor as Translation3d(distance, Rotation3d)
|
||||
distance = float(x)
|
||||
pitch = float(getattr(y, "pitch", 0.0))
|
||||
yaw = float(getattr(y, "yaw", 0.0))
|
||||
# approximate spherical -> cartesian
|
||||
self._x = distance * cos(pitch) * cos(yaw)
|
||||
self._y = distance * cos(pitch) * sin(yaw)
|
||||
self._z = distance * sin(pitch)
|
||||
else:
|
||||
self._x = float(x)
|
||||
self._y = float(y)
|
||||
self._z = float(z)
|
||||
|
||||
def X(self):
|
||||
return self._x
|
||||
|
||||
def Y(self):
|
||||
return self._y
|
||||
|
||||
def Z(self):
|
||||
return self._z
|
||||
|
||||
class Pose3d:
|
||||
def __init__(self, *args, **kwargs):
|
||||
pass
|
||||
|
||||
class Rotation2d:
|
||||
def __init__(self, *args):
|
||||
# Accept several initialization forms used in the real wpimath Rotation2d
|
||||
# - Rotation2d(angle)
|
||||
# - Rotation2d(fx, xOffset) used by SimCameraProperties.getPixelYaw
|
||||
if len(args) == 0:
|
||||
self._angle = 0.0
|
||||
elif len(args) == 1:
|
||||
self._angle = float(args[0])
|
||||
else:
|
||||
# fallback: when called with fx, xOffset, approximate angle as 0.0
|
||||
self._angle = 0.0
|
||||
|
||||
def degrees(self):
|
||||
from math import degrees
|
||||
|
||||
return degrees(self._angle)
|
||||
|
||||
def radians(self):
|
||||
return float(self._angle)
|
||||
|
||||
def __add__(self, other):
|
||||
# allow Rotation2d + Rotation2d or Rotation2d + numeric
|
||||
if hasattr(other, "_angle"):
|
||||
return Rotation2d(self._angle + float(other._angle))
|
||||
try:
|
||||
return Rotation2d(self._angle + float(other))
|
||||
except Exception:
|
||||
return NotImplemented
|
||||
|
||||
def __radd__(self, other):
|
||||
# numeric + Rotation2d
|
||||
return self.__add__(other)
|
||||
|
||||
def __sub__(self, other):
|
||||
if hasattr(other, "_angle"):
|
||||
return Rotation2d(self._angle - float(other._angle))
|
||||
try:
|
||||
return Rotation2d(self._angle - float(other))
|
||||
except Exception:
|
||||
return NotImplemented
|
||||
|
||||
def __neg__(self):
|
||||
return Rotation2d(-self._angle)
|
||||
|
||||
def __repr__(self):
|
||||
return f"Rotation2d({self._angle})"
|
||||
|
||||
class Translation2d:
|
||||
def __init__(self, x=0.0, y=0.0):
|
||||
self._x = float(x)
|
||||
self._y = float(y)
|
||||
|
||||
def X(self):
|
||||
return self._x
|
||||
|
||||
def Y(self):
|
||||
return self._y
|
||||
|
||||
class Pose2d:
|
||||
def __init__(self, *args, **kwargs):
|
||||
pass
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return "Pose2d()"
|
||||
|
||||
|
||||
class Transform3d:
|
||||
def __init__(self, *args, **kwargs):
|
||||
pass
|
||||
|
||||
|
||||
class Quaternion:
|
||||
def __init__(self, *args, **kwargs):
|
||||
pass
|
||||
|
||||
# Expose names commonly used by photonlibpy
|
||||
__all__ = ["Rotation3d", "Translation3d", "Pose3d", "Rotation2d", "Translation2d", "Pose2d"]
|
||||
12
photon-lib/py/docs/_stubs/wpimath/interpolation.py
Normal file
12
photon-lib/py/docs/_stubs/wpimath/interpolation.py
Normal file
@@ -0,0 +1,12 @@
|
||||
# Minimal interpolation stub for docs
|
||||
class TimeInterpolatableRotation2dBuffer:
|
||||
def __init__(self, *args, **kwargs):
|
||||
pass
|
||||
|
||||
def addSample(self, *args, **kwargs):
|
||||
pass
|
||||
|
||||
def sample(self, *args, **kwargs):
|
||||
return None
|
||||
|
||||
__all__ = ["TimeInterpolatableRotation2dBuffer"]
|
||||
@@ -0,0 +1,3 @@
|
||||
from ._interpolation import *
|
||||
|
||||
__all__ = ["TimeInterpolatableRotation2dBuffer"]
|
||||
@@ -0,0 +1,25 @@
|
||||
# Minimal interpolation submodule stub for docs
|
||||
class TimeInterpolatableRotation2dBuffer:
|
||||
def __init__(self, *args, **kwargs):
|
||||
pass
|
||||
|
||||
def addSample(self, *args, **kwargs):
|
||||
pass
|
||||
|
||||
def sample(self, *args, **kwargs):
|
||||
return None
|
||||
|
||||
|
||||
class TimeInterpolatablePose3dBuffer:
|
||||
def __init__(self, *args, **kwargs):
|
||||
# buffer of Pose3d-like objects for docs import
|
||||
pass
|
||||
|
||||
def addSample(self, *args, **kwargs):
|
||||
pass
|
||||
|
||||
def sample(self, *args, **kwargs):
|
||||
return None
|
||||
|
||||
|
||||
__all__ = ["TimeInterpolatableRotation2dBuffer", "TimeInterpolatablePose3dBuffer"]
|
||||
31
photon-lib/py/docs/_stubs/wpimath/units.py
Normal file
31
photon-lib/py/docs/_stubs/wpimath/units.py
Normal file
@@ -0,0 +1,31 @@
|
||||
"""Minimal wpimath.units stub for documentation builds."""
|
||||
|
||||
def degreesToRadians(deg: float) -> float:
|
||||
from math import pi
|
||||
|
||||
return deg * (pi / 180.0)
|
||||
|
||||
|
||||
# Represent seconds as a float alias for annotations
|
||||
seconds = float
|
||||
|
||||
__all__ = ["degreesToRadians", "seconds"]
|
||||
|
||||
# Common unit aliases used in type annotations in WPILib stubs
|
||||
meters = float
|
||||
meters_per_second = float
|
||||
meters_per_second_squared = float
|
||||
kilograms = float
|
||||
kilogram_square_meters = float
|
||||
|
||||
__all__.extend([
|
||||
"meters",
|
||||
"meters_per_second",
|
||||
"meters_per_second_squared",
|
||||
"kilograms",
|
||||
"kilogram_square_meters",
|
||||
"hertz",
|
||||
])
|
||||
|
||||
# frequency
|
||||
hertz = float
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user