Merge branch 'main' into py-docs

This commit is contained in:
Sam Freund
2025-10-23 16:14:46 -05:00
committed by GitHub
223 changed files with 12559 additions and 11613 deletions

15
.github/labeler.yml vendored Normal file
View File

@@ -0,0 +1,15 @@
"backend":
- changed-files:
- any-glob-to-any-file: [photon-core/**, photon-server/**]
"documentation":
- changed-files:
- any-glob-to-any-file: [docs/**, photon-docs/**]
"frontend":
- changed-files:
- any-glob-to-any-file: photon-client/**
"photonlib":
- changed-files:
- any-glob-to-any-file: photon-lib*/**
"website":
- changed-files:
- any-glob-to-any-file: website/**

View File

@@ -13,6 +13,6 @@ Merge checklist:
- [ ] The description documents the _what_ and _why_
- [ ] 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 v2024.3.1
- [ ] If this PR touches configuration, this is backwards compatible with settings back to v2025.3.2
- [ ] If this PR touches pipeline settings or anything related to data exchange, the frontend typing is updated
- [ ] If this PR addresses a bug, a regression test for it is added

View File

@@ -3,38 +3,23 @@ name: Build
on:
# Run on pushes to main and pushed tags, and on pull requests against main, but ignore the docs folder
push:
branches: [ main ]
tags:
- 'v*'
pull_request:
branches: [ main ]
merge_group:
concurrency:
group: ${{ github.workflow }}-${{ github.head_ref || github.ref }}
cancel-in-progress: true
env:
IMAGE_VERSION: v2026.0.4
jobs:
build-client:
name: "PhotonClient Build"
defaults:
run:
working-directory: photon-client
runs-on: ubuntu-22.04
validation:
name: "Validation"
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 22
- name: Install Dependencies
run: npm ci
- name: Build Production Client
run: npm run build
- uses: actions/upload-artifact@v4
with:
name: built-client
path: photon-client/dist/
- uses: gradle/actions/wrapper-validation@v4
build-examples:
strategy:
@@ -49,6 +34,7 @@ jobs:
name: "Photonlib - Build Examples - ${{ matrix.os }}"
runs-on: ${{ matrix.os }}
needs: [validation]
steps:
- name: Checkout code
@@ -76,6 +62,7 @@ jobs:
build-gradle:
name: "Gradle Build"
runs-on: ubuntu-22.04
needs: [validation]
steps:
# Checkout code.
- name: Checkout code
@@ -89,22 +76,22 @@ jobs:
with:
java-version: 17
distribution: temurin
- name: Install pnpm
uses: pnpm/action-setup@v4
with:
version: 10
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 22
- name: Install mrcal deps
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 -i --stacktrace
run: ./gradlew testHeadless --stacktrace
- name: Gradle Coverage
run: ./gradlew jacocoTestReport
- name: Publish Coverage Report
uses: codecov/codecov-action@v4
with:
file: ./photon-server/build/reports/jacoco/test/jacocoTestReport.xml
- name: Publish Core Coverage Report
uses: codecov/codecov-action@v4
with:
file: ./photon-core/build/reports/jacoco/test/jacocoTestReport.xml
build-offline-docs:
name: "Build Offline Docs"
runs-on: ubuntu-22.04
@@ -135,6 +122,7 @@ jobs:
build-photonlib-vendorjson:
name: "Build Vendor JSON"
runs-on: ubuntu-22.04
needs: [validation]
steps:
- uses: actions/checkout@v4
with:
@@ -179,6 +167,7 @@ jobs:
name: "Photonlib - Build Host - ${{ matrix.artifact-name }}"
runs-on: ${{ matrix.os }}
needs: [validation]
steps:
- uses: actions/checkout@v4
with:
@@ -190,7 +179,7 @@ jobs:
distribution: temurin
architecture: ${{ matrix.architecture }}
- run: git fetch --tags --force
- run: ./gradlew photon-targeting:build photon-lib:build -i
- run: ./gradlew photon-targeting:build photon-lib:build
name: Build with Gradle
- run: ./gradlew photon-lib:publish photon-targeting:publish
name: Publish
@@ -222,6 +211,7 @@ jobs:
runs-on: ubuntu-22.04
container: ${{ matrix.container }}
name: "Photonlib - Build Docker - ${{ matrix.artifact-name }}"
needs: [validation]
steps:
- uses: actions/checkout@v4
with:
@@ -231,7 +221,7 @@ jobs:
git config --global --add safe.directory /__w/photonvision/photonvision
- name: Build PhotonLib
# We don't need to run tests, since we specify only non-native platforms
run: ./gradlew photon-targeting:build photon-lib:build ${{ matrix.build-options }} -i -x test
run: ./gradlew photon-targeting:build photon-lib:build ${{ matrix.build-options }} -x test
- name: Publish
run: ./gradlew photon-lib:publish photon-targeting:publish ${{ matrix.build-options }}
env:
@@ -270,7 +260,7 @@ jobs:
path: output/*.zip
build-package:
needs: [build-client, build-gradle, build-offline-docs]
needs: [build-gradle, build-offline-docs]
strategy:
fail-fast: false
@@ -310,21 +300,19 @@ jobs:
java-version: 17
distribution: temurin
architecture: ${{ matrix.architecture }}
- name: Install pnpm
uses: pnpm/action-setup@v4
with:
version: 10
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 22
cache: pnpm
cache-dependency-path: photon-client/pnpm-lock.yaml
- name: Install Arm64 Toolchain
run: ./gradlew installArm64Toolchain
if: ${{ (matrix.artifact-name) == 'LinuxArm64' }}
- run: |
rm -rf photon-server/src/main/resources/web/*
mkdir -p photon-server/src/main/resources/web/docs
if: ${{ (matrix.os) != 'windows-latest' }}
- run: |
del photon-server\src\main\resources\web\*.*
mkdir photon-server\src\main\resources\web\docs
if: ${{ (matrix.os) == 'windows-latest' }}
- uses: actions/download-artifact@v4
with:
name: built-client
path: photon-server/src/main/resources/web/
- uses: actions/download-artifact@v4
with:
name: built-docs
@@ -374,7 +362,7 @@ jobs:
- run: |
sudo apt-get update
sudo apt-get install --yes libcholmod3 liblapack3 libsuitesparseconfig5
if: ${{ (matrix.os) == 'ubuntu-22.04' }}
if: ${{ (matrix.os) == 'ubuntu-24.04' }}
# and actually run the jar
- run: java -jar ${{ matrix.extraOpts }} *.jar --smoketest
if: ${{ (matrix.os) != 'windows-latest' }}
@@ -388,10 +376,10 @@ jobs:
fail-fast: false
matrix:
include:
- os: ubuntu-22.04
- os: ubuntu-24.04
artifact-name: LinuxArm64
image_suffix: RaspberryPi
image_url: https://github.com/PhotonVision/photon-image-modifier/releases/download/v2025.0.3/photonvision_raspi.img.xz
image_url: https://github.com/PhotonVision/photon-image-modifier/releases/download/$IMAGE_VERSION/photonvision_raspi.img.xz
cpu: cortex-a7
image_additional_mb: 0
extraOpts: -Djdk.lang.Process.launchMechanism=vfork
@@ -427,69 +415,81 @@ jobs:
fail-fast: false
matrix:
include:
- os: ubuntu-22.04
- os: ubuntu-24.04
artifact-name: LinuxArm64
image_suffix: RaspberryPi
image_url: https://github.com/PhotonVision/photon-image-modifier/releases/download/v2025.0.3/photonvision_raspi.img.xz
image_url: https://github.com/PhotonVision/photon-image-modifier/releases/download/$IMAGE_VERSION/photonvision_raspi.img.xz
cpu: cortex-a7
image_additional_mb: 0
- os: ubuntu-22.04
- os: ubuntu-24.04
artifact-name: LinuxArm64
image_suffix: limelight2
image_url: https://github.com/PhotonVision/photon-image-modifier/releases/download/v2025.0.3/photonvision_limelight.img.xz
image_url: https://github.com/PhotonVision/photon-image-modifier/releases/download/$IMAGE_VERSION/photonvision_limelight.img.xz
cpu: cortex-a7
image_additional_mb: 0
- os: ubuntu-22.04
- os: ubuntu-24.04
artifact-name: LinuxArm64
image_suffix: limelight3
image_url: https://github.com/PhotonVision/photon-image-modifier/releases/download/v2025.0.3/photonvision_limelight3.img.xz
image_url: https://github.com/PhotonVision/photon-image-modifier/releases/download/$IMAGE_VERSION/photonvision_limelight3.img.xz
cpu: cortex-a7
image_additional_mb: 0
- os: ubuntu-22.04
- os: ubuntu-24.04
artifact-name: LinuxArm64
image_suffix: limelight3G
image_url: https://github.com/PhotonVision/photon-image-modifier/releases/download/v2025.0.3/photonvision_limelight3g.img.xz
image_url: https://github.com/PhotonVision/photon-image-modifier/releases/download/$IMAGE_VERSION/photonvision_limelight3g.img.xz
cpu: cortex-a7
image_additional_mb: 0
- os: ubuntu-22.04
- os: ubuntu-24.04
artifact-name: LinuxArm64
image_suffix: limelight4
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
image_url: https://github.com/PhotonVision/photon-image-modifier/releases/download/v2025.0.3/photonvision_opi5.img.xz
image_url: https://github.com/PhotonVision/photon-image-modifier/releases/download/$IMAGE_VERSION/photonvision_opi5.img.xz
cpu: cortex-a8
image_additional_mb: 1024
- os: ubuntu-22.04
- os: ubuntu-24.04
artifact-name: LinuxArm64
image_suffix: orangepi5b
image_url: https://github.com/PhotonVision/photon-image-modifier/releases/download/v2025.0.3/photonvision_opi5b.img.xz
image_url: https://github.com/PhotonVision/photon-image-modifier/releases/download/$IMAGE_VERSION/photonvision_opi5b.img.xz
cpu: cortex-a8
image_additional_mb: 1024
- os: ubuntu-22.04
- os: ubuntu-24.04
artifact-name: LinuxArm64
image_suffix: orangepi5plus
image_url: https://github.com/PhotonVision/photon-image-modifier/releases/download/v2025.0.3/photonvision_opi5plus.img.xz
image_url: https://github.com/PhotonVision/photon-image-modifier/releases/download/$IMAGE_VERSION/photonvision_opi5plus.img.xz
cpu: cortex-a8
image_additional_mb: 1024
- os: ubuntu-22.04
- os: ubuntu-24.04
artifact-name: LinuxArm64
image_suffix: orangepi5pro
image_url: https://github.com/PhotonVision/photon-image-modifier/releases/download/v2025.0.3/photonvision_opi5pro.img.xz
image_url: https://github.com/PhotonVision/photon-image-modifier/releases/download/$IMAGE_VERSION/photonvision_opi5pro.img.xz
cpu: cortex-a8
image_additional_mb: 1024
- os: ubuntu-22.04
- os: ubuntu-24.04
artifact-name: LinuxArm64
image_suffix: orangepi5max
image_url: https://github.com/PhotonVision/photon-image-modifier/releases/download/v2025.0.3/photonvision_opi5max.img.xz
image_url: https://github.com/PhotonVision/photon-image-modifier/releases/download/$IMAGE_VERSION/photonvision_opi5max.img.xz
cpu: cortex-a8
image_additional_mb: 1024
- os: ubuntu-22.04
- os: ubuntu-24.04
artifact-name: LinuxArm64
image_suffix: rock5c
image_url: https://github.com/PhotonVision/photon-image-modifier/releases/download/v2025.0.3/photonvision_rock5c.img.xz
image_url: https://github.com/PhotonVision/photon-image-modifier/releases/download/$IMAGE_VERSION/photonvision_rock5c.img.xz
cpu: cortex-a8
image_additional_mb: 1024
runs-on: ${{ matrix.os }}
name: "Build image - ${{ matrix.image_url }}"
name: "Build image - ${{ matrix.image_suffix }}"
steps:
- name: Checkout code
@@ -523,8 +523,40 @@ jobs:
with:
name: image-${{ matrix.image_suffix }}
path: photonvision*.xz
build-rubik-image:
needs: [build-package]
if: ${{ github.event_name != 'pull_request' }}
runs-on: ubuntu-24.04
name: "Build image - Rubik Pi 3"
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: actions/download-artifact@v4
with:
name: jar-LinuxArm64
- name: Generate image
run: |
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:
name: image-rubikpi3
path: photonvision*.xz
release:
needs: [build-package, build-image, combine]
needs: [build-photonlib-vendorjson, build-package, build-image, build-rubik-image, combine]
runs-on: ubuntu-22.04
steps:
# Download all fat JARs
@@ -550,11 +582,12 @@ jobs:
- run: find
# Push to dev release
- uses: pyTooling/Actions/releaser@r0
- uses: pyTooling/Actions/releaser@r6
with:
token: ${{ secrets.GITHUB_TOKEN }}
tag: 'Dev'
rm: true
snapshots: false
files: |
**/*.xz
**/*linux*.jar
@@ -562,38 +595,12 @@ jobs:
**/photonlib*.json
**/photonlib*.zip
if: github.event_name == 'push'
# Upload all jars and xz archives
# Split into two uploads to work around max size limits in action-gh-releases
# https://github.com/softprops/action-gh-release/issues/353
- uses: softprops/action-gh-release@v2.0.9
with:
files: |
**/@(*orangepi5*|*rock5*).xz
if: startsWith(github.ref, 'refs/tags/v')
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- uses: softprops/action-gh-release@v2.0.9
with:
files: |
**/!(*orangepi5*|*rock5*).xz
**/*.jar
**/photonlib*.json
**/photonlib*.zip
if: startsWith(github.ref, 'refs/tags/v')
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
dispatch:
name: dispatch
needs: [build-photonlib-vendorjson, release]
runs-on: ubuntu-22.04
steps:
- uses: peter-evans/repository-dispatch@v3
if: |
github.repository == 'PhotonVision/photonvision' &&
startsWith(github.ref, 'refs/tags/v')
- name: Create Vendor JSON Repo PR
uses: wpilibsuite/vendor-json-repo/.github/actions/add_vendordep@main
with:
repo: PhotonVision/vendor-json-repo
token: ${{ secrets.VENDOR_JSON_REPO_PUSH_TOKEN }}
repository: PhotonVision/vendor-json-repo
event-type: tag
client-payload: '{"run_id": "${{ github.run_id }}", "package_version": "${{ github.ref_name }}"}'
vendordep_file: ${{ github.workspace }}/photonlib-${{ github.ref_name }}.json
pr_title: Update photonlib to ${{ github.ref_name }}
pr_branch: photonlib-${{ github.ref_name }}
if: github.repository == 'PhotonVision/photonvision' && startsWith(github.ref, 'refs/tags/v')

14
.github/workflows/labeler.yml vendored Normal file
View File

@@ -0,0 +1,14 @@
name: "Pull Request Labeler"
on:
- pull_request_target
jobs:
labeler:
permissions:
contents: read
pull-requests: write
runs-on: ubuntu-latest
steps:
- uses: actions/labeler@v5
with:
sync-labels: true

View File

@@ -3,18 +3,19 @@ name: Lint and Format
on:
# Run on pushes to main and pushed tags, and on pull requests against main, but ignore the docs folder
push:
branches: [ main ]
tags:
- 'v*'
pull_request:
branches: [ main ]
merge_group:
concurrency:
group: ${{ github.workflow }}-${{ github.head_ref || github.ref }}
cancel-in-progress: true
jobs:
validation:
name: "Validation"
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: gradle/actions/wrapper-validation@v4
wpiformat:
name: "wpiformat"
runs-on: ubuntu-22.04
@@ -30,7 +31,7 @@ jobs:
with:
python-version: 3.11
- name: Install wpiformat
run: pip3 install wpiformat==2025.33
run: pip3 install wpiformat==2025.34
- name: Run
run: wpiformat
- name: Check output
@@ -45,6 +46,7 @@ jobs:
if: ${{ failure() }}
javaformat:
name: "Java Formatting"
needs: [validation]
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v4
@@ -65,25 +67,19 @@ jobs:
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v4
- name: Install pnpm
uses: pnpm/action-setup@v4
with:
version: 10
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 22
cache: pnpm
cache-dependency-path: photon-client/pnpm-lock.yaml
- name: Install Dependencies
run: npm ci
run: pnpm i --frozen-lockfile
- name: Check Linting
run: npm run lint-ci
run: pnpm run lint-ci
- name: Check Formatting
run: npm run format-ci
server-index:
name: "Check server index.html not changed"
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v4
- name: Fetch all history and metadata
run: |
git fetch --prune --unshallow
git checkout -b pr
git branch -f main origin/main
- name: Check index.html not changed
run: git --no-pager diff --exit-code origin/main photon-server/src/main/resources/web/index.html
run: pnpm run format-ci

View File

@@ -3,12 +3,7 @@ name: Photon API Documentation
on:
# Run on pushes to main and pushed tags, and on pull requests against main, but ignore the docs folder
push:
branches: [ main ]
tags:
- 'v*'
pull_request:
branches: [ main ]
merge_group:
concurrency:
group: ${{ github.workflow }}-${{ github.head_ref || github.ref }}
@@ -21,6 +16,12 @@ permissions:
id-token: write
jobs:
validation:
name: "Validation"
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: gradle/actions/wrapper-validation@v4
build_demo:
name: Build PhotonClient Demo
defaults:
@@ -29,14 +30,20 @@ jobs:
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v4
- name: Install pnpm
uses: pnpm/action-setup@v4
with:
version: 10
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 22
cache: pnpm
cache-dependency-path: photon-client/pnpm-lock.yaml
- name: Install Dependencies
run: npm ci
run: pnpm i --frozen-lockfile
- name: Build Production Client
run: npm run build-demo
run: pnpm run build-demo
- uses: actions/upload-artifact@v4
with:
name: demo
@@ -44,6 +51,7 @@ jobs:
run_java_cpp_docs:
name: Build Java and C++ API Docs
needs: [validation]
runs-on: "ubuntu-22.04"
steps:
- name: Checkout code

View File

@@ -2,10 +2,7 @@ name: PhotonVision ReadTheDocs Checks
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
merge_group:
concurrency:
group: ${{ github.workflow }}-${{ github.head_ref || github.ref }}

View File

@@ -5,12 +5,7 @@ permissions:
on:
push:
branches: [ main ]
tags:
- 'v*'
pull_request:
branches: [ main ]
merge_group:
concurrency:
group: ${{ github.workflow }}-${{ github.head_ref || github.ref }}
@@ -47,13 +42,14 @@ jobs:
pip install --no-cache-dir dist/*.whl
pytest
- name: Run mypy type checking
uses: liskin/gh-problem-matcher-wrap@v3
with:
linters: mypy
run: |
mypy --show-column-numbers --config-file photon-lib/py/pyproject.toml photon-lib
# Disable due to robotpy issue. See
# https://github.com/PhotonVision/photonvision/issues/1968
# - name: Run mypy type checking
# uses: liskin/gh-problem-matcher-wrap@v3
# with:
# linters: mypy
# run: |
# mypy --show-column-numbers --config-file photon-lib/py/pyproject.toml photon-lib
- name: Upload artifacts
uses: actions/upload-artifact@master

View File

@@ -2,13 +2,7 @@ name: Website
on:
push:
# For now, run on all commits to main
branches: [ main ]
tags:
- 'v*'
pull_request:
branches: [ main ]
merge_group:
jobs:
rsync:
@@ -16,13 +10,21 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install pnpm
uses: pnpm/action-setup@v4
with:
version: 10
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: 22
cache: pnpm
cache-dependency-path: website/pnpm-lock.yaml
- name: Install packages
run: npm ci
run: pnpm i --frozen-lockfile
working-directory: website
- name: Build project
run: npm run build
run: pnpm run build
working-directory: website
- uses: up9cloud/action-rsync@v1.4
if: github.ref == 'refs/heads/main'
@@ -38,11 +40,19 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install pnpm
uses: pnpm/action-setup@v4
with:
version: 10
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: 22
cache: pnpm
cache-dependency-path: website/pnpm-lock.yaml
- name: Install Packages
run: npm ci
run: pnpm i --frozen-lockfile
working-directory: website
- name: Run Formatting Check
run: npx prettier -c .
run: pnpm prettier -c .
working-directory: website

4
.gitignore vendored
View File

@@ -5,7 +5,8 @@ __pycache__/
/.vs
backend/settings/
.vscode/
.vscode/*
!.vscode/settings.json
# Docs
_build
# Compiled class file
@@ -150,3 +151,4 @@ components.d.ts
# Py docs stuff
photon-lib/py/docs/build
photon-server/src/main/resources/web/index.html

1
.python-version Normal file
View File

@@ -0,0 +1 @@
3.11

View File

@@ -19,6 +19,7 @@ modifiableFileExclude {
\.webp$
\.ico$
\.rknn$
\.tflite$
\.mp4$
\.ttf$
\.woff2$

5
.vscode/settings.json vendored Normal file
View File

@@ -0,0 +1,5 @@
{
"python.testing.unittestEnabled": false,
"python.testing.pytestEnabled": true,
"python.testing.cwd": "photon-lib/py"
}

View File

@@ -24,7 +24,7 @@ If you are interested in contributing code or documentation to the project, plea
## Building
Gradle is used for all C++ and Java code, and NPM is used for the web UI. Instructions to compile PhotonVision yourself can be found [in our docs](https://docs.photonvision.org/en/latest/docs/contributing/building-photon.html#compiling-instructions).
Gradle is used for all C++ and Java code, and pnpm is used for the web UI. Instructions to compile PhotonVision yourself can be found [in our docs](https://docs.photonvision.org/en/latest/docs/contributing/building-photon.html#compiling-instructions).
You can run one of the many built in examples straight from the command line, too! They contain a fully featured robot project, and some include simulation support. The projects can be found inside the [`photonlib-java-examples`](photonlib-java-examples) and [`photonlib-cpp-examples`](photonlib-cpp-examples) subdirectories, respectively. Instructions for running these examples directly from the repo are found [in the docs](https://docs.photonvision.org/en/latest/docs/contributing/building-photon.html#running-examples).
@@ -42,6 +42,8 @@ Note that these are case sensitive!
* linuxarm64
* linuxathena
- `-PtgtIP`: Specifies where `./gradlew deploy` should try to copy the fat JAR to
- `-PtgtUser`: Specifies custom username for `./gradlew deploy` to SSH into
- `-PtgtPw`: Specifies custom password for `./gradlew deploy` to SSH into
- `-Pprofile`: enables JVM profiling
- `-PwithSanitizers`: On Linux, enables `-fsanitize=address,undefined,leak`

View File

@@ -8,10 +8,9 @@ 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
id "org.hidetake.ssh" version "2.11.2" apply false
}
allprojects {
@@ -37,9 +36,10 @@ ext {
wpimathVersion = wpilibVersion
openCVYear = "2025"
openCVversion = "4.10.0-3"
javalinVersion = "5.6.2"
libcameraDriverVersion = "v2025.0.3"
rknnVersion = "dev-v2025.0.0-1-g33b6263"
javalinVersion = "6.7.0"
libcameraDriverVersion = "v2025.0.4"
rknnVersion = "dev-v2025.0.0-5-g666c0c6"
rubikVersion = "dev-v2025.1.0-8-g067a316"
frcYear = "2025"
mrcalVersion = "v2025.0.0";
@@ -101,7 +101,7 @@ spotless {
}
wrapper {
gradleVersion '8.11'
gradleVersion = '8.14.3'
}
ext.getCurrentArch = {

View File

@@ -1,10 +1,8 @@
import argparse
import base64
import json
import os
from dataclasses import dataclass
import cv2
import mrcal
import numpy as np
from wpimath.geometry import Quaternion as _Quat
@@ -12,8 +10,8 @@ from wpimath.geometry import Quaternion as _Quat
@dataclass
class Size:
width: int
height: int
width: float
height: float
@dataclass
@@ -24,14 +22,6 @@ class JsonMatOfDoubles:
data: list[float]
@dataclass
class JsonMat:
rows: int
cols: int
type: int
data: str # Base64-encoded PNG data
@dataclass
class Point2:
x: float
@@ -84,8 +74,7 @@ class Observation:
# If we should use this observation when re-calculating camera calibration
includeObservationInCalibration: bool
snapshotName: str
# The actual image the snapshot is from
snapshotData: JsonMat
snapshotDataLocation: str
@dataclass
@@ -97,6 +86,7 @@ class CameraCalibration:
calobjectWarp: list[float]
calobjectSize: Size
calobjectSpacing: float
lensmodel: str
def __convert_cal_to_mrcal_cameramodel(
@@ -127,6 +117,13 @@ def __convert_cal_to_mrcal_cameramodel(
]
return np.concatenate((r, t))
imagersize = (int(cal.resolution.width), int(cal.resolution.height))
def fill_missing_corners(observations: list[list[float]], width: int, height: int):
num_corners = width * height
observations += [[0, 0, -1] for x in range(num_corners - len(observations))]
return observations
imagersize = (cal.resolution.width, cal.resolution.height)
# Always weight=1 for Photon data
@@ -135,8 +132,12 @@ def __convert_cal_to_mrcal_cameramodel(
[
# note that we expect row-major observations here. I think this holds
np.array(
list(map(lambda it: [it.x, it.y, WEIGHT], o.locationInImageSpace))
).reshape((cal.calobjectSize.width, cal.calobjectSize.height, 3))
fill_missing_corners(
list(map(lambda it: [it.x, it.y, WEIGHT], o.locationInImageSpace)),
int(cal.calobjectSize.width),
int(cal.calobjectSize.height),
)
).reshape((int(cal.calobjectSize.width), int(cal.calobjectSize.height), 3))
for o in cal.observations
]
)
@@ -206,14 +207,6 @@ def convert_photon_to_mrcal(photon_cal_json_path: str, output_folder: str):
if not os.path.exists(output_folder):
os.makedirs(output_folder)
# Decode each image and save it as a png
for obs in camera_cal_data.observations:
image = obs.snapshotData.data
decoded_data = base64.b64decode(image)
np_data = np.frombuffer(decoded_data, np.uint8)
img = cv2.imdecode(np_data, cv2.IMREAD_UNCHANGED)
cv2.imwrite(f"{output_folder}/{obs.snapshotName}", img)
# And create a VNL file for use with mrcal
with open(f"{output_folder}/corners.vnl", "w+") as vnl_file:
vnl_file.write("# filename x y level\n")

View File

@@ -11,6 +11,7 @@ modifiableFileExclude {
\.webp$
\.ico$
\.rknn$
\.tflite$
\.svg$
\.woff2$
gradlew

View File

@@ -24,7 +24,7 @@ pbr==6.1.1
pipreqs==0.5.0
Pygments==2.19.1
PyYAML==6.0.2
requests==2.32.3
requests==2.32.4
restructuredtext-lint==1.4.0
roman-numerals-py==3.1.0
setuptools==80.3.1
@@ -49,10 +49,10 @@ sphinxcontrib-qthelp==2.0.0
sphinxcontrib-serializinghtml==2.0.0
sphinxext-opengraph==0.10.0
sphinxext-remoteliteralinclude==0.5.0
starlette==0.46.2
starlette==0.47.2
stevedore==5.4.1
typing_extensions==4.13.2
urllib3==2.4.0
urllib3==2.5.0
uvicorn==0.34.2
watchfiles==1.0.5
websockets==15.0.1

View File

@@ -30,7 +30,6 @@ extensions = [
"sphinx_rtd_theme",
"sphinx.ext.autosectionlabel",
"sphinx.ext.todo",
"sphinx_tabs.tabs",
"notfound.extension",
"sphinxext.remoteliteralinclude",
"sphinxext.opengraph",
@@ -67,6 +66,10 @@ html_title = "PhotonVision Docs"
html_theme = "furo"
html_favicon = "assets/RoundLogo.png"
# Specify canonical root
# This tells search engines that this domain is preferred
html_baseurl = "https://docs.photonvision.org/en/latest/"
# Add any paths that contain custom static files (such as style sheets) here,
# relative to this directory. They are copied after the builtin static files,
# so a file named "default.css" will overwrite the builtin "default.css".
@@ -144,7 +147,11 @@ sphinx_tabs_valid_builders = ["epub", "linkcheck"]
# Excluded links for linkcheck
# These should be periodically checked by hand to ensure that they are still functional
linkcheck_ignore = [R"https://www.raspberrypi.com/software/", R"http://10\..+"]
linkcheck_ignore = [
R"https://www.raspberrypi.com/software/",
R"http://10\..+",
R"https://www.gnu.org/",
]
token = os.environ.get("GITHUB_TOKEN", None)
if token:

View File

@@ -28,7 +28,7 @@ This multi-target pose estimate can be accessed using PhotonLib. We suggest usin
```{eval-rst}
.. tab-set-code::
.. code-block:: Java
.. code-block:: java
var results = camera.getAllUnreadResults();
for (var result : results) {
@@ -39,7 +39,7 @@ This multi-target pose estimate can be accessed using PhotonLib. We suggest usin
}
.. code-block:: C++
.. code-block:: c++
auto results = camera.GetAllUnreadResults();
for (auto &result : results)
@@ -51,7 +51,7 @@ This multi-target pose estimate can be accessed using PhotonLib. We suggest usin
}
.. code-block:: Python
.. code-block:: python
results = camera.getAllUnreadResults()
for result in results:

View File

@@ -0,0 +1,8 @@
# Performance Benchmarks
```{toctree}
:maxdepth: 0
:titlesonly: true
rknn-model-benchmarks
```

View File

@@ -0,0 +1,125 @@
# RKNN Benchmarks
## Description
This benchmark compares the performance of four object detection models: YOLOv5, YOLOv5u, YOLOv8, and YOLOv11 on the [COCO 2017 Validation Set](http://images.cocodataset.org/zips/val2017.zip). The main purpose is to assess and compare the inference speed and detection accuracy of these models when deployed on the Orange Pi devices using the RKNN framework and int8 quantization.
## Methodology
- **Dataset**: [COCO 2017 Validation Set](http://images.cocodataset.org/zips/val2017.zip) (5,000 images)
- **Platform**: Orange Pi 5 with RK3588
- **Quantization**: int8 using 20 randomly selected images from the validation set
- **Framework**: RKNN Toolkit 2
## Operator-Level Benchmark Results
The following tables break down the average CPU time, NPU time, and total execution time (in microseconds) for each operator used by the models. Each value represents the mean ± standard deviation across 5,000 inferences.
### YOLOv5
| OpType | CPU Time (μs) | NPU Time (μs) | Total Time (μs) | Time Ratio (%) | Number of Times Called |
|-----------------|---------------------|----------------------|-----------------------|---------------------|-----------------------|
| ConvExSwish | 0.00 ± 0.00 | 10968.81 ± 1126.00 | 10968.81 ± 1126.00 | 73.06 ± 0.94 | 57 |
| ConvSigmoid | 0.00 ± 0.00 | 1243.49 ± 67.66 | 1243.49 ± 67.66 | 8.33 ± 0.57 | 3 |
| Concat | 0.00 ± 0.00 | 1080.68 ± 259.40 | 1080.68 ± 259.40 | 7.09 ± 0.87 | 13 |
| Conv | 0.00 ± 0.00 | 732.15 ± 29.42 | 732.15 ± 29.42 | 4.92 ± 0.42 | 1 |
| Add | 0.00 ± 0.00 | 473.71 ± 131.48 | 473.71 ± 131.48 | 3.10 ± 0.50 | 7 |
| MaxPool | 0.00 ± 0.00 | 272.40 ± 110.52 | 272.40 ± 110.52 | 1.76 ± 0.51 | 6 |
| Resize | 0.00 ± 0.00 | 147.61 ± 38.89 | 147.61 ± 38.89 | 0.97 ± 0.15 | 2 |
| OutputOperator | 106.60 ± 15.00 | 0.00 ± 0.00 | 106.60 ± 15.00 | 0.72 ± 0.13 | 3 |
| InputOperator | 8.64 ± 1.79 | 0.00 ± 0.00 | 8.64 ± 1.79 | 0.06 ± 0.02 | 1 |
| **Total** | **115.24 ± 16.16** | **14918.85 ± 1735.45**| **15034.09 ± 1734.28**| | **93** |
### YOLOv5u
| OpType | CPU Time (μs) | NPU Time (μs) | Total Time (μs) | Time Ratio (%) | Number of Times Called |
|-----------------|---------------------|----------------------|-----------------------|---------------------|-----------------------|
| ConvExSwish | 0.00 ± 0.00 | 16828.24 ± 1332.73 | 16828.24 ± 1332.73 | 83.04 ± 1.61 | 69 |
| Concat | 0.00 ± 0.00 | 1265.94 ± 250.24 | 1265.94 ± 250.24 | 6.17 ± 0.69 | 13 |
| ConvSigmoid | 0.00 ± 0.00 | 613.88 ± 62.97 | 613.88 ± 62.97 | 3.03 ± 0.15 | 3 |
| Add | 0.00 ± 0.00 | 553.75 ± 131.17 | 553.75 ± 131.17 | 2.69 ± 0.44 | 7 |
| Conv | 0.00 ± 0.00 | 298.61 ± 72.72 | 298.61 ± 72.72 | 1.45 ± 0.25 | 3 |
| ConvClip | 0.00 ± 0.00 | 256.02 ± 64.48 | 256.02 ± 64.48 | 1.24 ± 0.23 | 3 |
| MaxPool | 0.00 ± 0.00 | 178.68 ± 58.72 | 178.68 ± 58.72 | 0.86 ± 0.23 | 3 |
| Resize | 0.00 ± 0.00 | 170.87 ± 40.14 | 170.87 ± 40.14 | 0.83 ± 0.13 | 2 |
| OutputOperator | 126.89 ± 16.53 | 0.00 ± 0.00 | 126.89 ± 16.53 | 0.63 ± 0.10 | 9 |
| InputOperator | 8.69 ± 1.45 | 0.00 ± 0.00 | 8.69 ± 1.45 | 0.04 ± 0.01 | 1 |
| **Total** | **135.57 ± 17.51** | **20165.99 ± 1963.70**| **20301.56 ± 1965.88**| | **113** |
### YOLOv8
| OpType | CPU Time (μs) | NPU Time (μs) | Total Time (μs) | Time Ratio (%) | Number of Times Called |
|-----------------|---------------------|----------------------|-----------------------|---------------------|-----------------------|
| ConvExSwish | 0.00 ± 0.00 | 13017.04 ± 1165.76 | 13017.04 ± 1165.76 | 75.66 ± 1.96 | 57 |
| Concat | 0.00 ± 0.00 | 1489.94 ± 257.22 | 1489.94 ± 257.22 | 8.58 ± 0.53 | 13 |
| Split | 0.00 ± 0.00 | 681.47 ± 166.62 | 681.47 ± 166.62 | 3.89 ± 0.53 | 8 |
| ConvSigmoid | 0.00 ± 0.00 | 596.08 ± 75.01 | 596.08 ± 75.01 | 3.45 ± 0.18 | 3 |
| Add | 0.00 ± 0.00 | 443.60 ± 118.05 | 443.60 ± 118.05 | 2.53 ± 0.41 | 6 |
| Conv | 0.00 ± 0.00 | 269.61 ± 78.65 | 269.61 ± 78.65 | 1.54 ± 0.30 | 3 |
| Resize | 0.00 ± 0.00 | 236.79 ± 37.74 | 236.79 ± 37.74 | 1.37 ± 0.08 | 2 |
| ConvClip | 0.00 ± 0.00 | 231.82 ± 68.44 | 231.82 ± 68.44 | 1.32 ± 0.27 | 3 |
| MaxPool | 0.00 ± 0.00 | 156.85 ± 56.94 | 156.85 ± 56.94 | 0.89 ± 0.23 | 3 |
| OutputOperator | 124.86 ± 20.74 | 0.00 ± 0.00 | 124.86 ± 20.74 | 0.73 ± 0.15 | 9 |
| InputOperator | 8.47 ± 1.66 | 0.00 ± 0.00 | 8.47 ± 1.66 | 0.05 ± 0.01 | 1 |
| **Total** | **133.33 ± 21.95** | **17123.19 ± 1985.72**| **17256.52 ± 1986.77** | | **108** |
---
### YOLOv11
| OpType | CPU Time (μs) | NPU Time (μs) | Total Time (μs) | Time Ratio (%) | Number of Times Called |
|-----------------|---------------------|----------------------|-----------------------|---------------------|-----------------------|
| ConvExSwish | 0.00 ± 0.00 | 16034.00 ± 1331.95 | 16034.00 ± 1331.95 | 69.90 ± 1.55 | 77 |
| Concat | 0.00 ± 0.00 | 1888.89 ± 293.99 | 1888.89 ± 293.99 | 8.17 ± 0.51 | 17 |
| exSDPAttention | 0.00 ± 0.00 | 1210.88 ± 17.73 | 1210.88 ± 17.73 | 5.32 ± 0.52 | 1 |
| Split | 0.00 ± 0.00 | 908.30 ± 183.92 | 908.30 ± 183.92 | 3.91 ± 0.45 | 10 |
| Add | 0.00 ± 0.00 | 871.64 ± 212.79 | 871.64 ± 212.79 | 3.73 ± 0.60 | 12 |
| ConvSigmoid | 0.00 ± 0.00 | 617.61 ± 59.61 | 617.61 ± 59.61 | 2.69 ± 0.16 | 3 |
| Conv | 0.00 ± 0.00 | 419.72 ± 89.88 | 419.72 ± 89.88 | 1.80 ± 0.24 | 5 |
| Resize | 0.00 ± 0.00 | 272.09 ± 49.91 | 272.09 ± 49.91 | 1.18 ± 0.12 | 2 |
| ConvClip | 0.00 ± 0.00 | 260.08 ± 59.12 | 260.08 ± 59.12 | 1.12 ± 0.18 | 3 |
| MaxPool | 0.00 ± 0.00 | 181.93 ± 53.32 | 181.93 ± 53.32 | 0.78 ± 0.18 | 3 |
| OutputOperator | 131.48 ± 22.93 | 0.00 ± 0.00 | 131.48 ± 22.93 | 0.58 ± 0.12 | 9 |
| ConvAdd | 0.00 ± 0.00 | 126.79 ± 35.28 | 126.79 ± 35.28 | 0.54 ± 0.11 | 2 |
| Reshape | 0.00 ± 0.00 | 56.61 ± 18.03 | 56.61 ± 18.03 | 0.24 ± 0.06 | 3 |
| InputOperator | 8.66 ± 1.59 | 0.00 ± 0.00 | 8.66 ± 1.59 | 0.04 ± 0.01 | 1 |
| **Total** | **140.14 ± 24.26** | **22848.54 ± 2351.95**| **22988.68 ± 2355.97**| | **148** |
## Model Summary and Accuracy Metrics
The table below summarizes the mean average precision (mAP) and total inference time for each model. These metrics provide a high-level view of how each model performs in terms of both detection accuracy and runtime efficiency.
### Mean Average Precision (mAP) by Model
| Metric | YOLOv5 | YOLOv5u | YOLOv8 | YOLOv11 |
|--------|------------|------------|------------|------------|
| **mAP** | 0.2243 | 0.2745 | 0.3051 | 0.3251 |
| **mAP50** | 0.3538 | 0.3834 | 0.4145 | 0.4406 |
| **mAP75** | 0.2432 | 0.2997 | 0.3349 | 0.3568 |
| **mAP85** | 0.3054 | 0.3472 | 0.3867 | 0.4068 |
| **mAP95** | 0.3708 | 0.4822 | 0.5483 | 0.5858 |
### Model Execution Time and Call Frequency
| Model | Total Time (μs) | Number of Processing Calls |
|---------|------------------------|----------------------------|
| **YOLOv5** | 15034.09 ± 1734.28 | 93 |
| **YOLOv5u** | 20301.56 ± 1965.88 | 113 |
| **YOLOv8** | 17256.52 ± 1986.77 | 108 |
| **YOLOv11** | 22988.68 ± 2355.97 | 148 |
## Conclusion
The benchmark reveals a clear performance trade-off between inference time and detection accuracy:
- **YOLOv5** is the fastest model with the lowest total inference time, making it well-suited for situations where speed is more important than high detection precision.
- **YOLOv11** achieves the highest accuracy (mAP) across all IoU thresholds but comes with the longest inference time, which may limit its use in real-time applications.
- **YOLOv8** offers a strong balance between speed and accuracy, making it a practical choice when both factors matter.
- **YOLOv5u** improves accuracy compared to YOLOv5 but falls behind YOLOv8 in both speed and detection quality.
When choosing a model for edge devices like the Orange Pi 5, its important to weigh how much latency your system can tolerate versus how much accuracy you need. A faster model may give quicker results, while a more accurate one may offer better detection reliability, but at the cost of speed.

View File

@@ -1,4 +1,4 @@
# Camera-Specifc Configuration
# Camera-Specific Configuration
```{toctree}
:maxdepth: 2

View File

@@ -12,7 +12,11 @@ This section contains the build instructions from the source code available at [
**Node JS:**
The UI is written in Node JS. To compile the UI, Node 22.15.0 is required. To install Node JS follow the instructions for your platform [on the official Node JS website](https://nodejs.org/en/download/).
The UI is written in Node JS. To compile the UI, Node 22 or later is required. To install Node JS, follow the instructions for your platform [on the official Node JS website](https://nodejs.org/en/download/).
**pnpm:**
[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).
## Compiling Instructions
@@ -36,27 +40,7 @@ or alternatively download the source code from GitHub and extract the zip:
In the photon-client directory:
```bash
npm install
```
### Build and Copy UI to Java Source
In the root directory:
```{eval-rst}
.. tab-set::
.. tab-item:: Linux
``./gradlew buildAndCopyUI``
.. tab-item:: macOS
``./gradlew buildAndCopyUI``
.. tab-item:: Windows (cmd)
``gradlew buildAndCopyUI``
pnpm install
```
### Using hot reload on the UI
@@ -64,7 +48,7 @@ In the root directory:
In the photon-client directory:
```bash
npm run dev
pnpm run dev
```
This allows you to make UI changes quickly without having to spend time rebuilding the jar. Hot reload is enabled, so changes that you make and save are reflected in the UI immediately. Running this command will give you the URL for accessing the UI, which is on a different port than normal. You must use the printed URL to use hot reload.
@@ -77,14 +61,17 @@ To compile and run the project, issue the following command in the root director
.. tab-set::
.. tab-item:: Linux
:sync: linux
``./gradlew run``
.. tab-item:: macOS
:sync: macos
``./gradlew run``
.. tab-item:: Windows (cmd)
:sync: windows
``gradlew run``
```
@@ -95,21 +82,24 @@ Running the following command under the root directory will build the jar under
.. tab-set::
.. tab-item:: Linux
:sync: linux
``./gradlew shadowJar``
.. tab-item:: macOS
:sync: macos
``./gradlew shadowJar``
.. tab-item:: Windows (cmd)
:sync: windows
``gradlew shadowJar``
```
### Build and Run PhotonVision on a Raspberry Pi Coprocessor
As a convenience, the build has a built-in `deploy` command which builds, deploys, and starts the current source code on a coprocessor.
As a convenience, the build has a built-in `deploy` command which builds, deploys, and starts the current source code on a coprocessor. It uses [deploy-utils](https://github.com/wpilibsuite/deploy-utils/blob/main/README.md), so it works very similarly to deploys on robot projects.
An architecture override is required to specify the deploy target's architecture.
@@ -117,18 +107,21 @@ An architecture override is required to specify the deploy target's architecture
.. tab-set::
.. tab-item:: Linux
:sync: linux
``./gradlew clean``
``./gradlew deploy -PArchOverride=linuxarm64``
.. tab-item:: macOS
:sync: macos
``./gradlew clean``
``./gradlew deploy -PArchOverride=linuxarm64``
.. tab-item:: Windows (cmd)
:sync: windows
``gradlew clean``
@@ -147,14 +140,17 @@ The photonlib source can be published to your local maven repository after build
.. tab-set::
.. tab-item:: Linux
:sync: linux
``./gradlew publishToMavenLocal``
.. tab-item:: macOS
:sync: macos
``./gradlew publishToMavenLocal``
.. tab-item:: Windows (cmd)
:sync: windows
``gradlew publishToMavenLocal``
```
@@ -197,7 +193,7 @@ Similarly, a local instance of PhotonVision can be debugged in the same way usin
Set up a VSCode configuration in {code}`launch.json`
```
```json
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
@@ -279,3 +275,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.

View File

@@ -14,8 +14,10 @@ To do this, we'll use the _pitch_ of the target in the camera image and trigonom
```{eval-rst}
.. tab-set::
:sync-group: code
.. tab-item:: Java
:sync: java
.. rli:: https://raw.githubusercontent.com/PhotonVision/photonvision/abe95dfaa055bbe3609f72cfcaaba0f96ee7978c/photonlib-java-examples/aimandrange/src/main/java/frc/robot/Robot.java
:language: java
@@ -24,6 +26,7 @@ To do this, we'll use the _pitch_ of the target in the camera image and trigonom
:lineno-start: 84
.. tab-item:: C++ (Header)
:sync: c++
.. rli:: https://raw.githubusercontent.com/PhotonVision/photonvision/abe95dfaa055bbe3609f72cfcaaba0f96ee7978c/photonlib-cpp-examples/aimandrange/src/main/include/Robot.h
:language: c++
@@ -32,6 +35,7 @@ To do this, we'll use the _pitch_ of the target in the camera image and trigonom
:lineno-start: 25
.. tab-item:: C++ (Source)
:sync: c++
.. rli:: https://raw.githubusercontent.com/PhotonVision/photonvision/abe95dfaa055bbe3609f72cfcaaba0f96ee7978c/photonlib-cpp-examples/aimandrange/src/main/cpp/Robot.cpp
:language: c++
@@ -40,6 +44,7 @@ To do this, we'll use the _pitch_ of the target in the camera image and trigonom
:lineno-start: 58
.. tab-item:: Python
:sync: python
.. rli:: https://raw.githubusercontent.com/PhotonVision/photonvision/abe95dfaa055bbe3609f72cfcaaba0f96ee7978c/photonlib-python-examples/aimandrange/robot.py
:language: python

View File

@@ -19,8 +19,10 @@ In this example, while the operator holds a button down, the robot will turn tow
```{eval-rst}
.. tab-set::
:sync-group: code
.. tab-item:: Java
:sync: java
.. rli:: https://raw.githubusercontent.com/PhotonVision/photonvision/abe95dfaa055bbe3609f72cfcaaba0f96ee7978c/photonlib-java-examples/aimattarget/src/main/java/frc/robot/Robot.java
:language: java
@@ -29,6 +31,7 @@ In this example, while the operator holds a button down, the robot will turn tow
:lineno-start: 77
.. tab-item:: C++ (Header)
:sync: c++
.. rli:: https://raw.githubusercontent.com/PhotonVision/photonvision/abe95dfaa055bbe3609f72cfcaaba0f96ee7978c/photonlib-cpp-examples/aimattarget/src/main/include/Robot.h
:language: c++
@@ -37,6 +40,7 @@ In this example, while the operator holds a button down, the robot will turn tow
:lineno-start: 25
.. tab-item:: C++ (Source)
:sync: c++
.. rli:: https://raw.githubusercontent.com/PhotonVision/photonvision/abe95dfaa055bbe3609f72cfcaaba0f96ee7978c/photonlib-cpp-examples/aimattarget/src/main/cpp/Robot.cpp
:language: c++
@@ -45,6 +49,7 @@ In this example, while the operator holds a button down, the robot will turn tow
:lineno-start: 56
.. tab-item:: Python
:sync: python
.. rli:: https://raw.githubusercontent.com/PhotonVision/photonvision/abe95dfaa055bbe3609f72cfcaaba0f96ee7978c/photonlib-python-examples/aimattarget/robot.py
:language: python

View File

@@ -21,32 +21,24 @@ Please reference the [WPILib documentation](https://docs.wpilib.org/en/stable/do
We use the 2024 game's AprilTag Locations:
```{eval-rst}
.. tab-set::
.. tab-set-code::
.. rli:: https://raw.githubusercontent.com/PhotonVision/photonvision/abe95dfaa055bbe3609f72cfcaaba0f96ee7978c/photonlib-java-examples/poseest/src/main/java/frc/robot/Vision.java
:language: java
:lines: 68-68
:linenos:
:lineno-start: 68
.. tab-item:: Java
:sync: java
.. rli:: https://raw.githubusercontent.com/PhotonVision/photonvision/abe95dfaa055bbe3609f72cfcaaba0f96ee7978c/photonlib-cpp-examples/poseest/src/main/include/Constants.h
:language: c++
:lines: 42-43
:linenos:
:lineno-start: 42
.. rli:: https://raw.githubusercontent.com/PhotonVision/photonvision/abe95dfaa055bbe3609f72cfcaaba0f96ee7978c/photonlib-java-examples/poseest/src/main/java/frc/robot/Vision.java
:language: java
:lines: 68-68
:linenos:
:lineno-start: 68
.. tab-item:: C++
.. rli:: https://raw.githubusercontent.com/PhotonVision/photonvision/abe95dfaa055bbe3609f72cfcaaba0f96ee7978c/photonlib-cpp-examples/poseest/src/main/include/Constants.h
:language: c++
:lines: 42-43
:linenos:
:lineno-start: 42
.. tab-item:: Python
.. rli:: https://raw.githubusercontent.com/PhotonVision/photonvision/abe95dfaa055bbe3609f72cfcaaba0f96ee7978c/photonlib-python-examples/poseest/robot.py
:language: python
:lines: 46-46
:linenos:
:lineno-start: 46
.. rli:: https://raw.githubusercontent.com/PhotonVision/photonvision/abe95dfaa055bbe3609f72cfcaaba0f96ee7978c/photonlib-python-examples/poseest/robot.py
:language: python
:lines: 46-46
:linenos:
:lineno-start: 46
```
@@ -56,63 +48,47 @@ To incorporate PhotonVision, we need to create a {code}`PhotonCamera`:
```{eval-rst}
.. tab-set::
.. tab-set-code::
.. rli:: https://raw.githubusercontent.com/PhotonVision/photonvision/abe95dfaa055bbe3609f72cfcaaba0f96ee7978c/photonlib-java-examples/poseest/src/main/java/frc/robot/Vision.java
:language: java
:lines: 57-57
:linenos:
:lineno-start: 57
.. tab-item:: Java
:sync: java
.. rli:: https://raw.githubusercontent.com/PhotonVision/photonvision/abe95dfaa055bbe3609f72cfcaaba0f96ee7978c/photonlib-cpp-examples/poseest/src/main/include/Vision.h
:language: c++
:lines: 145-145
:linenos:
:lineno-start: 145
.. rli:: https://raw.githubusercontent.com/PhotonVision/photonvision/abe95dfaa055bbe3609f72cfcaaba0f96ee7978c/photonlib-java-examples/poseest/src/main/java/frc/robot/Vision.java
:language: java
:lines: 57-57
:linenos:
:lineno-start: 57
.. tab-item:: C++
.. rli:: https://raw.githubusercontent.com/PhotonVision/photonvision/abe95dfaa055bbe3609f72cfcaaba0f96ee7978c/photonlib-cpp-examples/poseest/src/main/include/Vision.h
:language: c++
:lines: 145-145
:linenos:
:lineno-start: 145
.. tab-item:: Python
.. rli:: https://raw.githubusercontent.com/PhotonVision/photonvision/abe95dfaa055bbe3609f72cfcaaba0f96ee7978c/photonlib-python-examples/poseest/robot.py
:language: python
:lines: 44-44
:linenos:
:lineno-start: 44
.. rli:: https://raw.githubusercontent.com/PhotonVision/photonvision/abe95dfaa055bbe3609f72cfcaaba0f96ee7978c/photonlib-python-examples/poseest/robot.py
:language: python
:lines: 44-44
:linenos:
:lineno-start: 44
```
During periodic execution, we read back camera results. If we see AprilTags in the image, we calculate the camera-measured pose of the robot and pass it to the {code}`Drivetrain`.
```{eval-rst}
.. tab-set::
.. tab-set-code::
.. rli:: https://raw.githubusercontent.com/PhotonVision/photonvision/abe95dfaa055bbe3609f72cfcaaba0f96ee7978c/photonlib-java-examples/poseest/src/main/java/frc/robot/Robot.java
:language: java
:lines: 64-74
:linenos:
:lineno-start: 64
.. tab-item:: Java
:sync: java
.. rli:: https://raw.githubusercontent.com/PhotonVision/photonvision/abe95dfaa055bbe3609f72cfcaaba0f96ee7978c/photonlib-cpp-examples/poseest/src/main/cpp/Robot.cpp
:language: c++
:lines: 38-46
:linenos:
:lineno-start: 38
.. rli:: https://raw.githubusercontent.com/PhotonVision/photonvision/abe95dfaa055bbe3609f72cfcaaba0f96ee7978c/photonlib-java-examples/poseest/src/main/java/frc/robot/Robot.java
:language: java
:lines: 64-74
:linenos:
:lineno-start: 64
.. tab-item:: C++
.. rli:: https://raw.githubusercontent.com/PhotonVision/photonvision/abe95dfaa055bbe3609f72cfcaaba0f96ee7978c/photonlib-cpp-examples/poseest/src/main/cpp/Robot.cpp
:language: c++
:lines: 38-46
:linenos:
:lineno-start: 38
.. tab-item:: Python
.. rli:: https://raw.githubusercontent.com/PhotonVision/photonvision/abe95dfaa055bbe3609f72cfcaaba0f96ee7978c/photonlib-python-examples/poseest/robot.py
:language: python
:lines: 54-56
:linenos:
:lineno-start: 54
.. rli:: https://raw.githubusercontent.com/PhotonVision/photonvision/abe95dfaa055bbe3609f72cfcaaba0f96ee7978c/photonlib-python-examples/poseest/robot.py
:language: python
:lines: 54-56
:linenos:
:lineno-start: 54
```
@@ -121,56 +97,45 @@ During periodic execution, we read back camera results. If we see AprilTags in t
First, we create a new {code}`VisionSystemSim` to represent our camera and coprocessor running PhotonVision, and moving around our simulated field.
```{eval-rst}
.. tab-set::
.. tab-set-code::
.. rli:: https://raw.githubusercontent.com/PhotonVision/photonvision/abe95dfaa055bbe3609f72cfcaaba0f96ee7978c/photonlib-java-examples/poseest/src/main/java/frc/robot/Vision.java
:language: java
:lines: 65-69
:linenos:
:lineno-start: 65
.. tab-item:: Java
:sync: java
.. rli:: https://raw.githubusercontent.com/PhotonVision/photonvision/abe95dfaa055bbe3609f72cfcaaba0f96ee7978c/photonlib-cpp-examples/poseest/src/main/include/Vision.h
:language: c++
:lines: 49-52
:linenos:
:lineno-start: 49
.. rli:: https://raw.githubusercontent.com/PhotonVision/photonvision/abe95dfaa055bbe3609f72cfcaaba0f96ee7978c/photonlib-java-examples/poseest/src/main/java/frc/robot/Vision.java
:language: java
:lines: 65-69
:linenos:
:lineno-start: 65
.. code-block:: python
.. tab-item:: C++
.. rli:: https://raw.githubusercontent.com/PhotonVision/photonvision/abe95dfaa055bbe3609f72cfcaaba0f96ee7978c/photonlib-cpp-examples/poseest/src/main/include/Vision.h
:language: c++
:lines: 49-52
:linenos:
:lineno-start: 49
.. tab-item:: Python
# Coming Soon!
# Coming Soon!
```
Then, we add configure the simulated vision system to match the camera system being simulated.
```{eval-rst}
.. tab-set::
.. tab-set-code::
.. tab-item:: Java
:sync: java
.. rli:: https://raw.githubusercontent.com/PhotonVision/photonvision/abe95dfaa055bbe3609f72cfcaaba0f96ee7978c/photonlib-java-examples/poseest/src/main/java/frc/robot/Vision.java
:language: java
:lines: 69-82
:linenos:
:lineno-start: 69
.. rli:: https://raw.githubusercontent.com/PhotonVision/photonvision/abe95dfaa055bbe3609f72cfcaaba0f96ee7978c/photonlib-java-examples/poseest/src/main/java/frc/robot/Vision.java
:language: java
:lines: 69-82
:linenos:
:lineno-start: 69
.. rli:: https://raw.githubusercontent.com/PhotonVision/photonvision/abe95dfaa055bbe3609f72cfcaaba0f96ee7978c/photonlib-cpp-examples/poseest/src/main/include/Vision.h
:language: c++
:lines: 53-65
:linenos:
:lineno-start: 53
.. tab-item:: C++
.. code-block:: python
.. rli:: https://raw.githubusercontent.com/PhotonVision/photonvision/abe95dfaa055bbe3609f72cfcaaba0f96ee7978c/photonlib-cpp-examples/poseest/src/main/include/Vision.h
:language: c++
:lines: 53-65
:linenos:
:lineno-start: 53
.. tab-item:: Python
# Coming Soon!
# Coming Soon!
```
@@ -179,28 +144,23 @@ Then, we add configure the simulated vision system to match the camera system be
During simulation, we periodically update the simulated vision system.
```{eval-rst}
.. tab-set::
.. tab-set-code::
.. tab-item:: Java
:sync: java
.. rli:: https://raw.githubusercontent.com/PhotonVision/photonvision/abe95dfaa055bbe3609f72cfcaaba0f96ee7978c/photonlib-java-examples/poseest/src/main/java/frc/robot/Robot.java
:language: java
:lines: 114-132
:linenos:
:lineno-start: 114
.. rli:: https://raw.githubusercontent.com/PhotonVision/photonvision/abe95dfaa055bbe3609f72cfcaaba0f96ee7978c/photonlib-java-examples/poseest/src/main/java/frc/robot/Robot.java
:language: java
:lines: 114-132
:linenos:
:lineno-start: 114
.. rli:: https://raw.githubusercontent.com/PhotonVision/photonvision/abe95dfaa055bbe3609f72cfcaaba0f96ee7978c/photonlib-cpp-examples/poseest/src/main/cpp/Robot.cpp
:language: c++
:lines: 95-109
:linenos:
:lineno-start: 95
.. tab-item:: C++
.. code-block:: python
.. rli:: https://raw.githubusercontent.com/PhotonVision/photonvision/abe95dfaa055bbe3609f72cfcaaba0f96ee7978c/photonlib-cpp-examples/poseest/src/main/cpp/Robot.cpp
:language: c++
:lines: 95-109
:linenos:
:lineno-start: 95
.. tab-item:: Python
# Coming Soon!
# Coming Soon!
```
The rest is done behind the scenes.

View File

@@ -43,7 +43,7 @@ A simple way to use a pose estimate is to activate robot functions automatically
```{eval-rst}
.. tab-set-code::
.. code-block:: Java
.. code-block:: java
Pose3d robotPose;
boolean launcherSpinCmd;

View File

@@ -2,9 +2,9 @@
## How does it work?
PhotonVision supports object detection using neural network accelerator hardware built into Orange Pi 5/5+ coprocessors. Please note that the Orange Pi 5/5+ are the only coprocessors that are currently supported. The Neural Processing Unit, or NPU, is [used by PhotonVision](https://github.com/PhotonVision/rknn_jni/tree/main) to massively accelerate certain math operations like those needed for running ML-based object detection.
PhotonVision supports object detection using neural network accelerator hardware, commonly known as an NPU. The two coprocessors currently supported are the {ref}`Orange Pi 5 <docs/objectDetection/opi:Orange Pi 5 (and variants) Object Detection>` and the {ref}`Rubik Pi 3 <docs/objectDetection/rubik:Rubik Pi 3 Object Detection>`.
For the 2025 season, PhotonVision ships with a pretrained ALGAE model. A model to detect coral is not currently stable, and interested teams should ask in the Photonvision discord.
PhotonVision currently ships with a model trained on the [COCO dataset](https://cocodataset.org/) by [Ultralytics](https://github.com/ultralytics/ultralytics) (this model is licensed under [AGPLv3](https://www.gnu.org/licenses/agpl-3.0.en.html)). This model is meant to be used for testing and other miscellaneous purposes. It is not meant to be used in competition. For the 2025 post-season, PhotonVision also ships with a pretrained ALGAE model. A model to detect coral is available in the PhotonVision discord, but will not be distributed with PhotonVision.
## Tracking Objects
@@ -18,7 +18,7 @@ This model output means that while its fairly easy to say that "this rectangle p
## Tuning and Filtering
Compared to other pipelines, object detection exposes very few tuning handles. The Confidence slider changes the minimum confidence that the model needs to have in a given detection to consider it valid, as a number between 0 and 1 (with 0 meaning completely uncertain and 1 meaning maximally certain).
Compared to other pipelines, object detection exposes very few tuning handles. The Confidence slider changes the minimum confidence that the model needs to have in a given detection to consider it valid, as a number between 0 and 1 (with 0 meaning completely uncertain and 1 meaning maximally certain). The Non-Maximum Suppresion (NMS) Threshold slider is used to filter out overlapping detections. Higher values mean more detections are allowed through, but may result in false positives. It's generally recommended that teams leave this set at the default, unless they find they're unable to get usable results with solely the Confidence slider.
```{raw} html
<video width="85%" controls>
@@ -33,31 +33,19 @@ The same area, aspect ratio, and target orientation/sort parameters from {ref}`r
Photonvision will letterbox your camera frame to 640x640. This means that if you select a resolution that is larger than 640 it will be scaled down to fit inside a 640x640 frame with black bars if needed. Smaller frames will be scaled up with black bars if needed.
## Training Custom Models
It is recommended that you select a resolution that results in the smaller dimension being just greater than, or equal to, 640. Anything above this will not see any increased performance.
:::{warning}
Power users only. This requires some setup, such as obtaining your own dataset and installing various tools. It's additionally advised to have a general knowledge of ML before attempting to train your own model. Additionally, this is not officially supported by Photonvision, and any problems that may arise are not attributable to Photonvision.
:::
## Custom Models
Before beginning, it is necessary to install the [rknn-toolkit2](https://github.com/airockchip/rknn-toolkit2). Then, install the relevant [Ultralytics repository](https://github.com/airockchip?tab=repositories&q=yolo&type=&language=&sort=) from this list. After training your model, export it to `rknn`. This will give you an `onnx` file, formatted for conversion. Copy this file to the relevant folder in [rknn_model_zoo](https://github.com/airockchip/rknn_model_zoo), and use the conversion script located there to convert it. If necessary, modify the script to provide the path to your training database for quantization.
For information regarding converting custom models and supported models for each platform, refer to the page detailing information about your specific coprocessor.
## Uploading Custom Models
- {ref}`Orange Pi 5 <docs/objectDetection/opi:Orange Pi 5 (and variants) Object Detection>`
- {ref}`Rubik Pi 3 <docs/objectDetection/rubik:Rubik Pi 3 Object Detection>`
:::{warning}
PhotonVision currently ONLY supports 640x640 Ultralytics YOLOv5, YOLOv8, and YOLOv11 models trained and converted to `.rknn` format for RK3588 CPUs! Other models require different post-processing code and will NOT work. The model conversion process is also highly particular. Proceed with care.
:::
### Training Custom Models
:::{warning}
Non-quantized models are not supported! If you have the option, make sure quantization is enabled when exporting to .rknn format. This will represent the weights and activations of the model as 8-bit integers, instead of 32-bit floats which PhotonVision doesn't support. Quantized models are also much faster.
:::
PhotonVision does not offer any support for training custom models, only conversion. For information on which models are supported for a given coprocessor, use the links above.
In the settings, under `Device Control`, there's an option to upload a new object detection model. Naming convention
should be `name-verticalResolution-horizontalResolution-yolovXXX`. The
`name` should only include alphanumeric characters, periods, and underscores. Additionally, the labels
file ought to have the same name as the RKNN file, with `-labels` appended to the end. For
example, if the RKNN file is named `Algae_1.03.2025-640-640-yolov5s.rknn`, the labels file should be
named `Algae_1.03.2025-640-640-yolov5s-labels.txt`.
### Managing Custom Models
:::{note}
Currently there is no way to delete custom models in the GUI, though this is a planned feature. To do this, you have to SSH into the coprocessor and delete the files manually from `/opt/photonvision/photonvision_config/models`.
:::
Custom models can now be managed from the Object Detection tab in settings. You can upload a custom model by clicking the "Upload Model" button, selecting your model file, and filling out the property fields. Models can also be exported, both individually and in bulk. Models exported in bulk can be imported using the `import bulk` button. Models exported individually must be re-imported as an individual model, and all the relevant metadata is stored in the filename of the model.

View File

@@ -1,8 +1,8 @@
# Object Detection
```{toctree}
:maxdepth: 0
:titlesonly: true
about-object-detection
opi
rubik
```

View File

@@ -0,0 +1,19 @@
# Orange Pi 5 (and variants) Object Detection
## How it works
PhotonVision runs object detection on the Orange Pi 5 by use of the RKNN model architecture, and [this JNI code](https://github.com/PhotonVision/rknn_jni).
## Supported models
PhotonVision currently ONLY supports 640x640 Ultralytics YOLOv5, YOLOv8, and YOLOv11 models trained and converted to `.rknn` format for RK3588 SOCs! Other models require different post-processing code and will NOT work.
## Converting Custom Models
:::{warning}
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.
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.

View File

@@ -0,0 +1,25 @@
# Rubik Pi 3 Object Detection
## How it works
PhotonVision runs object detection on the Rubik Pi 3 by use of [TensorflowLite](https://github.com/tensorflow/tensorflow), and [this JNI code](https://github.com/PhotonVision/rubik_jni).
## Supported models
PhotonVision currently ONLY supports 640x640 Ultralytics YOLOv8 and YOLOv11 models trained and converted to `.tflite` format for QCS6490 SOCs! Other models require different post-processing code and will NOT work.
## Converting Custom Models
:::{warning}
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.
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.
## Benchmarking
Before you can perform benchmarking, it's necessary to install `tensorflow-lite-qcom-apps` with apt.
By SSHing into your Rubik Pi and running this command, replacing `PATH/TO/MODEL` with the path to your model, `benchmark_model --graph=src/test/resources/yolov8nCoco.tflite --external_delegate_path=/usr/lib/libQnnTFLiteDelegate.so --external_delegate_options=backend_type:htp --external_delegate_options=htp_use_conv_hmx:1 --external_delegate_options=htp_performance_mode:2` you can determine how long it takes for inference to be performed with your model.

View File

@@ -4,17 +4,17 @@ You can control the vision LEDs of supported hardware via PhotonLib using the `s
```{eval-rst}
.. tab-set-code::
.. code-block:: Java
.. code-block:: java
// Blink the LEDs.
camera.setLED(VisionLEDMode.kBlink);
.. code-block:: C++
.. code-block:: c++
// Blink the LEDs.
camera.SetLED(photonlib::VisionLEDMode::kBlink);
.. code-block:: Python
.. code-block:: python
# Coming Soon!
```

View File

@@ -9,17 +9,17 @@ You can use the `setDriverMode()`/`SetDriverMode()` (Java and C++ respectively)
```{eval-rst}
.. tab-set-code::
.. code-block:: Java
.. code-block:: java
// Set driver mode to on.
camera.setDriverMode(true);
.. code-block:: C++
.. code-block:: c++
// Set driver mode to on.
camera.SetDriverMode(true);
.. code-block:: Python
.. code-block:: python
# Coming Soon!
```
@@ -31,17 +31,17 @@ You can use the `setPipelineIndex()`/`SetPipelineIndex()` (Java and C++ respecti
```{eval-rst}
.. tab-set-code::
.. code-block:: Java
.. code-block:: java
// Change pipeline to 2
camera.setPipelineIndex(2);
.. code-block:: C++
.. code-block:: c++
// Change pipeline to 2
camera.SetPipelineIndex(2);
.. code-block:: Python
.. code-block:: python
# Coming Soon!
```
@@ -52,17 +52,17 @@ You can also get the pipeline latency from a pipeline result using the `getLaten
```{eval-rst}
.. tab-set-code::
.. code-block:: Java
.. code-block:: java
// Get the pipeline latency.
double latencySeconds = result.getLatencyMillis() / 1000.0;
.. code-block:: C++
.. code-block:: c++
// Get the pipeline latency.
units::second_t latency = result.GetLatency();
.. code-block:: Python
.. code-block:: python
# Coming Soon!
```

View File

@@ -20,7 +20,7 @@ The `PhotonCamera` class has two constructors: one that takes a `NetworkTable` a
:language: c++
:lines: 42-43
.. code-block:: Python
.. code-block:: python
# Change this to match the name of your camera as shown in the web ui
self.camera = PhotonCamera("your_camera_name_here")
@@ -51,7 +51,7 @@ Use the `getLatestResult()`/`GetLatestResult()` (Java and C++ respectively) to o
:language: c++
:lines: 35-36
.. code-block:: Python
.. code-block:: python
# Query the latest result from PhotonVision
result = self.camera.getLatestResult()
@@ -69,17 +69,17 @@ Each pipeline result has a `hasTargets()`/`HasTargets()` (Java and C++ respectiv
```{eval-rst}
.. tab-set-code::
.. code-block:: Java
.. code-block:: java
// Check if the latest result has any targets.
boolean hasTargets = result.hasTargets();
.. code-block:: C++
.. code-block:: c++
// Check if the latest result has any targets.
bool hasTargets = result.HasTargets();
.. code-block:: Python
.. code-block:: python
# Check if the latest result has any targets.
hasTargets = result.hasTargets()
@@ -99,17 +99,17 @@ You can get a list of tracked targets using the `getTargets()`/`GetTargets()` (J
```{eval-rst}
.. tab-set-code::
.. code-block:: Java
.. code-block:: java
// Get a list of currently tracked targets.
List<PhotonTrackedTarget> targets = result.getTargets();
.. code-block:: C++
.. code-block:: c++
// Get a list of currently tracked targets.
wpi::ArrayRef<photonlib::PhotonTrackedTarget> targets = result.GetTargets();
.. code-block:: Python
.. code-block:: python
# Get a list of currently tracked targets.
targets = result.getTargets()
@@ -121,18 +121,18 @@ You can get the {ref}`best target <docs/reflectiveAndShape/contour-filtering:Con
```{eval-rst}
.. tab-set-code::
.. code-block:: Java
.. code-block:: java
// Get the current best target.
PhotonTrackedTarget target = result.getBestTarget();
.. code-block:: C++
.. code-block:: c++
// Get the current best target.
photonlib::PhotonTrackedTarget target = result.GetBestTarget();
.. code-block:: Python
.. code-block:: python
# Coming Soon!
@@ -149,7 +149,7 @@ You can get the {ref}`best target <docs/reflectiveAndShape/contour-filtering:Con
```{eval-rst}
.. tab-set-code::
.. code-block:: Java
.. code-block:: java
// Get information from target.
double yaw = target.getYaw();
@@ -159,7 +159,7 @@ You can get the {ref}`best target <docs/reflectiveAndShape/contour-filtering:Con
Transform2d pose = target.getCameraToTarget();
List<TargetCorner> corners = target.getCorners();
.. code-block:: C++
.. code-block:: c++
// Get information from target.
double yaw = target.GetYaw();
@@ -169,7 +169,7 @@ You can get the {ref}`best target <docs/reflectiveAndShape/contour-filtering:Con
frc::Transform2d pose = target.GetCameraToTarget();
wpi::SmallVector<std::pair<double, double>, 4> corners = target.GetCorners();
.. code-block:: Python
.. code-block:: python
# Get information from target.
yaw = target.getYaw()
@@ -193,7 +193,7 @@ All of the data above (**except skew**) is available when using AprilTags.
```{eval-rst}
.. tab-set-code::
.. code-block:: Java
.. code-block:: java
// Get information from target.
int targetID = target.getFiducialId();
@@ -201,7 +201,7 @@ All of the data above (**except skew**) is available when using AprilTags.
Transform3d bestCameraToTarget = target.getBestCameraToTarget();
Transform3d alternateCameraToTarget = target.getAlternateCameraToTarget();
.. code-block:: C++
.. code-block:: c++
// Get information from target.
int targetID = target.GetFiducialId();
@@ -209,7 +209,7 @@ All of the data above (**except skew**) is available when using AprilTags.
frc::Transform3d bestCameraToTarget = target.getBestCameraToTarget();
frc::Transform3d alternateCameraToTarget = target.getAlternateCameraToTarget();
.. code-block:: Python
.. code-block:: python
# Get information from target.
targetID = target.getFiducialId()
@@ -227,7 +227,7 @@ Images are stored within the PhotonVision configuration directory. Running the "
```{eval-rst}
.. tab-set-code::
.. code-block:: Java
.. code-block:: java
// Capture pre-process camera stream image
camera.takeInputSnapshot();
@@ -235,7 +235,7 @@ Images are stored within the PhotonVision configuration directory. Running the "
// Capture post-process camera stream image
camera.takeOutputSnapshot();
.. code-block:: C++
.. code-block:: c++
// Capture pre-process camera stream image
camera.TakeInputSnapshot();
@@ -243,7 +243,7 @@ Images are stored within the PhotonVision configuration directory. Running the "
// Capture post-process camera stream image
camera.TakeOutputSnapshot();
.. code-block:: Python
.. code-block:: python
# Capture pre-process camera stream image
camera.takeInputSnapshot()

View File

@@ -8,17 +8,17 @@ A `PhotonUtils` class with helpful common calculations is included within `Photo
```{eval-rst}
.. tab-set-code::
.. code-block:: Java
.. code-block:: java
// Calculate robot's field relative pose
if (aprilTagFieldLayout.getTagPose(target.getFiducialId()).isPresent()) {
Pose3d robotPose = PhotonUtils.estimateFieldToRobotAprilTag(target.getBestCameraToTarget(), aprilTagFieldLayout.getTagPose(target.getFiducialId()).get(), cameraToRobot);
}
.. code-block:: C++
.. code-block:: c++
//TODO
.. code-block:: Python
.. code-block:: python
# Coming Soon!
```
@@ -29,19 +29,19 @@ You can get your robot's `Pose2D` on the field using various camera data, target
```{eval-rst}
.. tab-set-code::
.. code-block:: Java
.. code-block:: java
// Calculate robot's field relative pose
Pose2D robotPose = PhotonUtils.estimateFieldToRobot(
kCameraHeight, kTargetHeight, kCameraPitch, kTargetPitch, Rotation2d.fromDegrees(-target.getYaw()), gyro.getRotation2d(), targetPose, cameraToRobot);
.. code-block:: C++
.. code-block:: c++
// Calculate robot's field relative pose
frc::Pose2D robotPose = photonlib::EstimateFieldToRobot(
kCameraHeight, kTargetHeight, kCameraPitch, kTargetPitch, frc::Rotation2d(units::degree_t(-target.GetYaw())), frc::Rotation2d(units::degree_t(gyro.GetRotation2d)), targetPose, cameraToRobot);
.. code-block:: Python
.. code-block:: python
# Coming Soon!
@@ -54,15 +54,15 @@ If your camera is at a fixed height on your robot and the height of the target i
```{eval-rst}
.. tab-set-code::
.. code-block:: Java
.. code-block:: java
// TODO
.. code-block:: C++
.. code-block:: c++
// TODO
.. code-block:: Python
.. code-block:: python
# Coming Soon!
@@ -78,15 +78,15 @@ The C++ version of PhotonLib uses the Units library. For more information, see [
```{eval-rst}
.. tab-set-code::
.. code-block:: Java
.. code-block:: java
double distanceToTarget = PhotonUtils.getDistanceToPose(robotPose, targetPose);
.. code-block:: C++
.. code-block:: c++
//TODO
.. code-block:: Python
.. code-block:: python
# Coming Soon!
```
@@ -97,19 +97,19 @@ You can get a [translation](https://docs.wpilib.org/en/latest/docs/software/adva
```{eval-rst}
.. tab-set-code::
.. code-block:: Java
.. code-block:: java
// Calculate a translation from the camera to the target.
Translation2d translation = PhotonUtils.estimateCameraToTargetTranslation(
distanceMeters, Rotation2d.fromDegrees(-target.getYaw()));
.. code-block:: C++
.. code-block:: c++
// Calculate a translation from the camera to the target.
frc::Translation2d translation = photonlib::PhotonUtils::EstimateCameraToTargetTranslation(
distance, frc::Rotation2d(units::degree_t(-target.GetYaw())));
.. code-block:: Python
.. code-block:: python
# Coming Soon!
@@ -125,14 +125,14 @@ We are negating the yaw from the camera from CV (computer vision) conventions to
```{eval-rst}
.. tab-set-code::
.. code-block:: Java
.. code-block:: java
Rotation2d targetYaw = PhotonUtils.getYawToPose(robotPose, targetPose);
.. code-block:: C++
.. code-block:: c++
//TODO
.. code-block:: Python
.. code-block:: python
# Coming Soon!
```

View File

@@ -75,15 +75,15 @@ If you would like to access your Ethernet-connected vision device from a compute
```{eval-rst}
.. tab-set-code::
.. code-block:: Java
.. code-block:: java
PortForwarder.add(5800, "photonvision.local", 5800);
.. code-block:: C++
.. code-block:: c++
wpi::PortForwarder::GetInstance().Add(5800, "photonvision.local", 5800);
.. code-block:: Python
.. code-block:: python
# Coming Soon!
```
@@ -99,3 +99,7 @@ The camera streams start at 1181 with two ports for each camera (ex. 1181 and 11
:::{warning}
If your camera stream isn't sent to the same port as it's originally found on, its stream will not be visible in the UI.
:::
## SSH Access
For advanced users, SSH access is available for coprocessors running PhotonVision. This allows you to perform advanced configurations and troubleshooting. The default credentials are: `photon:vision` for all devices using an image of `v2026.0.3` or later. The legacy credentials of `pi:raspberry` will still work, but it's recommended to switch to the new credentials as the old ones will be deprecated in a future release.

View File

@@ -1,22 +1,30 @@
# Quick Install
# Quick Installation Guide
## Install the latest image of photonvision for your coprocessor
- For the following supported coprocessors
- {ref}`Raspberry Pi 3,4,5 <docs/quick-start/quick-install:Raspberry Pi and Orange Pi Installation>`
- {ref}`Orange Pi 5, 5B, 5 Pro <docs/quick-start/quick-install:Raspberry Pi and Orange Pi Installation>`
- {ref}`Limelight 2, 2+, 3, 3G, 4 <docs/quick-start/quick-install:LimeLight Installation>`
- {ref}`Rubik Pi 3 <docs/quick-start/quick-install:Rubik Pi 3 Installation>`
- For the supported coprocessors
- RPI 3,4,5
- Orange Pi 5
- Limelight
For installing on non-supported devices {ref}`see. <docs/advanced-installation/sw_install/index:Software Installation>`
For installing on non-supported devices {ref}`see here. <docs/advanced-installation/sw_install/index:Software Installation>`
[Download the latest preconfigured image of photonvision for your coprocessor](https://github.com/PhotonVision/photonvision/releases/latest)
| Coprocessor | Image filename | Jar |
| -------------------- | ---------------------------------------------------- | ------------------------------------- |
| OrangePi 5 | photonvision-{version}-linuxarm64_orangepi5.img.xz | photonvision-{version}-linuxarm64.jar |
| Raspberry Pi 3, 4, 5 | photonvision-{version}-linuxarm64_RaspberryPi.img.xz | photonvision-{version}-linuxarm64.jar |
| Limelight 2 | photonvision-{version}-linuxarm64_limelight2.img.xz | photonvision-{version}-linuxarm64.jar |
| Limelight 3 | photonvision-{version}-linuxarm64_limelight3.img.xz | photonvision-{version}-linuxarm64.jar |
| Coprocessor | Image filename | Jar |
| -------------------- | -------------------------------------------------------- | ------------------------------------- |
| Raspberry Pi 3, 4, 5 | photonvision-{version}-linuxarm64_RaspberryPi.img.xz | photonvision-{version}-linuxarm64.jar |
| OrangePi 5 | photonvision-{version}-linuxarm64_orangepi5.img.xz | photonvision-{version}-linuxarm64.jar |
| OrangePi 5B | photonvision-{version}-linuxarm64_orangepi5b.img.xz | photonvision-{version}-linuxarm64.jar |
| OrangePi 5 Pro | photonvision-{version}-linuxarm64_orangepi5pro.img.xz | photonvision-{version}-linuxarm64.jar |
| Limelight 2 | photonvision-{version}-linuxarm64_limelight2.img.xz | photonvision-{version}-linuxarm64.jar |
| Limelight 3 | photonvision-{version}-linuxarm64_limelight3.img.xz | photonvision-{version}-linuxarm64.jar |
| Limelight 3G | photonvision-{version}-linuxarm64_limelight3G.img.xz | photonvision-{version}-linuxarm64.jar |
| Limelight 4 | photonvision-{version}-linuxarm64_limelight4.img.xz | photonvision-{version}-linuxarm64.jar |
| Rubik Pi 3 | photonvision-{version}-linuxarm64_rubikpi3.tar.xz | photonvision-{version}-linuxarm64.jar |
Unless otherwise noted in release notes or if updating from the prior years version, to update PhotonVision after the initial installation, use the offline update option in the settings page with the downloaded jar file from the latest release.
## Raspberry Pi and Orange Pi Installation
Use the [Raspberry Pi Imager](https://www.raspberrypi.com/software/) to flash the image onto the coprocessors microSD card. Select the downloaded `.img.xz` file, select your microSD card, and flash.
@@ -24,10 +32,25 @@ Use the [Raspberry Pi Imager](https://www.raspberrypi.com/software/) to flash th
Balena Etcher can also be used, but historically has had issues such as bootlooping (the system will repeatedly boot and restart) when imaging your device. Use at your own risk.
:::
Limelights have a different installation processes. Simply connect the limelight to your computer using the proper usb cable. Select the compute module. If it doesnt 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 Installation
Unless otherwise noted in release notes or if updating from the prior years version, to update PhotonVision after the initial installation, use the offline update option in the settings page with the downloaded jar file from the latest release.
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.
| 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. Currently only limelight 2 and 2+ files are available.
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
:::{warning}
The Qualcomm Launcher caches files. If you flash multiple times, you may need to clear the cache by navigating to your temp directory, and deleting the `qualcomm-launcher` folder.
:::
To flash the Rubik Pi 3 coprocessor, it's necessary to use the [Qualcomm Launcher](https://softwarecenter.qualcomm.com/catalog/item/Qualcomm_Launcher). Upload a custom image by selecting the *Custom* option in the launcher. Choose the downloaded PhotonVision `.tar.xz` file and follow the prompts to complete the installation. It is recommended to skip the *Configure Login* process, as PhotonVision will handle the necessary settings.

View File

@@ -54,7 +54,7 @@ A `VisionSystemSim` represents the simulated world for one or more cameras, and
```{eval-rst}
.. tab-set-code::
.. code-block:: Java
.. code-block:: java
// A vision system sim labelled as "main" in NetworkTables
VisionSystemSim visionSim = new VisionSystemSim("main");
@@ -67,7 +67,7 @@ Vision targets require a `TargetModel`, which describes the shape of the target.
```{eval-rst}
.. tab-set-code::
.. code-block:: Java
.. code-block:: java
// A 0.5 x 0.25 meter rectangular target
TargetModel targetModel = new TargetModel(0.5, 0.25);
@@ -78,7 +78,7 @@ These `TargetModel` are paired with a target pose to create a `VisionTargetSim`.
```{eval-rst}
.. tab-set-code::
.. code-block:: Java
.. code-block:: java
// The pose of where the target is on the field.
// Its rotation determines where "forward" or the target x-axis points.
@@ -100,7 +100,7 @@ For convenience, an `AprilTagFieldLayout` can also be added to automatically cre
```{eval-rst}
.. tab-set-code::
.. code-block:: Java
.. code-block:: java
// The layout of AprilTags which we want to add to the vision system
AprilTagFieldLayout tagLayout = AprilTagFieldLayout.loadFromResource(AprilTagFields.kDefaultField.m_resourceFile);
@@ -121,7 +121,7 @@ Before adding a simulated camera, we need to define its properties. This is done
```{eval-rst}
.. tab-set-code::
.. code-block:: Java
.. code-block:: java
// The simulated camera properties
SimCameraProperties cameraProp = new SimCameraProperties();
@@ -132,7 +132,7 @@ By default, this will create a 960 x 720 resolution camera with a 90 degree diag
```{eval-rst}
.. tab-set-code::
.. code-block:: Java
.. code-block:: java
// A 640 x 480 camera with a 100 degree diagonal FOV.
cameraProp.setCalibration(640, 480, Rotation2d.fromDegrees(100));
@@ -150,7 +150,7 @@ These properties are used in a `PhotonCameraSim`, which handles generating captu
```{eval-rst}
.. tab-set-code::
.. code-block:: Java
.. code-block:: java
// The PhotonCamera used in the real robot code.
PhotonCamera camera = new PhotonCamera("cameraName");
@@ -164,7 +164,7 @@ The `PhotonCameraSim` can now be added to the `VisionSystemSim`. We have to defi
```{eval-rst}
.. tab-set-code::
.. code-block:: Java
.. code-block:: java
// Our camera is mounted 0.1 meters forward and 0.5 meters up from the robot pose,
// (Robot pose is considered the center of rotation at the floor level, or Z = 0)
@@ -186,7 +186,7 @@ If the camera is mounted on a mobile mechanism (like a turret) this transform ca
```{eval-rst}
.. tab-set-code::
.. code-block:: Java
.. code-block:: java
// The turret the camera is mounted on is rotated 5 degrees
Rotation3d turretRotation = new Rotation3d(0, 0, Math.toRadians(5));
@@ -203,7 +203,7 @@ To update the `VisionSystemSim`, we simply have to pass in the simulated robot p
```{eval-rst}
.. tab-set-code::
.. code-block:: Java
.. code-block:: java
// Update with the simulated drivetrain pose. This should be called every loop in simulation.
visionSim.update(robotPoseMeters);
@@ -218,7 +218,7 @@ Each `VisionSystemSim` has its own built-in `Field2d` for displaying object pose
```{eval-rst}
.. tab-set-code::
.. code-block:: Java
.. code-block:: java
// Get the built-in Field2d used by this VisionSystemSim
visionSim.getDebugField();
@@ -233,7 +233,7 @@ A `PhotonCameraSim` can also draw and publish generated camera frames to a MJPEG
```{eval-rst}
.. tab-set-code::
.. code-block:: Java
.. code-block:: java
// Enable the raw and processed streams. These are enabled by default.
cameraSim.enableRawStream(true);

View File

@@ -122,11 +122,13 @@ systemctl status photonvision
View the PhotonVision logs:
```
journalctl -u photonvision
journalctl --output cat -u photonvision
```
View the PhotonVision logs in real-time:
```
journalctl -u photonvision -f
journalctl --output cat -u photonvision -f
```
`--output cat` is used to prevent journalctl from printing its own timestamps, because we log our own timestamps.

View File

@@ -127,6 +127,7 @@ docs/troubleshooting/index
docs/additional-resources/best-practices
docs/additional-resources/config
docs/additional-resources/nt-api
docs/benchmarks/index
docs/contributing/index
```

Binary file not shown.

View File

@@ -1,6 +1,6 @@
distributionBase=GRADLE_USER_HOME
distributionPath=permwrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.11-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.3-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME

9
gradlew vendored
View File

@@ -86,8 +86,7 @@ done
# shellcheck disable=SC2034
APP_BASE_NAME=${0##*/}
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s
' "$PWD" ) || exit
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD=maximum
@@ -115,7 +114,7 @@ case "$( uname )" in #(
NONSTOP* ) nonstop=true ;;
esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
CLASSPATH="\\\"\\\""
# Determine the Java command to use to start the JVM.
@@ -206,7 +205,7 @@ fi
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Collect all arguments for the java command:
# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
# and any embedded shellness will be escaped.
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
# treated as '${Hostname}' itself on the command line.
@@ -214,7 +213,7 @@ DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
set -- \
"-Dorg.gradle.appname=$APP_BASE_NAME" \
-classpath "$CLASSPATH" \
org.gradle.wrapper.GradleWrapperMain \
-jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \
"$@"
# Stop when "xargs" is not available.

4
gradlew.bat vendored
View File

@@ -70,11 +70,11 @@ goto fail
:execute
@rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
set CLASSPATH=
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %*
:end
@rem End local scope for the variables with windows NT shell

View File

@@ -1 +1,2 @@
src/assets/fonts/PromptRegular.ts
pnpm-lock.yaml

File diff suppressed because it is too large Load Diff

View File

@@ -5,9 +5,8 @@
"type": "module",
"scripts": {
"dev": "vite",
"build": "run-p build-only",
"preview": "vite preview --port 4173",
"build-only": "vite build",
"build": "vite build",
"build-demo": "vite build --mode demo",
"lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix",
"format": "prettier --write src/",
@@ -15,33 +14,32 @@
"format-ci": "prettier --check src/"
},
"dependencies": {
"@eslint/js": "^9.26.0",
"@fontsource/prompt": "^5.2.6",
"@mdi/font": "^7.4.47",
"@msgpack/msgpack": "^3.1.1",
"@vitejs/plugin-vue": "^5.2.3",
"axios": "^1.9.0",
"@msgpack/msgpack": "^3.1.2",
"axios": "^1.11.0",
"jspdf": "^3.0.1",
"pinia": "^3.0.2",
"three": "^0.176.0",
"vite-plugin-vuetify": "^2.1.1",
"three": "^0.178.0",
"vue": "^3.5.13",
"vue-router": "^4.5.1",
"vue3-virtual-scroll-list": "^0.2.1",
"vuetify": "^3.8.3"
},
"devDependencies": {
"@eslint/js": "^9.31.0",
"@types/node": "^22.15.14",
"@types/three": "^0.176.0",
"@types/three": "^0.178.0",
"@vitejs/plugin-vue": "^6.0.0",
"@vue/eslint-config-prettier": "^10.2.0",
"@vue/eslint-config-typescript": "^14.5.0",
"@vue/tsconfig": "^0.7.0",
"eslint": "^9.26.0",
"eslint-plugin-vue": "^10.1.0",
"npm-run-all": "^4.1.5",
"prettier": "^3.5.3",
"sass": "^1.87.0",
"eslint": "^9.31.0",
"eslint-plugin-vue": "^10.3.0",
"prettier": "^3.6.2",
"sass": "^1.89.2",
"typescript": "^5.8.3",
"vite": "^6.3.5"
"vite": "^7.0.5",
"vite-plugin-vuetify": "^2.1.1"
}
}

2853
photon-client/pnpm-lock.yaml generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -3,10 +3,12 @@ import { useStateStore } from "@/stores/StateStore";
import { useSettingsStore } from "@/stores/settings/GeneralSettingsStore";
import { useCameraSettingsStore } from "@/stores/settings/CameraSettingsStore";
import { AutoReconnectingWebsocket } from "@/lib/AutoReconnectingWebsocket";
import { inject } from "vue";
import { inject, onBeforeMount } from "vue";
import PhotonSidebar from "@/components/app/photon-sidebar.vue";
import PhotonLogView from "@/components/app/photon-log-view.vue";
import PhotonErrorSnackbar from "@/components/app/photon-error-snackbar.vue";
import { useTheme } from "vuetify";
import { restoreThemeConfig } from "@/lib/ThemeManager";
const is_demo = import.meta.env.MODE === "demo";
if (!is_demo) {
@@ -50,6 +52,11 @@ if (!is_demo) {
);
useStateStore().$patch({ websocket: websocket });
}
const theme = useTheme();
onBeforeMount(() => {
restoreThemeConfig(theme);
});
</script>
<template>
@@ -73,8 +80,9 @@ if (!is_demo) {
<style lang="scss">
@use "@/assets/styles/settings";
@use "@/assets/styles/variables";
@use "sass:map";
@media #{map-get(settings.$display-breakpoints, 'md-and-down')} {
@media #{map.get(settings.$display-breakpoints, 'md-and-down')} {
html {
font-size: 14px !important;
}
@@ -86,30 +94,26 @@ if (!is_demo) {
}
::-webkit-scrollbar-track {
background: #232c37;
background: rgb(var(--v-theme-background));
}
::-webkit-scrollbar-thumb {
background-color: #ffd843;
background-color: rgb(var(--v-theme-accent));
border-radius: 10px;
}
::-webkit-scrollbar-thumb:hover {
background-color: #e4c33c;
background-color: rgb(var(--v-theme-primary));
}
.main-container {
background-color: #232c37;
padding: 0 !important;
}
.v-overlay__scrim {
background-color: #202020;
background-color: #111111;
}
#title {
color: #ffd843;
}
div.v-layout {
overflow: unset !important;
}

View File

@@ -1,4 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100" preserveAspectRatio="xMidYMid" width="200" height="200" style="shape-rendering: auto; display: block; background: rgb(0, 100, 146);" xmlns:xlink="http://www.w3.org/1999/xlink"><g><g transform="translate(80,50)">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100" preserveAspectRatio="xMidYMid" width="200" height="200" style="shape-rendering: auto; display: block; background: rgba(0, 100, 146, 0);" xmlns:xlink="http://www.w3.org/1999/xlink"><g><g transform="translate(80,50)">
<g transform="rotate(0)">
<circle fill-opacity="1" fill="#ffd943" r="6" cy="0" cx="0">
<animateTransform repeatCount="indefinite" dur="0.9345794392523364s" keyTimes="0;1" values="1.5 1.5;1 1" begin="-0.8177570093457943s" type="scale" attributeName="transform"></animateTransform>

Before

Width:  |  Height:  |  Size: 4.3 KiB

After

Width:  |  Height:  |  Size: 4.3 KiB

View File

@@ -0,0 +1,25 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg width="100%" height="100%" viewBox="0 0 508 507" version="1.1" xmlns="http://www.w3.org/2000/svg"
xml:space="preserve"
style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
<g transform="matrix(1,0,0,1,-1279,0)">
<g id="PhotonVision-Icon-BG" transform="matrix(0.264062,0,0,0.469444,1279.5,0)">
<rect x="0" y="0" width="1920" height="1080" style="fill:none;"/>
<clipPath id="_clip1">
<rect x="0" y="0" width="1920" height="1080"/>
</clipPath>
<g clip-path="url(#_clip1)">
<g transform="matrix(4.27015,0,0,2.40196,-20444.8,-3235.56)">
<circle cx="5012.55" cy="1571.77" r="224.918" style="fill:rgb(0,100,146,0);"/>
</g>
<g transform="matrix(4.95901,0,0,2.78944,-13955,-10313.5)">
<path d="M3055.09,3977.51C3050.3,3984.25 3045,3990.56 3039.21,3996.35C2987.91,4047.65 2917.1,4038.77 2881.16,3976.54C2845.23,3914.3 2857.71,3822.13 2909.01,3770.83C2960.31,3719.53 3031.13,3728.41 3067.06,3790.64C3069.85,3795.48 3072.35,3800.49 3074.56,3805.67L3039.78,3811.64C3012.82,3769.64 2962.9,3764.58 2926.45,3801.04C2888.89,3838.59 2879.76,3906.07 2906.07,3951.63C2932.37,3997.19 2984.22,4003.69 3021.77,3966.14L3021.89,3966.01L3055.09,3977.51ZM3085.02,3841.47C3090.86,3875.56 3086.6,3912.35 3073.22,3944.57L3043.91,3934.42C3056.74,3907.59 3060.53,3875.54 3054.13,3846.78L3085.02,3841.47Z" style="fill:white;"/>
</g>
<g transform="matrix(4.95901,0,0,2.78944,-13955,-3827.86)">
<path d="M2906.78,1571.77L3111.02,1642.48L3116.61,1626.34L3147.2,1664.74L3099.42,1675.99L3105,1659.86L2910.03,1592.35C2908.25,1585.69 2907.18,1578.77 2906.78,1571.77ZM2917.45,1517.07L3114.77,1483.17L3111.88,1466.34L3157.2,1485.21L3120.78,1518.13L3117.88,1501.3L2910.22,1536.97C2911.99,1530.09 2914.41,1523.4 2917.45,1517.07Z" style="fill:rgb(255,216,67);"/>
</g>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.2 KiB

View File

@@ -1,9 +1,9 @@
@import "@fontsource/prompt";
@use "@fontsource/prompt";
$default-font: "Prompt", sans-serif !default;
$body-font-family: $default-font;
$heading-font-family: $default-font;
$body-background: #282c34;
$body-background: rgb(var(--v-theme-background));
body {
background: $body-background;
@@ -21,13 +21,54 @@ html {
> table
> tbody
> tr:hover:not(.v-data-table__expanded__content):not(.v-data-table__empty-wrapper) {
background: #005281 !important;
background: rgba(0, 0, 0, 0.2);
}
.v-card__title {
.v-card-title,
.v-dialog > .v-overlay__content > .v-card > .v-card-title,
.v-dialog > .v-overlay__content > form > .v-card > .v-card-title {
padding: 20px;
word-break: break-word !important;
}
.v-card-text,
.v-dialog > .v-overlay__content > .v-card > .v-card-text,
.v-dialog > .v-overlay__content > form > .v-card > .v-card-text {
font-size: 1rem;
padding: 20px;
}
.v-card-subtitle,
.v-dialog > .v-overlay__content > .v-card > .v-card-subtitle,
.v-dialog > .v-overlay__content > form > .v-card > .v-card-subtitle {
font-size: 1rem;
padding: 20px;
}
.v-field__input {
padding: 0px !important;
}
.pb-10px {
padding-bottom: 10px !important;
}
.pt-10px {
padding-top: 10px !important;
}
.pl-10px {
padding-left: 10px !important;
}
.pr-10px {
padding-right: 10px !important;
}
.pa-10px {
padding: 10px !important;
}
.rounded-12 {
border-radius: 12px;
}

View File

@@ -1,21 +1,24 @@
<script setup lang="ts">
import type { PhotonTarget } from "@/types/PhotonTrackingTypes";
// @ts-expect-error Intellisense says these conflict with the dynamic imports below
import type { Mesh, Object3D, PerspectiveCamera, Scene, WebGLRenderer } from "three";
// @ts-expect-error Intellisense says these conflict with the dynamic imports below
import type { TrackballControls } from "three/examples/jsm/controls/TrackballControls";
import { onBeforeUnmount, onMounted, watchEffect } from "vue";
import {
const {
ArrowHelper,
BoxGeometry,
Color,
ConeGeometry,
Mesh,
MeshNormalMaterial,
type Object3D,
PerspectiveCamera,
Quaternion,
Scene,
Vector3,
Scene,
WebGLRenderer
} from "three";
import { TrackballControls } from "three/examples/jsm/controls/TrackballControls";
} = await import("three");
const { TrackballControls } = await import("three/examples/jsm/controls/TrackballControls");
const props = defineProps<{
targets: PhotonTarget[];
@@ -114,7 +117,7 @@ const resetCamThirdPerson = () => {
}
};
onMounted(() => {
onMounted(async () => {
scene = new Scene();
camera = new PerspectiveCamera(75, 800 / 800, 0.1, 1000);

View File

@@ -2,10 +2,10 @@
import { computed, inject, ref, onBeforeUnmount } from "vue";
import { useStateStore } from "@/stores/StateStore";
import { useCameraSettingsStore } from "@/stores/settings/CameraSettingsStore";
import loadingImage from "@/assets/images/loading-transparent.svg";
import type { StyleValue } from "vue";
import PvIcon from "@/components/common/pv-icon.vue";
import type { UiCameraConfiguration } from "@/types/SettingTypes";
import PvLoading from "@/components/common/pv-loading.vue";
const props = defineProps<{
streamType: "Raw" | "Processed";
@@ -92,7 +92,7 @@ onBeforeUnmount(() => {
<template>
<div class="stream-container" :style="containerStyle">
<img :src="loadingImage" class="stream-loading" />
<pv-loading class="stream-loading" />
<img
:id="id"
ref="mjpgStream"
@@ -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"

View File

@@ -6,6 +6,7 @@ import { useStateStore } from "@/stores/StateStore";
<v-snackbar
v-model="useStateStore().snackbarData.show"
location="top"
variant="elevated"
:color="useStateStore().snackbarData.color"
:timeout="useStateStore().snackbarData.timeout"
>

View File

@@ -22,3 +22,8 @@ const logColorClass = computed<string>(() => {
<template>
<div :class="logColorClass">[{{ source.timestamp.toTimeString().split(" ")[0] }}] {{ source.message }}</div>
</template>
<style scoped>
div {
white-space: pre-wrap;
}
</style>

View File

@@ -74,7 +74,7 @@ document.addEventListener("keydown", (e) => {
<template>
<v-dialog v-model="useStateStore().showLogModal" width="1500" dark>
<v-card class="dialog-container pa-6" color="primary" flat>
<v-card class="dialog-container pa-5" color="surface" flat>
<!-- Logs header -->
<v-row class="pb-3">
<v-col cols="4">
@@ -82,7 +82,7 @@ document.addEventListener("keydown", (e) => {
</v-col>
<v-col class="align-self-center pl-3" style="text-align: right">
<v-btn variant="text" color="white" @click="handleLogExport">
<v-icon start class="menu-icon"> mdi-download </v-icon>
<v-icon start class="menu-icon" size="large"> mdi-download </v-icon>
<span class="menu-label">Download</span>
<!-- Special hidden link that gets 'clicked' when the user exports journalctl logs -->
@@ -95,11 +95,11 @@ document.addEventListener("keydown", (e) => {
/>
</v-btn>
<v-btn variant="text" color="white" @click="handleLogClear">
<v-icon start class="menu-icon"> mdi-trash-can-outline </v-icon>
<v-icon start class="menu-icon" size="large"> mdi-trash-can-outline </v-icon>
<span class="menu-label">Clear Client Logs</span>
</v-btn>
<v-btn variant="text" color="white" @click="() => (useStateStore().showLogModal = false)">
<v-icon start class="menu-icon"> mdi-close </v-icon>
<v-icon start class="menu-icon" size="large"> mdi-close </v-icon>
<span class="menu-label">Close</span>
</v-btn>
</v-col>
@@ -110,26 +110,31 @@ document.addEventListener("keydown", (e) => {
<div class="dialog-data">
<!-- Log view options -->
<v-row no-gutters class="pt-4 pt-md-0" style="display: flex; justify-content: space-between">
<v-col cols="12" md="7" style="display: flex; align-items: center">
<v-col cols="12" md="7" style="display: flex; align-items: center" class="pr-3">
<v-text-field
v-model="searchQuery"
density="compact"
clearable
hide-details="auto"
prepend-icon="mdi-magnify"
color="accent"
color="primary"
label="Search"
variant="underlined"
/>
<input v-model="timeInput" type="time" step="1" class="text-white pl-3" />
<v-btn icon variant="flat" @click="timeInput = undefined">
<v-icon>mdi-close-circle-outline</v-icon>
<v-icon>mdi-close</v-icon>
</v-btn>
</v-col>
<v-col v-for="level in [0, 1, 2, 3]" :key="level" class="pr-3">
<div class="pb-0 pt-0" style="display: flex; align-items: center; flex: min-content">
{{ getLogLevelFromIndex(level)
}}<v-switch v-model="selectedLogLevels[level]" class="pl-2" hide-details color="#ffd843"></v-switch>
}}<v-switch
v-model="selectedLogLevels[level]"
class="pl-2"
hide-details
color="rgb(var(--v-theme-primary))"
></v-switch>
</div>
</v-col>
</v-row>
@@ -168,9 +173,9 @@ document.addEventListener("keydown", (e) => {
.log-display {
/* Dialog data size - options */
height: calc(100% - 66px);
height: calc(100% - 56px);
padding: 10px;
background-color: #232c37 !important;
background-color: rgb(var(--v-theme-logsBackground)) !important;
border-radius: 5px;
}

View File

@@ -3,9 +3,9 @@ import { computed } from "vue";
import { useSettingsStore } from "@/stores/settings/GeneralSettingsStore";
import { useStateStore } from "@/stores/StateStore";
import { useCameraSettingsStore } from "@/stores/settings/CameraSettingsStore";
import { PlaceholderCameraSettings } from "@/types/SettingTypes";
import { useRoute } from "vue-router";
import { useDisplay } from "vuetify";
import { useDisplay, useTheme } from "vuetify";
import { toggleTheme } from "@/lib/ThemeManager";
const compact = computed<boolean>({
get: () => {
@@ -17,24 +17,19 @@ const compact = computed<boolean>({
});
const { mdAndUp } = useDisplay();
const renderCompact = computed<boolean>(() => compact.value || !mdAndUp.value);
const theme = useTheme();
const needsCamerasConfigured = computed<boolean>(() => {
return (
Object.values(useCameraSettingsStore().cameras).length === 0 ||
useCameraSettingsStore().cameras["PlaceHolder Name"] === PlaceholderCameraSettings
);
});
const renderCompact = computed<boolean>(() => compact.value || !mdAndUp.value);
</script>
<template>
<v-navigation-drawer permanent :rail="renderCompact" color="primary">
<v-list nav>
<v-navigation-drawer permanent :rail="renderCompact" color="sidebar">
<v-list nav color="primary">
<!-- List item for the heading; note that there are some tricks in setting padding and image width make things look right -->
<v-list-item :class="renderCompact ? 'pr-0 pl-0' : ''" style="display: flex; justify-content: center">
<template #prepend>
<img v-if="!renderCompact" class="logo" src="@/assets/images/logoLarge.svg" alt="large logo" />
<img v-else class="logo" src="@/assets/images/logoSmall.svg" alt="small logo" />
<img v-else class="logo" src="@/assets/images/logoSmallTransparent.svg" alt="small logo" />
</template>
</v-list-item>
@@ -50,12 +45,18 @@ const needsCamerasConfigured = computed<boolean>(() => {
<v-list-item
link
to="/cameraConfigs"
:class="{ cameraicon: needsCamerasConfigured && useRoute().path !== '/cameraConfigs' }"
:class="{
cameraicon: useCameraSettingsStore().needsCameraConfiguration && useRoute().path !== '/cameraConfigs'
}"
>
<template #prepend>
<v-icon :class="{ 'text-red': needsCamerasConfigured }">mdi-swap-horizontal-bold</v-icon>
<v-icon :class="{ 'text-red': useCameraSettingsStore().needsCameraConfiguration }"
>mdi-swap-horizontal-bold</v-icon
>
</template>
<v-list-item-title :class="{ 'text-red': needsCamerasConfigured }">Camera Matching</v-list-item-title>
<v-list-item-title :class="{ 'text-red': useCameraSettingsStore().needsCameraConfiguration }"
>Camera Matching</v-list-item-title
>
</v-list-item>
<v-list-item link to="/docs" prepend-icon="mdi-bookshelf">
<v-list-item-title>Documentation</v-list-item-title>
@@ -69,20 +70,35 @@ const needsCamerasConfigured = computed<boolean>(() => {
:prepend-icon="`mdi-chevron-${compact || !mdAndUp ? 'right' : 'left'}`"
@click="() => (compact = !compact)"
>
<v-list-item-title>Compact Mode</v-list-item-title>
<v-list-item-title>Compact</v-list-item-title>
</v-list-item>
<v-list-item
:prepend-icon="
useSettingsStore().network.runNTServer
? 'mdi-server'
: useStateStore().ntConnectionStatus.connected
? 'mdi-robot'
: 'mdi-robot-off'
"
link
:prepend-icon="theme.global.name.value === 'LightTheme' ? 'mdi-white-balance-sunny' : 'mdi-weather-night'"
@click="() => toggleTheme(theme)"
>
<v-list-item-title>Theme</v-list-item-title>
</v-list-item>
<v-list-item>
<template #prepend>
<v-icon
:icon="
useSettingsStore().network.runNTServer
? 'mdi-server'
: useStateStore().ntConnectionStatus.connected
? 'mdi-robot'
: 'mdi-robot-off'
"
:color="
useSettingsStore().network.runNTServer || useStateStore().ntConnectionStatus.connected
? '#00ff00'
: '#ff0000'
"
/>
</template>
<v-list-item-title v-if="useSettingsStore().network.runNTServer" v-show="!renderCompact" class="text-wrap">
NetworkTables server running for
<span class="text-accent">{{ useStateStore().ntConnectionStatus.clients || 0 }}</span> clients
<span class="text-primary">{{ useStateStore().ntConnectionStatus.clients || 0 }}</span> clients
</v-list-item-title>
<v-list-item-title
v-else-if="useStateStore().ntConnectionStatus.connected && useStateStore().backendConnected"
@@ -91,9 +107,7 @@ const needsCamerasConfigured = computed<boolean>(() => {
style="flex-direction: column; display: flex"
>
NetworkTables Server Connected!
<span class="text-accent">
{{ useStateStore().ntConnectionStatus.address }}
</span>
<span class="text-primary"> {{ useStateStore().ntConnectionStatus.address }} </span>
</v-list-item-title>
<v-list-item-title
v-else
@@ -104,10 +118,15 @@ const needsCamerasConfigured = computed<boolean>(() => {
Not connected to NetworkTables Server!
</v-list-item-title>
</v-list-item>
<v-list-item :prepend-icon="useStateStore().backendConnected ? 'mdi-server-network' : 'mdi-server-network-off'">
<v-list-item>
<template #prepend>
<v-icon
:icon="useStateStore().backendConnected ? 'mdi-server-network' : 'mdi-server-network-off'"
:color="useStateStore().backendConnected ? '#00ff00' : '#ff0000'"
/>
</template>
<v-list-item-title v-show="!renderCompact" class="text-wrap">
{{ useStateStore().backendConnected ? "Backend connected" : "Trying to connect to backend" }}
{{ useStateStore().backendConnected ? "Backend connected" : "Trying to connect to backend..." }}
</v-list-item-title>
</v-list-item>
</v-list>
@@ -116,6 +135,14 @@ const needsCamerasConfigured = computed<boolean>(() => {
</template>
<style scoped>
.v-navigation-drawer {
border: none;
}
.v-navigation-drawer--rail {
border: none;
}
.v-list-item-title {
font-size: 1rem !important;
line-height: 1.2rem !important;

View File

@@ -2,8 +2,6 @@
import { computed, ref } from "vue";
import { useCameraSettingsStore } from "@/stores/settings/CameraSettingsStore";
import { CalibrationBoardTypes, CalibrationTagFamilies, type VideoFormat } from "@/types/SettingTypes";
import JsPDF from "jspdf";
import { font as PromptRegular } from "@/assets/fonts/PromptRegular";
import MonoLogo from "@/assets/images/logoMono.png";
import CharucoImage from "@/assets/images/ChArUco_Marker8x8.png";
import PvSlider from "@/components/common/pv-slider.vue";
@@ -15,6 +13,12 @@ import { WebsocketPipelineType } from "@/types/WebsocketDataTypes";
import { getResolutionString, resolutionsAreEqual } from "@/lib/PhotonUtils";
import CameraCalibrationInfoCard from "@/components/cameras/CameraCalibrationInfoCard.vue";
import { useSettingsStore } from "@/stores/settings/GeneralSettingsStore";
import { useTheme } from "vuetify";
const PromptRegular = import("@/assets/fonts/PromptRegular");
const jspdf = import("jspdf");
const theme = useTheme();
const settingsValid = ref(true);
@@ -88,10 +92,12 @@ const tooManyPoints = computed(
() => useStateStore().calibrationData.imageCount * patternWidth.value * patternHeight.value > 700000
);
const downloadCalibBoard = () => {
const doc = new JsPDF({ unit: "in", format: "letter" });
const downloadCalibBoard = async () => {
const { jsPDF } = await jspdf;
const { font } = await PromptRegular;
const doc = new jsPDF({ unit: "in", format: "letter" });
doc.addFileToVFS("Prompt-Regular.tff", PromptRegular);
doc.addFileToVFS("Prompt-Regular.tff", font);
doc.addFont("Prompt-Regular.tff", "Prompt-Regular", "normal");
doc.setFont("Prompt-Regular");
doc.setFontSize(12);
@@ -128,10 +134,7 @@ const downloadCalibBoard = () => {
charucoImage.src = CharucoImage;
doc.addImage(charucoImage, "PNG", 0.25, 1.5, 8, 8);
doc.text("8 x 8 | 1in & 0.75in", paperWidth - 1, 1.0, {
maxWidth: (paperWidth - 2.0) / 2,
align: "right"
});
doc.text("8 x 8 | 1in & 0.75in", paperWidth - 1, 1.0, { maxWidth: (paperWidth - 2.0) / 2, align: "right" });
break;
}
@@ -179,8 +182,10 @@ const startCalibration = () => {
const showCalibEndDialog = ref(false);
const calibCanceled = ref(false);
const calibSuccess = ref<boolean | undefined>(undefined);
const calibEndpointFail = ref(false);
const endCalibration = () => {
calibSuccess.value = undefined;
calibEndpointFail.value = false;
if (!useStateStore().calibrationData.hasEnoughImages) {
calibCanceled.value = true;
@@ -193,7 +198,13 @@ const endCalibration = () => {
.then(() => {
calibSuccess.value = true;
})
.catch(() => {
.catch((e) => {
if (e.response) {
// Server returned a status code
} else if (e.request) {
// Something went wrong. Unsure if calibration actually worked
calibEndpointFail.value = true;
}
calibSuccess.value = false;
})
.finally(() => {
@@ -214,10 +225,10 @@ const setSelectedVideoFormat = (format: VideoFormat) => {
<template>
<div>
<v-card class="mb-3" color="primary" dark>
<v-card-title class="pa-6 pb-3">Camera Calibration</v-card-title>
<v-card-text v-show="!isCalibrating">
<v-card-subtitle class="pt-3 pl-2 pb-4 text-white">Current Calibration</v-card-subtitle>
<v-card class="mb-3 rounded-12" color="surface" dark>
<v-card-title>Camera Calibration</v-card-title>
<v-card-text v-if="!isCalibrating" class="pb-0">
<v-card-subtitle class="pa-0 pb-3 text-white">Current Calibrations</v-card-subtitle>
<v-table fixed-header height="100%" density="compact">
<thead>
<tr>
@@ -241,296 +252,303 @@ const setSelectedVideoFormat = (format: VideoFormat) => {
<v-tooltip location="bottom">
<template #activator="{ props }">
<td v-bind="props" @click="setSelectedVideoFormat(value)">
<v-icon size="small" class="mr-2">mdi-information</v-icon>
<v-icon size="small" color="primary">mdi-information</v-icon>
</td>
</template>
<span>Click for more info on this calibration.</span>
<span>View calibration information</span>
</v-tooltip>
</tr>
</tbody>
</v-table>
</v-card-text>
<v-card-text v-if="useCameraSettingsStore().isConnected" class="d-flex flex-column pa-6 pt-0">
<v-card-subtitle v-show="!isCalibrating" class="pl-0 pb-3 pt-3 text-white"
>Configure New Calibration</v-card-subtitle
>
<v-form ref="form" v-model="settingsValid">
<!-- TODO: the default videoFormatIndex is 0, but the list of unique video mode indexes might not include 0. getUniqueVideoResolutionStrings indexing is also different from the normal video mode indexing -->
<pv-select
v-model="useStateStore().calibrationData.videoFormatIndex"
label="Resolution"
:select-cols="8"
:disabled="isCalibrating"
tooltip="Resolution to calibrate at (you will have to calibrate every resolution you use 3D mode on)"
:items="getUniqueVideoResolutionStrings()"
/>
<pv-select
v-show="isCalibrating && boardType != CalibrationBoardTypes.Charuco"
v-model="useCameraSettingsStore().currentPipelineSettings.streamingFrameDivisor"
label="Decimation"
tooltip="Resolution to which camera frames are downscaled for detection. Calibration still uses full-res"
:items="calibrationDivisors"
:select-cols="8"
<v-card-text class="pt-0">
<div v-if="useCameraSettingsStore().isConnected" class="d-flex flex-column">
<v-card-subtitle v-if="!isCalibrating" class="pl-0 pb-3 pt-3 text-white"
>Configure New Calibration</v-card-subtitle
>
<v-form ref="form" v-model="settingsValid">
<v-alert
closable
density="compact"
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'tonal'"
:color="useSettingsStore().general.mrCalWorking ? 'buttonPassive' : 'error'"
:icon="useSettingsStore().general.mrCalWorking ? 'mdi-check' : 'mdi-close'"
:text="
useSettingsStore().general.mrCalWorking
? 'Mrcal was successfully loaded and will be used!'
: '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"
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()"
/>
<pv-select
v-model="boardType"
label="Board Type"
tooltip="Calibration board pattern to use"
:select-cols="8"
:items="['Chessboard', 'Charuco']"
:disabled="isCalibrating"
/>
<pv-select
v-if="boardType !== CalibrationBoardTypes.Charuco"
v-model="useCameraSettingsStore().currentPipelineSettings.streamingFrameDivisor"
label="Decimation"
tooltip="Resolution to which camera frames are downscaled for detection. Calibration still uses full-res"
:items="calibrationDivisors"
:select-cols="8"
@update:modelValue="
(v) => useCameraSettingsStore().changeCurrentPipelineSetting({ streamingFrameDivisor: +v }, false)
"
/>
<pv-select
v-if="boardType === CalibrationBoardTypes.Charuco"
v-model="tagFamily"
label="Tag Family"
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"
/>
<pv-number-input
v-model="squareSizeIn"
label="Pattern Spacing (in)"
tooltip="Spacing between pattern features in inches"
:disabled="isCalibrating"
:rules="[(v) => v > 0 || 'Size must be positive']"
:label-cols="4"
/>
<pv-number-input
v-if="boardType === CalibrationBoardTypes.Charuco"
v-model="markerSizeIn"
label="Marker Size (in)"
tooltip="Size of the tag markers in inches must be smaller than pattern spacing"
:disabled="isCalibrating"
:rules="[(v) => v > 0 || 'Size must be positive']"
:label-cols="4"
/>
<pv-number-input
v-model="patternWidth"
label="Board Width (squares)"
tooltip="Width of the board in dots or chessboard squares"
:disabled="isCalibrating"
:rules="[(v) => v >= 4 || 'Width must be at least 4']"
:label-cols="4"
/>
<pv-number-input
v-model="patternHeight"
label="Board Height (squares)"
tooltip="Height of the board in dots or chessboard squares"
:disabled="isCalibrating"
:rules="[(v) => v >= 4 || 'Height must be at least 4']"
:label-cols="4"
/>
<pv-switch
v-if="boardType === CalibrationBoardTypes.Charuco"
v-model="useOldPattern"
label="Old OpenCV Pattern"
:disabled="isCalibrating"
tooltip="If enabled, Photon will use the old OpenCV pattern for calibration."
:label-cols="4"
/>
</v-form>
</div>
<div v-if="isCalibrating">
<pv-switch
v-model="drawAllSnapshots"
label="Draw Collected Corners"
:switch-cols="8"
tooltip="Draw all snapshots"
@update:modelValue="
(v) => useCameraSettingsStore().changeCurrentPipelineSetting({ streamingFrameDivisor: +v }, false)
(args) => useCameraSettingsStore().changeCurrentPipelineSetting({ drawAllSnapshots: args }, false)
"
/>
<pv-select
v-model="boardType"
label="Board Type"
tooltip="Calibration board pattern to use"
:select-cols="8"
:items="['Chessboard', 'Charuco']"
:disabled="isCalibrating"
/>
<pv-select
v-show="boardType == CalibrationBoardTypes.Charuco"
v-model="tagFamily"
label="Tag Family"
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"
/>
<pv-number-input
v-model="squareSizeIn"
label="Pattern Spacing (in)"
tooltip="Spacing between pattern features in inches"
:disabled="isCalibrating"
:rules="[(v) => v > 0 || 'Size must be positive']"
:label-cols="4"
/>
<pv-number-input
v-show="boardType == CalibrationBoardTypes.Charuco"
v-model="markerSizeIn"
label="Marker Size (in)"
tooltip="Size of the tag markers in inches must be smaller than pattern spacing"
:disabled="isCalibrating"
:rules="[(v) => v > 0 || 'Size must be positive']"
:label-cols="4"
/>
<pv-number-input
v-model="patternWidth"
label="Board Width (squares)"
tooltip="Width of the board in dots or chessboard squares"
:disabled="isCalibrating"
:rules="[(v) => v >= 4 || 'Width must be at least 4']"
:label-cols="4"
/>
<pv-number-input
v-model="patternHeight"
label="Board Height (squares)"
tooltip="Height of the board in dots or chessboard squares"
:disabled="isCalibrating"
:rules="[(v) => v >= 4 || 'Height must be at least 4']"
:label-cols="4"
/>
<pv-switch
v-show="boardType == CalibrationBoardTypes.Charuco"
v-model="useOldPattern"
label="Old OpenCV Pattern"
:disabled="isCalibrating"
tooltip="If enabled, Photon will use the old OpenCV pattern for calibration."
v-model="useCameraSettingsStore().currentPipelineSettings.cameraAutoExposure"
label="Auto Exposure"
:label-cols="4"
tooltip="Enables or Disables camera automatic adjustment for current lighting conditions"
@update:modelValue="
(args) => useCameraSettingsStore().changeCurrentPipelineSetting({ cameraAutoExposure: args }, false)
"
/>
<v-banner
v-if="useSettingsStore().general.mrCalWorking"
rounded
color="secondary"
text-color="white"
class="mt-3"
icon="mdi-alert-circle-outline"
<pv-slider
v-model="useCameraSettingsStore().currentPipelineSettings.cameraExposureRaw"
:disabled="useCameraSettingsStore().currentCameraSettings.pipelineSettings.cameraAutoExposure"
label="Exposure"
tooltip="Directly controls how long the camera shutter remains open. Units are dependant on the underlying driver."
:min="useCameraSettingsStore().minExposureRaw"
:max="useCameraSettingsStore().maxExposureRaw"
:slider-cols="8"
:step="1"
@update:modelValue="
(args) => useCameraSettingsStore().changeCurrentPipelineSetting({ cameraExposureRaw: args }, false)
"
/>
<pv-slider
v-model="useCameraSettingsStore().currentPipelineSettings.cameraBrightness"
label="Brightness"
:min="0"
:max="100"
:slider-cols="8"
@update:modelValue="
(args) => useCameraSettingsStore().changeCurrentPipelineSetting({ cameraBrightness: args }, false)
"
/>
<pv-slider
v-if="useCameraSettingsStore().currentPipelineSettings.cameraGain >= 0"
v-model="useCameraSettingsStore().currentPipelineSettings.cameraGain"
label="Camera Gain"
tooltip="Controls camera gain, similar to brightness"
:min="0"
:max="100"
:slider-cols="8"
@update:modelValue="
(args) => useCameraSettingsStore().changeCurrentPipelineSetting({ cameraGain: args }, false)
"
/>
<pv-slider
v-if="useCameraSettingsStore().currentPipelineSettings.cameraRedGain !== -1"
v-model="useCameraSettingsStore().currentPipelineSettings.cameraRedGain"
label="Red AWB Gain"
:min="0"
:max="100"
:slider-cols="8"
tooltip="Controls red automatic white balance gain, which affects how the camera captures colors in different conditions"
@update:modelValue="
(args) => useCameraSettingsStore().changeCurrentPipelineSetting({ cameraRedGain: args }, false)
"
/>
<pv-slider
v-if="useCameraSettingsStore().currentPipelineSettings.cameraBlueGain !== -1"
v-model="useCameraSettingsStore().currentPipelineSettings.cameraBlueGain"
label="Blue AWB Gain"
:min="0"
:max="100"
:slider-cols="8"
tooltip="Controls blue automatic white balance gain, which affects how the camera captures colors in different conditions"
@update:modelValue="
(args) => useCameraSettingsStore().changeCurrentPipelineSetting({ cameraBlueGain: args }, false)
"
/>
</div>
<div v-if="isCalibrating" class="d-flex justify-center align-center pt-10px pb-5">
<v-chip
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'tonal'"
label
:color="useStateStore().calibrationData.hasEnoughImages ? 'buttonPassive' : 'light-grey'"
>
Mrcal was successfully loaded and will be used!
</v-banner>
<v-banner v-else rounded color="error" text-color="white" class="mt-3" icon="mdi-alert-circle-outline">
MrCal JNI could not be loaded! Consult journalctl logs for additional details.
</v-banner>
</v-form>
</v-card-text>
<v-card-text v-if="isCalibrating" class="pa-6 pt-0">
<pv-switch
v-model="drawAllSnapshots"
label="Draw Collected Corners"
:switch-cols="8"
tooltip="Draw all snapshots"
@update:modelValue="
(args) => useCameraSettingsStore().changeCurrentPipelineSetting({ drawAllSnapshots: args }, false)
"
/>
<pv-switch
v-model="useCameraSettingsStore().currentPipelineSettings.cameraAutoExposure"
label="Auto Exposure"
:label-cols="4"
tooltip="Enables or Disables camera automatic adjustment for current lighting conditions"
@update:modelValue="
(args) => useCameraSettingsStore().changeCurrentPipelineSetting({ cameraAutoExposure: args }, false)
"
/>
<pv-slider
v-model="useCameraSettingsStore().currentPipelineSettings.cameraExposureRaw"
:disabled="useCameraSettingsStore().currentCameraSettings.pipelineSettings.cameraAutoExposure"
label="Exposure"
tooltip="Directly controls how long the camera shutter remains open. Units are dependant on the underlying driver."
:min="useCameraSettingsStore().minExposureRaw"
:max="useCameraSettingsStore().maxExposureRaw"
:slider-cols="7"
:step="1"
@update:modelValue="
(args) => useCameraSettingsStore().changeCurrentPipelineSetting({ cameraExposureRaw: args }, false)
"
/>
<pv-slider
v-model="useCameraSettingsStore().currentPipelineSettings.cameraBrightness"
label="Brightness"
:min="0"
:max="100"
:slider-cols="7"
@update:modelValue="
(args) => useCameraSettingsStore().changeCurrentPipelineSetting({ cameraBrightness: args }, false)
"
/>
<pv-slider
v-if="useCameraSettingsStore().currentPipelineSettings.cameraGain >= 0"
v-model="useCameraSettingsStore().currentPipelineSettings.cameraGain"
label="Camera Gain"
tooltip="Controls camera gain, similar to brightness"
:min="0"
:max="100"
:slider-cols="7"
@update:modelValue="
(args) => useCameraSettingsStore().changeCurrentPipelineSetting({ cameraGain: args }, false)
"
/>
<pv-slider
v-if="useCameraSettingsStore().currentPipelineSettings.cameraRedGain !== -1"
v-model="useCameraSettingsStore().currentPipelineSettings.cameraRedGain"
label="Red AWB Gain"
:min="0"
:max="100"
:slider-cols="7"
tooltip="Controls red automatic white balance gain, which affects how the camera captures colors in different conditions"
@update:modelValue="
(args) => useCameraSettingsStore().changeCurrentPipelineSetting({ cameraRedGain: args }, false)
"
/>
<pv-slider
v-if="useCameraSettingsStore().currentPipelineSettings.cameraBlueGain !== -1"
v-model="useCameraSettingsStore().currentPipelineSettings.cameraBlueGain"
label="Blue AWB Gain"
:min="0"
:max="100"
:slider-cols="7"
tooltip="Controls blue automatic white balance gain, which affects how the camera captures colors in different conditions"
@update:modelValue="
(args) => useCameraSettingsStore().changeCurrentPipelineSetting({ cameraBlueGain: args }, false)
"
/>
<v-banner
Snapshots: {{ useStateStore().calibrationData.imageCount }} of at least
{{ useStateStore().calibrationData.minimumImageCount }}
</v-chip>
</div>
<div>
<v-btn
color="buttonPassive"
size="small"
block
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
:disabled="!settingsValid"
@click="downloadCalibBoard"
>
<v-icon start class="calib-btn-icon" size="large"> mdi-download </v-icon>
<span class="calib-btn-label">Generate Board</span>
</v-btn>
</div>
<v-alert
v-if="tooManyPoints"
rounded
class="mt-3"
class="mt-5"
color="error"
text-color="white"
density="compact"
text="Too many corners. Finish calibration now!"
icon="mdi-alert-circle-outline"
>
Too many corners. Finish calibration now!
</v-banner>
</v-card-text>
<v-card-text v-if="isCalibrating" class="d-flex justify-center align-center pa-6 pt-0">
<v-chip
variant="flat"
label
:color="useStateStore().calibrationData.hasEnoughImages ? 'secondary' : 'grey-darken-2'"
>
Snapshots: {{ useStateStore().calibrationData.imageCount }} of at least
{{ useStateStore().calibrationData.minimumImageCount }}
</v-chip>
</v-card-text>
<v-card-text class="d-flex pa-6 pt-0">
<v-col cols="6" class="pa-0 pr-2">
<v-btn
size="small"
block
color="secondary"
:disabled="!settingsValid || tooManyPoints"
@click="isCalibrating ? useCameraSettingsStore().takeCalibrationSnapshot() : startCalibration()"
>
<v-icon start class="calib-btn-icon"> {{ isCalibrating ? "mdi-camera" : "mdi-flag-outline" }} </v-icon>
<span class="calib-btn-label">{{ isCalibrating ? "Take Snapshot" : "Start Calibration" }}</span>
</v-btn>
</v-col>
<v-col cols="6" class="pa-0 pl-2">
<v-btn
size="small"
block
:color="useStateStore().calibrationData.hasEnoughImages ? 'accent' : 'error'"
:disabled="!isCalibrating || !settingsValid"
@click="endCalibration"
>
<v-icon start class="calib-btn-icon">
{{ useStateStore().calibrationData.hasEnoughImages ? "mdi-flag-checkered" : "mdi-flag-off-outline" }}
</v-icon>
<span class="calib-btn-label">{{
useStateStore().calibrationData.hasEnoughImages ? "Finish Calibration" : "Cancel Calibration"
}}</span>
</v-btn>
</v-col>
</v-card-text>
<v-card-text class="pa-6 pt-0">
<v-btn
color="accent"
size="small"
block
variant="outlined"
:disabled="!settingsValid"
@click="downloadCalibBoard"
>
<v-icon start class="calib-btn-icon"> mdi-download </v-icon>
<span class="calib-btn-label">Generate Board</span>
</v-btn>
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'tonal'"
/>
<div class="d-flex pt-5">
<v-col cols="6" class="pa-0 pr-2">
<v-btn
size="small"
block
color="buttonActive"
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
:disabled="!settingsValid || tooManyPoints"
@click="isCalibrating ? useCameraSettingsStore().takeCalibrationSnapshot() : startCalibration()"
>
<v-icon start class="calib-btn-icon" size="large">
{{ isCalibrating ? "mdi-camera" : "mdi-flag-outline" }}
</v-icon>
<span class="calib-btn-label">{{ isCalibrating ? "Take Snapshot" : "Start Calibration" }}</span>
</v-btn>
</v-col>
<v-col cols="6" class="pa-0 pl-2">
<v-btn
size="small"
block
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
:color="useStateStore().calibrationData.hasEnoughImages ? 'buttonActive' : 'error'"
:disabled="!isCalibrating || !settingsValid"
@click="endCalibration"
>
<v-icon start class="calib-btn-icon" size="large">
{{ useStateStore().calibrationData.hasEnoughImages ? "mdi-flag-checkered" : "mdi-flag-off-outline" }}
</v-icon>
<span class="calib-btn-label">{{
useStateStore().calibrationData.hasEnoughImages ? "Finish Calibration" : "Cancel Calibration"
}}</span>
</v-btn>
</v-col>
</div>
</v-card-text>
</v-card>
<v-dialog v-model="showCalibEndDialog" width="500px" :persistent="true">
<v-card color="primary" dark>
<v-card-title class="pb-8"> Camera Calibration </v-card-title>
<div class="ml-3">
<v-col style="text-align: center">
<template v-if="calibCanceled">
<v-icon color="blue" size="70"> mdi-cancel </v-icon>
<v-card-text
>Camera Calibration has been Canceled, the backend is attempting to cleanly cancel the calibration
process.</v-card-text
>
</template>
<!-- No result reported yet -->
<template v-else-if="calibSuccess === undefined">
<v-progress-circular indeterminate :size="70" :width="8" color="accent" />
<v-card-text>Camera is being calibrated. This process may take several minutes...</v-card-text>
</template>
<!-- Got positive result -->
<template v-else-if="calibSuccess">
<v-icon color="green" size="70"> mdi-check-bold </v-icon>
<v-card-text>
Camera has been successfully calibrated for
{{
getUniqueVideoResolutionStrings().find(
(v) => v.value === useStateStore().calibrationData.videoFormatIndex
)?.name
}}!
</v-card-text>
</template>
<template v-else>
<v-icon color="red" size="70"> mdi-close </v-icon>
<v-card-text
>Camera calibration failed! Make sure that the photos are taken such that the rainbow grid circles align
with the corners of the chessboard, and try again. More information is available in the program
logs.</v-card-text
>
</template>
</v-col>
<v-card color="surface" dark>
<v-card-title> Camera Calibration </v-card-title>
<div style="text-align: center">
<template v-if="calibCanceled">
<v-icon color="primary" size="70"> mdi-cancel </v-icon>
<v-card-text>
Camera calibration has been canceled. The backend is attempting to cleanly cancel the calibration process.
</v-card-text>
</template>
<!-- No result reported yet -->
<template v-else-if="calibSuccess === undefined">
<v-progress-circular indeterminate :size="70" :width="8" color="primary" />
<v-card-text>Camera is being calibrated. This process may take several minutes...</v-card-text>
</template>
<!-- Got positive result -->
<template v-else-if="calibSuccess">
<v-icon color="#00ff00" size="70"> mdi-check </v-icon>
<v-card-text>
Camera has been successfully calibrated for
{{
getUniqueVideoResolutionStrings().find(
(v) => v.value === useStateStore().calibrationData.videoFormatIndex
)?.name
}}!
</v-card-text>
</template>
<template v-else-if="calibEndpointFail">
<v-icon color="gray" size="70"> mdi-help-circle-outline </v-icon>
<v-card-text
>Unable to determine if calibration was successful. Refresh this page and manually check if calibration
was successful.</v-card-text
>
</template>
<template v-else>
<v-icon color="red" size="70"> mdi-close </v-icon>
<v-card-text>
Camera calibration failed! Make sure that the photos are taken such that the rainbow grid circles align
with the corners of the chessboard, and try again. More information is available in the program logs.
</v-card-text>
</template>
</div>
<v-card-actions>
<v-card-actions class="pa-5 pt-0">
<v-spacer />
<v-btn v-if="!isCalibrating" color="white" variant="text" @click="showCalibEndDialog = false"> OK </v-btn>
</v-card-actions>
@@ -543,18 +561,21 @@ const setSelectedVideoFormat = (format: VideoFormat) => {
</template>
<style scoped lang="scss">
th {
text-align: center !important;
padding: 0 8px !important;
}
.v-table {
text-align: center;
width: 100%;
th,
td {
background-color: #006492 !important;
font-size: 1rem !important;
}
tbody :hover td {
background-color: #005281 !important;
cursor: pointer;
}
@@ -570,7 +591,7 @@ const setSelectedVideoFormat = (format: VideoFormat) => {
}
::-webkit-scrollbar-thumb {
background-color: #ffd843;
background-color: rgb(var(--v-theme-accent));
border-radius: 10px;
}
}

View File

@@ -4,6 +4,9 @@ import { useCameraSettingsStore } from "@/stores/settings/CameraSettingsStore";
import { useStateStore } from "@/stores/StateStore";
import { computed, inject, ref } from "vue";
import { getResolutionString, parseJsonFile } from "@/lib/PhotonUtils";
import { useTheme } from "vuetify";
const theme = useTheme();
const props = defineProps<{
videoFormat: VideoFormat;
@@ -88,16 +91,20 @@ const exportCalibrationURL = computed<string>(() =>
const calibrationImageURL = (index: number) =>
useCameraSettingsStore().getCalImageUrl(inject<string>("backendHost") as string, props.videoFormat.resolution, index);
</script>
<template>
<v-card color="primary" dark>
<div class="d-flex flex-wrap pr-md-3">
<v-card color="surface" dark>
<div class="d-flex flex-wrap pt-2 pl-2 pr-2">
<v-col cols="12" md="6">
<v-card-title class="pl-3 pb-0 pb-md-4"> Calibration Details </v-card-title>
<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-btn color="secondary" style="width: 100%" @click="openUploadPhotonCalibJsonPrompt">
<v-icon start> mdi-import</v-icon>
<v-btn
color="buttonPassive"
style="width: 100%"
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
@click="openUploadPhotonCalibJsonPrompt"
>
<v-icon start size="large"> mdi-import</v-icon>
<span>Import</span>
</v-btn>
<input
@@ -110,12 +117,13 @@ const calibrationImageURL = (index: number) =>
</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="secondary"
color="buttonPassive"
:disabled="!currentCalibrationCoeffs"
style="width: 100%"
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
@click="openExportCalibrationPrompt"
>
<v-icon start>mdi-export</v-icon>
<v-icon start size="large">mdi-export</v-icon>
<span>Export</span>
</v-btn>
<a
@@ -126,21 +134,18 @@ const calibrationImageURL = (index: number) =>
/>
</v-col>
</div>
<v-card-title class="pl-6 pt-0 pb-0"
<v-card-title class="pt-0 pb-0"
>{{ useCameraSettingsStore().currentCameraName }}@{{ getResolutionString(videoFormat.resolution) }}</v-card-title
>
<v-card-text v-if="!currentCalibrationCoeffs">
<v-banner
rounded
bg-color="secondary"
color="secondary"
text-color="white"
class="pt-3 pb-3 mt-3"
<v-alert
class="pt-3 pb-3"
color="primary"
density="compact"
text="The selected video format has not been calibrated."
icon="mdi-alert-circle-outline"
>
The selected video format has not been calibrated.
</v-banner>
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'tonal'"
/>
</v-card-text>
<v-card-text class="pt-0">
<v-table density="compact" style="width: 100%">
@@ -248,8 +253,8 @@ const calibrationImageURL = (index: number) =>
</template>
</v-table>
</v-card-text>
<v-card-title v-if="currentCalibrationCoeffs" class="pt-0">Individual Observations</v-card-title>
<v-card-text v-if="currentCalibrationCoeffs">
<v-card-title v-if="currentCalibrationCoeffs" class="pt-0 pb-0">Individual Observations</v-card-title>
<v-card-text v-if="currentCalibrationCoeffs" class="pt-0">
<v-data-table
density="compact"
style="width: 100%"
@@ -287,9 +292,6 @@ const calibrationImageURL = (index: number) =>
</template>
<style scoped>
.v-data-table {
background-color: #006492 !important;
}
.snapshot-preview {
max-width: 55%;
}

View File

@@ -2,6 +2,9 @@
import { ref } from "vue";
import axios from "axios";
import { useStateStore } from "@/stores/StateStore";
import { useTheme } from "vuetify";
const theme = useTheme();
interface SnapshotMetadata {
snapshotName: string;
@@ -91,22 +94,39 @@ const expanded = ref([]);
</script>
<template>
<v-card style="background-color: #006492">
<v-card color="surface" class="rounded-12">
<v-card-title>Camera Control</v-card-title>
<v-card-text>
<v-btn color="secondary" @click="fetchSnapshots">
<v-icon start class="open-icon"> mdi-folder </v-icon>
<v-card-text class="pt-0">
<v-btn
color="buttonPassive"
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
@click="fetchSnapshots"
>
<v-icon start class="open-icon" size="large"> mdi-folder </v-icon>
<span class="open-label">Show Saved Snapshots</span>
</v-btn>
</v-card-text>
<v-dialog v-model="showSnapshotViewerDialog">
<v-card class="pt-3 pl-5 pr-5" color="primary" flat>
<v-card-title> View Saved Frame Snapshots </v-card-title>
<v-divider />
<v-card-text v-if="imgData.length === 0" style="font-size: 18px; font-weight: 600" class="pt-4">
There are no snapshots saved
<v-card color="surface" flat>
<v-card-title> Saved Frame Snapshots </v-card-title>
<v-card-text v-if="imgData.length === 0" class="pt-0">
<v-alert
color="buttonPassive"
density="compact"
text="There are currently no saved snapshots."
icon="mdi-information-outline"
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'tonal'"
/>
</v-card-text>
<div v-else class="pb-2">
<v-card-text v-else class="pt-0">
<v-alert
closable
color="buttonPassive"
density="compact"
text="Snapshot timestamps depend on when the coprocessor was last connected to the internet."
icon="mdi-information-outline"
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'tonal'"
/>
<v-data-table
v-model:expanded="expanded"
:headers="[
@@ -151,11 +171,7 @@ const expanded = ref([]);
</div>
</template>
</v-data-table>
<span
>Snapshot Timestamps may be incorrect as they depend on when the coprocessor was last connected to the
internet</span
>
</div>
</v-card-text>
</v-card>
</v-dialog>
</v-card>
@@ -170,18 +186,12 @@ const expanded = ref([]);
}
.v-table {
text-align: center;
background-color: #006492 !important;
th,
td {
background-color: #005281 !important;
font-size: 1rem !important;
}
tbody :hover tr {
background-color: #005281 !important;
}
::-webkit-scrollbar {
width: 0;
height: 0.55em;
@@ -194,7 +204,7 @@ const expanded = ref([]);
}
::-webkit-scrollbar-thumb {
background-color: #ffd843;
background-color: rgb(var(--v-theme-accent));
border-radius: 10px;
}
}

View File

@@ -4,9 +4,12 @@ import PvInput from "@/components/common/pv-input.vue";
import PvNumberInput from "@/components/common/pv-number-input.vue";
import { useCameraSettingsStore } from "@/stores/settings/CameraSettingsStore";
import { useStateStore } from "@/stores/StateStore";
import { computed, inject, ref, watchEffect } from "vue";
import { computed, ref, watchEffect } from "vue";
import { type CameraSettingsChangeRequest, ValidQuirks } from "@/types/SettingTypes";
import axios from "axios";
import { useTheme } from "vuetify";
const theme = useTheme();
const tempSettingsStruct = ref<CameraSettingsChangeRequest>({
fov: useCameraSettingsStore().currentCameraSettings.fov.value,
@@ -73,10 +76,7 @@ const saveCameraSettings = () => {
useCameraSettingsStore()
.updateCameraSettings(tempSettingsStruct.value)
.then((response) => {
useStateStore().showSnackbarMessage({
color: "success",
message: response.data.text || response.data
});
useStateStore().showSnackbarMessage({ color: "success", message: response.data.text || response.data });
// Update the local settings cause the backend checked their validity. Assign is to deref value
useCameraSettingsStore().currentCameraSettings.fov.value = tempSettingsStruct.value.fov;
@@ -112,22 +112,13 @@ watchEffect(() => {
});
const showDeleteCamera = ref(false);
const address = inject<string>("backendHost");
const exportSettings = ref();
const openExportSettingsPrompt = () => {
exportSettings.value.click();
};
const yesDeleteMySettingsText = ref("");
const deletingCamera = ref(false);
const deleteThisCamera = () => {
if (deletingCamera.value) return;
deletingCamera.value = true;
const payload = {
cameraUniqueName: useStateStore().currentCameraUniqueName
};
const payload = { cameraUniqueName: useStateStore().currentCameraUniqueName };
axios
.post("/utils/nukeOneCamera", payload)
@@ -169,9 +160,9 @@ const wrappedCameras = computed<SelectItem[]>(() =>
</script>
<template>
<v-card class="mb-3" color="primary" dark>
<v-card-title class="pa-6 pb-0">Camera Settings</v-card-title>
<v-card-text class="pa-6 pt-3">
<v-card class="mb-3 rounded-12" color="surface" dark>
<v-card-title class="pb-0">Camera Settings</v-card-title>
<v-card-text class="pt-3">
<pv-select
v-model="useStateStore().currentCameraUniqueName"
label="Camera"
@@ -202,45 +193,42 @@ const wrappedCameras = computed<SelectItem[]>(() =>
:select-cols="8"
/>
</v-card-text>
<v-card-text class="d-flex pa-6 pt-0">
<v-card-text class="d-flex pt-0">
<v-col cols="6" class="pa-0 pr-2">
<v-btn block size="small" color="secondary" :disabled="!settingsHaveChanged()" @click="saveCameraSettings">
<v-icon start> mdi-content-save </v-icon>
<v-btn
block
size="small"
color="primary"
:disabled="!settingsHaveChanged()"
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
@click="saveCameraSettings"
>
<v-icon start size="large"> mdi-content-save </v-icon>
Save Changes
</v-btn>
</v-col>
<v-col cols="6" class="pa-0 pl-2">
<v-btn block size="small" color="error" @click="() => (showDeleteCamera = true)">
<v-icon start> mdi-trash-can-outline </v-icon>
<v-btn
block
size="small"
color="error"
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
@click="() => (showDeleteCamera = true)"
>
<v-icon start size="large"> mdi-trash-can-outline </v-icon>
Delete Camera
</v-btn>
</v-col>
</v-card-text>
<v-dialog v-model="showDeleteCamera" width="800">
<v-card class="dialog-container pa-3 pb-2" color="primary" flat>
<v-card color="surface" flat>
<v-card-title> Delete {{ useCameraSettingsStore().currentCameraSettings.nickname }}? </v-card-title>
<v-card-text>
<v-row class="align-center pt-6">
<v-col cols="12" md="6">
<span class="text-white"> This will delete ALL OF YOUR SETTINGS and restart PhotonVision. </span>
</v-col>
<v-col cols="12" md="6">
<v-btn color="secondary" block @click="openExportSettingsPrompt">
<v-icon start class="open-icon"> 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 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>
<v-card-text class="pt-0 pb-10px">
<pv-input
v-model="yesDeleteMySettingsText"
:label="'Type &quot;' + useCameraSettingsStore().currentCameraName + '&quot;:'"
@@ -248,20 +236,28 @@ const wrappedCameras = computed<SelectItem[]>(() =>
:input-cols="6"
/>
</v-card-text>
<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
block
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"> mdi-trash-can-outline </v-icon>
<span class="open-label">DELETE (UNRECOVERABLE)</span>
<v-icon start class="open-icon" size="large"> mdi-trash-can-outline </v-icon>
<span class="open-label">Delete</span>
</v-btn>
</v-card-text>
</v-card-actions>
</v-card>
</v-dialog>
</v-card>

View File

@@ -5,6 +5,9 @@ import { useCameraSettingsStore } from "@/stores/settings/CameraSettingsStore";
import { PipelineType } from "@/types/PipelineTypes";
import { useStateStore } from "@/stores/StateStore";
import { useSettingsStore } from "@/stores/settings/GeneralSettingsStore";
import { useTheme } from "vuetify";
const theme = useTheme();
const value = defineModel<number[]>({ required: true });
@@ -29,37 +32,39 @@ const fpsTooLow = computed<boolean>(() => {
</script>
<template>
<v-card id="camera-settings-camera-view-card" class="camera-settings-camera-view-card" color="primary" dark>
<v-card-title class="justify-space-between align-content-center pa-0 pl-6 pr-6">
<div class="d-flex flex-wrap pt-4 pb-4">
<div>
<span class="mr-4" style="white-space: nowrap"> Cameras </span>
</div>
<div>
<v-chip
v-if="useCameraSettingsStore().currentCameraSettings.isConnected"
label
:color="fpsTooLow ? 'error' : 'transparent'"
style="font-size: 1rem; padding: 0; margin: 0"
<v-card
id="camera-settings-camera-view-card"
class="camera-settings-camera-view-card rounded-12"
color="surface"
dark
>
<v-card-title class="justify-space-between align-content-center pt-0 pb-0">
<div class="d-flex flex-wrap align-center pt-4 pb-4">
<span class="mr-4" style="white-space: nowrap"> Cameras </span>
<v-chip
v-if="useCameraSettingsStore().currentCameraSettings.isConnected"
label
:color="fpsTooLow ? 'error' : 'transparent'"
style="font-size: 1rem; padding: 0; margin: 0"
>
<span
class="pr-1"
:style="{ color: fpsTooLow ? 'rgb(var(--v-theme-error))' : 'rgb(var(--v-theme-primary))' }"
>
<span class="pr-1" :style="{ color: fpsTooLow ? '#C7EA46' : '#ff4d00' }">
{{ Math.round(useStateStore().currentPipelineResults?.fps || 0) }}&nbsp;FPS &ndash;
{{ Math.min(Math.round(useStateStore().currentPipelineResults?.latency || 0), 9999) }} ms latency
</span>
</v-chip>
<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>
</div>
</div>
<div class="d-flex align-center">
&nbsp;{{ Math.round(useStateStore().currentPipelineResults?.fps || 0) }}&nbsp;FPS &ndash;
{{ Math.min(Math.round(useStateStore().currentPipelineResults?.latency || 0), 9999) }} ms latency
</span>
</v-chip>
<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-switch
v-model="driverMode"
:disabled="useCameraSettingsStore().isCalibrationMode || useCameraSettingsStore().pipelineNames.length === 0"
label="Driver Mode"
style="margin-left: auto"
color="accent"
class="pt-2 pb-2"
color="primary"
density="compact"
hide-details="auto"
/>
</div>
@@ -85,28 +90,23 @@ const fpsTooLow = computed<boolean>(() => {
</div>
</v-card-text>
<v-card-text class="pt-0">
<v-btn-toggle
v-model="value"
:multiple="true"
mandatory
class="fill"
style="width: 100%"
base-color="surface-variant"
>
<v-btn-toggle v-model="value" :multiple="true" mandatory class="fill" style="width: 100%">
<v-btn
color="secondary"
color="buttonPassive"
class="fill"
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
:disabled="useCameraSettingsStore().isDriverMode || useCameraSettingsStore().isCalibrationMode"
>
<v-icon start class="mode-btn-icon">mdi-import</v-icon>
<v-icon start class="mode-btn-icon" size="large">mdi-import</v-icon>
<span class="mode-btn-label">Raw</span>
</v-btn>
<v-btn
color="secondary"
color="buttonPassive"
class="fill"
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
:disabled="useCameraSettingsStore().isDriverMode || useCameraSettingsStore().isCalibrationMode"
>
<v-icon start class="mode-btn-icon">mdi-export</v-icon>
<v-icon start class="mode-btn-icon" size="large">mdi-export</v-icon>
<span class="mode-btn-label">Processed</span>
</v-btn>
</v-btn-toggle>
@@ -127,10 +127,6 @@ th {
text-align: center;
}
.v-input--switch {
margin-top: 0;
}
.stream-container {
display: flex;
justify-content: center;

View File

@@ -38,25 +38,25 @@ const handleKeydown = ({ key }) => {
break;
}
};
</script>
// TODO: fix error text theming
</script>
<template>
<div class="d-flex">
<v-col :cols="labelCols || 12 - inputCols" class="d-flex align-center pl-0">
<v-col :cols="labelCols || 12 - inputCols" class="d-flex align-center pl-0 pt-10px pb-10px">
<tooltipped-label :tooltip="tooltip" :label="label" />
</v-col>
<v-col :cols="inputCols" class="d-flex align-center pr-0">
<v-col :cols="inputCols" class="d-flex align-center pr-0 pt-10px pb-10px">
<v-text-field
v-model="value"
density="compact"
color="accent"
color="primary"
:placeholder="placeholder"
:disabled="disabled"
:error-messages="errorMessage"
:rules="rules"
hide-details="auto"
class="light-error"
variant="underlined"
@keydown="handleKeydown"
/>

View File

@@ -0,0 +1,205 @@
<template>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 100 100"
preserveAspectRatio="xMidYMid"
width="200"
height="200"
style="shape-rendering: auto; display: block; background: rgba(0, 100, 146, 0)"
xmlns:xlink="http://www.w3.org/1999/xlink"
>
<g>
<g transform="translate(80,50)">
<g transform="rotate(0)">
<circle class="loader-circle" fill-opacity="1" fill="#ffd943" r="6" cy="0" cx="0">
<animateTransform
repeatCount="indefinite"
dur="0.9345794392523364s"
keyTimes="0;1"
values="1.5 1.5;1 1"
begin="-0.8177570093457943s"
type="scale"
attributeName="transform"
></animateTransform>
<animate
begin="-0.8177570093457943s"
values="1;0"
repeatCount="indefinite"
dur="0.9345794392523364s"
keyTimes="0;1"
attributeName="fill-opacity"
></animate>
</circle>
</g>
</g>
<g transform="translate(71.21320343559643,71.21320343559643)">
<g transform="rotate(45)">
<circle class="loader-circle" fill-opacity="0.875" fill="#ffd943" r="6" cy="0" cx="0">
<animateTransform
repeatCount="indefinite"
dur="0.9345794392523364s"
keyTimes="0;1"
values="1.5 1.5;1 1"
begin="-0.7009345794392523s"
type="scale"
attributeName="transform"
></animateTransform>
<animate
begin="-0.7009345794392523s"
values="1;0"
repeatCount="indefinite"
dur="0.9345794392523364s"
keyTimes="0;1"
attributeName="fill-opacity"
></animate>
</circle>
</g>
</g>
<g transform="translate(50,80)">
<g transform="rotate(90)">
<circle class="loader-circle" fill-opacity="0.75" fill="#ffd943" r="6" cy="0" cx="0">
<animateTransform
repeatCount="indefinite"
dur="0.9345794392523364s"
keyTimes="0;1"
values="1.5 1.5;1 1"
begin="-0.5841121495327103s"
type="scale"
attributeName="transform"
></animateTransform>
<animate
begin="-0.5841121495327103s"
values="1;0"
repeatCount="indefinite"
dur="0.9345794392523364s"
keyTimes="0;1"
attributeName="fill-opacity"
></animate>
</circle>
</g>
</g>
<g transform="translate(28.786796564403577,71.21320343559643)">
<g transform="rotate(135)">
<circle class="loader-circle" fill-opacity="0.625" fill="#ffd943" r="6" cy="0" cx="0">
<animateTransform
repeatCount="indefinite"
dur="0.9345794392523364s"
keyTimes="0;1"
values="1.5 1.5;1 1"
begin="-0.4672897196261682s"
type="scale"
attributeName="transform"
></animateTransform>
<animate
begin="-0.4672897196261682s"
values="1;0"
repeatCount="indefinite"
dur="0.9345794392523364s"
keyTimes="0;1"
attributeName="fill-opacity"
></animate>
</circle>
</g>
</g>
<g transform="translate(20,50.00000000000001)">
<g transform="rotate(180)">
<circle class="loader-circle" fill-opacity="0.5" fill="#ffd943" r="6" cy="0" cx="0">
<animateTransform
repeatCount="indefinite"
dur="0.9345794392523364s"
keyTimes="0;1"
values="1.5 1.5;1 1"
begin="-0.35046728971962615s"
type="scale"
attributeName="transform"
></animateTransform>
<animate
begin="-0.35046728971962615s"
values="1;0"
repeatCount="indefinite"
dur="0.9345794392523364s"
keyTimes="0;1"
attributeName="fill-opacity"
></animate>
</circle>
</g>
</g>
<g transform="translate(28.78679656440357,28.786796564403577)">
<g transform="rotate(225)">
<circle class="loader-circle" fill-opacity="0.375" fill="#ffd943" r="6" cy="0" cx="0">
<animateTransform
repeatCount="indefinite"
dur="0.9345794392523364s"
keyTimes="0;1"
values="1.5 1.5;1 1"
begin="-0.2336448598130841s"
type="scale"
attributeName="transform"
></animateTransform>
<animate
begin="-0.2336448598130841s"
values="1;0"
repeatCount="indefinite"
dur="0.9345794392523364s"
keyTimes="0;1"
attributeName="fill-opacity"
></animate>
</circle>
</g>
</g>
<g transform="translate(49.99999999999999,20)">
<g transform="rotate(270)">
<circle class="loader-circle" fill-opacity="0.25" fill="#ffd943" r="6" cy="0" cx="0">
<animateTransform
repeatCount="indefinite"
dur="0.9345794392523364s"
keyTimes="0;1"
values="1.5 1.5;1 1"
begin="-0.11682242990654206s"
type="scale"
attributeName="transform"
></animateTransform>
<animate
begin="-0.11682242990654206s"
values="1;0"
repeatCount="indefinite"
dur="0.9345794392523364s"
keyTimes="0;1"
attributeName="fill-opacity"
></animate>
</circle>
</g>
</g>
<g transform="translate(71.21320343559643,28.78679656440357)">
<g transform="rotate(315)">
<circle class="loader-circle" fill-opacity="0.125" fill="#ffd943" r="6" cy="0" cx="0">
<animateTransform
repeatCount="indefinite"
dur="0.9345794392523364s"
keyTimes="0;1"
values="1.5 1.5;1 1"
begin="0s"
type="scale"
attributeName="transform"
></animateTransform>
<animate
begin="0s"
values="1;0"
repeatCount="indefinite"
dur="0.9345794392523364s"
keyTimes="0;1"
attributeName="fill-opacity"
></animate>
</circle>
</g>
</g>
<g></g>
</g>
<!-- [ldio] generated by https://loading.io -->
</svg>
</template>
<style scoped lang="scss">
.loader-circle {
fill: rgb(var(--v-theme-buttonActive));
}
</style>

View File

@@ -28,17 +28,17 @@ const localValue = computed({
<template>
<div class="d-flex">
<v-col :cols="labelCols" class="d-flex pl-0 align-center">
<v-col :cols="labelCols" class="d-flex pl-0 pt-10px pb-10px align-center">
<tooltipped-label :tooltip="tooltip" :label="label" />
</v-col>
<v-col class="pr-0">
<v-col class="pr-0 pt-10px pb-10px">
<v-text-field
v-model="localValue"
class="mt-0 pt-0"
density="compact"
hide-details
single-line
color="accent"
color="primary"
type="number"
variant="underlined"
style="width: 70px"

View File

@@ -21,16 +21,16 @@ withDefaults(
<template>
<div class="d-flex">
<v-col :cols="12 - inputCols" class="d-flex align-center pl-0">
<v-col :cols="12 - inputCols" class="d-flex align-center pl-0 pt-10px pb-10px">
<tooltipped-label :tooltip="tooltip" :label="label" />
</v-col>
<v-col :cols="inputCols" class="d-flex align-center pr-0">
<v-radio-group v-model="value" row:mandatory="true" hide-details="auto">
<v-col :cols="inputCols" class="pr-0 pt-10px pb-10px">
<v-radio-group v-model="value" row:mandatory="true" inline hide-details="auto">
<v-radio
v-for="(radioName, index) in list"
:key="index"
:value="index"
color="#ffd843"
color="rgb(var(--v-theme-primary))"
:label="radioName"
:model-value="index"
:disabled="disabled"
@@ -39,9 +39,3 @@ withDefaults(
</v-col>
</div>
</template>
<style scoped>
.v-input--radio-group {
padding-top: 0;
margin-top: 0;
}
</style>

View File

@@ -67,15 +67,15 @@ const checkNumberRange = (v: string): boolean => {
:disabled="disabled"
hide-details
class="align-center ml-0 mr-0"
:color="inverted ? 'rgba(255, 255, 255, 0.2)' : 'accent'"
:track-color="inverted ? 'accent' : undefined"
thumb-color="accent"
color="primary"
:track-color="inverted ? 'primary' : undefined"
thumb-color="primary"
:step="step"
>
<template #prepend>
<v-text-field
:model-value="localValue[0]"
color="accent"
color="primary"
class="mt-0 pt-0"
density="compact"
hide-details
@@ -93,7 +93,7 @@ const checkNumberRange = (v: string): boolean => {
<template #append>
<v-text-field
:model-value="localValue[1]"
color="accent"
color="primary"
class="mt-0 pt-0"
density="compact"
hide-details

View File

@@ -40,10 +40,10 @@ const items = computed<SelectItem[]>(() => {
<template>
<div class="d-flex">
<v-col :cols="12 - selectCols" class="d-flex align-center pl-0">
<v-col :cols="12 - selectCols" class="d-flex align-center pl-0 pt-10px pb-10px">
<tooltipped-label :tooltip="tooltip" :label="label" />
</v-col>
<v-col :cols="selectCols" class="d-flex align-center pr-0">
<v-col :cols="selectCols" class="d-flex align-center pr-0 pt-10px pb-10px">
<v-select
v-model="value"
:items="items"

View File

@@ -13,15 +13,9 @@ const props = withDefaults(
disabled?: boolean;
sliderCols?: number;
}>(),
{
step: 1,
disabled: false,
sliderCols: 8
}
{ step: 1, disabled: false, sliderCols: 8 }
);
const emit = defineEmits<{
(e: "update:modelValue", value: number): void;
}>();
const emit = defineEmits<{ (e: "update:modelValue", value: number): void }>();
// Debounce function
function debounce(func: (...args: any[]) => void, wait: number) {
@@ -44,17 +38,17 @@ const localValue = computed({
<template>
<div class="d-flex">
<v-col :cols="12 - sliderCols" class="pl-0 d-flex align-center">
<v-col :cols="12 - sliderCols" class="pl-0 pt-10px pb-10px d-flex align-center">
<tooltipped-label :tooltip="tooltip" :label="label" />
</v-col>
<v-col :cols="sliderCols - 1" class="pl-0">
<v-col :cols="sliderCols - 1" class="pl-0 pt-10px pb-10px">
<v-slider
v-model="localValue"
class="align-center"
:max="max"
:min="min"
hide-details
color="accent"
color="primary"
:disabled="disabled"
:step="step"
append-icon="mdi-menu-right"
@@ -63,10 +57,10 @@ const localValue = computed({
@click:prepend="localValue -= step"
/>
</v-col>
<v-col :cols="1" class="pr-0">
<v-col :cols="1" class="pr-0 pt-10px pb-10px">
<v-text-field
:model-value="localValue"
color="accent"
color="primary"
:max="max"
:min="min"
:disabled="disabled"

View File

@@ -3,33 +3,24 @@ import TooltippedLabel from "@/components/common/pv-tooltipped-label.vue";
const value = defineModel<boolean>();
withDefaults(
defineProps<{
label?: string;
tooltip?: string;
disabled?: boolean;
labelCols?: number;
switchCols?: number;
}>(),
{
disabled: false,
labelCols: 2,
switchCols: 8
}
defineProps<{ label?: string; tooltip?: string; disabled?: boolean; labelCols?: number; switchCols?: number }>(),
{ disabled: false, labelCols: 2, switchCols: 8 }
);
</script>
<template>
<div class="d-flex">
<v-col :cols="12 - switchCols || labelCols" class="d-flex align-center pl-0 pt-2 pb-2">
<v-col :cols="12 - switchCols || labelCols" class="d-flex align-center pl-0">
<tooltipped-label :tooltip="tooltip" :label="label" />
</v-col>
<v-col :cols="switchCols || 12 - labelCols" class="d-flex align-center pr-0 pt-2 pb-2">
<v-switch v-model="value" :disabled="disabled" color="#ffd843" hide-details density="compact" />
<v-col :cols="switchCols || 12 - labelCols" class="d-flex align-center pr-0">
<v-switch v-model="value" :disabled="disabled" color="primary" hide-details density="compact" />
</v-col>
</div>
</template>
<style scoped>
.v-input--selection-controls {
margin-top: 0px;
.v-col {
padding-top: 6px !important;
padding-bottom: 6px !important;
}
</style>

View File

@@ -8,6 +8,9 @@ import PvIcon from "@/components/common/pv-icon.vue";
import PvInput from "@/components/common/pv-input.vue";
import { PipelineType } from "@/types/PipelineTypes";
import { useSettingsStore } from "@/stores/settings/GeneralSettingsStore";
import { useTheme } from "vuetify";
const theme = useTheme();
const changeCurrentCameraUniqueName = (cameraUniqueName: string) => {
useCameraSettingsStore().setCurrentCameraUniqueName(cameraUniqueName, true);
@@ -53,10 +56,7 @@ const saveCameraNameEdit = (newName: string) => {
useCameraSettingsStore()
.changeCameraNickname(newName, false)
.then((response) => {
useStateStore().showSnackbarMessage({
color: "success",
message: response.data.text || response.data
});
useStateStore().showSnackbarMessage({ color: "success", message: response.data.text || response.data });
useCameraSettingsStore().currentCameraSettings.nickname = newName;
})
.catch((error) => {
@@ -241,8 +241,8 @@ const wrappedCameras = computed<SelectItem[]>(() =>
</script>
<template>
<v-card color="primary">
<v-row style="padding: 20px 12px 0 30px">
<v-card color="surface" class="rounded-12">
<v-row no-gutters class="pl-4 pt-2 pb-0">
<v-col cols="10" class="pa-0">
<pv-select
v-if="!isCameraNameEdit"
@@ -281,7 +281,7 @@ const wrappedCameras = computed<SelectItem[]>(() =>
/>
</v-col>
</v-row>
<v-row style="padding: 0 12px 0 30px">
<v-row no-gutters class="pl-4 pb-0 pt-0">
<v-col cols="10" class="pa-0">
<pv-select
v-if="!isPipelineNameEdit"
@@ -326,19 +326,24 @@ const wrappedCameras = computed<SelectItem[]>(() =>
<pv-icon color="#c5c5c5" :right="true" icon-name="mdi-pencil" tooltip="Edit pipeline name" />
</v-list-item-title>
</v-list-item>
<v-list-item @click="duplicateCurrentPipeline">
<v-list-item-title>
<pv-icon color="#c5c5c5" :right="true" icon-name="mdi-content-copy" tooltip="Duplicate pipeline" />
</v-list-item-title>
</v-list-item>
<v-list-item @click="showCreatePipelineDialog">
<v-list-item-title>
<pv-icon color="#c5c5c5" :right="true" icon-name="mdi-plus" tooltip="Add new pipeline" />
<pv-icon color="green" :right="true" icon-name="mdi-plus" tooltip="Add new pipeline" />
</v-list-item-title>
</v-list-item>
<v-list-item @click="showPipelineDeletionConfirmationDialog = true">
<v-list-item-title>
<pv-icon color="red-darken-2" :right="true" icon-name="mdi-delete" tooltip="Delete pipeline" />
</v-list-item-title>
</v-list-item>
<v-list-item @click="duplicateCurrentPipeline">
<v-list-item-title>
<pv-icon color="#c5c5c5" :right="true" icon-name="mdi-content-copy" tooltip="Duplicate pipeline" />
<pv-icon
color="red-darken-2"
:right="true"
icon-name="mdi-trash-can-outline"
tooltip="Delete pipeline"
/>
</v-list-item-title>
</v-list-item>
</v-list>
@@ -353,7 +358,7 @@ const wrappedCameras = computed<SelectItem[]>(() =>
/>
</v-col>
</v-row>
<v-row style="padding: 0 12px 24px 30px">
<v-row no-gutters class="pl-4 pt-0 pb-4">
<v-col cols="10" class="pa-0">
<pv-select
v-model="currentPipelineType"
@@ -370,79 +375,94 @@ const wrappedCameras = computed<SelectItem[]>(() =>
</v-col>
</v-row>
<v-dialog v-model="showPipelineCreationDialog" persistent width="500">
<v-card color="primary">
<v-card-title> Create New Pipeline </v-card-title>
<v-card-text>
<v-card color="surface">
<v-card-title class="pb-0"> Create New Pipeline </v-card-title>
<v-card-text class="pt-0 pb-0">
<pv-input
v-model="newPipelineName"
placeholder="Pipeline Name"
:label-cols="3"
:input-cols="12 - 3"
:label-cols="4"
:input-cols="12 - 4"
label="Pipeline Name"
:rules="[(v) => checkPipelineName(v)]"
/>
<pv-select
v-model="newPipelineType"
:select-cols="12 - 3"
:select-cols="12 - 4"
label="Tracking Type"
tooltip="Pipeline type, which changes the type of processing that will happen on input frames"
:items="validNewPipelineTypes"
/>
</v-card-text>
<v-divider />
<v-card-actions>
<v-spacer />
<v-card-actions class="pr-5 pt-10px pb-5">
<v-btn
color="#ffd843"
color="buttonPassive"
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
@click="cancelPipelineCreation"
>
Cancel
</v-btn>
<v-btn
color="buttonActive"
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
:disabled="checkPipelineName(newPipelineName) !== true"
variant="flat"
@click="createNewPipeline"
>
Save
Create
</v-btn>
<v-btn color="error" variant="elevated" @click="cancelPipelineCreation"> Cancel </v-btn>
</v-card-actions>
</v-card>
</v-dialog>
<v-dialog v-model="showPipelineDeletionConfirmationDialog" width="500">
<v-card color="primary">
<v-card-title> Pipeline Deletion Confirmation </v-card-title>
<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 the pipeline
<b style="color: white; font-weight: bold">{{
useCameraSettingsStore().currentPipelineSettings.pipelineNickname
}}</b
>? This cannot be undone.
Are you sure you want to delete
<span style="color: white">"{{ useCameraSettingsStore().currentPipelineSettings.pipelineNickname }}"</span>?
This cannot be undone.
</v-card-text>
<v-divider />
<v-card-actions>
<v-spacer />
<v-btn variant="flat" color="error" @click="confirmDeleteCurrentPipeline"> Yes, I'm sure </v-btn>
<v-card-actions class="pa-5 pt-0">
<v-btn
variant="flat"
color="#ffd843"
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
color="primary"
class="text-black"
@click="showPipelineDeletionConfirmationDialog = false"
>
No, take me back
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>
<v-dialog v-model="showPipelineTypeChangeDialog" persistent width="600">
<v-card color="primary" dark>
<v-card-title>Change Pipeline Type</v-card-title>
<v-card color="surface" dark>
<v-card-title class="pb-0">Change Pipeline Type</v-card-title>
<v-card-text>
Are you sure you want to change the current pipeline type? This will cause all the pipeline settings to be
overwritten and they will be lost. If this isn't what you want, duplicate this pipeline first or export
settings.
</v-card-text>
<v-divider />
<v-card-actions>
<v-spacer />
<v-btn color="error" variant="elevated" @click="confirmChangePipelineType"> Yes, I'm sure </v-btn>
<v-btn color="#ffd843" variant="elevated" class="text-black" @click="cancelChangePipelineType">
No, take me back
<v-card-actions class="pa-5 pt-0">
<v-btn
color="buttonPassive"
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
class="text-black"
@click="cancelChangePipelineType"
>
Cancel
</v-btn>
<v-btn
color="buttonActive"
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
@click="confirmChangePipelineType"
>
Confirm
</v-btn>
</v-card-actions>
</v-card>

View File

@@ -39,19 +39,17 @@ const performanceRecommendation = computed<string>(() => {
</script>
<template>
<v-card color="primary" height="100%" class="d-flex flex-column" dark>
<v-card color="surface" height="100%" class="d-flex flex-column rounded-12" dark>
<v-card-title class="justify-space-between align-center pt-1 pb-1 d-flex">
<span>Cameras</span>
<v-chip
v-if="useCameraSettingsStore().currentCameraSettings.isConnected"
label
:color="fpsTooLow ? 'error' : ''"
style="font-size: 1rem; padding: 0; margin: 0"
:variant="fpsTooLow ? 'tonal' : 'text'"
:style="{ color: fpsTooLow ? '#C7EA46' : '#ff4d00' }"
:color="fpsTooLow ? 'error' : 'primary'"
style="font-size: 1.1rem; padding: 0; margin: 0"
variant="text"
>
<span class="pr-1"
>Processing @ {{ Math.round(useStateStore().currentPipelineResults?.fps || 0) }}&nbsp;FPS &ndash;</span
<span class="pr-1">{{ Math.round(useStateStore().currentPipelineResults?.fps || 0) }}&nbsp;FPS &ndash;</span
><span>{{ performanceRecommendation }}</span>
</v-chip>
<v-chip v-else label variant="text" color="red" style="font-size: 1rem; padding: 0; margin: 0">
@@ -61,7 +59,7 @@ const performanceRecommendation = computed<string>(() => {
v-model="driverMode"
:disabled="useCameraSettingsStore().isCalibrationMode || useCameraSettingsStore().pipelineNames.length === 0"
label="Driver Mode"
color="accent"
color="primary"
hide-details="auto"
/>
</v-card-title>
@@ -88,9 +86,6 @@ const performanceRecommendation = computed<string>(() => {
</template>
<style scoped>
.v-input--switch {
margin-top: 0;
}
.stream-viewer-container {
display: flex;
justify-content: center;

View File

@@ -15,6 +15,9 @@ import PnPTab from "@/components/dashboard/tabs/PnPTab.vue";
import Map3DTab from "@/components/dashboard/tabs/Map3DTab.vue";
import { WebsocketPipelineType } from "@/types/WebsocketDataTypes";
import { useDisplay } from "vuetify/lib/composables/display";
import { useTheme } from "vuetify";
const theme = useTheme();
interface ConfigOption {
tabName: string;
@@ -22,46 +25,16 @@ interface ConfigOption {
}
const allTabs = Object.freeze({
inputTab: {
tabName: "Input",
component: InputTab
},
thresholdTab: {
tabName: "Threshold",
component: ThresholdTab
},
contoursTab: {
tabName: "Contours",
component: ContoursTab
},
apriltagTab: {
tabName: "AprilTag",
component: AprilTagTab
},
arucoTab: {
tabName: "Aruco",
component: ArucoTab
},
objectDetectionTab: {
tabName: "Object Detection",
component: ObjectDetectionTab
},
outputTab: {
tabName: "Output",
component: OutputTab
},
targetsTab: {
tabName: "Targets",
component: TargetsTab
},
pnpTab: {
tabName: "PnP",
component: PnPTab
},
map3dTab: {
tabName: "3D",
component: Map3DTab
}
inputTab: { tabName: "Input", component: InputTab },
thresholdTab: { tabName: "Threshold", component: ThresholdTab },
contoursTab: { tabName: "Contours", component: ContoursTab },
apriltagTab: { tabName: "AprilTag", component: AprilTagTab },
arucoTab: { tabName: "Aruco", component: ArucoTab },
objectDetectionTab: { tabName: "Object Detection", component: ObjectDetectionTab },
outputTab: { tabName: "Output", component: OutputTab },
targetsTab: { tabName: "Targets", component: TargetsTab },
pnpTab: { tabName: "PnP", component: PnPTab },
map3dTab: { tabName: "3D", component: Map3DTab }
});
const selectedTabs = ref([0, 0, 0, 0]);
@@ -144,13 +117,13 @@ const onBeforeTabUpdate = () => {
<template>
<v-row no-gutters class="tabGroups">
<template v-if="!useCameraSettingsStore().hasConnected">
<v-col cols="12">
<v-card color="error">
<v-card-title class="text-white">
Camera has not connected. Please check your connection and try again.
</v-card-title>
</v-card>
</v-col>
<v-alert
color="error"
density="compact"
text="Camera is not connected. Please check your connection and try again."
icon="mdi-alert-circle-outline"
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'tonal'"
/>
</template>
<template v-else>
<v-col
@@ -160,13 +133,13 @@ const onBeforeTabUpdate = () => {
:class="tabGroupIndex !== tabGroups.length - 1 && 'pr-3'"
@vue:before-update="onBeforeTabUpdate"
>
<v-card color="primary" height="100%" class="pr-4 pl-4">
<v-tabs v-model="selectedTabs[tabGroupIndex]" grow bg-color="primary" height="48" slider-color="accent">
<v-card color="surface" height="100%" class="pr-5 pl-5 rounded-12">
<v-tabs v-model="selectedTabs[tabGroupIndex]" grow bg-color="surface" height="48" slider-color="buttonActive">
<v-tab v-for="(tabConfig, index) in tabGroupData" :key="index">
{{ tabConfig.tabName }}
</v-tab>
</v-tabs>
<div class="pl-2 pr-2 pt-3 pb-3">
<div class="pt-10px pb-10px">
<KeepAlive>
<Component :is="tabGroupData[selectedTabs[tabGroupIndex]].component" />
</KeepAlive>
@@ -178,6 +151,11 @@ const onBeforeTabUpdate = () => {
</template>
<style>
.v-slide-group {
transition-duration: 0.28s;
transition-property: box-shadow, opacity, background;
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
}
.v-slide-group__next--disabled,
.v-slide-group__prev--disabled {
display: none !important;

View File

@@ -2,6 +2,10 @@
import { computed } from "vue";
import { useCameraSettingsStore } from "@/stores/settings/CameraSettingsStore";
import { useStateStore } from "@/stores/StateStore";
import { useTheme } from "vuetify";
import { PipelineType } from "@/types/PipelineTypes";
const theme = useTheme();
const value = defineModel<number[]>();
@@ -18,30 +22,39 @@ const processingMode = computed<number>({
<template>
<v-card
:disabled="useCameraSettingsStore().isDriverMode || useStateStore().colorPickingMode"
class="mt-3"
color="primary"
class="mt-3 rounded-12"
color="surface"
style="flex-grow: 1; display: flex; flex-direction: column"
>
<v-row class="pa-3 pb-0 align-center">
<v-col class="pa-4">
<p style="color: white">Processing Mode</p>
<v-btn-toggle v-model="processingMode" mandatory base-color="surface-variant" class="fill w-100">
<v-btn-toggle v-model="processingMode" mandatory class="fill w-100">
<v-btn
color="secondary"
color="buttonPassive"
:disabled="!useCameraSettingsStore().hasConnected"
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
class="w-50"
prepend-icon="mdi-square-outline"
>
<template #prepend>
<v-icon size="large">mdi-square-outline</v-icon>
</template>
<span>2D</span>
</v-btn>
<v-btn
color="secondary"
color="buttonPassive"
:disabled="
!useCameraSettingsStore().hasConnected || !useCameraSettingsStore().isCurrentVideoFormatCalibrated
!useCameraSettingsStore().hasConnected ||
!useCameraSettingsStore().isCurrentVideoFormatCalibrated ||
useCameraSettingsStore().currentPipelineSettings.pipelineType == PipelineType.ObjectDetection ||
useCameraSettingsStore().currentPipelineSettings.pipelineType == PipelineType.ColoredShape
"
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
class="w-50"
prepend-icon="mdi-cube-outline"
>
<template #prepend>
<v-icon size="large">mdi-cube-outline</v-icon>
</template>
<span>3D</span>
</v-btn>
</v-btn-toggle>
@@ -50,13 +63,21 @@ const processingMode = computed<number>({
<v-row class="pa-3 pt-0 align-center">
<v-col class="pa-4 pt-0">
<p style="color: white">Stream Display</p>
<v-btn-toggle v-model="value" :multiple="true" mandatory base-color="surface-variant" class="fill w-100">
<v-btn color="secondary" class="fill w-50">
<v-icon start class="mode-btn-icon">mdi-import</v-icon>
<v-btn-toggle v-model="value" :multiple="true" mandatory class="fill w-100">
<v-btn
color="buttonPassive"
class="fill w-50"
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
>
<v-icon start class="mode-btn-icon" size="large">mdi-import</v-icon>
<span class="mode-btn-label">Raw</span>
</v-btn>
<v-btn color="secondary" class="fill w-50">
<v-icon start class="mode-btn-icon">mdi-export</v-icon>
<v-btn
color="buttonPassive"
class="fill w-50"
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
>
<v-icon start class="mode-btn-icon" size="large">mdi-export</v-icon>
<span class="mode-btn-label">Processed</span>
</v-btn>
</v-btn-toggle>

View File

@@ -16,7 +16,12 @@ const trackedTargets = computed<PhotonTarget[]>(() => useStateStore().currentPip
</v-row>
<v-row style="width: 100%">
<v-col style="display: flex; align-items: center; justify-content: center">
<photon3d-visualizer :targets="trackedTargets" />
<Suspense>
<!-- Allows us to import three js when it's actually needed -->
<photon3d-visualizer :targets="trackedTargets" />
<template #fallback> Loading... </template>
</Suspense>
</v-col>
</v-row>
</div>

View File

@@ -8,6 +8,7 @@ import { computed } from "vue";
import { useStateStore } from "@/stores/StateStore";
import { useSettingsStore } from "@/stores/settings/GeneralSettingsStore";
import { useDisplay } from "vuetify";
import type { ObjectDetectionModelProperties } from "@/types/SettingTypes";
// TODO fix pipeline typing in order to fix this, the store settings call should be able to infer that only valid pipeline type settings are exposed based on pre-checks for the entire config section
// Defer reference to store access method
@@ -32,17 +33,32 @@ const interactiveCols = computed(() =>
);
// Filters out models that are not supported by the current backend, and returns a flattened list.
const supportedModels = computed(() => {
const supportedModels = computed<ObjectDetectionModelProperties[]>(() => {
const { availableModels, supportedBackends } = useSettingsStore().general;
return supportedBackends.flatMap((backend) => availableModels[backend] || []);
const isSupported = (model: ObjectDetectionModelProperties) => {
// Check if model's family is in the list of supported backends
return supportedBackends.some((backend: string) => backend.toLowerCase() === model.family.toLowerCase());
};
// Filter models where the family is supported and flatten the list
return availableModels.filter(isSupported);
});
const selectedModel = computed({
get: () => {
const index = supportedModels.value.indexOf(currentPipelineSettings.value.model);
const currentModel = currentPipelineSettings.value.model;
if (!currentModel) return undefined;
const index = supportedModels.value.findIndex((model) => model.modelPath === currentModel.modelPath);
return index === -1 ? undefined : index;
},
set: (v) => v && useCameraSettingsStore().changeCurrentPipelineSetting({ model: supportedModels.value[v] }, false)
set: (v) => {
if (v !== undefined && v >= 0 && v < supportedModels.value.length) {
const newModel = supportedModels.value[v];
useCameraSettingsStore().changeCurrentPipelineSetting({ model: newModel }, true);
}
}
});
</script>
@@ -53,8 +69,9 @@ const selectedModel = computed({
label="Model"
tooltip="The model used to detect objects in the camera feed"
:select-cols="interactiveCols"
:items="supportedModels"
:items="supportedModels.map((model) => model.nickname)"
/>
<pv-slider
v-model="currentPipelineSettings.confidence"
class="pt-2"
@@ -68,6 +85,17 @@ const selectedModel = computed({
(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ confidence: value }, false)
"
/>
<pv-slider
v-model="currentPipelineSettings.nms"
class="pt-2"
:slider-cols="interactiveCols"
label="NMS Threshold"
tooltip="The Non-Maximum Suppression threshold used to filter out overlapping detections. Higher values mean more detections are allowed through, but may result in false positives."
:min="0"
:max="1"
:step="0.01"
@update:modelValue="(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ nms: value }, false)"
/>
<pv-range-slider
v-model="contourArea"
label="Area"
@@ -98,7 +126,11 @@ const selectedModel = computed({
:items="['Portrait', 'Landscape']"
:select-cols="interactiveCols"
@update:modelValue="
(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ contourTargetOrientation: value }, false)
(value) =>
useCameraSettingsStore().changeCurrentPipelineSetting(
{ contourTargetOrientation: typeof value === 'string' ? Number(value) : value },
false
)
"
/>
<pv-select
@@ -108,7 +140,11 @@ const selectedModel = computed({
:select-cols="interactiveCols"
:items="['Largest', 'Smallest', 'Highest', 'Lowest', 'Rightmost', 'Leftmost', 'Centermost']"
@update:modelValue="
(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ contourSortMode: value }, false)
(value) =>
useCameraSettingsStore().changeCurrentPipelineSetting(
{ contourSortMode: typeof value === 'string' ? Number(value) : value },
false
)
"
/>
</div>

View File

@@ -7,6 +7,9 @@ import { computed } from "vue";
import { RobotOffsetType } from "@/types/SettingTypes";
import { useStateStore } from "@/stores/StateStore";
import { useDisplay } from "vuetify";
import { useTheme } from "vuetify";
const theme = useTheme();
const isTagPipeline = computed(
() =>
@@ -132,16 +135,20 @@ const interactiveCols = computed(() =>
v-if="useCameraSettingsStore().currentPipelineSettings.offsetRobotOffsetMode !== RobotOffsetPointMode.None"
class="metrics-table mt-3 mb-3"
>
<tr>
<th v-for="(item, itemIndex) in offsetPoints" :key="itemIndex" class="metric-item metric-item-title">
{{ item.header }}
</th>
</tr>
<tr>
<td v-for="(item, itemIndex) in offsetPoints" :key="itemIndex" class="metric-item">
{{ item.value }}
</td>
</tr>
<thead>
<tr>
<th v-for="(item, itemIndex) in offsetPoints" :key="itemIndex" class="metric-item metric-item-title">
{{ item.header }}
</th>
</tr>
</thead>
<tbody>
<tr>
<td v-for="(item, itemIndex) in offsetPoints" :key="itemIndex" class="metric-item">
{{ item.value }}
</td>
</tr>
</tbody>
</table>
<div
v-if="useCameraSettingsStore().currentPipelineSettings.offsetRobotOffsetMode !== RobotOffsetPointMode.None"
@@ -155,8 +162,9 @@ const interactiveCols = computed(() =>
<v-btn
size="small"
block
color="accent"
color="primary"
class="text-black"
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
@click="useCameraSettingsStore().takeRobotOffsetPoint(RobotOffsetType.Single)"
>
Take Point
@@ -166,7 +174,8 @@ const interactiveCols = computed(() =>
<v-btn
size="small"
block
color="yellow-darken-3"
color="error"
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
@click="useCameraSettingsStore().takeRobotOffsetPoint(RobotOffsetType.Clear)"
>
Clear All Points
@@ -181,8 +190,9 @@ const interactiveCols = computed(() =>
<v-btn
size="small"
block
color="accent"
color="primary"
class="text-black"
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
@click="useCameraSettingsStore().takeRobotOffsetPoint(RobotOffsetType.DualFirst)"
>
Take First Point
@@ -192,8 +202,9 @@ const interactiveCols = computed(() =>
<v-btn
size="small"
block
color="accent"
color="primary"
class="text-black"
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
@click="useCameraSettingsStore().takeRobotOffsetPoint(RobotOffsetType.DualSecond)"
>
Take Second Point
@@ -203,7 +214,8 @@ const interactiveCols = computed(() =>
<v-btn
size="small"
block
color="yellow-darken-3"
color="error"
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
@click="useCameraSettingsStore().takeRobotOffsetPoint(RobotOffsetType.Clear)"
>
Clear All Points
@@ -234,6 +246,6 @@ const interactiveCols = computed(() =>
.metric-item-title {
font-size: 18px;
text-decoration: underline;
text-decoration-color: #ffd843;
text-decoration-color: rgb(var(--v-theme-primary));
}
</style>

View File

@@ -4,6 +4,9 @@ import { type ActivePipelineSettings, PipelineType } from "@/types/PipelineTypes
import { useStateStore } from "@/stores/StateStore";
import { angleModulus, toDeg } from "@/lib/MathUtils";
import { computed } from "vue";
import { useTheme } from "vuetify";
const theme = useTheme();
// TODO fix pipeline typing in order to fix this, the store settings call should be able to infer that only valid pipeline type settings are exposed based on pre-checks for the entire config section
// Defer reference to store access method
@@ -105,7 +108,7 @@ const resetCurrentBuffer = () => {
<td class="text-center">{{ target.pitch.toFixed(2) }}&deg;</td>
<td class="text-center">{{ target.yaw.toFixed(2) }}&deg;</td>
<td class="text-center">{{ target.skew.toFixed(2) }}&deg;</td>
<td class="text-center">{{ target.area.toFixed(2) }}&deg;</td>
<td class="text-center">{{ target.area.toFixed(2) }}%</td>
</template>
<template v-else>
<td class="text-center">{{ target.pose?.x.toFixed(3) }}&nbsp;m</td>
@@ -200,7 +203,12 @@ const resetCurrentBuffer = () => {
>Multi-tag pose standard deviation over the last
{{ useStateStore().currentMultitagBuffer?.length || "NaN" }}/100 samples
</v-card-subtitle>
<v-btn color="secondary" class="mb-4 mt-1" style="width: min-content" variant="flat" @click="resetCurrentBuffer"
<v-btn
color="buttonActive"
class="mb-4 mt-1"
style="width: min-content"
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
@click="resetCurrentBuffer"
>Reset Samples</v-btn
>
<v-table density="compact">
@@ -269,8 +277,11 @@ const resetCurrentBuffer = () => {
</template>
<style scoped lang="scss">
th {
padding-left: 8px !important;
padding-right: 8px !important;
}
.v-table {
background-color: #006492 !important;
width: 100%;
font-size: 1rem !important;
@@ -283,13 +294,9 @@ const resetCurrentBuffer = () => {
}
}
tbody {
:hover {
td {
background-color: #005281 !important;
}
}
tr {
td {
padding: 0 !important;
font-size: 1rem !important;
color: white !important;
}
@@ -308,7 +315,7 @@ const resetCurrentBuffer = () => {
}
::-webkit-scrollbar-thumb {
background-color: #ffd843;
background-color: rgb(var(--v-theme-accent));
border-radius: 10px;
}
}

View File

@@ -6,6 +6,9 @@ import PvSwitch from "@/components/common/pv-switch.vue";
import { useStateStore } from "@/stores/StateStore";
import { ColorPicker, type HSV } from "@/lib/ColorPicker";
import { useDisplay } from "vuetify";
import { useTheme } from "vuetify";
const theme = useTheme();
const averageHue = computed<number>(() => {
const isHueInverted = useCameraSettingsStore().currentPipelineSettings.hueInverted;
@@ -186,17 +189,25 @@ const interactiveCols = computed(() =>
<v-btn
size="small"
block
color="accent"
color="primary"
class="text-black"
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
@click="enableColorPicking(useCameraSettingsStore().currentPipelineSettings.hueInverted ? 2 : 3)"
>
<v-icon start> mdi-minus </v-icon>
<v-icon start size="large"> mdi-minus </v-icon>
Shrink Range
</v-btn>
</v-col>
<v-col cols="4" class="pl-0 pr-0">
<v-btn color="accent" class="text-black" size="small" block @click="enableColorPicking(1)">
<v-icon start> mdi-plus-minus </v-icon>
<v-btn
color="primary"
class="text-black"
size="small"
block
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
@click="enableColorPicking(1)"
>
<v-icon start size="large"> mdi-plus-minus </v-icon>
{{ useCameraSettingsStore().currentPipelineSettings.hueInverted ? "Exclude" : "Set to" }} Average
</v-btn>
</v-col>
@@ -204,18 +215,28 @@ const interactiveCols = computed(() =>
<v-btn
size="small"
block
color="accent"
color="primary"
class="text-black"
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
@click="enableColorPicking(useCameraSettingsStore().currentPipelineSettings.hueInverted ? 3 : 2)"
>
<v-icon start> mdi-plus </v-icon>
<v-icon start size="large"> mdi-plus </v-icon>
Expand Range
</v-btn>
</v-col>
</template>
<template v-else>
<v-card-text class="pa-0 pt-3 pb-3">
<v-btn block color="accent" class="text-black" size="small" @click="disableColorPicking"> Cancel </v-btn>
<v-btn
block
color="primary"
class="text-black"
size="small"
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
@click="disableColorPicking"
>
Cancel
</v-btn>
</v-card-text>
</template>
</div>

View File

@@ -1,30 +1,26 @@
<script setup lang="ts">
import { useSettingsStore } from "@/stores/settings/GeneralSettingsStore";
import { Euler, Quaternion as ThreeQuat } from "three";
import type { Quaternion } from "@/types/PhotonTrackingTypes";
import { toDeg } from "@/lib/MathUtils";
const { Euler, Quaternion: ThreeQuat } = await import("three");
const quaternionToEuler = (rot_quat: Quaternion): { x: number; y: number; z: number } => {
const quat = new ThreeQuat(rot_quat.X, rot_quat.Y, rot_quat.Z, rot_quat.W);
const euler = new Euler().setFromQuaternion(quat, "ZYX");
return {
x: toDeg(euler.x),
y: toDeg(euler.y),
z: toDeg(euler.z)
};
return { x: toDeg(euler.x), y: toDeg(euler.y), z: toDeg(euler.z) };
};
</script>
<template>
<v-card style="background-color: #006492">
<v-card-title class="pa-6">AprilTag Field Layout</v-card-title>
<v-card-text class="pa-6 pt-0">
<v-card color="surface" class="rounded-12">
<v-card-title>AprilTag Field Layout</v-card-title>
<v-card-text class="pt-0">
<p>Field width: {{ useSettingsStore().currentFieldLayout.field.width.toFixed(2) }} meters</p>
<p>Field length: {{ useSettingsStore().currentFieldLayout.field.length.toFixed(2) }} meters</p>
<!-- Simple table height must be set here and in the CSS for the fixed-header to work -->
<v-table fixed-header height="100%" density="compact" dark>
<v-table fixed-header height="100%" density="compact">
<template #default>
<thead style="font-size: 1.25rem">
<tr>
@@ -57,11 +53,9 @@ const quaternionToEuler = (rot_quat: Quaternion): { x: number; y: number; z: num
width: 100%;
height: 100%;
text-align: center;
background-color: #006492 !important;
th,
td {
background-color: #006492 !important;
font-size: 1rem !important;
color: white !important;
}
@@ -70,10 +64,6 @@ const quaternionToEuler = (rot_quat: Quaternion): { x: number; y: number; z: num
font-family: monospace !important;
}
tbody :hover td {
background-color: #005281 !important;
}
::-webkit-scrollbar {
width: 0;
height: 0.55em;
@@ -86,7 +76,7 @@ const quaternionToEuler = (rot_quat: Quaternion): { x: number; y: number; z: num
}
::-webkit-scrollbar-thumb {
background-color: #ffd843;
background-color: rgb(var(--v-theme-accent));
border-radius: 10px;
}
}

View File

@@ -4,15 +4,15 @@ 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 { useTheme } from "vuetify";
const theme = useTheme();
const restartProgram = () => {
axios
.post("/utils/restartProgram")
.then(() => {
useStateStore().showSnackbarMessage({
message: "Successfully sent program restart request",
color: "success"
});
useStateStore().showSnackbarMessage({ message: "Successfully sent program restart request", color: "success" });
})
.catch((error) => {
// This endpoint always return 204 regardless of outcome
@@ -98,10 +98,7 @@ const handleOfflineUpdate = () => {
}
})
.then((response) => {
useStateStore().showSnackbarMessage({
message: response.data.text || response.data,
color: "success"
});
useStateStore().showSnackbarMessage({ message: response.data.text || response.data, color: "success" });
})
.catch((error) => {
if (error.response) {
@@ -170,14 +167,9 @@ const handleSettingsImport = () => {
}
axios
.post(`/settings${settingsEndpoint}`, formData, {
headers: { "Content-Type": "multipart/form-data" }
})
.post(`/settings${settingsEndpoint}`, formData, { headers: { "Content-Type": "multipart/form-data" } })
.then((response) => {
useStateStore().showSnackbarMessage({
message: response.data.text || response.data,
color: "success"
});
useStateStore().showSnackbarMessage({ message: response.data.text || response.data, color: "success" });
})
.catch((error) => {
if (error.response) {
@@ -218,7 +210,7 @@ const nukePhotonConfigDirectory = () => {
.catch((error) => {
if (error.response) {
useStateStore().showSnackbarMessage({
message: "The backend is unable to fulfil the request to reset the device.",
message: "The backend is unable to fulfill the request to reset the device.",
color: "error"
});
} else if (error.request) {
@@ -238,35 +230,50 @@ const nukePhotonConfigDirectory = () => {
</script>
<template>
<v-card class="mb-3" style="background-color: #006492">
<v-card-title class="pa-6">Device Control</v-card-title>
<div class="pa-6 pt-0">
<v-card class="mb-3 rounded-12" color="surface">
<v-card-title>Device Control</v-card-title>
<div class="pa-5 pt-0">
<v-row>
<v-col cols="12" lg="4" md="6">
<v-btn color="error" @click="restartProgram">
<v-icon start class="open-icon"> mdi-restart </v-icon>
<v-btn
color="buttonActive"
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
@click="restartProgram"
>
<v-icon start class="open-icon" size="large"> mdi-restart </v-icon>
<span class="open-label">Restart PhotonVision</span>
</v-btn>
</v-col>
<v-col cols="12" lg="4" md="6">
<v-btn color="error" @click="restartDevice">
<v-icon start class="open-icon"> mdi-restart-alert </v-icon>
<v-btn
color="buttonActive"
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
@click="restartDevice"
>
<v-icon start class="open-icon" size="large"> mdi-restart-alert </v-icon>
<span class="open-label">Restart Device</span>
</v-btn>
</v-col>
<v-col cols="12" lg="4">
<v-btn color="secondary" @click="openOfflineUpdatePrompt">
<v-icon start class="open-icon"> mdi-upload </v-icon>
<v-btn
color="buttonPassive"
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
@click="openOfflineUpdatePrompt"
>
<v-icon start class="open-icon" size="large"> mdi-upload </v-icon>
<span class="open-label">Offline Update</span>
</v-btn>
<input ref="offlineUpdate" type="file" accept=".jar" style="display: none" @change="handleOfflineUpdate" />
</v-col>
</v-row>
<v-divider class="mt-3 pb-3" />
<v-row>
<v-col cols="12" sm="6">
<v-btn color="secondary" @click="() => (showImportDialog = true)">
<v-icon start class="open-icon"> mdi-import </v-icon>
<v-btn
color="buttonPassive"
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
@click="() => (showImportDialog = true)"
>
<v-icon start class="open-icon" size="large"> mdi-import </v-icon>
<span class="open-label">Import Settings</span>
</v-btn>
<v-dialog
@@ -279,11 +286,11 @@ const nukePhotonConfigDirectory = () => {
}
"
>
<v-card color="primary" dark>
<v-card-title>Import Settings</v-card-title>
<v-card color="surface" dark>
<v-card-title class="pb-0">Import Settings</v-card-title>
<v-card-text>
Upload and apply previously saved or exported PhotonVision settings to this device
<v-row class="mt-6 ml-4">
<div class="pa-5 pb-0">
<pv-select
v-model="importType"
label="Type"
@@ -298,28 +305,35 @@ const nukePhotonConfigDirectory = () => {
:select-cols="10"
style="width: 100%"
/>
</v-row>
<v-row class="mt-6 ml-4 mr-8">
<v-file-input
v-model="importFile"
class="pb-5"
variant="underlined"
:disabled="importType === undefined"
:error-messages="importType === undefined ? 'Settings type not selected' : ''"
:accept="importType === ImportType.AllSettings ? '.zip' : '.json'"
/>
</v-row>
<v-row class="mt-12 ml-8 mr-8 mb-1" style="display: flex; align-items: center; justify-content: center">
<v-btn color="secondary" :disabled="importFile === null" @click="handleSettingsImport">
<v-btn
color="primary"
:disabled="importFile === null"
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
@click="handleSettingsImport"
>
<v-icon start class="open-icon"> mdi-import </v-icon>
<span class="open-label">Import Settings</span>
</v-btn>
</v-row>
</div>
</v-card-text>
</v-card>
</v-dialog>
</v-col>
<v-col cols="12" sm="6">
<v-btn color="secondary" @click="openExportSettingsPrompt">
<v-icon start class="open-icon"> mdi-export </v-icon>
<v-btn
color="buttonPassive"
: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">Export Settings</span>
</v-btn>
<a
@@ -331,8 +345,12 @@ const nukePhotonConfigDirectory = () => {
/>
</v-col>
<v-col cols="12" sm="6">
<v-btn color="secondary" @click="openExportLogsPrompt">
<v-icon start class="open-icon"> mdi-download </v-icon>
<v-btn
color="buttonPassive"
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
@click="openExportLogsPrompt"
>
<v-icon start class="open-icon" size="large"> mdi-download </v-icon>
<span class="open-label">Download logs</span>
<!-- Special hidden link that gets 'clicked' when the user exports journalctl logs -->
@@ -346,46 +364,51 @@ const nukePhotonConfigDirectory = () => {
</v-btn>
</v-col>
<v-col cols="12" sm="6">
<v-btn color="secondary" @click="useStateStore().showLogModal = true">
<v-icon start class="open-icon"> mdi-eye </v-icon>
<span class="open-label">View program logs</span>
<v-btn
color="buttonPassive"
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
@click="useStateStore().showLogModal = true"
>
<v-icon start class="open-icon" size="large"> mdi-eye </v-icon>
<span class="open-label">View logs</span>
</v-btn>
</v-col>
</v-row>
<v-divider class="mt-3 pb-3" />
<v-row>
<v-col cols="12">
<v-btn color="error" @click="() => (showFactoryReset = true)">
<v-icon start class="open-icon"> mdi-skull-crossbones </v-icon>
<span class="open-icon">
{{
$vuetify.display.mdAndUp
? "Factory Reset PhotonVision and delete EVERYTHING"
: "Factory Reset PhotonVision"
}}
</span>
<v-btn
color="error"
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
@click="() => (showFactoryReset = true)"
>
<v-icon start class="open-icon" size="large"> mdi-trash-can-outline </v-icon>
<span class="open-icon"> Factory Reset PhotonVision </span>
</v-btn>
</v-col>
</v-row>
</div>
<v-dialog v-model="showFactoryReset" width="800" dark>
<v-card color="primary" class="pa-3" flat>
<v-card-title style="justify-content: center" class="pb-6">
<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">mdi-nuke</v-icon>
<v-icon end color="red" class="open-icon ma-1" size="large">mdi-alert-outline</v-icon>
Factory Reset PhotonVision
<v-icon end color="error" class="open-icon ma-1">mdi-nuke</v-icon>
<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-3">
<v-card-text class="pt-0 pb-10px">
<v-row class="align-center text-white">
<v-col cols="12" md="6">
<span class="mt-3"> This will delete ALL OF YOUR SETTINGS and restart PhotonVision. </span>
<span> This will delete ALL OF YOUR SETTINGS and restart PhotonVision. </span>
</v-col>
<v-col cols="12" md="6">
<v-btn color="secondary" style="float: right" @click="openExportSettingsPrompt">
<v-icon start class="open-icon"> mdi-export </v-icon>
<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"
@@ -398,7 +421,7 @@ const nukePhotonConfigDirectory = () => {
</v-col>
</v-row>
</v-card-text>
<v-card-text>
<v-card-text class="pt-0 pb-0">
<pv-input
v-model="yesDeleteMySettingsText"
:label="'Type &quot;' + expected + '&quot;:'"
@@ -406,13 +429,14 @@ const nukePhotonConfigDirectory = () => {
:input-cols="6"
/>
</v-card-text>
<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"> mdi-trash-can-outline </v-icon>
<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>

View File

@@ -4,18 +4,16 @@ import { useSettingsStore } from "@/stores/settings/GeneralSettingsStore";
</script>
<template>
<v-card class="mb-3 pr-6 pb-3" style="background-color: #006492">
<v-card-title>LED Control</v-card-title>
<div class="ml-5">
<v-card class="mb-3 rounded-12" color="surface">
<v-card-title class="pb-10px">LED Control</v-card-title>
<v-card-text>
<pv-slider
v-model="useSettingsStore().lighting.brightness"
label="Brightness"
class="pt-2"
:slider-cols="12"
:min="0"
:max="100"
@update:modelValue="(args) => useSettingsStore().changeLEDBrightness(args)"
/>
</div>
</v-card-text>
</v-card>
</template>

View File

@@ -10,77 +10,82 @@ interface MetricItem {
const generalMetrics = computed<MetricItem[]>(() => {
const stats = [
{
header: "Version",
value: useSettingsStore().general.version || "Unknown"
},
{
header: "Hardware Model",
value: useSettingsStore().general.hardwareModel || "Unknown"
},
{
header: "Platform",
value: useSettingsStore().general.hardwarePlatform || "Unknown"
},
{
header: "GPU Acceleration",
value: useSettingsStore().general.gpuAcceleration || "Unknown"
}
{ header: "Version", value: useSettingsStore().general.version || "Unknown" },
{ header: "Hardware Model", value: useSettingsStore().general.hardwareModel || "Unknown" },
{ header: "Platform", value: useSettingsStore().general.hardwarePlatform || "Unknown" },
{ header: "GPU Acceleration", value: useSettingsStore().general.gpuAcceleration || "Unknown" }
];
if (!useSettingsStore().network.networkingDisabled) {
stats.push({
header: "IP Address",
value: useSettingsStore().metrics.ipAddress || "Unknown"
});
stats.push({ header: "IP Address", value: useSettingsStore().metrics.ipAddress || "Unknown" });
}
return stats;
});
// @ts-expect-error This uses Intl.DurationFormat which is newly implemented and not available in TS.
const durationFormatter = new Intl.DurationFormat("en", { style: "narrow" });
const platformMetrics = computed<MetricItem[]>(() => {
const metrics = useSettingsStore().metrics;
const stats = [
{
header: "CPU Temp",
value: useSettingsStore().metrics.cpuTemp === undefined ? "Unknown" : `${useSettingsStore().metrics.cpuTemp}°C`
value: metrics.cpuTemp === undefined || metrics.cpuTemp == -1 ? "Unknown" : `${metrics.cpuTemp}°C`
},
{
header: "CPU Usage",
value: useSettingsStore().metrics.cpuUtil === undefined ? "Unknown" : `${useSettingsStore().metrics.cpuUtil}%`
value: metrics.cpuUtil === undefined ? "Unknown" : `${metrics.cpuUtil}%`
},
{
header: "CPU Memory Usage",
value:
useSettingsStore().metrics.ramUtil === undefined || useSettingsStore().metrics.cpuMem === undefined
? "Unknown"
: `${useSettingsStore().metrics.ramUtil || "Unknown"}MB of ${useSettingsStore().metrics.cpuMem}MB`
metrics.ramUtil && metrics.ramMem && metrics.ramUtil >= 0 && metrics.ramMem >= 0
? `${metrics.ramUtil}MB of ${metrics.ramMem}MB`
: "Unknown"
},
{
header: "GPU Memory Usage",
value:
useSettingsStore().metrics.gpuMemUtil === undefined || useSettingsStore().metrics.gpuMem === undefined
? "Unknown"
: `${useSettingsStore().metrics.gpuMemUtil}MB of ${useSettingsStore().metrics.gpuMem}MB`
},
{
header: "CPU Throttling",
value: useSettingsStore().metrics.cpuThr || "Unknown"
},
{
header: "CPU Uptime",
value: useSettingsStore().metrics.cpuUptime || "Unknown"
header: "Uptime",
value: (() => {
const seconds = metrics.uptime;
if (seconds === undefined) return "Unknown";
const days = Math.floor(seconds / 86400);
const hours = Math.floor((seconds % 86400) / 3600);
const minutes = Math.floor((seconds % 3600) / 60);
const secs = Math.floor(seconds % 60);
return durationFormatter.format({
days: days,
hours: hours,
minutes: minutes,
seconds: secs
});
})()
},
{
header: "Disk Usage",
value: useSettingsStore().metrics.diskUtilPct || "Unknown"
value: metrics.diskUtilPct === undefined ? "Unknown" : `${metrics.diskUtilPct}%`
}
];
if (useSettingsStore().metrics.npuUsage) {
if (metrics.npuUsage && metrics.npuUsage.length > 0) {
stats.push({
header: "NPU Usage",
value: useSettingsStore().metrics.npuUsage || "Unknown"
value: metrics.npuUsage?.map((usage, index) => `Core${index} ${usage}%`).join(", ") || "Unknown"
});
}
if (metrics.gpuMem && metrics.gpuMemUtil && metrics.gpuMem > 0 && metrics.gpuMemUtil > 0) {
stats.push({
header: "GPU Memory Usage",
value: `${metrics.gpuMemUtil}MB of ${metrics.gpuMem}MB`
});
}
if (metrics.cpuThr) {
stats.push({
header: "CPU Throttling",
value: metrics.cpuThr.toString()
});
}
@@ -120,16 +125,16 @@ onBeforeMount(() => {
</script>
<template>
<v-card class="mb-3" style="background-color: #006492">
<v-card-title class="pl-6" style="display: flex; justify-content: space-between">
<span class="pt-2 pb-2">Stats</span>
<v-card class="mb-3 rounded-12" color="surface">
<v-card-title style="display: flex; justify-content: space-between">
<span>Metrics</span>
<v-btn variant="text" @click="fetchMetrics">
<v-icon start class="open-icon">mdi-reload</v-icon>
<v-icon start class="open-icon" size="large">mdi-reload</v-icon>
Last Fetched: {{ metricsLastFetched }}
</v-btn>
</v-card-title>
<v-card-text class="pa-6 pt-0 pb-3">
<v-card-subtitle class="pa-0" style="font-size: 16px">General Metrics</v-card-subtitle>
<v-card-text class="pt-0 pb-3">
<v-card-subtitle class="pa-0" style="font-size: 16px">General</v-card-subtitle>
<v-table class="metrics-table mt-3">
<thead>
<tr>
@@ -165,8 +170,8 @@ onBeforeMount(() => {
</tbody>
</v-table>
</v-card-text>
<v-card-text class="pa-6 pt-4">
<v-card-subtitle class="pa-0 pb-1" style="font-size: 16px">Hardware Metrics</v-card-subtitle>
<v-card-text class="pt-4">
<v-card-subtitle class="pa-0 pb-1" style="font-size: 16px">Hardware</v-card-subtitle>
<v-table class="metrics-table mt-3">
<thead>
<tr>
@@ -212,46 +217,52 @@ onBeforeMount(() => {
text-align: center;
}
$stats-table-border: rgba(255, 255, 255, 0.5);
$stats-table-inner: rgba(255, 255, 255, 0.1);
.t {
border-top: 1px solid white;
border-right: 1px solid white;
border-top: 1px solid $stats-table-border;
border-right: 1px solid $stats-table-border;
border-bottom: 1px solid $stats-table-inner !important;
}
.b {
border-bottom: 1px solid white;
border-right: 1px solid white;
border-bottom: 1px solid $stats-table-border;
border-right: 1px solid $stats-table-border;
}
.tl {
border-top: 1px solid white;
border-left: 1px solid white;
border-right: 1px solid white;
border-top: 1px solid $stats-table-border;
border-left: 1px solid $stats-table-border;
border-right: 1px solid $stats-table-border;
border-bottom: 1px solid $stats-table-inner !important;
border-top-left-radius: 5px;
}
.tr {
border-top: 1px solid white;
border-right: 1px solid white;
border-top: 1px solid $stats-table-border;
border-right: 1px solid $stats-table-border;
border-bottom: 1px solid $stats-table-inner !important;
border-top-right-radius: 5px;
}
.bl {
border-bottom: 1px solid white;
border-left: 1px solid white;
border-right: 1px solid white;
border-bottom: 1px solid $stats-table-border;
border-left: 1px solid $stats-table-border;
border-right: 1px solid $stats-table-border;
border-bottom-left-radius: 5px;
}
.br {
border-bottom: 1px solid white;
border-right: 1px solid white;
border-bottom: 1px solid $stats-table-border;
border-right: 1px solid $stats-table-border;
border-bottom-right-radius: 5px;
}
.metric-item {
font-size: 16px !important;
padding: 1px 15px 1px 10px;
border-right: 1px solid;
border-right: 1px solid $stats-table-border;
font-weight: normal;
color: white !important;
text-align: center !important;
@@ -259,22 +270,9 @@ onBeforeMount(() => {
.metric-item-title {
font-size: 18px !important;
text-decoration: underline;
text-decoration-color: #ffd843;
}
.v-table {
thead,
tbody {
background-color: #006492;
}
:hover {
tbody > tr {
background-color: #005281 !important;
}
}
::-webkit-scrollbar {
width: 0;
height: 0.55em;
@@ -287,7 +285,7 @@ onBeforeMount(() => {
}
::-webkit-scrollbar-thumb {
background-color: #ffd843;
background-color: rgb(var(--v-theme-accent));
border-radius: 10px;
}
}

View File

@@ -7,6 +7,10 @@ import PvSwitch from "@/components/common/pv-switch.vue";
import PvSelect from "@/components/common/pv-select.vue";
import { type ConfigurableNetworkSettings, NetworkConnectionType } from "@/types/SettingTypes";
import { useStateStore } from "@/stores/StateStore";
import { useTheme } from "vuetify";
import { getThemeColor, setThemeColor, resetTheme } from "@/lib/ThemeManager";
const theme = useTheme();
// Copy object to remove reference to store
const tempSettingsStruct = ref<ConfigurableNetworkSettings>(Object.assign({}, useSettingsStore().network));
@@ -16,6 +20,19 @@ const resetTempSettingsStruct = () => {
const settingsValid = ref(true);
const showThemeConfig = ref(false);
const backgroundColor = ref("");
const primaryColor = ref("");
const secondaryColor = ref("");
const surfaceColor = ref("");
const loadCurrentColors = () => {
backgroundColor.value = getThemeColor(theme, "background");
primaryColor.value = getThemeColor(theme, "primary");
secondaryColor.value = getThemeColor(theme, "secondary");
surfaceColor.value = getThemeColor(theme, "surface");
};
const isValidNetworkTablesIP = (v: string | undefined): boolean => {
// Check if it is a valid team number between 1-99999 (5 digits)
const teamNumberRegex = /^[1-9][0-9]{0,4}$/;
@@ -83,16 +100,10 @@ const saveGeneralSettings = () => {
useSettingsStore()
.updateGeneralSettings(payload)
.then((response) => {
useStateStore().showSnackbarMessage({
message: response.data.text || response.data,
color: "success"
});
useStateStore().showSnackbarMessage({ message: response.data.text || response.data, color: "success" });
// Update the local settings cause the backend checked their validity. Assign is to deref value
useSettingsStore().network = {
...useSettingsStore().network,
...Object.assign({}, tempSettingsStruct.value)
};
useSettingsStore().network = { ...useSettingsStore().network, ...Object.assign({}, tempSettingsStruct.value) };
})
.catch((error) => {
resetTempSettingsStruct();
@@ -141,11 +152,24 @@ watchEffect(() => {
</script>
<template>
<v-card class="mb-3" style="background-color: #006492">
<v-card-title class="pa-6">Global Settings</v-card-title>
<div class="pa-6 pt-0">
<v-divider class="pb-3" />
<v-card-title class="pl-0 pt-3 pb-3">Networking</v-card-title>
<v-card class="mb-3 rounded-12" color="surface">
<v-card-title style="display: flex; justify-content: space-between">
<span>Global Settings</span>
<v-btn
variant="text"
@click="
() => {
loadCurrentColors();
showThemeConfig = true;
}
"
>
<v-icon size="x-large">mdi-palette-outline</v-icon>
Theme
</v-btn>
</v-card-title>
<div class="pa-5 pt-0">
<v-card-title class="pl-0 pt-0 pb-10px">Networking</v-card-title>
<v-form ref="form" v-model="settingsValid">
<pv-input
v-model="tempSettingsStruct.ntServerAddress"
@@ -159,16 +183,15 @@ watchEffect(() => {
'The NetworkTables Server Address must be a valid Team Number, IP address, or Hostname'
]"
/>
<v-banner
<v-alert
v-if="!isValidNetworkTablesIP(tempSettingsStruct.ntServerAddress) && !tempSettingsStruct.runNTServer"
rounded
bg-color="error"
text-color="white"
style="margin: 10px 0"
class="pt-3 pb-3"
color="error"
density="compact"
text="The NetworkTables Server Address is not set or is invalid. NetworkTables is unable to connect."
icon="mdi-alert-circle-outline"
>
The NetworkTables Server Address is not set or is invalid. NetworkTables is unable to connect.
</v-banner>
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'tonal'"
/>
<pv-radio
v-show="!useSettingsStore().network.networkingDisabled"
v-model="tempSettingsStruct.connectionType"
@@ -207,8 +230,7 @@ watchEffect(() => {
useSettingsStore().network.networkingDisabled
"
/>
<v-divider class="mt-3 pb-3" />
<v-card-title class="pl-0 pt-3 pb-3">Advanced Networking</v-card-title>
<v-card-title class="pl-0 pt-3 pb-10px">Advanced Networking</v-card-title>
<pv-switch
v-show="!useSettingsStore().network.networkingDisabled"
v-model="tempSettingsStruct.shouldManage"
@@ -230,58 +252,54 @@ watchEffect(() => {
tooltip="Name of the interface PhotonVision should manage the IP address of"
:items="useSettingsStore().networkInterfaceNames"
/>
<v-banner
<v-alert
v-if="
!useSettingsStore().networkInterfaceNames.length &&
tempSettingsStruct.shouldManage &&
useSettingsStore().network.canManage &&
!useSettingsStore().network.networkingDisabled
"
rounded
bg-color="error"
text-color="white"
icon="mdi-information-outline"
>
Photon cannot detect any wired connections! Please send program logs to the developers for help.
</v-banner>
class="pt-3 pb-3"
color="error"
density="compact"
text="Cannot detect any wired connections! Send program logs to the developers for help."
icon="mdi-alert-circle-outline"
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'tonal'"
/>
<pv-switch
v-model="tempSettingsStruct.runNTServer"
label="Run NetworkTables Server (Debugging Only)"
tooltip="If enabled, this device will create a NT server. This is useful for home debugging, but should be disabled on-robot."
:label-cols="4"
/>
<v-banner
<v-alert
v-if="tempSettingsStruct.runNTServer"
rounded
bg-color="error"
text-color="white"
color="buttonActive"
density="compact"
text="This mode is intended for debugging and should be off for proper usage. PhotonLib will NOT work!"
icon="mdi-information-outline"
>
This mode is intended for debugging; it should be off for proper usage. PhotonLib will NOT work!
</v-banner>
<v-divider class="mt-3 pb-3" />
<v-card-title class="pl-0 pt-3 pb-3">Miscellaneous</v-card-title>
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'tonal'"
/>
<v-card-title class="pl-0 pt-3 pb-10px">Miscellaneous</v-card-title>
<pv-switch
v-model="tempSettingsStruct.shouldPublishProto"
label="Also Publish Protobuf"
tooltip="If enabled, Photon will publish all pipeline results in both the Packet and Protobuf formats. This is useful for visualizing pipeline results from NT viewers such as glass and logging software such as AdvantageScope. Note: photon-lib will ignore this value and is not recommended on the field for performance."
:label-cols="4"
/>
<v-banner
<v-alert
v-if="tempSettingsStruct.shouldPublishProto"
rounded
bg-color="error"
text-color="white"
color="buttonActive"
density="compact"
text="This mode is intended for debugging and may reduce performance; it should be off for field use."
icon="mdi-information-outline"
>
This mode is intended for debugging; it should be off for field use. You may notice a performance hit by using
this mode.
</v-banner>
<v-divider class="mt-3 mb-6" />
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'tonal'"
/>
</v-form>
<v-btn
color="accent"
:variant="!settingsValid || !settingsHaveChanged() ? 'tonal' : 'elevated'"
color="primary"
class="mt-3"
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
style="color: black; width: 100%"
:disabled="!settingsValid || !settingsHaveChanged()"
@click="saveGeneralSettings"
@@ -289,11 +307,89 @@ watchEffect(() => {
Save
</v-btn>
</div>
<v-dialog v-model="showThemeConfig" width="800" dark>
<v-card color="surface" flat>
<v-card-title class="text-center">Theme Configuration</v-card-title>
<v-card-text class="pt-0 pb-10px">
<v-row>
<v-col class="text-center">
Background
<v-color-picker
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)"
></v-color-picker>
</v-col>
<v-col class="text-center">
Surface
<v-color-picker
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)"
></v-color-picker>
</v-col>
</v-row>
<v-row>
<v-col class="text-center">
Primary
<v-color-picker
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)"
></v-color-picker>
</v-col>
<v-col class="text-center">
Secondary
<v-color-picker
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)"
></v-color-picker>
</v-col>
</v-row>
</v-card-text>
<v-card-actions class="pa-5 pt-0">
<v-btn
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
color="buttonPassive"
class="text-black"
@click="showThemeConfig = false"
>
Close
</v-btn>
<v-btn
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
color="buttonActive"
class="text-black"
@click="
() => {
resetTheme(theme);
loadCurrentColors();
}
"
>
Reset Default
</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
</v-card>
</template>
<style>
.v-banner__wrapper {
padding: 6px !important;
.mt-10px {
margin-top: 10px !important;
}
</style>

View File

@@ -3,40 +3,39 @@ 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 { useTheme } from "vuetify";
const theme = useTheme();
const showImportDialog = ref(false);
const importRKNNFile = ref<File | null>(null);
const importLabelsFile = ref<File | null>(null);
const showInfo = ref({ show: false, model: {} as ObjectDetectionModelProperties });
const confirmDeleteDialog = ref({ show: false, model: {} as ObjectDetectionModelProperties });
const showRenameDialog = ref({
show: false,
model: {} as ObjectDetectionModelProperties,
newName: ""
});
const host = inject<string>("backendHost");
const address = inject<string>("backendHost");
const areValidFileNames = (weights: string | null, labels: string | null) => {
const weightsRegex = /^([a-zA-Z0-9._]+)-(\d+)-(\d+)-(yolov(?:5|8|11)[nsmlx]*)\.rknn$/;
const labelsRegex = /^([a-zA-Z0-9._]+)-(\d+)-(\d+)-(yolov(?:5|8|11)[nsmlx]*)-labels\.txt$/;
if (weights && labels) {
const weightsMatch = weights.match(weightsRegex);
const labelsMatch = labels.match(labelsRegex);
if (weightsMatch && labelsMatch) {
return (
weightsMatch[1] === labelsMatch[1] &&
weightsMatch[2] === labelsMatch[2] &&
weightsMatch[3] === labelsMatch[3] &&
weightsMatch[4] === labelsMatch[4]
);
}
}
return false;
};
const importModelFile = ref<File | null>(null);
const importLabels = ref<string | null>(null);
const importHeight = ref<number | null>(null);
const importWidth = ref<number | null>(null);
const importVersion = ref<string | null>(null);
// TODO gray out the button when model is uploading
const handleImport = async () => {
if (importRKNNFile.value === null || importLabelsFile.value === null) return;
if (importModelFile.value === null) return;
const formData = new FormData();
formData.append("rknn", importRKNNFile.value);
formData.append("labels", importLabelsFile.value);
formData.append("modelFile", importModelFile.value);
formData.append("labels", importLabels.value?.toString() || "");
formData.append("height", importHeight.value?.toString() || "");
formData.append("width", importWidth.value?.toString() || "");
formData.append("version", importVersion.value?.toString() || "");
useStateStore().showSnackbarMessage({
message: "Importing Object Detection Model...",
@@ -45,7 +44,7 @@ const handleImport = async () => {
});
axios
.post("/utils/importObjectDetectionModel", formData, {
.post("/objectdetection/import", formData, {
headers: { "Content-Type": "multipart/form-data" }
})
.then((response) => {
@@ -75,94 +74,571 @@ const handleImport = async () => {
showImportDialog.value = false;
importRKNNFile.value = null;
importLabelsFile.value = null;
importModelFile.value = null;
importLabels.value = null;
importHeight.value = null;
importWidth.value = null;
importVersion.value = null;
};
const deleteModel = async (model: ObjectDetectionModelProperties) => {
useStateStore().showSnackbarMessage({
message: "Deleting Object Detection Model...",
color: "secondary",
timeout: -1
});
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) => {
useStateStore().showSnackbarMessage({
message: "Renaming Object Detection Model...",
color: "secondary",
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."
});
}
});
showRenameDialog.value.show = false;
};
// Filters out models that are not supported by the current backend, and returns a flattened list.
const supportedModels = computed(() => {
const { availableModels, supportedBackends } = useSettingsStore().general;
return supportedBackends.flatMap((backend) => availableModels[backend] || []);
const isSupported = (model: any) => {
// Check if model's family is in the list of supported backends
return supportedBackends.some((backend: string) => backend.toLowerCase() === model.family.toLowerCase());
};
// Filter models where the family is supported and flatten the list
return availableModels.filter(isSupported);
});
const exportModels = ref();
const openExportPrompt = () => {
exportModels.value.click();
};
const exportIndividualModel = ref();
const openExportIndividualModelPrompt = () => {
exportIndividualModel.value.click();
};
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;
};
const showBulkImportDialog = ref(false);
const importFile = ref<File | null>(null);
const handleBulkImport = () => {
if (importFile.value === null) return;
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) {
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."
});
}
});
showImportDialog.value = false;
importFile.value = null;
};
</script>
<template>
<v-card class="mb-3" style="background-color: #006492">
<v-card-title class="pa-6">Object Detection</v-card-title>
<div class="pa-6 pt-0">
<v-card class="mb-3" color="surface">
<v-card-title>Object Detection</v-card-title>
<div class="pa-5 pt-0">
<v-row>
<v-col cols="12 ">
<v-btn color="secondary" class="justify-center" @click="() => (showImportDialog = true)">
<v-col cols="12" sm="6">
<v-btn
color="buttonActive"
class="justify-center"
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
@click="() => (showImportDialog = true)"
>
<v-icon start class="open-icon"> mdi-import </v-icon>
<span class="open-label">Import New Model</span>
<span class="open-label">Import Model</span>
</v-btn>
<v-dialog
v-model="showImportDialog"
width="600"
@update:modelValue="
() => {
importRKNNFile = null;
importLabelsFile = null;
importModelFile = null;
importLabels = null;
importHeight = null;
importWidth = null;
importVersion = null;
}
"
>
<v-card color="primary" dark>
<v-card-title>Import New Object Detection Model</v-card-title>
<v-card color="surface" dark>
<v-card-title class="pb-0">Import New Object Detection Model</v-card-title>
<v-card-text>
Upload a new object detection model to this device that can be used in a pipeline. Note that ONLY
640x640 YOLOv5, YOLOv8, and YOLOv11 models trained and converted to `.rknn` format for RK3588 CPUs are
currently supported! See [the documentation]({{
host
}}/docs/objectDetection/about-object-detection.html) for more details.
<v-row class="mt-6 ml-4 mr-8">
<v-file-input v-model="importRKNNFile" label="RKNN File" accept=".rknn" />
</v-row>
<v-row class="mt-6 ml-4 mr-8">
<v-file-input v-model="importLabelsFile" label="Labels File" accept=".txt" />
</v-row>
<v-row class="mt-12 ml-8 mr-8 mb-1" style="display: flex; align-items: center; justify-content: center">
<v-btn
color="secondary"
:disabled="
importRKNNFile === null ||
importLabelsFile === null ||
!areValidFileNames(importRKNNFile.name, importLabelsFile.name)
<span v-if="useSettingsStore().general.supportedBackends?.includes('RKNN')"
>Upload a new object detection model to this device that can be used in a pipeline. Note that ONLY
640x640 YOLOv5, YOLOv8, and YOLOv11 models trained and converted to `.rknn` format for RK3588 SOCs are
currently supporter!</span
>
<span v-else-if="useSettingsStore().general.supportedBackends?.includes('RUBIK')"
>Upload a new object detection model to this device that can be used in a pipeline. Note that ONLY
640x640 YOLOv8 and YOLOv11 models trained and converted to `.tflite` format for QCS6490 compatible
backends are currently supported!
</span>
<span v-else>
If you're seeing this, something broke; please file a ticket and tell us the details of your
situation.</span
>
<div class="pa-5 pb-0">
<v-file-input
v-model="importModelFile"
variant="underlined"
label="Model File"
:accept="
useSettingsStore().general.supportedBackends?.includes('RKNN')
? '.rknn'
: useSettingsStore().general.supportedBackends?.includes('RUBIK')
? '.tflite'
: ''
"
@click="handleImport"
/>
<v-text-field
v-model="importLabels"
label="Labels"
placeholder="Comma separated labels, no spaces"
type="text"
variant="underlined"
/>
<v-text-field v-model="importWidth" variant="underlined" label="Width" type="number" />
<v-text-field v-model="importHeight" variant="underlined" label="Height" type="number" />
<v-select
v-model="importVersion"
variant="underlined"
label="Model Version"
:items="
useSettingsStore().general.supportedBackends?.includes('RKNN')
? ['YOLOv5', 'YOLOv8', 'YOLO11']
: ['YOLOv8', 'YOLO11']
"
/>
<v-btn
color="buttonActive"
width="100%"
:disabled="
importModelFile === null ||
importLabels === null ||
importWidth === null ||
importHeight === null ||
importVersion === null
"
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
@click="handleImport()"
>
<v-icon start class="open-icon"> mdi-import </v-icon>
<v-icon start class="open-icon" size="large"> mdi-import </v-icon>
<span class="open-label">Import Object Detection Model</span>
</v-btn>
</v-row>
</div>
</v-card-text>
</v-card>
</v-dialog>
</v-col>
<v-col cols="12" sm="6">
<v-btn
color="buttonActive"
class="justify-center"
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
@click="() => (showBulkImportDialog = true)"
>
<v-icon start class="open-icon"> mdi-import </v-icon>
<span class="open-label">Bulk Import</span>
</v-btn>
<v-dialog v-model="showBulkImportDialog" width="600">
<v-card color="surface" dark>
<v-card-title class="pb-0">Import Multiple Object Detection Models</v-card-title>
<v-card-text>
Upload a zip file containing multiple object detection models to this device. Note this zip file should
only come from a previous export of object detection models.
<div class="pa-5 pb-0">
<v-file-input v-model="importFile" variant="underlined" label="Zip File" accept=".zip" />
<v-btn
color="buttonActive"
width="100%"
:disabled="importFile === null"
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
@click="handleBulkImport()"
>
<v-icon start class="open-icon" size="large"> mdi-import </v-icon>
<span class="open-label">Bulk Import</span>
</v-btn>
</div>
</v-card-text>
</v-card>
</v-dialog>
</v-col>
<v-col cols="12" sm="6">
<v-btn
color="buttonPassive"
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
@click="openExportPrompt"
>
<v-icon start class="open-icon"> mdi-export </v-icon>
<span class="open-label">Export Models</span>
</v-btn>
<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-col>
<v-col cols="12" sm="6">
<v-btn
color="error"
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
@click="() => (showNukeDialog = true)"
>
<v-icon left class="open-icon"> mdi-trash </v-icon>
<span class="open-label">Clear and reset models</span>
</v-btn>
</v-col>
</v-row>
<v-row>
<v-col cols="">
<v-table fixed-header height="100%" density="compact" dark>
<thead style="font-size: 1.25rem">
<tr>
<th>Model Nicknames</th>
<th>Labels</th>
<th>Delete</th>
<th>Edit</th>
<th>Info</th>
</tr>
</thead>
<tbody>
<tr v-for="model in supportedModels" :key="model.modelPath">
<td>{{ model.nickname }}</td>
<td>{{ model.labels.join(", ") }}</td>
<td class="text-right">
<v-btn
icon
small
color="error"
title="Delete Model"
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
@click="() => (confirmDeleteDialog = { show: true, model })"
>
<v-icon size="large">mdi-trash-can-outline</v-icon>
</v-btn>
</td>
<td class="text-right">
<v-btn
icon
small
color="buttonActive"
title="Rename Model"
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
@click="() => (showRenameDialog = { show: true, model, newName: '' })"
>
<v-icon size="large">mdi-pencil</v-icon>
</v-btn>
</td>
<td class="text-right">
<v-btn
icon
small
color="buttonPassive"
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
@click="() => (showInfo = { show: true, model })"
>
<v-icon size="large">mdi-information</v-icon>
</v-btn>
</td>
</tr>
</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>
<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 }}:
<div class="pa-5 pb-0">
<v-text-field v-model="showRenameDialog.newName" hide-details label="New Name" variant="underlined" />
</div>
<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="error"
@click="showRenameDialog.show = false"
>Cancel</v-btn
>
<v-btn
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
color="buttonActive"
@click="renameModel(showRenameDialog.model, showRenameDialog.newName)"
>Rename</v-btn
>
</v-card-actions>
</v-card-text>
</v-card>
</v-dialog>
<v-dialog v-model="showInfo.show" width="600">
<v-card color="surface" dark>
<v-card-title>Object Detection Model Info</v-card-title>
<v-card-text class="pt-0">
<v-btn
color="buttonPassive"
width="100%"
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
@click="openExportIndividualModelPrompt"
>
<v-icon left class="open-icon" size="large"> mdi-export </v-icon>
<span class="open-label">Export Model</span>
</v-btn>
<a
ref="exportIndividualModel"
style="color: black; text-decoration: none; display: none"
:href="`http://${address}/api/objectdetection/exportIndividual?modelPath=${showInfo.model.modelPath.replace('file:', '')}`"
:download="`${showInfo.model.nickname}_${showInfo.model.family}_${showInfo.model.version}_${showInfo.model.resolutionWidth}x${showInfo.model.resolutionHeight}_${showInfo.model.labels.join('_')}.${showInfo.model.family.toLowerCase()}`"
target="_blank"
/>
<div class="pt-5">
<p>Model Path: {{ showInfo.model.modelPath }}</p>
<p>Model Nickname: {{ showInfo.model.nickname }}</p>
<p>Model Family: {{ showInfo.model.family }}</p>
<p>Model Version: {{ showInfo.model.version }}</p>
<p>Model Label(s): {{ showInfo.model.labels.join(", ") }}</p>
<p>Model Resolution: {{ showInfo.model.resolutionWidth }} x {{ showInfo.model.resolutionHeight }}</p>
</div>
</v-card-text>
</v-card>
</v-dialog>
</v-col>
</v-row>
<v-row>
<v-col cols="12">
<v-table fixed-header height="100%" density="compact" dark>
<thead style="font-size: 1.25rem">
<tr>
<th class="text-left">Available Models</th>
</tr>
</thead>
<tbody>
<tr v-for="model in supportedModels" :key="model">
<td>{{ model }}</td>
</tr>
</tbody>
</v-table>
</v-col>
</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 &quot;' + expected + '&quot;:'"
: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>
</v-card>
</template>
<style scoped lang="scss">
.v-btn {
.v-col-12 > .v-btn {
width: 100%;
}
.pt-10px {
padding-top: 10px !important;
}
@media only screen and (max-width: 351px) {
.open-icon {
margin: 0 !important;
@@ -175,23 +651,18 @@ const supportedModels = computed(() => {
width: 100%;
height: 100%;
text-align: center;
background-color: #006492 !important;
th,
td {
background-color: #006492 !important;
font-size: 1rem !important;
color: white !important;
text-align: center !important;
}
td {
font-family: monospace !important;
}
tbody :hover td {
background-color: #005281 !important;
}
::-webkit-scrollbar {
width: 0;
height: 0.55em;
@@ -204,7 +675,7 @@ const supportedModels = computed(() => {
}
::-webkit-scrollbar-thumb {
background-color: #ffd843;
background-color: rgb(var(--v-theme-accent));
border-radius: 10px;
}
}

View File

@@ -0,0 +1,65 @@
import { type ThemeInstance } from "vuetify";
import { LightTheme, DarkTheme } from "@/plugins/vuetify";
export const resetTheme = (theme: ThemeInstance) => {
const themeType = theme.global.name.value === "LightTheme" ? "light" : "dark";
localStorage.removeItem(`${themeType}-background`);
localStorage.removeItem(`${themeType}-primary`);
localStorage.removeItem(`${themeType}-secondary`);
localStorage.removeItem(`${themeType}-surface`);
restoreThemeConfig(theme);
};
export const getThemeColor = (theme: ThemeInstance, color: string): string => {
const themeType = theme.global.name.value === "LightTheme" ? "light" : "dark";
const defaultTheme = theme.global.name.value === "LightTheme" ? LightTheme : DarkTheme;
return localStorage.getItem(`${themeType}-${color}`) ?? defaultTheme.colors![color]!;
};
export const setThemeColor = (theme: ThemeInstance, color: string, value: string | null) => {
const themeType = theme.global.name.value === "LightTheme" ? "light" : "dark";
if (value) localStorage.setItem(`${themeType}-${color}`, value);
else localStorage.removeItem(`${themeType}-${color}`);
restoreThemeConfig(theme);
};
export const toggleTheme = (theme: ThemeInstance) => {
const currentTheme = localStorage.getItem("theme");
localStorage.setItem("theme", currentTheme === "LightTheme" ? "DarkTheme" : "LightTheme");
restoreThemeConfig(theme);
};
export const restoreThemeConfig = (theme: ThemeInstance) => {
// Restore theme preference
const storedTheme = localStorage.getItem("theme");
if (storedTheme) theme.global.name.value = storedTheme;
// Restore custom theme colors
const themeType = theme.global.name.value === "LightTheme" ? "light" : "dark";
const defaultTheme = theme.global.name.value === "LightTheme" ? LightTheme : DarkTheme;
const customBackground = localStorage.getItem(`${themeType}-background`);
const customPrimary = localStorage.getItem(`${themeType}-primary`);
const customSecondary = localStorage.getItem(`${themeType}-secondary`);
const customSurface = localStorage.getItem(`${themeType}-surface`);
theme.themes.value[theme.global.name.value].colors.background = customBackground ?? defaultTheme.colors!.background!;
theme.themes.value[theme.global.name.value].colors.sidebar = theme.themes.value[theme.global.name.value].dark
? (customBackground ?? defaultTheme.colors!.sidebar!)
: (customSurface ?? defaultTheme.colors!.sidebar!);
theme.themes.value[theme.global.name.value].colors.primary = customPrimary ?? defaultTheme.colors!.primary!;
theme.themes.value[theme.global.name.value].colors.buttonActive = customPrimary ?? defaultTheme.colors!.buttonActive!;
theme.themes.value[theme.global.name.value].colors.secondary = customSecondary ?? defaultTheme.colors!.secondary!;
theme.themes.value[theme.global.name.value].colors.buttonPassive =
customSecondary ?? defaultTheme.colors!.buttonPassive!;
theme.themes.value[theme.global.name.value].colors.accent = customSecondary ?? defaultTheme.colors!.accent!;
theme.themes.value[theme.global.name.value].colors.toggle = customSecondary ?? defaultTheme.colors!.toggle!;
theme.themes.value[theme.global.name.value].colors.surface = customSurface ?? defaultTheme.colors!.surface!;
};

View File

@@ -1,56 +1,72 @@
import "vuetify/styles";
import "@mdi/font/css/materialdesignicons.css";
import("@mdi/font/css/materialdesignicons.css");
import type { ThemeDefinition } from "vuetify/lib/composables/theme";
import { createVuetify } from "vuetify";
const commonColors = {
error: "#b80000",
info: "#2196F3",
success: "#4CAF50",
warning: "#FFC107"
const CommonColors = {
photonBlue: "#006492",
photonYellow: "#FFD843",
lightBlue: "#39A4D5",
darkGray: "#151515",
gray: "#1c232c",
lightGray: "#232C37"
};
const DarkTheme: ThemeDefinition = {
export const DarkTheme: ThemeDefinition = {
dark: true,
colors: {
primary: "#006492",
secondary: "#39A4D5",
accent: "#FFD843",
background: "#232C37",
...commonColors
background: CommonColors.darkGray,
sidebar: CommonColors.darkGray,
surface: CommonColors.gray,
primary: CommonColors.lightBlue,
secondary: CommonColors.photonYellow,
accent: CommonColors.photonBlue,
toggle: CommonColors.photonBlue,
logsBackground: CommonColors.darkGray,
buttonActive: CommonColors.photonYellow,
buttonPassive: CommonColors.lightBlue,
"surface-variant": "#485b70",
"on-surface-variant": "#f0f0f0",
error: "#ff2e2e",
info: "#2196F3",
success: "#4CAF50",
warning: "#FFC107"
}
};
const LightTheme: ThemeDefinition = {
export const LightTheme: ThemeDefinition = {
dark: false,
colors: {
background: "#232C37",
primary: "#006492",
surface: "#006492",
secondary: "#39A4D5",
"surface-variant": "#358AB0",
accent: "#FFD843",
"surface-light": "#FFD843",
...commonColors
background: CommonColors.lightGray,
sidebar: CommonColors.photonBlue,
surface: CommonColors.photonBlue,
primary: CommonColors.photonYellow,
secondary: CommonColors.lightBlue,
accent: CommonColors.photonYellow,
toggle: CommonColors.lightBlue,
logsBackground: CommonColors.lightGray,
buttonActive: CommonColors.photonYellow,
buttonPassive: CommonColors.lightBlue,
"surface-variant": "#8f8f8fff",
error: "#b80000",
info: "#2196F3",
success: "#4CAF50",
warning: "#FFC107"
},
variables: {
"medium-emphasis-opacity": 1,
"high-emphasis-opacity": 1
}
variables: { "medium-emphasis-opacity": 1, "high-emphasis-opacity": 1 }
};
export default createVuetify({
theme: {
defaultTheme: "LightTheme",
themes: {
LightTheme: LightTheme,
DarkTheme: DarkTheme
}
},
display: {
thresholds: {
md: 1460,
lg: 2000
}
}
theme: { defaultTheme: "LightTheme", themes: { LightTheme: LightTheme, DarkTheme: DarkTheme } },
display: { thresholds: { md: 1460, lg: 2000 } }
});

View File

@@ -26,6 +26,12 @@ export const useCameraSettingsStore = defineStore("cameraSettings", {
cameras: { [PlaceholderCameraSettings.uniqueName]: PlaceholderCameraSettings }
}),
getters: {
needsCameraConfiguration(): boolean {
return (
JSON.stringify(useCameraSettingsStore().cameras[PlaceholderCameraSettings.uniqueName]) ===
JSON.stringify(PlaceholderCameraSettings)
);
},
// TODO update types to update this value being undefined. This would be a decently large change.
currentCameraSettings(): UiCameraConfiguration {
const currentCameraUniqueName = useStateStore().currentCameraUniqueName;
@@ -136,7 +142,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;
}, {});
@@ -205,6 +212,7 @@ export const useCameraSettingsStore = defineStore("cameraSettings", {
cameraUniqueName: cameraUniqueName
}
};
if (updateStore) {
this.changePipelineSettingsInStore(settings, cameraUniqueName);
}

View File

@@ -27,8 +27,10 @@ export const useSettingsStore = defineStore("settings", {
hardwareModel: undefined,
hardwarePlatform: undefined,
mrCalWorking: true,
availableModels: {},
supportedBackends: []
availableModels: [],
supportedBackends: [],
conflictingHostname: false,
conflictingCameras: ""
},
network: {
ntServerAddress: "",
@@ -54,15 +56,15 @@ export const useSettingsStore = defineStore("settings", {
metrics: {
cpuTemp: undefined,
cpuUtil: undefined,
cpuMem: undefined,
gpuMem: undefined,
ramUtil: undefined,
gpuMemUtil: undefined,
cpuThr: undefined,
cpuUptime: undefined,
ramMem: undefined,
ramUtil: undefined,
gpuMem: undefined,
gpuMemUtil: undefined,
diskUtilPct: undefined,
npuUsage: undefined,
ipAddress: undefined
ipAddress: undefined,
uptime: undefined
},
currentFieldLayout: {
field: {
@@ -88,15 +90,15 @@ export const useSettingsStore = defineStore("settings", {
this.metrics = {
cpuTemp: data.cpuTemp || undefined,
cpuUtil: data.cpuUtil || undefined,
cpuMem: data.cpuMem || undefined,
gpuMem: data.gpuMem || undefined,
ramUtil: data.ramUtil || undefined,
gpuMemUtil: data.gpuMemUtil || undefined,
cpuThr: data.cpuThr || undefined,
cpuUptime: data.cpuUptime || undefined,
ramMem: data.ramMem || undefined,
ramUtil: data.ramUtil || undefined,
gpuMem: data.gpuMem || undefined,
gpuMemUtil: data.gpuMemUtil || undefined,
diskUtilPct: data.diskUtilPct || undefined,
npuUsage: data.npuUsage || undefined,
ipAddress: data.ipAddress || undefined
ipAddress: data.ipAddress || undefined,
uptime: data.uptime || undefined
};
},
updateGeneralSettingsFromWebsocket(data: WebsocketSettingsUpdate) {
@@ -107,7 +109,9 @@ export const useSettingsStore = defineStore("settings", {
gpuAcceleration: data.general.gpuAcceleration || undefined,
mrCalWorking: data.general.mrCalWorking,
availableModels: data.general.availableModels || undefined,
supportedBackends: data.general.supportedBackends || []
supportedBackends: data.general.supportedBackends || [],
conflictingHostname: data.general.conflictingHostname || false,
conflictingCameras: data.general.conflictingCameras || ""
};
this.lighting = data.lighting;
this.network = data.networkSettings;

View File

@@ -1,4 +1,5 @@
import type { WebsocketNumberPair } from "@/types/WebsocketDataTypes";
import type { ObjectDetectionModelProperties } from "@/types/SettingTypes";
export enum PipelineType {
DriverMode = 1,
@@ -296,8 +297,9 @@ export interface ObjectDetectionPipelineSettings extends PipelineSettings {
confidence: number;
nms: number;
box_thresh: number;
model: string;
model: ObjectDetectionModelProperties;
}
export type ConfigurableObjectDetectionPipelineSettings = Partial<
Omit<ObjectDetectionPipelineSettings, "pipelineType">
> &
@@ -313,7 +315,7 @@ export const DefaultObjectDetectionPipelineSettings: ObjectDetectionPipelineSett
confidence: 0.9,
nms: 0.45,
box_thresh: 0.25,
model: ""
model: {} as ObjectDetectionModelProperties
};
export interface Calibration3dPipelineSettings extends PipelineSettings {

View File

@@ -8,22 +8,34 @@ export interface GeneralSettings {
hardwareModel?: string;
hardwarePlatform?: string;
mrCalWorking: boolean;
availableModels: Record<string, string[]>;
availableModels: ObjectDetectionModelProperties[];
supportedBackends: string[];
conflictingHostname: boolean;
conflictingCameras: string;
}
export interface ObjectDetectionModelProperties {
modelPath: string;
nickname: string;
labels: string[];
resolutionWidth: number;
resolutionHeight: number;
family: "RKNN" | "RUBIK";
version: "YOLOV5" | "YOLOV8" | "YOLOV11";
}
export interface MetricData {
cpuTemp?: string;
cpuUtil?: string;
cpuMem?: string;
gpuMem?: string;
ramUtil?: string;
gpuMemUtil?: string;
cpuTemp?: number;
cpuUtil?: number;
cpuThr?: string;
cpuUptime?: string;
diskUtilPct?: string;
npuUsage?: string;
ramMem?: number;
ramUtil?: number;
gpuMem?: number;
gpuMemUtil?: number;
diskUtilPct?: number;
npuUsage?: number[];
ipAddress?: string;
uptime?: number;
}
export enum NetworkConnectionType {
@@ -254,6 +266,7 @@ export interface UiCameraConfiguration {
matchedCameraInfo: PVCameraInfo;
isConnected: boolean;
hasConnected: boolean;
mismatch: boolean;
}
export interface CameraSettingsChangeRequest {
@@ -278,17 +291,20 @@ export const PlaceholderCameraSettings: UiCameraConfiguration = {
{
resolution: { width: 1920, height: 1080 },
fps: 60,
pixelFormat: "RGB"
pixelFormat: "RGB",
index: 0
},
{
resolution: { width: 1280, height: 720 },
fps: 60,
pixelFormat: "RGB"
pixelFormat: "RGB",
index: 1
},
{
resolution: { width: 640, height: 480 },
fps: 30,
pixelFormat: "RGB"
pixelFormat: "RGB",
index: 2
}
],
completeCalibrations: [
@@ -373,7 +389,8 @@ export const PlaceholderCameraSettings: UiCameraConfiguration = {
PVUsbCameraInfo: undefined
},
isConnected: true,
hasConnected: true
hasConnected: true,
mismatch: false
};
export enum CalibrationBoardTypes {

View File

@@ -69,6 +69,7 @@ export interface WebsocketCameraSettingsUpdate {
matchedCameraInfo: PVCameraInfo;
isConnected: boolean;
hasConnected: boolean;
mismatch: boolean;
}
export interface WebsocketNTUpdate {
connected: boolean;

View File

@@ -17,9 +17,11 @@ 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();
const formatUrl = (port) => `http://${inject("backendHostname")}:${port}/stream.mjpg`;
const host = inject<string>("backendHost");
const activatingModule = ref(false);
const activateModule = (moduleUniqueName: string) => {
@@ -97,7 +99,6 @@ const deactivatingModule = ref(false);
const deactivateModule = (cameraUniqueName: string) => {
if (deactivatingModule.value) return;
deactivatingModule.value = true;
axios
.post("/utils/unassignCamera", { cameraUniqueName: cameraUniqueName })
.then(() => {
@@ -167,64 +168,7 @@ const deleteThisCamera = (cameraName: string) => {
});
};
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
}
);
};
const cameraCononected = (uniquePath: string): boolean => {
const cameraConnected = (uniquePath: string): boolean => {
return (
useStateStore().vsmState.allConnectedCameras.find((it) => cameraInfoFor(it).uniquePath === uniquePath) !== undefined
);
@@ -251,8 +195,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)
)
);
@@ -273,9 +217,44 @@ const setCameraDeleting = (camera: UiCameraConfiguration | WebsocketCameraSettin
cameraToDelete.value = camera;
};
const yesDeleteMySettingsText = ref("");
const exportSettings = ref();
const openExportSettingsPrompt = () => {
exportSettings.value.click();
/**
* 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
}
);
};
</script>
@@ -289,30 +268,39 @@ const openExportSettingsPrompt = () => {
cols="12"
sm="6"
lg="4"
class="pr-0"
>
<v-card color="primary">
<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)" class="pb-2"
<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)
"
class="pb-2"
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 class="pb-2">Status: <span class="mismatch-status">Mismatch</span></v-card-subtitle>
<v-card-text>
<v-card-subtitle v-else>Status: <span class="mismatch-status">Mismatch</span></v-card-subtitle>
<v-card-text class="pt-3">
<v-table density="compact">
<tbody>
<tr>
<td>Streams:</td>
<tr
v-if="
cameraConnected(cameraInfoFor(module.matchedCameraInfo).uniquePath) &&
useStateStore().backendResults[module.uniqueName]
"
>
<td style="width: 50%">Frames Processed</td>
<td>
<a :href="formatUrl(module.stream.inputPort)" target="_blank" class="stream-link"> Input </a>
/
<a :href="formatUrl(module.stream.outputPort)" target="_blank" class="stream-link"> Output </a>
{{ useStateStore().backendResults[module.uniqueName].sequenceID }} ({{
useStateStore().backendResults[module.uniqueName].fps
}}
FPS)
</td>
</tr>
<tr v-else>
<td>Name</td>
<td>
{{ module.nickname }}
</td>
</tr>
<tr>
@@ -328,24 +316,18 @@ const openExportSettingsPrompt = () => {
}}
</td>
</tr>
<tr
v-if="
cameraCononected(cameraInfoFor(module.matchedCameraInfo).uniquePath) &&
useStateStore().backendResults[module.uniqueName]
"
>
<td style="width: 50%">Frames Processed</td>
<tr>
<td>Streams:</td>
<td>
{{ useStateStore().backendResults[module.uniqueName].sequenceID }} ({{
useStateStore().backendResults[module.uniqueName].fps
}}
FPS)
<a :href="formatUrl(module.stream.inputPort)" target="_blank" class="stream-link"> Input </a>
/
<a :href="formatUrl(module.stream.outputPort)" target="_blank" class="stream-link"> Output </a>
</td>
</tr>
</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"
@@ -361,12 +343,13 @@ const openExportSettingsPrompt = () => {
<v-row>
<v-col cols="12" md="4" class="pr-md-0 pb-0 pb-md-3">
<v-btn
color="secondary"
color="buttonPassive"
style="width: 100%"
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
@click="
setCameraView(
module.matchedCameraInfo,
cameraCononected(cameraInfoFor(module.matchedCameraInfo).uniquePath)
cameraConnected(cameraInfoFor(module.matchedCameraInfo).uniquePath)
)
"
>
@@ -376,8 +359,9 @@ const openExportSettingsPrompt = () => {
<v-col cols="6" md="5" class="pr-0">
<v-btn
class="text-black"
color="accent"
color="buttonActive"
style="width: 100%"
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
:loading="deactivatingModule"
@click="deactivateModule(module.uniqueName)"
>
@@ -385,8 +369,14 @@ const openExportSettingsPrompt = () => {
</v-btn>
</v-col>
<v-col cols="6" md="3">
<v-btn class="pa-0" color="error" style="width: 100%" @click="setCameraDeleting(module)">
<v-icon>mdi-trash-can-outline</v-icon>
<v-btn
class="pa-0"
color="error"
style="width: 100%"
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
@click="setCameraDeleting(module)"
>
<v-icon size="x-large">mdi-trash-can-outline</v-icon>
</v-btn>
</v-col>
</v-row>
@@ -394,28 +384,31 @@ const openExportSettingsPrompt = () => {
</v-card>
</v-col>
<!-- Disabled modules -->
<v-col v-for="module in disabledVisionModules" :key="`disabled-${module.uniqueName}`" cols="12" sm="6" lg="4">
<v-card color="primary">
<v-card-title>{{ module.nickname }}</v-card-title>
<v-card-subtitle class="pb-2">Status: <span class="inactive-status">Deactivated</span></v-card-subtitle>
<v-card-text>
<!-- Deactivated modules -->
<v-col
v-for="module in disabledVisionModules"
:key="`disabled-${module.uniqueName}`"
cols="12"
sm="6"
lg="4"
class="pr-0"
>
<v-card class="pr-0 rounded-12" color="surface">
<v-card-title>{{ module.cameraQuirks.baseName }}</v-card-title>
<v-card-subtitle>Status: <span class="inactive-status">Deactivated</span></v-card-subtitle>
<v-card-text class="pt-3">
<v-table density="compact">
<tbody>
<tr>
<td>Name</td>
<td>
{{ module.cameraQuirks.baseName }}
{{ module.nickname }}
</td>
</tr>
<tr>
<td>Pipelines</td>
<td>{{ module.pipelineNicknames.join(", ") }}</td>
</tr>
<tr>
<td>Connected</td>
<td>{{ cameraCononected(cameraInfoFor(module.matchedCameraInfo).uniquePath) }}</td>
</tr>
<tr>
<td>Calibrations</td>
<td>
@@ -425,6 +418,10 @@ const openExportSettingsPrompt = () => {
}}
</td>
</tr>
<tr>
<td>Connected</td>
<td>{{ cameraConnected(cameraInfoFor(module.matchedCameraInfo).uniquePath) }}</td>
</tr>
</tbody>
</v-table>
</v-card-text>
@@ -432,12 +429,13 @@ const openExportSettingsPrompt = () => {
<v-row>
<v-col cols="12" md="4" class="pr-md-0 pb-0 pb-md-3">
<v-btn
color="secondary"
color="buttonPassive"
style="width: 100%"
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
@click="
setCameraView(
module.matchedCameraInfo,
cameraCononected(cameraInfoFor(module.matchedCameraInfo).uniquePath)
cameraConnected(cameraInfoFor(module.matchedCameraInfo).uniquePath)
)
"
>
@@ -447,8 +445,9 @@ const openExportSettingsPrompt = () => {
<v-col cols="6" md="5" class="pr-0">
<v-btn
class="text-black"
color="accent"
color="buttonActive"
style="width: 100%"
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
:loading="activatingModule"
@click="activateModule(module.uniqueName)"
>
@@ -456,8 +455,14 @@ const openExportSettingsPrompt = () => {
</v-btn>
</v-col>
<v-col cols="6" md="3">
<v-btn class="pa-0" color="error" style="width: 100%" @click="setCameraDeleting(module)">
<v-icon>mdi-trash-can-outline</v-icon>
<v-btn
class="pa-0"
color="error"
style="width: 100%"
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
@click="setCameraDeleting(module)"
>
<v-icon size="x-large">mdi-trash-can-outline</v-icon>
</v-btn>
</v-col>
</v-row>
@@ -466,9 +471,9 @@ const openExportSettingsPrompt = () => {
</v-col>
<!-- Unassigned cameras -->
<v-col v-for="(camera, index) in unmatchedCameras" :key="index" cols="12" sm="6" lg="4">
<v-card color="primary">
<v-card-title class="pb-2">
<v-col v-for="(camera, index) in unmatchedCameras" :key="index" cols="12" sm="6" lg="4" class="pr-0">
<v-card class="pr-0 rounded-12" color="surface">
<v-card-title>
<span v-if="camera.PVUsbCameraInfo">USB Camera:</span>
<span v-else-if="camera.PVCSICameraInfo">CSI Camera:</span>
<span v-else-if="camera.PVFileCameraInfo">File Camera:</span>
@@ -476,22 +481,28 @@ const openExportSettingsPrompt = () => {
&nbsp;<span>{{ cameraInfoFor(camera)?.name ?? cameraInfoFor(camera)?.baseName }}</span>
</v-card-title>
<v-card-subtitle>Status: Unassigned</v-card-subtitle>
<v-card-text>
<v-card-text class="pt-3">
<span style="word-break: break-all">{{ cameraInfoFor(camera)?.path }}</span>
</v-card-text>
<v-card-text class="pt-0">
<v-row>
<v-col cols="6" class="pr-0">
<v-btn color="secondary" style="width: 100%" @click="setCameraView(camera, false)">
<v-btn
color="buttonPassive"
style="width: 100%"
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
@click="setCameraView(camera, false)"
>
<span>Details</span>
</v-btn>
</v-col>
<v-col cols="6">
<v-btn
class="text-black"
color="accent"
color="buttonActive"
style="width: 100%"
:loading="assigningCamera"
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
@click="assignCamera(camera)"
>
Activate
@@ -503,14 +514,14 @@ const openExportSettingsPrompt = () => {
</v-col>
<!-- Info card -->
<v-col cols="12" sm="6" lg="4">
<v-col cols="12" sm="6" lg="4" class="pr-0">
<v-card
dark
flat
class="pl-6 pr-6 d-flex flex-column justify-center"
style="background-color: transparent; height: 100%"
>
<v-card-text class="d-flex flex-column align-center justify-center">
<v-card-text class="d-flex flex-column align-center justify-center" style="flex-grow: 0">
<v-icon size="64" color="primary">mdi-plus</v-icon>
</v-card-text>
<v-card-title>Additional plugged in cameras will display here!</v-card-title>
@@ -520,21 +531,31 @@ const openExportSettingsPrompt = () => {
<!-- Camera details modal -->
<v-dialog v-model="viewingDetails" max-width="800">
<v-card v-if="viewingCamera[0] !== null" flat color="primary">
<v-card v-if="viewingCamera[0] !== null" flat color="surface">
<v-card-title class="d-flex justify-space-between">
<span>{{ cameraInfoFor(viewingCamera[0])?.name ?? cameraInfoFor(viewingCamera[0])?.baseName }}</span>
<v-btn variant="text" @click="setCameraView(null, null)">
<v-icon>mdi-close-thick</v-icon>
<v-icon size="x-large">mdi-close</v-icon>
</v-btn>
</v-card-title>
<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-banner rounded color="error" text-color="white" icon="mdi-information-outline" class="mb-3">
It looks like a different camera may have been connected to this device! Compare the following information
carefully.
</v-banner>
<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"
density="compact"
text="A different camera may have been connected to this device! Compare the following information carefully."
icon="mdi-information-outline"
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'tonal'"
/>
<PvCameraMatchCard :saved="viewingCamera[0]" :current="getMatchedDevice(viewingCamera[0])" />
</v-card-text>
<v-card-text v-else>
@@ -545,29 +566,12 @@ const openExportSettingsPrompt = () => {
<!-- Camera delete modal -->
<v-dialog v-model="viewingDeleteCamera" width="800">
<v-card v-if="cameraToDelete !== null" class="dialog-container pa-3 pb-2" color="primary" flat>
<v-card v-if="cameraToDelete !== null" class="dialog-container" color="surface" flat>
<v-card-title> Delete {{ cameraToDelete.nickname }}? </v-card-title>
<v-card-text>
<v-row class="align-center pt-6">
<v-col cols="12" md="6">
<span class="text-white"> This will delete ALL OF YOUR SETTINGS and restart PhotonVision. </span>
</v-col>
<v-col cols="12" md="6">
<v-btn color="secondary" block @click="openExportSettingsPrompt">
<v-icon start class="open-icon"> mdi-export </v-icon>
<span class="open-label">Backup Settings</span>
<a
ref="exportSettings"
style="color: black; text-decoration: none; display: none"
:href="`http://${host}/api/settings/photonvision_config.zip`"
download="photonvision-settings.zip"
target="_blank"
/>
</v-btn>
</v-col>
</v-row>
<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>
<v-card-text class="pt-0 pb-10px">
<pv-input
v-model="yesDeleteMySettingsText"
:label="'Type &quot;' + cameraToDelete.nickname + '&quot;:'"
@@ -575,30 +579,44 @@ const openExportSettingsPrompt = () => {
:input-cols="6"
/>
</v-card-text>
<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
block
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"> mdi-trash-can-outline </v-icon>
<span class="open-label">DELETE (UNRECOVERABLE)</span>
<v-icon start class="open-icon" size="large"> mdi-trash-can-outline </v-icon>
<span class="open-label">Delete</span>
</v-btn>
</v-card-text>
</v-card-actions>
</v-card>
</v-dialog>
</div>
</template>
<style scoped>
.v-card-title {
text-wrap-mode: wrap !important;
td {
padding: 0 !important;
}
.v-table {
background-color: #006492 !important;
.v-card-subtitle {
padding-top: 0px !important;
padding-bottom: 8px !important;
}
.v-card-title {
padding-bottom: 0 !important;
text-wrap-mode: wrap !important;
}
.active-status {
@@ -614,7 +632,6 @@ const openExportSettingsPrompt = () => {
}
a:hover {
color: pink;
background-color: transparent;
text-decoration: underline;
}

View File

@@ -1,11 +1,15 @@
<script setup lang="ts">
import { computed } from "vue";
import { computed, ref } from "vue";
import CamerasCard from "@/components/dashboard/CamerasCard.vue";
import CameraAndPipelineSelectCard from "@/components/dashboard/CameraAndPipelineSelectCard.vue";
import StreamConfigCard from "@/components/dashboard/StreamConfigCard.vue";
import PipelineConfigCard from "@/components/dashboard/ConfigOptions.vue";
import { useCameraSettingsStore } from "@/stores/settings/CameraSettingsStore";
import { useStateStore } from "@/stores/StateStore";
import { useSettingsStore } from "@/stores/settings/GeneralSettingsStore";
import { useTheme } from "vuetify";
const theme = useTheme();
import { PlaceholderCameraSettings } from "@/types/SettingTypes";
const cameraViewType = computed<number[]>({
@@ -39,14 +43,6 @@ const cameraViewType = computed<number[]>({
}
});
// TODO - deduplicate with needsCamerasConfigured
const warningShown = computed<boolean>(() => {
return (
Object.values(useCameraSettingsStore().cameras).length === 0 ||
useCameraSettingsStore().cameras["Placeholder Name"] === PlaceholderCameraSettings
);
});
const arducamWarningShown = computed<boolean>(() => {
return Object.values(useCameraSettingsStore().cameras).some(
(c) =>
@@ -58,13 +54,71 @@ 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;
});
const conflictingCameraShown = computed<boolean>(() => {
return useSettingsStore().general.conflictingCameras.length > 0;
});
const showCameraSetupDialog = ref(useCameraSettingsStore().needsCameraConfiguration);
</script>
<template>
<v-container class="pa-3" fluid>
<v-banner
<v-alert
v-if="arducamWarningShown"
v-model="arducamWarningShown"
class="mb-3"
color="error"
density="compact"
icon="mdi-alert-circle-outline"
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'tonal'"
>
<span>
Arducam camera detected! Please configure the camera model in the <a href="#/cameras">Camera tab</a>!
</span>
</v-alert>
<v-alert
v-if="conflictingHostnameShown"
class="mb-3"
color="error"
density="compact"
icon="mdi-alert-circle-outline"
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'tonal'"
>
<span>
Conflicting hostname detected! Please change the hostname in the <a href="#/settings">Settings tab</a>!
</span>
</v-alert>
<v-alert
v-if="conflictingCameraShown"
class="mb-3"
color="error"
density="compact"
icon="mdi-alert-circle-outline"
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'tonal'"
>
<span
>Conflicting camera name(s) detected! Please change the name(s) of
{{ useSettingsStore().general.conflictingCameras }}!
</span>
</v-alert>
<v-banner
v-if="cameraMismatchWarningShown"
v-model="cameraMismatchWarningShown"
rounded
color="error"
dark
@@ -72,7 +126,9 @@ const arducamWarningShown = computed<boolean>(() => {
icon="mdi-alert-circle-outline"
>
<span
>Arducam Camera Detected! Please configure the camera model in the <a href="#/cameras">Cameras tab</a>!
>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>
@@ -87,11 +143,17 @@ const arducamWarningShown = computed<boolean>(() => {
<PipelineConfigCard />
<!-- TODO - not sure this belongs here -->
<v-dialog v-if="warningShown" v-model="warningShown" :persistent="false" max-width="800" dark>
<v-card flat color="primary">
<v-card-title>Setup some cameras to get started!</v-card-title>
<v-card-text>
No cameras activated - head to the <a href="#/cameraConfigs">Camera matching tab</a> to set some up!
<v-dialog
v-if="useCameraSettingsStore().needsCameraConfiguration"
v-model="showCameraSetupDialog"
max-width="800"
dark
>
<v-card flat color="surface">
<v-card-title>Set up some cameras to get started!</v-card-title>
<v-card-text class="pt-0">
No cameras activated - head to the
<router-link to="/cameraConfigs">camera matching tab</router-link> to set some up!
</v-card-text>
</v-card>
</v-dialog>
@@ -100,23 +162,20 @@ const arducamWarningShown = computed<boolean>(() => {
<style scoped>
a:link {
color: #ffd843;
color: rgb(var(--v-theme-buttonActive));
background-color: transparent;
text-decoration: none;
}
a:visited {
color: #ffd843;
color: rgb(var(--v-theme-buttonActive));
background-color: transparent;
text-decoration: none;
}
a:hover {
color: pink;
background-color: transparent;
text-decoration: underline;
}
a:active {
color: yellow;
background-color: transparent;
text-decoration: none;
}

View File

@@ -1,14 +1,23 @@
<script setup lang="ts">
const devMode = process.env.NODE_ENV === "development";
</script>
<template>
<div style="overflow: hidden; height: 100vh; width: 100%">
<div v-if="devMode" style="width: 100%; height: 100%; padding: 16px">
<span style="color: white; font-weight: bold">
PhotonClient is in development mode so the documentation page will not load. Please recompile in production mode
with the documentation copied over after a full build.
</span>
<div v-if="devMode" style="width: 60%; height: 100%; margin: auto">
<v-card
dark
flat
class="pl-6 pr-6 d-flex flex-column justify-center align-center"
style="background-color: transparent; height: 100%"
>
<v-card-text class="d-flex flex-column" style="flex: 0">
<v-icon size="64" color="primary">mdi-web-off</v-icon>
</v-card-text>
<v-card-text style="width: 100%; flex-grow: 0; text-align: center">
PhotonClient is in development mode so the documentation page will not load. Please recompile in production
mode with the documentation copied over after a full build.
</v-card-text>
</v-card>
</div>
<div v-else style="width: 100%; height: 100%">
<!--suppress HtmlUnknownTarget -->

View File

@@ -15,6 +15,11 @@ import ApriltagControlCard from "@/components/settings/ApriltagControlCard.vue";
<NetworkingCard />
<ObjectDetectionCard v-if="useSettingsStore().general.supportedBackends.length > 0" />
<LightingControlCard v-if="useSettingsStore().lighting.supported" />
<ApriltagControlCard />
<Suspense>
<!-- Allows us to import three js when it's actually needed -->
<ApriltagControlCard />
<template #fallback> Loading... </template>
</Suspense>
</div>
</template>

View File

@@ -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'
@@ -36,6 +38,12 @@ dependencies {
implementation("org.photonvision:rknn_jni-java:$rknnVersion") {
transitive = false
}
implementation("org.photonvision:rubik_jni-jni:$rubikVersion:linuxarm64") {
transitive = false
}
implementation("org.photonvision:rubik_jni-java:$rubikVersion") {
transitive = false
}
implementation("org.photonvision:photon-libcamera-gl-driver-jni:$libcameraDriverVersion:linuxarm64") {
transitive = false
}

View File

@@ -34,6 +34,7 @@ import org.opencv.core.Size;
import org.photonvision.common.logging.LogGroup;
import org.photonvision.common.logging.Logger;
import org.photonvision.common.util.file.FileUtils;
import org.photonvision.common.util.file.JacksonUtils;
import org.photonvision.vision.processes.VisionSource;
import org.zeroturnaround.zip.ZipUtil;
@@ -51,7 +52,8 @@ public class ConfigManager {
private final Thread settingsSaveThread;
private long saveRequestTimestamp = -1;
// special case flag to disable flushing settings to disk at shutdown. Avoids the jvm shutdown
// special case flag to disable flushing settings to disk at shutdown. Avoids
// the jvm shutdown
// hook overwriting the settings we just uploaded
private boolean flushOnShutdown = true;
private boolean allowWriteTask = true;
@@ -62,7 +64,8 @@ public class ConfigManager {
ATOMIC_ZIP
}
// This logic decides which kind of ConfigManager we load as the default. If we want to switch
// This logic decides which kind of ConfigManager we load as the default. If we
// want to switch
// back to the legacy config manager, change this constant
private static final ConfigSaveStrategy m_saveStrat = ConfigSaveStrategy.SQL;
@@ -109,18 +112,21 @@ public class ConfigManager {
} catch (IOException e) {
logger.error("Exception moving cameras to cameras_bak!", e);
// Try to just copy from cams to cams-bak instead of moving? Windows sometimes needs us to
// Try to just copy from cams to cams-bak instead of moving? Windows sometimes
// needs us to
// do that
try {
org.apache.commons.io.FileUtils.copyDirectory(maybeCams, maybeCamsBak);
} catch (IOException e1) {
// So we can't move to cams_bak, and we can't copy and delete either? We just have to give
// So we can't move to cams_bak, and we can't copy and delete either? We just
// have to give
// up here on preserving the old folder
logger.error("Exception while backup-copying cameras to cameras_bak!", e);
e1.printStackTrace();
}
// Delete the directory because we were successfully able to load the config but were unable
// Delete the directory because we were successfully able to load the config but
// were unable
// to save or copy the folder.
if (maybeCams.exists()) FileUtils.deleteDirectory(maybeCams.toPath());
}
@@ -217,6 +223,29 @@ public class ConfigManager {
return out;
}
public File getObjectDetectionExportAsZip() {
File out =
Path.of(System.getProperty("java.io.tmpdir"), "photonvision-object-detection-models.zip")
.toFile();
// We create the properties file inside of the models directory so that when we zip it, it's
// included in the zip and simplifies packaging
File tempProperties =
Path.of(getModelsDirectory().toString(), "photonvision-object-detection-models.json")
.toFile();
try {
JacksonUtils.serialize(
tempProperties.toPath(), this.getConfig().neuralNetworkPropertyManager());
ZipUtil.pack(getModelsDirectory(), out);
// Now delete the tempProperties
if (tempProperties.exists()) {
Files.delete(tempProperties.toPath());
}
} catch (Exception e) {
e.printStackTrace();
}
return out;
}
public void setNetworkSettings(NetworkConfig networkConfig) {
getConfig().setNetworkConfig(networkConfig);
requestSave();
@@ -294,6 +323,10 @@ public class ConfigManager {
return m_provider.saveUploadedAprilTagFieldLayout(uploadPath);
}
public boolean saveUploadedNeuralNetworkProperties(Path uploadPath) {
return m_provider.saveUploadedNeuralNetworkProperties(uploadPath);
}
public void requestSave() {
logger.trace("Requesting save...");
saveRequestTimestamp = System.currentTimeMillis();

Some files were not shown because too many files have changed in this diff Show More