mirror of
https://github.com/PhotonVision/photonvision
synced 2026-06-25 01:41:40 +00:00
Merge branch 'main' into py-docs
This commit is contained in:
15
.github/labeler.yml
vendored
Normal file
15
.github/labeler.yml
vendored
Normal 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/**
|
||||
2
.github/pull_request_template.md
vendored
2
.github/pull_request_template.md
vendored
@@ -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
|
||||
|
||||
219
.github/workflows/build.yml
vendored
219
.github/workflows/build.yml
vendored
@@ -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
14
.github/workflows/labeler.yml
vendored
Normal 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
|
||||
38
.github/workflows/lint-format.yml
vendored
38
.github/workflows/lint-format.yml
vendored
@@ -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
|
||||
|
||||
22
.github/workflows/photon-api-docs.yml
vendored
22
.github/workflows/photon-api-docs.yml
vendored
@@ -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
|
||||
|
||||
3
.github/workflows/photonvision-rtd.yml
vendored
3
.github/workflows/photonvision-rtd.yml
vendored
@@ -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 }}
|
||||
|
||||
20
.github/workflows/python.yml
vendored
20
.github/workflows/python.yml
vendored
@@ -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
|
||||
|
||||
30
.github/workflows/website.yml
vendored
30
.github/workflows/website.yml
vendored
@@ -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
4
.gitignore
vendored
@@ -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
1
.python-version
Normal file
@@ -0,0 +1 @@
|
||||
3.11
|
||||
@@ -19,6 +19,7 @@ modifiableFileExclude {
|
||||
\.webp$
|
||||
\.ico$
|
||||
\.rknn$
|
||||
\.tflite$
|
||||
\.mp4$
|
||||
\.ttf$
|
||||
\.woff2$
|
||||
|
||||
5
.vscode/settings.json
vendored
Normal file
5
.vscode/settings.json
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"python.testing.unittestEnabled": false,
|
||||
"python.testing.pytestEnabled": true,
|
||||
"python.testing.cwd": "photon-lib/py"
|
||||
}
|
||||
@@ -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`
|
||||
|
||||
|
||||
12
build.gradle
12
build.gradle
@@ -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 = {
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -11,6 +11,7 @@ modifiableFileExclude {
|
||||
\.webp$
|
||||
\.ico$
|
||||
\.rknn$
|
||||
\.tflite$
|
||||
\.svg$
|
||||
\.woff2$
|
||||
gradlew
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
8
docs/source/docs/benchmarks/index.md
Normal file
8
docs/source/docs/benchmarks/index.md
Normal file
@@ -0,0 +1,8 @@
|
||||
# Performance Benchmarks
|
||||
|
||||
```{toctree}
|
||||
:maxdepth: 0
|
||||
:titlesonly: true
|
||||
|
||||
rknn-model-benchmarks
|
||||
```
|
||||
125
docs/source/docs/benchmarks/rknn-model-benchmarks.md
Normal file
125
docs/source/docs/benchmarks/rknn-model-benchmarks.md
Normal 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, it’s 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.
|
||||
@@ -1,4 +1,4 @@
|
||||
# Camera-Specifc Configuration
|
||||
# Camera-Specific Configuration
|
||||
|
||||
```{toctree}
|
||||
:maxdepth: 2
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
# Object Detection
|
||||
|
||||
```{toctree}
|
||||
:maxdepth: 0
|
||||
:titlesonly: true
|
||||
|
||||
about-object-detection
|
||||
opi
|
||||
rubik
|
||||
```
|
||||
|
||||
19
docs/source/docs/objectDetection/opi.md
Normal file
19
docs/source/docs/objectDetection/opi.md
Normal 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.
|
||||
25
docs/source/docs/objectDetection/rubik.md
Normal file
25
docs/source/docs/objectDetection/rubik.md
Normal 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.
|
||||
@@ -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!
|
||||
```
|
||||
|
||||
@@ -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!
|
||||
```
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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!
|
||||
```
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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 doesn’t show up after 30s try using another USB port, initialization may take a while. If prompted, install the recommended missing drivers. Select the image, and flash.
|
||||
## Limelight 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.
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
```
|
||||
|
||||
|
||||
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
Binary file not shown.
2
gradle/wrapper/gradle-wrapper.properties
vendored
2
gradle/wrapper/gradle-wrapper.properties
vendored
@@ -1,6 +1,6 @@
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=permwrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.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
9
gradlew
vendored
@@ -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
4
gradlew.bat
vendored
@@ -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
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
src/assets/fonts/PromptRegular.ts
|
||||
pnpm-lock.yaml
|
||||
|
||||
6983
photon-client/package-lock.json
generated
6983
photon-client/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -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
2853
photon-client/pnpm-lock.yaml
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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 |
25
photon-client/src/assets/images/logoSmallTransparent.svg
Normal file
25
photon-client/src/assets/images/logoSmallTransparent.svg
Normal 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 |
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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%;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 "' + useCameraSettingsStore().currentCameraName + '":'"
|
||||
@@ -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>
|
||||
|
||||
@@ -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) }} FPS –
|
||||
{{ 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">
|
||||
{{ Math.round(useStateStore().currentPipelineResults?.fps || 0) }} FPS –
|
||||
{{ 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;
|
||||
|
||||
@@ -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"
|
||||
/>
|
||||
|
||||
205
photon-client/src/components/common/pv-loading.vue
Normal file
205
photon-client/src/components/common/pv-loading.vue
Normal 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>
|
||||
@@ -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"
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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) }} FPS –</span
|
||||
<span class="pr-1">{{ Math.round(useStateStore().currentPipelineResults?.fps || 0) }} FPS –</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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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) }}°</td>
|
||||
<td class="text-center">{{ target.yaw.toFixed(2) }}°</td>
|
||||
<td class="text-center">{{ target.skew.toFixed(2) }}°</td>
|
||||
<td class="text-center">{{ target.area.toFixed(2) }}°</td>
|
||||
<td class="text-center">{{ target.area.toFixed(2) }}%</td>
|
||||
</template>
|
||||
<template v-else>
|
||||
<td class="text-center">{{ target.pose?.x.toFixed(3) }} 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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 "' + expected + '":'"
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 "' + expected + '":'"
|
||||
:label-cols="6"
|
||||
:input-cols="6"
|
||||
/>
|
||||
</v-card-text>
|
||||
<v-card-text class="pt-10px">
|
||||
<v-btn
|
||||
color="error"
|
||||
width="100%"
|
||||
:disabled="yesDeleteMyModelsText.toLowerCase() !== expected.toLowerCase()"
|
||||
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
|
||||
@click="nukeModels"
|
||||
>
|
||||
<v-icon start class="open-icon" size="large"> mdi-trash-can-outline </v-icon>
|
||||
<span class="open-label">
|
||||
{{ $vuetify.display.mdAndUp ? "Delete models, I have backed up what I need" : "Delete Models" }}
|
||||
</span>
|
||||
</v-btn>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
</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;
|
||||
}
|
||||
}
|
||||
|
||||
65
photon-client/src/lib/ThemeManager.ts
Normal file
65
photon-client/src/lib/ThemeManager.ts
Normal 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!;
|
||||
};
|
||||
@@ -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 } }
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -69,6 +69,7 @@ export interface WebsocketCameraSettingsUpdate {
|
||||
matchedCameraInfo: PVCameraInfo;
|
||||
isConnected: boolean;
|
||||
hasConnected: boolean;
|
||||
mismatch: boolean;
|
||||
}
|
||||
export interface WebsocketNTUpdate {
|
||||
connected: boolean;
|
||||
|
||||
@@ -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 = () => {
|
||||
<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 "' + cameraToDelete.nickname + '":'"
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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 -->
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user