mirror of
https://github.com/PhotonVision/photonvision
synced 2026-06-21 01:01:41 +00:00
Compare commits
110 Commits
v2025.3.2
...
v2026.0.0-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9f6d8caf48 | ||
|
|
3cbac8117e | ||
|
|
8e88a9a780 | ||
|
|
7cb3b7a37b | ||
|
|
054ed8b6a1 | ||
|
|
d44480ddad | ||
|
|
c71921c41e | ||
|
|
ee4501f1d6 | ||
|
|
d9b86a718e | ||
|
|
1ac185c247 | ||
|
|
7170c29efe | ||
|
|
4f549ba579 | ||
|
|
b531fe6b81 | ||
|
|
373ed2ff05 | ||
|
|
115bc09f2e | ||
|
|
7497566f56 | ||
|
|
85f155c77b | ||
|
|
797936865f | ||
|
|
831df409f7 | ||
|
|
b89ab49d34 | ||
|
|
099c88e0b7 | ||
|
|
1ee2ecb608 | ||
|
|
82d6b6b845 | ||
|
|
8ec041493a | ||
|
|
da608a5070 | ||
|
|
17c23b0390 | ||
|
|
e486ff5a50 | ||
|
|
e84e3e7c7c | ||
|
|
a1d06a9920 | ||
|
|
04e9bffeb7 | ||
|
|
4b01b66ab7 | ||
|
|
ee56bf7597 | ||
|
|
058ca19262 | ||
|
|
b43d0dde20 | ||
|
|
3300b90823 | ||
|
|
f58416fe16 | ||
|
|
62eb66a493 | ||
|
|
fcbc392d83 | ||
|
|
7d927aca3b | ||
|
|
bd2c5062f9 | ||
|
|
354f4e945e | ||
|
|
2eb224a55f | ||
|
|
2ab7a2e389 | ||
|
|
27a1cfcb12 | ||
|
|
c7f5edc262 | ||
|
|
6fe96316a4 | ||
|
|
b32d9c6ee3 | ||
|
|
7766d99ca6 | ||
|
|
81a5a48ac4 | ||
|
|
29e183660f | ||
|
|
8676649ebc | ||
|
|
35dcc3ce5a | ||
|
|
9277960018 | ||
|
|
e23df8c9a4 | ||
|
|
22490b8c38 | ||
|
|
3ac509b40d | ||
|
|
0ea108e17f | ||
|
|
da715244cb | ||
|
|
ab854e91e5 | ||
|
|
a930852bee | ||
|
|
65c214ac2d | ||
|
|
bf8073ab26 | ||
|
|
2bf166bc3f | ||
|
|
2c98d10a92 | ||
|
|
923f9564dc | ||
|
|
ad64bfeaa9 | ||
|
|
ffd4d1f80e | ||
|
|
1310640e10 | ||
|
|
753123844b | ||
|
|
ba1c0db7e1 | ||
|
|
fce54d12c1 | ||
|
|
3e19cd45cc | ||
|
|
6b49e92d00 | ||
|
|
29e24bbac2 | ||
|
|
cefaa313df | ||
|
|
4b5bc6ae84 | ||
|
|
758fbb9110 | ||
|
|
02e6b6d3e2 | ||
|
|
af689b61d5 | ||
|
|
8215cafbae | ||
|
|
ed58f69275 | ||
|
|
a62d5e0eee | ||
|
|
2e97c95be1 | ||
|
|
6610b21b6e | ||
|
|
ef5e6463cb | ||
|
|
7f6edcd567 | ||
|
|
d7e536dda9 | ||
|
|
dbbb00f955 | ||
|
|
78f57600cc | ||
|
|
d341ebbadf | ||
|
|
d88ea4a75d | ||
|
|
46ac1baa69 | ||
|
|
f802e8c10c | ||
|
|
647c238987 | ||
|
|
4a648b302a | ||
|
|
cc7923eeb4 | ||
|
|
a9c26202a0 | ||
|
|
783d9d73be | ||
|
|
efc997dfbd | ||
|
|
3df58485c2 | ||
|
|
25851525ce | ||
|
|
4b740a5485 | ||
|
|
413cf8c4af | ||
|
|
d2193037f9 | ||
|
|
52125067ac | ||
|
|
db591f720c | ||
|
|
c81d4addb9 | ||
|
|
aa0760e97a | ||
|
|
74322affde | ||
|
|
bec8092660 |
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: 18
|
||||
- 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
|
||||
40
.github/workflows/lint-format.yml
vendored
40
.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: 18
|
||||
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
|
||||
|
||||
36
.github/workflows/photon-api-docs.yml
vendored
36
.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,21 +30,28 @@ 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: 18
|
||||
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: built-demo
|
||||
path: photon-client/dist/
|
||||
|
||||
run_api_docs:
|
||||
name: Build API Docs
|
||||
run_java_cpp_docs:
|
||||
name: Build Java and C++ API Docs
|
||||
needs: [validation]
|
||||
runs-on: "ubuntu-22.04"
|
||||
steps:
|
||||
- name: Checkout code
|
||||
@@ -63,18 +71,18 @@ jobs:
|
||||
./gradlew photon-docs:generateJavaDocs photon-docs:doxygen
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: built-docs
|
||||
name: docs-java-cpp
|
||||
path: photon-docs/build/docs
|
||||
|
||||
publish_api_docs:
|
||||
name: Publish API Docs
|
||||
needs: [run_api_docs]
|
||||
needs: [run_java_cpp_docs]
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
# Download docs artifact
|
||||
- uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: built-docs
|
||||
pattern: docs-*
|
||||
- run: find .
|
||||
- name: Publish Docs To Development
|
||||
if: github.ref == 'refs/heads/main'
|
||||
@@ -83,7 +91,7 @@ jobs:
|
||||
HOST: ${{ secrets.WEBMASTER_SSH_HOST }}
|
||||
USER: ${{ secrets.WEBMASTER_SSH_USERNAME }}
|
||||
KEY: ${{secrets.WEBMASTER_SSH_KEY}}
|
||||
TARGET: /var/www/html/photonvision-docs/development
|
||||
TARGET: /var/www/html/photonvision-docs/development/
|
||||
- name: Publish Docs To Release
|
||||
if: startsWith(github.ref, 'refs/tags/v')
|
||||
uses: up9cloud/action-rsync@v1.4
|
||||
|
||||
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
|
||||
@@ -146,3 +147,4 @@ photon-server/src/main/resources/web/*
|
||||
node_modules
|
||||
dist
|
||||
components.d.ts
|
||||
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"
|
||||
}
|
||||
10
README.md
10
README.md
@@ -17,13 +17,13 @@ If you are interested in contributing code or documentation to the project, plea
|
||||
## Documentation
|
||||
|
||||
- Our main documentation page: [docs.photonvision.org](https://docs.photonvision.org)
|
||||
- Photon UI demo: [http://photonvision.global/](http://photonvision.global/) (or [manual link](https://photonvision.github.io/photonvision/built-client/))
|
||||
- Javadocs: [javadocs.photonvision.org](https://javadocs.photonvision.org) (or [manual link](https://photonvision.github.io/photonvision/built-docs/javadoc/))
|
||||
- C++ Doxygen [cppdocs.photonvision.org](https://cppdocs.photonvision.org) (or [manual link](https://photonvision.github.io/photonvision/built-docs/doxygen/html/))
|
||||
- Photon UI demo: [http://photonvision.global/](http://photonvision.global/)
|
||||
- Javadocs: [javadocs.photonvision.org](https://javadocs.photonvision.org)
|
||||
- C++ Doxygen [cppdocs.photonvision.org](https://cppdocs.photonvision.org)
|
||||
|
||||
## 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).
|
||||
|
||||
@@ -41,6 +41,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
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
alabaster==1.0.0
|
||||
alabaster==0.7.16
|
||||
anyio==4.9.0
|
||||
babel==2.17.0
|
||||
beautifulsoup4==4.13.3
|
||||
certifi==2025.1.31
|
||||
charset-normalizer==3.4.1
|
||||
beautifulsoup4==4.13.4
|
||||
certifi==2025.4.26
|
||||
charset-normalizer==3.4.2
|
||||
click==8.1.8
|
||||
colorama==0.4.6
|
||||
doc8==1.1.2
|
||||
docopt==0.6.2
|
||||
docutils==0.21.2
|
||||
docutils==0.20.1
|
||||
furo==2024.8.6
|
||||
h11==0.14.0
|
||||
h11==0.16.0
|
||||
idna==3.10
|
||||
imagesize==1.4.1
|
||||
Jinja2==3.1.6
|
||||
@@ -19,20 +19,20 @@ MarkupSafe==3.0.2
|
||||
mdit-py-plugins==0.4.2
|
||||
mdurl==0.1.2
|
||||
myst-parser==4.0.1
|
||||
packaging==24.2
|
||||
packaging==25.0
|
||||
pbr==6.1.1
|
||||
pipreqs==0.4.13
|
||||
pipreqs==0.5.0
|
||||
Pygments==2.19.1
|
||||
PyYAML==6.0.2
|
||||
requests==2.32.3
|
||||
restructuredtext_lint==1.4.0
|
||||
requests==2.32.4
|
||||
restructuredtext-lint==1.4.0
|
||||
roman-numerals-py==3.1.0
|
||||
setuptools==77.0.3
|
||||
setuptools==80.3.1
|
||||
six==1.17.0
|
||||
sniffio==1.3.1
|
||||
snowballstemmer==2.2.0
|
||||
soupsieve==2.6
|
||||
Sphinx==8.2.3
|
||||
snowballstemmer==3.0.0.1
|
||||
soupsieve==2.7
|
||||
Sphinx==8.1.3
|
||||
sphinx-autobuild==2024.10.3
|
||||
sphinx-basic-ng==1.0.0b2
|
||||
sphinx-notfound-page==1.1.0
|
||||
@@ -47,13 +47,13 @@ sphinxcontrib-jquery==4.1
|
||||
sphinxcontrib-jsmath==1.0.1
|
||||
sphinxcontrib-qthelp==2.0.0
|
||||
sphinxcontrib-serializinghtml==2.0.0
|
||||
sphinxext-opengraph==0.9.1
|
||||
sphinxext-opengraph==0.10.0
|
||||
sphinxext-remoteliteralinclude==0.5.0
|
||||
starlette==0.46.1
|
||||
starlette==0.47.2
|
||||
stevedore==5.4.1
|
||||
typing_extensions==4.12.2
|
||||
urllib3==2.3.0
|
||||
uvicorn==0.34.0
|
||||
watchfiles==1.0.4
|
||||
typing_extensions==4.13.2
|
||||
urllib3==2.5.0
|
||||
uvicorn==0.34.2
|
||||
watchfiles==1.0.5
|
||||
websockets==15.0.1
|
||||
yarg==0.1.10
|
||||
yarg==0.1.9
|
||||
|
||||
@@ -1,74 +0,0 @@
|
||||
{# Import the theme's layout. #}
|
||||
{% extends '!layout.html' %}
|
||||
|
||||
{%- block extrahead %}
|
||||
<script>
|
||||
if (localStorage.getItem("colorTheme") === "dark") {
|
||||
document.documentElement.setAttribute('data-theme', 'dark');
|
||||
} else if (localStorage.getItem("colorTheme") === "light") {
|
||||
document.documentElement.setAttribute('data-theme', 'light');
|
||||
} else {
|
||||
var userPrefersDark = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches;
|
||||
if (userPrefersDark) {
|
||||
document.documentElement.setAttribute('data-theme', 'dark');
|
||||
} else {
|
||||
document.documentElement.setAttribute('data-theme', 'light');
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
{# Call the parent block #}
|
||||
{{ super() }}
|
||||
{% endblock %}
|
||||
|
||||
{%- block extrafooter %}
|
||||
{# Add custom things to the head HTML tag #}
|
||||
|
||||
<div class="dark-mode-toggle-container">
|
||||
<strong class="light-label md-icon"></strong>
|
||||
|
||||
<div class="dark-mode-toggle">
|
||||
<input type="checkbox" id="switch" name="theme"/><label class="toggle" for="switch">Toggle</label>
|
||||
</div>
|
||||
|
||||
<strong class="dark-label md-icon"></strong>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
var checkbox = document.querySelector('input[name=theme]');
|
||||
|
||||
var element = document.documentElement.getAttribute('data-theme');
|
||||
|
||||
if (element == 'dark') {
|
||||
// Auto check the checkbox if the set theme is "dark".
|
||||
checkbox.checked = true;
|
||||
}
|
||||
|
||||
checkbox.addEventListener('change', function() {
|
||||
if (this.checked) {
|
||||
document.documentElement.setAttribute('data-theme', 'dark');
|
||||
localStorage.setItem("colorTheme", "dark");
|
||||
} else {
|
||||
document.documentElement.setAttribute('data-theme', 'light');
|
||||
localStorage.setItem("colorTheme", "light");
|
||||
}
|
||||
});
|
||||
|
||||
window.matchMedia('(prefers-color-scheme: dark)')
|
||||
.addEventListener('change', event => {
|
||||
if (event.matches) {
|
||||
document.documentElement.setAttribute('data-theme', 'dark');
|
||||
localStorage.setItem("colorTheme", "dark");
|
||||
checkbox.checked = true;
|
||||
} else {
|
||||
document.documentElement.setAttribute('data-theme', 'light');
|
||||
localStorage.setItem("colorTheme", "light");
|
||||
checkbox.checked = false;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
{# Call the parent block #}
|
||||
{{ super() }}
|
||||
{%- endblock %}
|
||||
@@ -21,6 +21,29 @@ project = "PhotonVision"
|
||||
copyright = "2024, PhotonVision"
|
||||
author = "Banks Troutman, Matt Morley"
|
||||
|
||||
# -- Git configuration -----------------------------------------------------
|
||||
import subprocess
|
||||
|
||||
try:
|
||||
# Use closest tag
|
||||
git_tag_ref = (
|
||||
subprocess.check_output(
|
||||
[
|
||||
"git",
|
||||
"describe",
|
||||
"--tags",
|
||||
],
|
||||
stderr=subprocess.DEVNULL,
|
||||
)
|
||||
.strip()
|
||||
.decode()
|
||||
)
|
||||
except subprocess.CalledProcessError:
|
||||
# Couldn't find closest tag, fallback to main
|
||||
git_tag_ref = "main"
|
||||
|
||||
myst_substitutions = {"git_tag_ref": git_tag_ref}
|
||||
|
||||
# -- General configuration ---------------------------------------------------
|
||||
|
||||
# Add any Sphinx extension module names here, as strings. They can be
|
||||
@@ -30,7 +53,6 @@ extensions = [
|
||||
"sphinx_rtd_theme",
|
||||
"sphinx.ext.autosectionlabel",
|
||||
"sphinx.ext.todo",
|
||||
"sphinx_tabs.tabs",
|
||||
"notfound.extension",
|
||||
"sphinxext.remoteliteralinclude",
|
||||
"sphinxext.opengraph",
|
||||
@@ -47,9 +69,6 @@ ogp_site_url = "https://docs.photonvision.org/en/latest/"
|
||||
ogp_site_name = "PhotonVision Documentation"
|
||||
ogp_image = "https://raw.githubusercontent.com/PhotonVision/photonvision-docs/main/source/assets/RectLogo.png"
|
||||
|
||||
# Add any paths that contain templates here, relative to this directory.
|
||||
templates_path = ["_templates"]
|
||||
|
||||
# List of patterns, relative to source directory, that match files and
|
||||
# directories to ignore when looking for source files.
|
||||
# This pattern also affects html_static_path and html_extra_path.
|
||||
@@ -70,6 +89,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".
|
||||
@@ -147,11 +170,15 @@ 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:
|
||||
linkcheck_auth = [(R"https://github.com/.+", token)]
|
||||
|
||||
# MyST configuration (https://myst-parser.readthedocs.io/en/latest/configuration.html)
|
||||
myst_enable_extensions = ["colon_fence"]
|
||||
myst_enable_extensions = ["colon_fence", "substitution"]
|
||||
|
||||
@@ -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,17 +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 18.20.4 to Node 20.0.0 is required. To install Node JS follow the instructions for your platform [on the official Node JS website](https://nodejs.org/en/download/). However, modify this line
|
||||
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/).
|
||||
|
||||
```bash
|
||||
nvm install 20
|
||||
```
|
||||
**pnpm:**
|
||||
|
||||
so that it instead reads
|
||||
|
||||
```javascript
|
||||
nvm install 18.20.4
|
||||
```
|
||||
[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
|
||||
|
||||
@@ -46,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
|
||||
@@ -74,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.
|
||||
@@ -87,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``
|
||||
```
|
||||
@@ -105,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.
|
||||
|
||||
@@ -127,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``
|
||||
|
||||
@@ -157,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``
|
||||
```
|
||||
@@ -207,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.
|
||||
@@ -289,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/{}/scripts/rknn_conversion.ipynb)'.format(git_tag_ref) }} 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/{}/scripts/rubik_conversion.ipynb)'.format(git_tag_ref) }} 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);
|
||||
|
||||
@@ -7,4 +7,5 @@ common-errors
|
||||
logging
|
||||
camera-troubleshooting
|
||||
networking-troubleshooting
|
||||
unix-commands
|
||||
```
|
||||
|
||||
134
docs/source/docs/troubleshooting/unix-commands.md
Normal file
134
docs/source/docs/troubleshooting/unix-commands.md
Normal file
@@ -0,0 +1,134 @@
|
||||
# Useful Unix Commands
|
||||
|
||||
## Networking
|
||||
|
||||
### SSH
|
||||
|
||||
[SSH (Secure Shell)](https://www.mankier.com/1/ssh) is used to securely connect from a local to a remote system (ex. from a laptop to a coprocessor). Unlike other commands on this page, ssh is not Unix specific and can be done on Windows and MacOS from their respective terminals.
|
||||
|
||||
:::{note}
|
||||
You may see a warning similar to `The authenticity of host 'xxx' can't be established...` or `WARNING: REMOTE HOST IDENTIFICATION HAS CHANGED!`, in most cases this can be safely ignored if you have confirmed that you are connecting to the correct host over a secure connection, and the fingerprint will change when your operating system is reinstalled or PhotonVision's coprocessor image is re-flashed. This can also occur if you have multiple coprocessors with the same hostname on your network. You can read more about it [here](https://superuser.com/questions/421997/what-is-a-ssh-key-fingerprint-and-how-is-it-generated)
|
||||
:::
|
||||
|
||||
Example:
|
||||
|
||||
```
|
||||
ssh pi@hostname
|
||||
```
|
||||
|
||||
For PhotonVision, the username will be `pi` and the password will be `raspberry`.
|
||||
|
||||
### ip
|
||||
|
||||
Run [ip address](https://www.mankier.com/8/ip) with your coprocessor connected to a monitor in order to see its IP address and other network configuration information.
|
||||
|
||||
Your output might look something like this:
|
||||
|
||||
```
|
||||
2: end1: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc mq state UP group default qlen 1000
|
||||
link/ether de:9a:8f:7d:31:aa brd ff:ff:ff:ff:ff:ff
|
||||
inet 10.88.47.12/24 brd 10.88.47.255 scope global dynamic noprefixroute end1
|
||||
valid_lft 27367sec preferred_lft 27367sec
|
||||
```
|
||||
|
||||
In this example, the numbers following `inet` (10.88.47.12) are your IP address.
|
||||
|
||||
### ping
|
||||
|
||||
[ping](https://www.mankier.com/8/ping) is a command-line utility used to test the reachability of a host on an IP network. It also measures the round-trip time for messages sent from the originating host to a destination computer. It can be used to determine if a network interface is available, which can be helpful when debugging.
|
||||
|
||||
## File Transfer
|
||||
|
||||
All files under `/opt/photonvision` are owned by the root user. This means that if you want to modify them, the commands to do so must be ran as sudo.
|
||||
|
||||
### SCP
|
||||
|
||||
[SCP (Secure Copy)](https://www.mankier.com/1/scp) is used to securely transfer files between local and remote systems.
|
||||
|
||||
Example:
|
||||
|
||||
```
|
||||
scp [file] pi@hostname:/path/to/destination
|
||||
```
|
||||
|
||||
### SFTP
|
||||
|
||||
[SFTP (SSH File Transfer Protocol)](https://www.mankier.com/1/sftp#) is another option for transferring files between local and remote systems.
|
||||
|
||||
### Filezilla
|
||||
|
||||
[Filezilla](https://filezilla-project.org/) is a GUI alternative to SCP and SFTP. It is available for Windows, MacOS, and Linux.
|
||||
|
||||
## Miscellaneous
|
||||
|
||||
### v4l2-ctl
|
||||
|
||||
[v4l2-ctl](https://www.mankier.com/1/v4l2-ctl) is a command-line tool for controlling video devices.
|
||||
|
||||
List available video devices (used to verify the device recognized a connected camera):
|
||||
|
||||
```
|
||||
v4l2-ctl --list-devices
|
||||
```
|
||||
|
||||
List supported formats and resolutions for a specific video device:
|
||||
|
||||
```
|
||||
v4l2-ctl --list-formats-ext --device /path/to/video_device
|
||||
```
|
||||
|
||||
List all video device's controls and their values:
|
||||
|
||||
```
|
||||
v4l2-ctl --list-ctrls --device path/to/video_device
|
||||
```
|
||||
|
||||
:::{note}
|
||||
This command is especially useful in helping to debug when certain camera controls, like exposure, aren't behaving as expected. If you see an error in the logs similar to `WARNING 30: failed to set property [property name] (UsbCameraImpl.cpp:646)`, that means that PhotonVision is trying to use a control that doesn't exist or has a different name on your hardware. If you encounter this issue, please [file an issue](https://github.com/PhotonVision/photonvision/issues) with the necessary logs and output of the `v4l2-ctl --list-ctrls` command.
|
||||
:::
|
||||
|
||||
### systemctl
|
||||
|
||||
[systemctl](https://www.mankier.com/1/systemctl) is a command that controls the `systemd` system and service manager.
|
||||
|
||||
Start PhotonVision:
|
||||
|
||||
```
|
||||
systemctl start photonvision
|
||||
```
|
||||
|
||||
Stop PhotonVision:
|
||||
|
||||
```
|
||||
systemctl stop photonvision
|
||||
```
|
||||
|
||||
Restart PhotonVision:
|
||||
|
||||
```
|
||||
systemctl restart photonvision
|
||||
```
|
||||
|
||||
Check the status of PhotonVision:
|
||||
|
||||
```
|
||||
systemctl status photonvision
|
||||
```
|
||||
|
||||
### journalctl
|
||||
|
||||
[journalctl](https://www.mankier.com/1/journalctl) is a command that queries the systemd journal, which is a logging system used by many Linux distributions.
|
||||
|
||||
View the PhotonVision logs:
|
||||
|
||||
```
|
||||
journalctl --output cat -u photonvision
|
||||
```
|
||||
|
||||
View the PhotonVision logs in real-time:
|
||||
|
||||
```
|
||||
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,21 +0,0 @@
|
||||
{
|
||||
"root": true,
|
||||
"extends": [
|
||||
"plugin:vue/vue3-recommended",
|
||||
"eslint:recommended",
|
||||
"@vue/eslint-config-typescript",
|
||||
"@vue/eslint-config-prettier/skip-formatting"
|
||||
],
|
||||
"rules": {
|
||||
"quotes": ["error", "double"],
|
||||
"comma-dangle": ["error", "never"],
|
||||
"comma-spacing": ["error", { "before": false, "after": true }],
|
||||
"semi": ["error", "always"],
|
||||
"eol-last": "error",
|
||||
"object-curly-spacing": ["error", "always"],
|
||||
"quote-props": ["error", "as-needed"],
|
||||
"no-case-declarations": "off",
|
||||
"vue/require-default-prop": "off",
|
||||
"vue/v-on-event-hyphenation": "off"
|
||||
}
|
||||
}
|
||||
@@ -1 +1,2 @@
|
||||
src/assets/fonts/PromptRegular.ts
|
||||
pnpm-lock.yaml
|
||||
|
||||
38
photon-client/eslint.config.mjs
Normal file
38
photon-client/eslint.config.mjs
Normal file
@@ -0,0 +1,38 @@
|
||||
import pluginVue from "eslint-plugin-vue";
|
||||
import { defineConfigWithVueTs, vueTsConfigs } from "@vue/eslint-config-typescript";
|
||||
|
||||
import skipFormattingConfig from "@vue/eslint-config-prettier/skip-formatting";
|
||||
|
||||
export default defineConfigWithVueTs(
|
||||
pluginVue.configs["flat/recommended"],
|
||||
vueTsConfigs.recommended,
|
||||
skipFormattingConfig,
|
||||
{
|
||||
ignores: ["**/dist/**"]
|
||||
},
|
||||
{
|
||||
//extends: ["js/recommended"],
|
||||
rules: {
|
||||
quotes: ["error", "double"],
|
||||
"comma-dangle": ["error", "never"],
|
||||
|
||||
"comma-spacing": [
|
||||
"error",
|
||||
{
|
||||
before: false,
|
||||
after: true
|
||||
}
|
||||
],
|
||||
|
||||
semi: ["error", "always"],
|
||||
"eol-last": "error",
|
||||
"object-curly-spacing": ["error", "always"],
|
||||
"quote-props": ["error", "as-needed"],
|
||||
"no-case-declarations": "off",
|
||||
"vue/require-default-prop": "off",
|
||||
"vue/v-on-event-hyphenation": "off",
|
||||
"@typescript-eslint/no-explicit-any": "off",
|
||||
"vue/valid-v-slot": ["error", { allowModifiers: true }]
|
||||
}
|
||||
}
|
||||
);
|
||||
5543
photon-client/package-lock.json
generated
5543
photon-client/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -5,48 +5,41 @@
|
||||
"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 --ignore-path .gitignore",
|
||||
"lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix",
|
||||
"format": "prettier --write src/",
|
||||
"lint-ci": "eslint . --max-warnings 0 --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix --ignore-path .gitignore",
|
||||
"lint-ci": "eslint . --max-warnings 0 --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix",
|
||||
"format-ci": "prettier --check src/"
|
||||
},
|
||||
"dependencies": {
|
||||
"@fontsource/prompt": "^5.0.9",
|
||||
"@fontsource/prompt": "^5.2.6",
|
||||
"@mdi/font": "^7.4.47",
|
||||
"@msgpack/msgpack": "^3.0.0-beta2",
|
||||
"axios": "^1.6.3",
|
||||
"jspdf": "^2.5.1",
|
||||
"lodash": "^4.17.21",
|
||||
"pinia": "^2.1.4",
|
||||
"three": "^0.160.0",
|
||||
"vue": "^2.7.14",
|
||||
"vue-router": "^3.6.5",
|
||||
"vue-virtual-scroll-list": "^2.3.5",
|
||||
"vue2-helpers": "^2.1.1",
|
||||
"vuetify": "^2.7.1"
|
||||
"@msgpack/msgpack": "^3.1.2",
|
||||
"axios": "^1.11.0",
|
||||
"jspdf": "^3.0.1",
|
||||
"pinia": "^3.0.2",
|
||||
"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": {
|
||||
"@rushstack/eslint-patch": "^1.3.2",
|
||||
"@types/node": "^18.19.45",
|
||||
"@types/three": "^0.160.0",
|
||||
"@vitejs/plugin-vue2": "^2.3.1",
|
||||
"@vue/eslint-config-prettier": "^9.0.0",
|
||||
"@vue/eslint-config-typescript": "^12.0.0",
|
||||
"@vue/tsconfig": "^0.5.1",
|
||||
"deepmerge": "^4.3.1",
|
||||
"eslint": "^8.56.0",
|
||||
"eslint-plugin-vue": "^9.19.2",
|
||||
"npm-run-all": "^4.1.5",
|
||||
"prettier": "3.2.2",
|
||||
"sass": "~1.32",
|
||||
"sass-loader": "^13.3.2",
|
||||
"terser": "^5.14.2",
|
||||
"typescript": "^5.3.3",
|
||||
"unplugin-vue-components": "^0.26.0",
|
||||
"vite": "^5.4.2"
|
||||
"@eslint/js": "^9.31.0",
|
||||
"@types/node": "^22.15.14",
|
||||
"@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.31.0",
|
||||
"eslint-plugin-vue": "^10.3.0",
|
||||
"prettier": "^3.6.2",
|
||||
"sass": "^1.89.2",
|
||||
"typescript": "^5.8.3",
|
||||
"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>
|
||||
@@ -58,9 +65,9 @@ if (!is_demo) {
|
||||
<v-main>
|
||||
<v-container class="main-container" fluid fill-height>
|
||||
<v-layout>
|
||||
<v-flex>
|
||||
<v-container class="align-start pa-0 ma-0" fluid>
|
||||
<router-view />
|
||||
</v-flex>
|
||||
</v-container>
|
||||
</v-layout>
|
||||
</v-container>
|
||||
</v-main>
|
||||
@@ -71,9 +78,11 @@ if (!is_demo) {
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
@import "vuetify/src/styles/settings/_variables";
|
||||
@use "@/assets/styles/settings";
|
||||
@use "@/assets/styles/variables";
|
||||
@use "sass:map";
|
||||
|
||||
@media #{map-get($display-breakpoints, 'md-and-down')} {
|
||||
@media #{map.get(settings.$display-breakpoints, 'md-and-down')} {
|
||||
html {
|
||||
font-size: 14px !important;
|
||||
}
|
||||
@@ -85,24 +94,27 @@ 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;
|
||||
}
|
||||
|
||||
#title {
|
||||
color: #ffd843;
|
||||
.v-overlay__scrim {
|
||||
background-color: #111111;
|
||||
}
|
||||
|
||||
div.v-layout {
|
||||
overflow: unset !important;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -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 |
4
photon-client/src/assets/styles/settings.scss
Normal file
4
photon-client/src/assets/styles/settings.scss
Normal file
@@ -0,0 +1,4 @@
|
||||
@forward "vuetify/settings" with (
|
||||
$button-colored-disabled: false,
|
||||
$button-disabled-opacity: 0.4
|
||||
);
|
||||
@@ -1,29 +1,74 @@
|
||||
@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;
|
||||
}
|
||||
|
||||
.v-application {
|
||||
html {
|
||||
font-family: $default-font !important;
|
||||
}
|
||||
|
||||
.v-row-group__header {
|
||||
background: #005281 !important;
|
||||
}
|
||||
.theme--dark.v-data-table
|
||||
> .v-data-table__wrapper
|
||||
.v-table
|
||||
> .v-table__wrapper
|
||||
> 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/types/jsx";
|
||||
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"
|
||||
|
||||
@@ -5,7 +5,8 @@ import { useStateStore } from "@/stores/StateStore";
|
||||
<template>
|
||||
<v-snackbar
|
||||
v-model="useStateStore().snackbarData.show"
|
||||
top
|
||||
location="top"
|
||||
variant="elevated"
|
||||
:color="useStateStore().snackbarData.color"
|
||||
:timeout="useStateStore().snackbarData.timeout"
|
||||
>
|
||||
|
||||
@@ -7,13 +7,13 @@ const props = defineProps<{ source: LogMessage }>();
|
||||
const logColorClass = computed<string>(() => {
|
||||
switch (props.source.level) {
|
||||
case LogLevel.ERROR:
|
||||
return "red--text";
|
||||
return "text-red";
|
||||
case LogLevel.WARN:
|
||||
return "yellow--text";
|
||||
return "text-yellow";
|
||||
case LogLevel.INFO:
|
||||
return "light-blue--text";
|
||||
return "text-light-blue";
|
||||
case LogLevel.DEBUG:
|
||||
return "white--text";
|
||||
return "text-white";
|
||||
}
|
||||
return "";
|
||||
});
|
||||
@@ -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>
|
||||
|
||||
@@ -3,7 +3,7 @@ import { computed, inject, ref, watch } from "vue";
|
||||
import { LogLevel, type LogMessage } from "@/types/SettingTypes";
|
||||
import { useStateStore } from "@/stores/StateStore";
|
||||
import LogEntry from "@/components/app/photon-log-entry.vue";
|
||||
import VirtualList from "vue-virtual-scroll-list";
|
||||
import VirtualList from "vue3-virtual-scroll-list";
|
||||
|
||||
const backendHost = inject<string>("backendHost");
|
||||
|
||||
@@ -74,15 +74,15 @@ document.addEventListener("keydown", (e) => {
|
||||
|
||||
<template>
|
||||
<v-dialog v-model="useStateStore().showLogModal" width="1500" dark>
|
||||
<v-card dark class="dialog-container pa-6" color="primary" flat>
|
||||
<v-card class="dialog-container pa-5" color="surface" flat>
|
||||
<!-- Logs header -->
|
||||
<v-row class="no-gutters pb-3">
|
||||
<v-row class="pb-3">
|
||||
<v-col cols="4">
|
||||
<v-card-title class="pa-0">Program Logs</v-card-title>
|
||||
</v-col>
|
||||
<v-col class="align-self-center pl-3" style="text-align: right">
|
||||
<v-btn text color="white" @click="handleLogExport">
|
||||
<v-icon left class="menu-icon"> mdi-download </v-icon>
|
||||
<v-btn variant="text" color="white" @click="handleLogExport">
|
||||
<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 -->
|
||||
@@ -94,12 +94,12 @@ document.addEventListener("keydown", (e) => {
|
||||
target="_blank"
|
||||
/>
|
||||
</v-btn>
|
||||
<v-btn text color="white" @click="handleLogClear">
|
||||
<v-icon left class="menu-icon"> mdi-trash-can-outline </v-icon>
|
||||
<v-btn variant="text" color="white" @click="handleLogClear">
|
||||
<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 text color="white" @click="() => (useStateStore().showLogModal = false)">
|
||||
<v-icon left class="menu-icon"> mdi-close </v-icon>
|
||||
<v-btn variant="text" color="white" @click="() => (useStateStore().showLogModal = false)">
|
||||
<v-icon start class="menu-icon" size="large"> mdi-close </v-icon>
|
||||
<span class="menu-label">Close</span>
|
||||
</v-btn>
|
||||
</v-col>
|
||||
@@ -109,38 +109,33 @@ document.addEventListener("keydown", (e) => {
|
||||
|
||||
<div class="dialog-data">
|
||||
<!-- Log view options -->
|
||||
<v-row class="pt-4 pt-md-0 no-gutters">
|
||||
<v-col cols="12" md="5" class="align-self-center">
|
||||
<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" class="pr-3">
|
||||
<v-text-field
|
||||
v-model="searchQuery"
|
||||
dark
|
||||
dense
|
||||
density="compact"
|
||||
clearable
|
||||
hide-details="auto"
|
||||
prepend-icon="mdi-magnify"
|
||||
color="accent"
|
||||
color="primary"
|
||||
label="Search"
|
||||
variant="underlined"
|
||||
/>
|
||||
</v-col>
|
||||
<v-col cols="12" md="2" style="display: flex; align-items: center">
|
||||
<input v-model="timeInput" type="time" step="1" class="white--text pl-0 pl-md-8" />
|
||||
<v-btn icon class="ml-3" @click="timeInput = undefined">
|
||||
<v-icon>mdi-close-circle-outline</v-icon>
|
||||
<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</v-icon>
|
||||
</v-btn>
|
||||
</v-col>
|
||||
<v-col cols="12" md="5" class="pr-3">
|
||||
<v-row class="no-gutters">
|
||||
<v-col v-for="level in [0, 1, 2, 3]" :key="level">
|
||||
<v-row dense align="center">
|
||||
<v-col cols="6" md="8" style="text-align: right">
|
||||
{{ getLogLevelFromIndex(level) }}
|
||||
</v-col>
|
||||
<v-col cols="6" md="4">
|
||||
<v-switch v-model="selectedLogLevels[level]" dark color="#ffd843" />
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<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="rgb(var(--v-theme-primary))"
|
||||
></v-switch>
|
||||
</div>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
@@ -178,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;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, getCurrentInstance } from "vue";
|
||||
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 "vue2-helpers/vue-router";
|
||||
import { useRoute } from "vue-router";
|
||||
import { useDisplay, useTheme } from "vuetify";
|
||||
import { toggleTheme } from "@/lib/ThemeManager";
|
||||
|
||||
const compact = computed<boolean>({
|
||||
get: () => {
|
||||
@@ -14,128 +15,139 @@ const compact = computed<boolean>({
|
||||
useStateStore().setSidebarFolded(val);
|
||||
}
|
||||
});
|
||||
const { mdAndUp } = useDisplay();
|
||||
|
||||
// Vuetify2 doesn't yet support the useDisplay API so this is required to access the prop when using the Composition API
|
||||
const mdAndUp = computed<boolean>(() => getCurrentInstance()?.proxy.$vuetify.breakpoint.mdAndUp || false);
|
||||
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 dark app permanent :mini-variant="compact || !mdAndUp" color="primary">
|
||||
<v-list>
|
||||
<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="compact || !mdAndUp ? 'pr-0 pl-0' : ''" style="display: flex; justify-content: center">
|
||||
<v-list-item-icon class="mr-0">
|
||||
<img v-if="!(compact || !mdAndUp)" class="logo" src="@/assets/images/logoLarge.svg" alt="large logo" />
|
||||
<img v-else class="logo" src="@/assets/images/logoSmall.svg" alt="small logo" />
|
||||
</v-list-item-icon>
|
||||
<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/logoSmallTransparent.svg" alt="small logo" />
|
||||
</template>
|
||||
</v-list-item>
|
||||
|
||||
<v-list-item link to="/dashboard">
|
||||
<v-list-item-icon>
|
||||
<v-icon>mdi-view-dashboard</v-icon>
|
||||
</v-list-item-icon>
|
||||
<v-list-item-content>
|
||||
<v-list-item-title>Dashboard</v-list-item-title>
|
||||
</v-list-item-content>
|
||||
<v-list-item link to="/dashboard" prepend-icon="mdi-view-dashboard">
|
||||
<v-list-item-title>Dashboard</v-list-item-title>
|
||||
</v-list-item>
|
||||
<v-list-item link to="/settings">
|
||||
<v-list-item-icon>
|
||||
<v-icon>mdi-cog</v-icon>
|
||||
</v-list-item-icon>
|
||||
<v-list-item-content>
|
||||
<v-list-item-title>Settings</v-list-item-title>
|
||||
</v-list-item-content>
|
||||
<v-list-item link to="/settings" prepend-icon="mdi-cog">
|
||||
<v-list-item-title>Settings</v-list-item-title>
|
||||
</v-list-item>
|
||||
<v-list-item ref="camerasTabOpener" link to="/cameras">
|
||||
<v-list-item-icon>
|
||||
<v-icon>mdi-camera</v-icon>
|
||||
</v-list-item-icon>
|
||||
<v-list-item-content>
|
||||
<v-list-item-title>Camera</v-list-item-title>
|
||||
</v-list-item-content>
|
||||
<v-list-item ref="camerasTabOpener" link to="/cameras" prepend-icon="mdi-camera">
|
||||
<v-list-item-title>Camera</v-list-item-title>
|
||||
</v-list-item>
|
||||
<v-list-item
|
||||
link
|
||||
to="/cameraConfigs"
|
||||
:class="{ cameraicon: needsCamerasConfigured && useRoute().path !== '/cameraConfigs' }"
|
||||
:class="{
|
||||
cameraicon: useCameraSettingsStore().needsCameraConfiguration && useRoute().path !== '/cameraConfigs'
|
||||
}"
|
||||
>
|
||||
<v-list-item-icon>
|
||||
<v-icon :class="{ 'red--text': needsCamerasConfigured }">mdi-swap-horizontal-bold</v-icon>
|
||||
</v-list-item-icon>
|
||||
<v-list-item-content>
|
||||
<v-list-item-title :class="{ 'red--text': needsCamerasConfigured }">Camera Matching</v-list-item-title>
|
||||
</v-list-item-content>
|
||||
<template #prepend>
|
||||
<v-icon :class="{ 'text-red': useCameraSettingsStore().needsCameraConfiguration }"
|
||||
>mdi-swap-horizontal-bold</v-icon
|
||||
>
|
||||
</template>
|
||||
<v-list-item-title :class="{ 'text-red': useCameraSettingsStore().needsCameraConfiguration }"
|
||||
>Camera Matching</v-list-item-title
|
||||
>
|
||||
</v-list-item>
|
||||
<v-list-item link to="/docs">
|
||||
<v-list-item-icon>
|
||||
<v-icon>mdi-bookshelf</v-icon>
|
||||
</v-list-item-icon>
|
||||
<v-list-item-content>
|
||||
<v-list-item-title>Documentation</v-list-item-title>
|
||||
</v-list-item-content>
|
||||
<v-list-item link to="/docs" prepend-icon="mdi-bookshelf">
|
||||
<v-list-item-title>Documentation</v-list-item-title>
|
||||
</v-list-item>
|
||||
|
||||
<div style="position: absolute; bottom: 0; left: 0">
|
||||
<v-list-item v-if="mdAndUp" link @click="() => (compact = !compact)">
|
||||
<v-list-item-icon>
|
||||
<v-icon v-if="compact || !mdAndUp"> mdi-chevron-right </v-icon>
|
||||
<v-icon v-else> mdi-chevron-left </v-icon>
|
||||
</v-list-item-icon>
|
||||
<v-list-item-content>
|
||||
<v-list-item-title>Compact Mode</v-list-item-title>
|
||||
</v-list-item-content>
|
||||
</v-list-item>
|
||||
<v-list-item>
|
||||
<v-list-item-icon>
|
||||
<v-icon v-if="useSettingsStore().network.runNTServer"> mdi-server </v-icon>
|
||||
<v-icon v-else-if="useStateStore().ntConnectionStatus.connected"> mdi-robot </v-icon>
|
||||
<v-icon v-else style="border-radius: 100%"> mdi-robot-off </v-icon>
|
||||
</v-list-item-icon>
|
||||
<v-list-item-content>
|
||||
<v-list-item-title v-if="useSettingsStore().network.runNTServer" class="text-wrap">
|
||||
NetworkTables server running for
|
||||
<span class="accent--text">{{ useStateStore().ntConnectionStatus.clients || 0 }}</span> clients
|
||||
</v-list-item-title>
|
||||
<v-list-item-title
|
||||
v-else-if="useStateStore().ntConnectionStatus.connected && useStateStore().backendConnected"
|
||||
class="text-wrap"
|
||||
style="flex-direction: column; display: flex"
|
||||
>
|
||||
NetworkTables Server Connected!
|
||||
<span class="accent--text">
|
||||
{{ useStateStore().ntConnectionStatus.address }}
|
||||
</span>
|
||||
</v-list-item-title>
|
||||
<v-list-item-title v-else class="text-wrap" style="flex-direction: column; display: flex">
|
||||
Not connected to NetworkTables Server!
|
||||
</v-list-item-title>
|
||||
</v-list-item-content>
|
||||
</v-list-item>
|
||||
|
||||
<v-list-item>
|
||||
<v-list-item-icon>
|
||||
<v-icon v-if="useStateStore().backendConnected"> mdi-server-network </v-icon>
|
||||
<v-icon v-else style="border-radius: 100%"> mdi-server-network-off </v-icon>
|
||||
</v-list-item-icon>
|
||||
<v-list-item-content>
|
||||
<v-list-item-title class="text-wrap">
|
||||
{{ useStateStore().backendConnected ? "Backend connected" : "Trying to connect to backend" }}
|
||||
</v-list-item-title>
|
||||
</v-list-item-content>
|
||||
</v-list-item>
|
||||
</div>
|
||||
</v-list>
|
||||
<template #append>
|
||||
<v-list nav>
|
||||
<v-list-item
|
||||
v-if="mdAndUp"
|
||||
link
|
||||
:prepend-icon="`mdi-chevron-${compact || !mdAndUp ? 'right' : 'left'}`"
|
||||
@click="() => (compact = !compact)"
|
||||
>
|
||||
<v-list-item-title>Compact</v-list-item-title>
|
||||
</v-list-item>
|
||||
<v-list-item
|
||||
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-primary">{{ useStateStore().ntConnectionStatus.clients || 0 }}</span> clients
|
||||
</v-list-item-title>
|
||||
<v-list-item-title
|
||||
v-else-if="useStateStore().ntConnectionStatus.connected && useStateStore().backendConnected"
|
||||
v-show="!renderCompact"
|
||||
class="text-wrap"
|
||||
style="flex-direction: column; display: flex"
|
||||
>
|
||||
NetworkTables Server Connected!
|
||||
<span class="text-primary"> {{ useStateStore().ntConnectionStatus.address }} </span>
|
||||
</v-list-item-title>
|
||||
<v-list-item-title
|
||||
v-else
|
||||
v-show="!renderCompact"
|
||||
class="text-wrap"
|
||||
style="flex-direction: column; display: flex"
|
||||
>
|
||||
Not connected to NetworkTables Server!
|
||||
</v-list-item-title>
|
||||
</v-list-item>
|
||||
<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..." }}
|
||||
</v-list-item-title>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</template>
|
||||
</v-navigation-drawer>
|
||||
</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;
|
||||
}
|
||||
|
||||
.logo {
|
||||
width: 100%;
|
||||
height: 70px;
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, ref } from "vue";
|
||||
import { computed, ref, watchEffect } from "vue";
|
||||
import { useCameraSettingsStore } from "@/stores/settings/CameraSettingsStore";
|
||||
import { CalibrationBoardTypes, CalibrationTagFamilies, type VideoFormat } from "@/types/SettingTypes";
|
||||
import 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);
|
||||
|
||||
@@ -75,6 +79,18 @@ const calibrationDivisors = computed(() =>
|
||||
})
|
||||
);
|
||||
|
||||
const uniqueVideoResolutionString = ref("");
|
||||
|
||||
// Use a watchEffect so the value is populated/reacts when the stores become available or update.
|
||||
// This avoids trying to index into an array that may be empty during page reload.
|
||||
watchEffect(() => {
|
||||
const currentIndex = useCameraSettingsStore().currentVideoFormat.index ?? 0;
|
||||
useStateStore().calibrationData.videoFormatIndex = currentIndex;
|
||||
const names = useCameraSettingsStore().currentCameraSettings.validVideoFormats.map((f) =>
|
||||
getResolutionString(f.resolution)
|
||||
);
|
||||
uniqueVideoResolutionString.value = names[currentIndex] ?? names[0] ?? "";
|
||||
});
|
||||
const squareSizeIn = ref(1);
|
||||
const markerSizeIn = ref(0.75);
|
||||
const patternWidth = ref(8);
|
||||
@@ -88,10 +104,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);
|
||||
@@ -101,9 +119,8 @@ const downloadCalibBoard = () => {
|
||||
|
||||
switch (boardType.value) {
|
||||
case CalibrationBoardTypes.Chessboard:
|
||||
// eslint-disable-next-line no-case-declarations
|
||||
const chessboardStartX = (paperWidth - patternWidth.value * squareSizeIn.value) / 2;
|
||||
// eslint-disable-next-line no-case-declarations
|
||||
|
||||
const chessboardStartY = (paperHeight - patternWidth.value * squareSizeIn.value) / 2;
|
||||
|
||||
for (let squareY = 0; squareY < patternHeight.value; squareY++) {
|
||||
@@ -129,10 +146,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;
|
||||
}
|
||||
@@ -180,8 +194,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;
|
||||
@@ -194,7 +210,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(() => {
|
||||
@@ -203,10 +225,10 @@ const endCalibration = () => {
|
||||
});
|
||||
};
|
||||
|
||||
let drawAllSnapshots = ref(true);
|
||||
const drawAllSnapshots = ref(true);
|
||||
|
||||
let showCalDialog = ref(false);
|
||||
let selectedVideoFormat = ref<VideoFormat | undefined>(undefined);
|
||||
const showCalDialog = ref(false);
|
||||
const selectedVideoFormat = ref<VideoFormat | undefined>(undefined);
|
||||
const setSelectedVideoFormat = (format: VideoFormat) => {
|
||||
selectedVideoFormat.value = format;
|
||||
showCalDialog.value = true;
|
||||
@@ -215,11 +237,11 @@ 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 white--text">Current Calibration</v-card-subtitle>
|
||||
<v-simple-table fixed-header height="100%" dense>
|
||||
<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>
|
||||
<th>Resolution</th>
|
||||
@@ -239,275 +261,311 @@ const setSelectedVideoFormat = (format: VideoFormat) => {
|
||||
<td>{{ value.horizontalFOV !== undefined ? value.horizontalFOV.toFixed(2) + "°" : "-" }}</td>
|
||||
<td>{{ value.verticalFOV !== undefined ? value.verticalFOV.toFixed(2) + "°" : "-" }}</td>
|
||||
<td>{{ value.diagonalFOV !== undefined ? value.diagonalFOV.toFixed(2) + "°" : "-" }}</td>
|
||||
<v-tooltip bottom>
|
||||
<template #activator="{ on, attrs }">
|
||||
<td v-bind="attrs" v-on="on" @click="setSelectedVideoFormat(value)">
|
||||
<v-icon small class="mr-2">mdi-information</v-icon>
|
||||
<v-tooltip location="bottom">
|
||||
<template #activator="{ props }">
|
||||
<td v-bind="props" @click="setSelectedVideoFormat(value)">
|
||||
<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-simple-table>
|
||||
</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 white--text"
|
||||
>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"
|
||||
@input="(v) => useCameraSettingsStore().changeCurrentPipelineSetting({ streamingFrameDivisor: +v }, 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"
|
||||
<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.'
|
||||
"
|
||||
/>
|
||||
<pv-select
|
||||
v-model="uniqueVideoResolutionString"
|
||||
label="Resolution"
|
||||
:select-cols="8"
|
||||
:disabled="isCalibrating"
|
||||
tooltip="Resolution to calibrate at (you will have to calibrate every resolution you use 3D mode on)"
|
||||
@update:model-value="
|
||||
useStateStore().calibrationData.videoFormatIndex =
|
||||
getUniqueVideoResolutionStrings().find((v) => v.value === $event)?.value || 0
|
||||
"
|
||||
: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="
|
||||
(args) => useCameraSettingsStore().changeCurrentPipelineSetting({ drawAllSnapshots: args }, false)
|
||||
"
|
||||
/>
|
||||
<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"
|
||||
@input="(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"
|
||||
@input="(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"
|
||||
@input="(args) => useCameraSettingsStore().changeCurrentPipelineSetting({ cameraExposureRaw: args }, false)"
|
||||
/>
|
||||
<pv-slider
|
||||
v-model="useCameraSettingsStore().currentPipelineSettings.cameraBrightness"
|
||||
label="Brightness"
|
||||
:min="0"
|
||||
:max="100"
|
||||
:slider-cols="7"
|
||||
@input="(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"
|
||||
@input="(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"
|
||||
@input="(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"
|
||||
@input="(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 label :color="useStateStore().calibrationData.hasEnoughImages ? 'secondary' : 'gray'">
|
||||
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
|
||||
small
|
||||
block
|
||||
color="secondary"
|
||||
:disabled="!settingsValid || tooManyPoints"
|
||||
@click="isCalibrating ? useCameraSettingsStore().takeCalibrationSnapshot() : startCalibration()"
|
||||
>
|
||||
<v-icon left 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
|
||||
small
|
||||
block
|
||||
:color="useStateStore().calibrationData.hasEnoughImages ? 'accent' : 'error'"
|
||||
:class="useStateStore().calibrationData.hasEnoughImages ? 'black--text' : 'white---text'"
|
||||
:disabled="!isCalibrating || !settingsValid"
|
||||
@click="endCalibration"
|
||||
>
|
||||
<v-icon left 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" small block outlined :disabled="!settingsValid" @click="downloadCalibBoard">
|
||||
<v-icon left 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
|
||||
{{
|
||||
useCameraSettingsStore().currentCameraSettings.validVideoFormats.map((f) =>
|
||||
getResolutionString(f.resolution)
|
||||
)[useStateStore().calibrationData.videoFormatIndex]
|
||||
}}!
|
||||
</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" text @click="showCalibEndDialog = false"> OK </v-btn>
|
||||
<v-btn v-if="!isCalibrating" color="white" variant="text" @click="showCalibEndDialog = false"> OK </v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
@@ -518,18 +576,21 @@ const setSelectedVideoFormat = (format: VideoFormat) => {
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.v-data-table {
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -545,7 +606,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 left> 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 left>mdi-export</v-icon>
|
||||
<v-icon start size="large">mdi-export</v-icon>
|
||||
<span>Export</span>
|
||||
</v-btn>
|
||||
<a
|
||||
@@ -126,16 +134,21 @@ const calibrationImageURL = (index: number) =>
|
||||
/>
|
||||
</v-col>
|
||||
</div>
|
||||
<v-card-title class="pt-0 pb-3"
|
||||
<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 color="secondary" text-color="white" class="mt-3" icon="mdi-alert-circle-outline">
|
||||
The selected video format has not been calibrated.
|
||||
</v-banner>
|
||||
<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"
|
||||
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'tonal'"
|
||||
/>
|
||||
</v-card-text>
|
||||
<v-card-text>
|
||||
<v-simple-table dense style="width: 100%">
|
||||
<v-card-text class="pt-0">
|
||||
<v-table density="compact" style="width: 100%">
|
||||
<template #default>
|
||||
<thead>
|
||||
<tr>
|
||||
@@ -238,25 +251,36 @@ const calibrationImageURL = (index: number) =>
|
||||
</tr>
|
||||
</tbody>
|
||||
</template>
|
||||
</v-simple-table>
|
||||
</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
|
||||
dense
|
||||
density="compact"
|
||||
style="width: 100%"
|
||||
:headers="[
|
||||
{ text: 'Observation Id', value: 'index' },
|
||||
{ text: 'Mean Reprojection Error', value: 'mean' },
|
||||
{ text: '', value: 'data-table-expand' }
|
||||
{ title: 'Observation Id', key: 'index' },
|
||||
{ title: 'Mean Reprojection Error', key: 'mean' },
|
||||
{ title: '', key: 'data-table-expand' }
|
||||
]"
|
||||
:items="getObservationDetails()"
|
||||
item-key="index"
|
||||
item-value="index"
|
||||
show-expand
|
||||
expand-icon="mdi-eye"
|
||||
>
|
||||
<template #expanded-item="{ headers, item }">
|
||||
<td :colspan="headers.length">
|
||||
<template #item.data-table-expand="{ internalItem, toggleExpand }">
|
||||
<v-btn
|
||||
icon="mdi-eye"
|
||||
class="text-none"
|
||||
color="medium-emphasis"
|
||||
size="small"
|
||||
variant="text"
|
||||
slim
|
||||
@click="toggleExpand(internalItem)"
|
||||
></v-btn>
|
||||
</template>
|
||||
|
||||
<template #expanded-row="{ columns, item }">
|
||||
<td :colspan="columns.length">
|
||||
<div style="display: flex; justify-content: center; width: 100%">
|
||||
<img :src="calibrationImageURL(item.index)" alt="observation image" class="snapshot-preview pt-2 pb-2" />
|
||||
</div>
|
||||
@@ -268,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,41 +94,69 @@ const expanded = ref([]);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<v-card dark 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 left 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 dark 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="[
|
||||
{ text: 'Snapshot Name', value: 'snapshotShortName', sortable: false },
|
||||
{ text: 'Camera Unique Name', value: 'cameraUniqueName' },
|
||||
{ text: 'Camera Nickname', value: 'cameraNickname' },
|
||||
{ text: 'Stream Type', value: 'streamType' },
|
||||
{ text: 'Time Created', value: 'timeCreated' },
|
||||
{ text: 'Actions', value: 'actions', sortable: false }
|
||||
{ title: 'Snapshot Name', key: 'snapshotShortName', sortable: false },
|
||||
{ title: 'Camera Unique Name', key: 'cameraUniqueName' },
|
||||
{ title: 'Camera Nickname', key: 'cameraNickname' },
|
||||
{ title: 'Stream Type', key: 'streamType' },
|
||||
{ title: 'Time Created', key: 'timeCreated' },
|
||||
{ title: 'Actions', key: 'actions', sortable: false }
|
||||
]"
|
||||
:items="imgData"
|
||||
group-by="cameraUniqueName"
|
||||
:group-by="[{ key: 'cameraUniqueName' }]"
|
||||
class="elevation-0"
|
||||
item-key="index"
|
||||
item-value="index"
|
||||
show-expand
|
||||
expand-icon="mdi-eye"
|
||||
>
|
||||
<template #expanded-item="{ headers, item }">
|
||||
<td :colspan="headers.length">
|
||||
<template #item.data-table-expand="{ internalItem, toggleExpand }">
|
||||
<v-btn
|
||||
icon="mdi-eye"
|
||||
class="text-none"
|
||||
color="medium-emphasis"
|
||||
size="small"
|
||||
variant="text"
|
||||
slim
|
||||
@click="toggleExpand(internalItem)"
|
||||
></v-btn>
|
||||
</template>
|
||||
|
||||
<template #expanded-row="{ item, columns }">
|
||||
<td :colspan="columns.length">
|
||||
<div style="display: flex; justify-content: center; width: 100%">
|
||||
<img :src="item.snapshotSrc" alt="snapshot-image" class="snapshot-preview pt-2 pb-2" />
|
||||
</div>
|
||||
@@ -135,16 +166,12 @@ const expanded = ref([]);
|
||||
<template #item.actions="{ item }">
|
||||
<div style="display: flex; justify-content: center">
|
||||
<a :download="item.snapshotName" :href="item.snapshotSrc">
|
||||
<v-icon small> mdi-download </v-icon>
|
||||
<v-icon size="small"> mdi-download </v-icon>
|
||||
</a>
|
||||
</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>
|
||||
@@ -157,20 +184,14 @@ const expanded = ref([]);
|
||||
.v-btn {
|
||||
width: 100%;
|
||||
}
|
||||
.v-data-table {
|
||||
.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;
|
||||
@@ -183,7 +204,7 @@ const expanded = ref([]);
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background-color: #ffd843;
|
||||
background-color: rgb(var(--v-theme-accent));
|
||||
border-radius: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,15 @@
|
||||
<script setup lang="ts">
|
||||
import PvSelect, { type SelectItem } from "@/components/common/pv-select.vue";
|
||||
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,
|
||||
@@ -72,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;
|
||||
@@ -111,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)
|
||||
@@ -168,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"
|
||||
@@ -201,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 small color="secondary" :disabled="!settingsHaveChanged()" @click="saveCameraSettings">
|
||||
<v-icon left> 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 small color="error" @click="() => (showDeleteCamera = true)">
|
||||
<v-icon left> 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" dark width="800">
|
||||
<v-card dark class="dialog-container pa-3 pb-2" color="primary" flat>
|
||||
<v-dialog v-model="showDeleteCamera" width="800">
|
||||
<v-card color="surface" flat>
|
||||
<v-card-title> Delete {{ useCameraSettingsStore().currentCameraSettings.nickname }}? </v-card-title>
|
||||
<v-card-text>
|
||||
<v-row class="align-center pt-6">
|
||||
<v-col cols="12" md="6">
|
||||
<span class="white--text"> 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 left 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 + '":'"
|
||||
@@ -247,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 left 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,20 +5,11 @@ 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 props = defineProps<{
|
||||
// TODO fully update v-model usage in custom components on Vue3 update
|
||||
value: number[];
|
||||
}>();
|
||||
const theme = useTheme();
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: "input", value: number[]): void;
|
||||
}>();
|
||||
|
||||
const localValue = computed({
|
||||
get: () => props.value,
|
||||
set: (v) => emit("input", v)
|
||||
});
|
||||
const value = defineModel<number[]>({ required: true });
|
||||
|
||||
const driverMode = computed<boolean>({
|
||||
get: () => useCameraSettingsStore().isDriverMode,
|
||||
@@ -41,38 +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'"
|
||||
:text-color="fpsTooLow ? '#C7EA46' : '#ff4d00'"
|
||||
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">
|
||||
{{ 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="transparent" text-color="red" 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>
|
||||
@@ -98,21 +90,23 @@ const fpsTooLow = computed<boolean>(() => {
|
||||
</div>
|
||||
</v-card-text>
|
||||
<v-card-text class="pt-0">
|
||||
<v-btn-toggle v-model="localValue" :multiple="true" mandatory dark class="fill" style="width: 100%">
|
||||
<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 left 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 left 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>
|
||||
@@ -123,7 +117,6 @@ const fpsTooLow = computed<boolean>(() => {
|
||||
<style scoped>
|
||||
.v-btn-toggle.fill {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
.v-btn-toggle.fill > .v-btn {
|
||||
width: 50%;
|
||||
@@ -134,10 +127,6 @@ th {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.v-input--switch {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.stream-container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
|
||||
@@ -24,7 +24,7 @@ const cameraInfoFor: any = (camera: PVCameraInfo) => {
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<v-simple-table dense :style="{ backgroundColor: 'var(--v-primary-base)' }">
|
||||
<v-table density="compact" :style="{ backgroundColor: 'var(--v-primary-base)' }">
|
||||
<tbody>
|
||||
<tr v-if="cameraInfoFor(camera).dev !== undefined && cameraInfoFor(camera).dev !== null">
|
||||
<td>Device Number:</td>
|
||||
@@ -66,6 +66,6 @@ const cameraInfoFor: any = (camera: PVCameraInfo) => {
|
||||
<td>{{ cameraInfoFor(camera).otherPaths }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</v-simple-table>
|
||||
</v-table>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -1,6 +1,19 @@
|
||||
<script setup lang="ts">
|
||||
import { PVCameraInfo } from "@/types/SettingTypes";
|
||||
import _ from "lodash";
|
||||
|
||||
function isEqual<T>(a: T, b: T): boolean {
|
||||
if (a === b) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const bothAreObjects = a && b && typeof a === "object" && typeof b === "object";
|
||||
|
||||
return (
|
||||
bothAreObjects &&
|
||||
Object.keys(a).length === Object.keys(b).length &&
|
||||
Object.entries(a).every(([k, v]) => isEqual(v, b[k as keyof T]))
|
||||
);
|
||||
}
|
||||
|
||||
const { saved, current } = defineProps({
|
||||
saved: {
|
||||
@@ -29,7 +42,7 @@ const cameraInfoFor = (camera: PVCameraInfo): any => {
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<v-simple-table dense :style="{ backgroundColor: 'var(--v-primary-base)' }">
|
||||
<v-table density="compact" :style="{ backgroundColor: 'var(--v-primary-base)' }">
|
||||
<tbody>
|
||||
<tr>
|
||||
<th></th>
|
||||
@@ -105,14 +118,14 @@ const cameraInfoFor = (camera: PVCameraInfo): any => {
|
||||
</tr>
|
||||
<tr
|
||||
v-if="cameraInfoFor(saved).otherPaths !== undefined && cameraInfoFor(saved).otherPaths !== null"
|
||||
:class="!_.isEqual(cameraInfoFor(saved).otherPaths, cameraInfoFor(current).otherPaths) ? 'mismatch' : ''"
|
||||
:class="isEqual(cameraInfoFor(saved).otherPaths, cameraInfoFor(current).otherPaths) ? '' : 'mismatch'"
|
||||
>
|
||||
<td>Other Paths:</td>
|
||||
<td>{{ cameraInfoFor(saved).otherPaths }}</td>
|
||||
<td>{{ cameraInfoFor(current).otherPaths }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</v-simple-table>
|
||||
</v-table>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
const props = withDefaults(
|
||||
withDefaults(
|
||||
defineProps<{
|
||||
iconName: string;
|
||||
disabled?: boolean;
|
||||
@@ -18,20 +18,17 @@ const props = withDefaults(
|
||||
defineEmits<{
|
||||
(e: "click"): void;
|
||||
}>();
|
||||
|
||||
const hoverClass = props.hover ? "hover" : "";
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<v-tooltip :right="right" :bottom="!right" nudge-right="10" :disabled="tooltip === undefined">
|
||||
<template #activator="{ on, attrs }">
|
||||
<v-tooltip :right="right" :location="!right ? 'bottom' : undefined" offset="10" :disabled="tooltip === undefined">
|
||||
<template #activator="{ props }">
|
||||
<v-icon
|
||||
:class="hoverClass"
|
||||
:class="hover ? 'hover' : ''"
|
||||
:color="color"
|
||||
v-bind="attrs"
|
||||
v-bind="props"
|
||||
:disabled="disabled"
|
||||
v-on="on"
|
||||
@click="$emit('click')"
|
||||
>
|
||||
{{ iconName }}
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from "vue";
|
||||
import TooltippedLabel from "@/components/common/pv-tooltipped-label.vue";
|
||||
|
||||
const value = defineModel<string>({ required: true });
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
label?: string;
|
||||
tooltip?: string;
|
||||
// TODO fully update v-model usage in custom components on Vue3 update
|
||||
value: string;
|
||||
disabled?: boolean;
|
||||
errorMessage?: string;
|
||||
placeholder?: string;
|
||||
@@ -22,49 +21,43 @@ const props = withDefaults(
|
||||
);
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: "input", value: string): void;
|
||||
(e: "onEnter", value: string): void;
|
||||
(e: "onEscape"): void;
|
||||
}>();
|
||||
|
||||
const localValue = computed({
|
||||
get: () => props.value,
|
||||
set: (v) => emit("input", v)
|
||||
});
|
||||
|
||||
const handleKeydown = ({ key }) => {
|
||||
switch (key) {
|
||||
case "Enter":
|
||||
// Explicitly check that all rule props return true
|
||||
if (!props.rules?.every((rule) => rule(localValue.value) === true)) return;
|
||||
if (!props.rules?.every((rule) => rule(value.value) === true)) return;
|
||||
|
||||
emit("onEnter", localValue.value);
|
||||
emit("onEnter", value.value);
|
||||
break;
|
||||
case "Escape":
|
||||
emit("onEscape");
|
||||
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="localValue"
|
||||
dark
|
||||
dense
|
||||
color="accent"
|
||||
v-model="value"
|
||||
density="compact"
|
||||
color="primary"
|
||||
:placeholder="placeholder"
|
||||
:disabled="disabled"
|
||||
:error-messages="errorMessage"
|
||||
:rules="rules"
|
||||
hide-details="auto"
|
||||
class="light-error"
|
||||
variant="underlined"
|
||||
@keydown="handleKeydown"
|
||||
/>
|
||||
</v-col>
|
||||
@@ -75,9 +68,3 @@ const handleKeydown = ({ key }) => {
|
||||
margin-top: 0px;
|
||||
}
|
||||
</style>
|
||||
<style>
|
||||
.light-error .error--text {
|
||||
color: red !important;
|
||||
caret-color: red !important;
|
||||
}
|
||||
</style>
|
||||
|
||||
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>
|
||||
@@ -1,13 +1,13 @@
|
||||
<script setup lang="ts">
|
||||
import TooltippedLabel from "@/components/common/pv-tooltipped-label.vue";
|
||||
import { computed } from "vue";
|
||||
|
||||
const props = withDefaults(
|
||||
const value = defineModel<number>({
|
||||
required: true
|
||||
});
|
||||
withDefaults(
|
||||
defineProps<{
|
||||
label?: string;
|
||||
tooltip?: string;
|
||||
// TODO fully update v-model usage in custom components on Vue3 update
|
||||
value: number;
|
||||
disabled?: boolean;
|
||||
labelCols?: number;
|
||||
rules?: ((v: number) => boolean | string)[];
|
||||
@@ -20,30 +20,27 @@ const props = withDefaults(
|
||||
}
|
||||
);
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: "input", value: number): void;
|
||||
}>();
|
||||
|
||||
const localValue = computed({
|
||||
get: () => props.value,
|
||||
set: (v) => emit("input", parseFloat(v as unknown as string))
|
||||
get: () => value.value,
|
||||
set: (v) => (value.value = parseFloat(v as unknown as string))
|
||||
});
|
||||
</script>
|
||||
|
||||
<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"
|
||||
dark
|
||||
class="mt-0 pt-0"
|
||||
density="compact"
|
||||
hide-details
|
||||
single-line
|
||||
color="accent"
|
||||
color="primary"
|
||||
type="number"
|
||||
variant="underlined"
|
||||
style="width: 70px"
|
||||
:step="step"
|
||||
:disabled="disabled"
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from "vue";
|
||||
import TooltippedLabel from "@/components/common/pv-tooltipped-label.vue";
|
||||
const value = defineModel<number>({
|
||||
required: true
|
||||
});
|
||||
|
||||
const props = withDefaults(
|
||||
withDefaults(
|
||||
defineProps<{
|
||||
label?: string;
|
||||
tooltip?: string;
|
||||
// TODO fully update v-model usage in custom components on Vue3 update
|
||||
value: number;
|
||||
disabled?: boolean;
|
||||
inputCols?: number;
|
||||
list: string[];
|
||||
@@ -17,39 +17,25 @@ const props = withDefaults(
|
||||
inputCols: 8
|
||||
}
|
||||
);
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: "input", value: number): void;
|
||||
}>();
|
||||
|
||||
const localValue = computed({
|
||||
get: () => props.value,
|
||||
set: (v) => emit("input", v)
|
||||
});
|
||||
</script>
|
||||
|
||||
<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="localValue" row dark :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"
|
||||
color="#ffd843"
|
||||
:label="radioName"
|
||||
:value="index"
|
||||
color="rgb(var(--v-theme-primary))"
|
||||
:label="radioName"
|
||||
:model-value="index"
|
||||
:disabled="disabled"
|
||||
/>
|
||||
</v-radio-group>
|
||||
</v-col>
|
||||
</div>
|
||||
</template>
|
||||
<style scoped>
|
||||
.v-input--radio-group {
|
||||
padding-top: 0;
|
||||
margin-top: 0;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from "vue";
|
||||
import TooltippedLabel from "@/components/common/pv-tooltipped-label.vue";
|
||||
import type { WebsocketNumberPair } from "@/types/WebsocketDataTypes";
|
||||
|
||||
const value = defineModel<[number, number] | WebsocketNumberPair>({
|
||||
required: true
|
||||
});
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
label?: string;
|
||||
tooltip?: string;
|
||||
// TODO fully update v-model usage in custom components on Vue3 update
|
||||
// value: [number, number] | WebsocketNumberPair, // Vue doesnt like Union types for the value prop for some reason.
|
||||
value: [number, number];
|
||||
min: number;
|
||||
max: number;
|
||||
step?: number;
|
||||
@@ -24,19 +25,15 @@ const props = withDefaults(
|
||||
}
|
||||
);
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: "input", value: [number, number]): void;
|
||||
}>();
|
||||
|
||||
const localValue = computed<[number, number]>({
|
||||
get: (): [number, number] => {
|
||||
return Object.values(props.value) as [number, number];
|
||||
return Object.values(value.value) as [number, number];
|
||||
},
|
||||
set: (v) => {
|
||||
for (let i = 0; i < v.length; i++) {
|
||||
v[i] = parseFloat(v[i] as unknown as string);
|
||||
}
|
||||
emit("input", v);
|
||||
value.value = v;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -69,45 +66,46 @@ const checkNumberRange = (v: string): boolean => {
|
||||
:min="min"
|
||||
:disabled="disabled"
|
||||
hide-details
|
||||
class="align-center"
|
||||
dark
|
||||
:color="inverted ? 'rgba(255, 255, 255, 0.2)' : 'accent'"
|
||||
:track-color="inverted ? 'accent' : undefined"
|
||||
thumb-color="accent"
|
||||
class="align-center ml-0 mr-0"
|
||||
color="primary"
|
||||
:track-color="inverted ? 'primary' : undefined"
|
||||
thumb-color="primary"
|
||||
:step="step"
|
||||
>
|
||||
<template #prepend>
|
||||
<v-text-field
|
||||
:value="localValue[0]"
|
||||
dark
|
||||
color="accent"
|
||||
:model-value="localValue[0]"
|
||||
color="primary"
|
||||
class="mt-0 pt-0"
|
||||
density="compact"
|
||||
hide-details
|
||||
single-line
|
||||
variant="underlined"
|
||||
:max="max"
|
||||
:min="min"
|
||||
:step="step"
|
||||
:rules="[checkNumberRange]"
|
||||
type="number"
|
||||
style="width: 60px"
|
||||
@input="(v) => changeFromSlot(v, 0)"
|
||||
@update:modelValue="(v) => changeFromSlot(v, 0)"
|
||||
/>
|
||||
</template>
|
||||
<template #append>
|
||||
<v-text-field
|
||||
:value="localValue[1]"
|
||||
dark
|
||||
color="accent"
|
||||
:model-value="localValue[1]"
|
||||
color="primary"
|
||||
class="mt-0 pt-0"
|
||||
density="compact"
|
||||
hide-details
|
||||
single-line
|
||||
variant="underlined"
|
||||
:max="max"
|
||||
:min="min"
|
||||
:step="step"
|
||||
:rules="[checkNumberRange]"
|
||||
type="number"
|
||||
style="width: 60px"
|
||||
@input="(v) => changeFromSlot(v, 1)"
|
||||
@update:modelValue="(v) => changeFromSlot(v, 1)"
|
||||
/>
|
||||
</template>
|
||||
</v-range-slider>
|
||||
|
||||
@@ -7,14 +7,13 @@ export interface SelectItem {
|
||||
value: string | number;
|
||||
disabled?: boolean;
|
||||
}
|
||||
const value = defineModel<string | number | undefined>({ required: true });
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
label?: string;
|
||||
tooltip?: string;
|
||||
selectCols?: number;
|
||||
// TODO fully update v-model usage in custom components on Vue3 update
|
||||
value: any;
|
||||
disabled?: boolean;
|
||||
items: string[] | number[] | SelectItem[];
|
||||
}>(),
|
||||
@@ -24,15 +23,6 @@ const props = withDefaults(
|
||||
}
|
||||
);
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: "input", value: string): void;
|
||||
}>();
|
||||
|
||||
const localValue = computed({
|
||||
get: () => props.value,
|
||||
set: (v) => emit("input", v)
|
||||
});
|
||||
|
||||
// Computed in case items changes
|
||||
const items = computed<SelectItem[]>(() => {
|
||||
// Trivial case for empty list; we have no data
|
||||
@@ -50,21 +40,20 @@ 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="localValue"
|
||||
v-model="value"
|
||||
:items="items"
|
||||
item-text="name"
|
||||
item-title="name"
|
||||
item-value="value"
|
||||
item-disabled="disabled"
|
||||
dark
|
||||
color="accent"
|
||||
item-color="secondary"
|
||||
item-props.disabled="disabled"
|
||||
:disabled="disabled"
|
||||
hide-details="auto"
|
||||
variant="underlined"
|
||||
density="compact"
|
||||
/>
|
||||
</v-col>
|
||||
</div>
|
||||
|
||||
@@ -1,29 +1,21 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from "vue";
|
||||
import TooltippedLabel from "@/components/common/pv-tooltipped-label.vue";
|
||||
import { computed } from "vue";
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
label?: string;
|
||||
tooltip?: string;
|
||||
// TODO fully update v-model usage in custom components on Vue3 update
|
||||
value: number;
|
||||
modelValue: number;
|
||||
min: number;
|
||||
max: number;
|
||||
step?: number;
|
||||
disabled?: boolean;
|
||||
sliderCols?: number;
|
||||
}>(),
|
||||
{
|
||||
step: 1,
|
||||
disabled: false,
|
||||
sliderCols: 8
|
||||
}
|
||||
{ step: 1, disabled: false, sliderCols: 8 }
|
||||
);
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: "input", value: number): void;
|
||||
}>();
|
||||
const emit = defineEmits<{ (e: "update:modelValue", value: number): void }>();
|
||||
|
||||
// Debounce function
|
||||
function debounce(func: (...args: any[]) => void, wait: number) {
|
||||
@@ -35,29 +27,28 @@ function debounce(func: (...args: any[]) => void, wait: number) {
|
||||
}
|
||||
|
||||
const debouncedEmit = debounce((v: number) => {
|
||||
emit("input", v);
|
||||
emit("update:modelValue", v);
|
||||
}, 20);
|
||||
|
||||
const localValue = computed({
|
||||
get: () => props.value,
|
||||
get: () => props.modelValue,
|
||||
set: (v) => debouncedEmit(parseFloat(v as unknown as string))
|
||||
});
|
||||
</script>
|
||||
|
||||
<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">
|
||||
<v-col :cols="sliderCols - 1" class="pl-0 pt-10px pb-10px">
|
||||
<v-slider
|
||||
v-model="localValue"
|
||||
dark
|
||||
class="align-center"
|
||||
:max="max"
|
||||
:min="min"
|
||||
hide-details
|
||||
color="accent"
|
||||
color="primary"
|
||||
:disabled="disabled"
|
||||
:step="step"
|
||||
append-icon="mdi-menu-right"
|
||||
@@ -66,18 +57,19 @@ 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
|
||||
:value="localValue"
|
||||
dark
|
||||
color="accent"
|
||||
:model-value="localValue"
|
||||
color="primary"
|
||||
:max="max"
|
||||
:min="min"
|
||||
:disabled="disabled"
|
||||
class="mt-0 pt-0"
|
||||
density="compact"
|
||||
hide-details
|
||||
single-line
|
||||
type="number"
|
||||
variant="underlined"
|
||||
style="width: 100%"
|
||||
:step="step"
|
||||
:hide-spin-buttons="true"
|
||||
|
||||
@@ -1,34 +1,11 @@
|
||||
<script setup lang="ts">
|
||||
import TooltippedLabel from "@/components/common/pv-tooltipped-label.vue";
|
||||
import { computed } from "vue";
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
label?: string;
|
||||
tooltip?: string;
|
||||
// TODO fully update v-model usage in custom components on Vue3 update
|
||||
value: boolean;
|
||||
disabled?: boolean;
|
||||
labelCols?: number;
|
||||
switchCols?: number;
|
||||
dense?: boolean;
|
||||
}>(),
|
||||
{
|
||||
disabled: false,
|
||||
labelCols: 2,
|
||||
switchCols: 8,
|
||||
dense: false
|
||||
}
|
||||
const value = defineModel<boolean>();
|
||||
withDefaults(
|
||||
defineProps<{ label?: string; tooltip?: string; disabled?: boolean; labelCols?: number; switchCols?: number }>(),
|
||||
{ disabled: false, labelCols: 2, switchCols: 8 }
|
||||
);
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: "input", value: boolean): void;
|
||||
}>();
|
||||
|
||||
const localValue = computed({
|
||||
get: () => props.value,
|
||||
set: (v) => emit("input", v)
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -37,12 +14,13 @@ const localValue = computed({
|
||||
<tooltipped-label :tooltip="tooltip" :label="label" />
|
||||
</v-col>
|
||||
<v-col :cols="switchCols || 12 - labelCols" class="d-flex align-center pr-0">
|
||||
<v-switch v-model="localValue" dark :disabled="disabled" color="#ffd843" hide-details="auto" class="pb-1" />
|
||||
<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>
|
||||
|
||||
@@ -7,9 +7,9 @@ defineProps<{
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<v-tooltip :disabled="tooltip === undefined" right open-delay="300">
|
||||
<template #activator="{ on, attrs }">
|
||||
<span style="cursor: text !important" class="white--text" v-bind="attrs" v-on="on">{{ label }}</span>
|
||||
<v-tooltip :disabled="tooltip === undefined" location="right" open-delay="300">
|
||||
<template #activator="{ props }">
|
||||
<span style="cursor: text !important" class="text-white" v-bind="props">{{ label }}</span>
|
||||
</template>
|
||||
<span>{{ tooltip }}</span>
|
||||
</v-tooltip>
|
||||
|
||||
@@ -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,15 +241,15 @@ 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"
|
||||
v-model="useStateStore().currentCameraUniqueName"
|
||||
label="Camera"
|
||||
:items="wrappedCameras"
|
||||
@input="changeCurrentCameraUniqueName"
|
||||
@update:modelValue="changeCurrentCameraUniqueName"
|
||||
/>
|
||||
<pv-input
|
||||
v-else
|
||||
@@ -270,7 +270,7 @@ const wrappedCameras = computed<SelectItem[]>(() =>
|
||||
:disabled="checkCameraName(currentCameraName) !== true"
|
||||
@click="() => saveCameraNameEdit(currentCameraName)"
|
||||
/>
|
||||
<pv-icon icon-name="mdi-cancel" color="red darken-2" @click="cancelCameraNameEdit" />
|
||||
<pv-icon icon-name="mdi-cancel" color="red-darken-2" @click="cancelCameraNameEdit" />
|
||||
</div>
|
||||
<pv-icon
|
||||
v-else
|
||||
@@ -281,11 +281,11 @@ 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"
|
||||
:value="useCameraSettingsStore().currentCameraSettings.currentPipelineIndex"
|
||||
:model-value="useCameraSettingsStore().currentCameraSettings.currentPipelineIndex"
|
||||
label="Pipeline"
|
||||
tooltip="Each pipeline runs on a camera output and stores a unique set of processing settings"
|
||||
:disabled="
|
||||
@@ -294,7 +294,7 @@ const wrappedCameras = computed<SelectItem[]>(() =>
|
||||
!useCameraSettingsStore().hasConnected
|
||||
"
|
||||
:items="pipelineNamesWrapper"
|
||||
@input="(args) => useCameraSettingsStore().changeCurrentPipelineIndex(args, true)"
|
||||
@update:modelValue="(args) => useCameraSettingsStore().changeCurrentPipelineIndex(args, true)"
|
||||
/>
|
||||
<pv-input
|
||||
v-else
|
||||
@@ -314,31 +314,36 @@ const wrappedCameras = computed<SelectItem[]>(() =>
|
||||
:disabled="checkPipelineName(currentPipelineName) !== true"
|
||||
@click="() => savePipelineNameEdit(currentPipelineName)"
|
||||
/>
|
||||
<pv-icon icon-name="mdi-cancel" color="red darken-2" @click="cancelPipelineNameEdit" />
|
||||
<pv-icon icon-name="mdi-cancel" color="red-darken-2" @click="cancelPipelineNameEdit" />
|
||||
</div>
|
||||
<v-menu v-else-if="!useCameraSettingsStore().isDriverMode" offset-y nudge-bottom="7" auto>
|
||||
<template #activator="{ on }">
|
||||
<v-icon color="#c5c5c5" v-on="on" @click="cancelPipelineNameEdit"> mdi-menu </v-icon>
|
||||
<v-menu v-else-if="!useCameraSettingsStore().isDriverMode" offset="7">
|
||||
<template #activator="{ props }">
|
||||
<v-icon color="#c5c5c5" v-bind="props" @click="cancelPipelineNameEdit"> mdi-menu </v-icon>
|
||||
</template>
|
||||
<v-list dark dense color="primary">
|
||||
<v-list density="compact" color="primary">
|
||||
<v-list-item @click="startPipelineNameEdit">
|
||||
<v-list-item-title>
|
||||
<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"
|
||||
@@ -365,78 +370,100 @@ const wrappedCameras = computed<SelectItem[]>(() =>
|
||||
!useCameraSettingsStore().hasConnected
|
||||
"
|
||||
:items="pipelineTypesWrapper"
|
||||
@input="showPipelineTypeChangeDialog = true"
|
||||
@update:modelValue="showPipelineTypeChangeDialog = true"
|
||||
/>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-dialog v-model="showPipelineCreationDialog" dark persistent width="500">
|
||||
<v-card dark color="primary">
|
||||
<v-card-title> Create New Pipeline </v-card-title>
|
||||
<v-card-text>
|
||||
<v-dialog v-model="showPipelineCreationDialog" persistent width="500">
|
||||
<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"
|
||||
class="black--text"
|
||||
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"
|
||||
@click="createNewPipeline"
|
||||
>
|
||||
Save
|
||||
Create
|
||||
</v-btn>
|
||||
<v-btn color="error" @click="cancelPipelineCreation"> Cancel </v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
<v-dialog v-model="showPipelineDeletionConfirmationDialog" dark width="500">
|
||||
<v-card dark color="primary">
|
||||
<v-card-title> Pipeline Deletion Confirmation </v-card-title>
|
||||
<v-dialog v-model="showPipelineDeletionConfirmationDialog" width="500">
|
||||
<v-card color="surface">
|
||||
<v-card-title class="pb-0">Delete Pipeline</v-card-title>
|
||||
<v-card-text>
|
||||
Are you sure you want to delete 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 color="error" @click="confirmDeleteCurrentPipeline"> Yes, I'm sure </v-btn>
|
||||
<v-btn color="#ffd843" class="black--text" @click="showPipelineDeletionConfirmationDialog = false">
|
||||
No, take me back
|
||||
<v-card-actions class="pa-5 pt-0">
|
||||
<v-btn
|
||||
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
|
||||
color="primary"
|
||||
class="text-black"
|
||||
@click="showPipelineDeletionConfirmationDialog = false"
|
||||
>
|
||||
Cancel
|
||||
</v-btn>
|
||||
<v-btn
|
||||
color="error"
|
||||
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
|
||||
@click="confirmDeleteCurrentPipeline"
|
||||
>
|
||||
Delete
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
<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" @click="confirmChangePipelineType"> Yes, I'm sure </v-btn>
|
||||
<v-btn color="#ffd843" class="black--text" @click="cancelChangePipelineType"> No, take me back </v-btn>
|
||||
<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>
|
||||
</v-dialog>
|
||||
|
||||
@@ -6,10 +6,7 @@ import { useSettingsStore } from "@/stores/settings/GeneralSettingsStore";
|
||||
import { PipelineType } from "@/types/PipelineTypes";
|
||||
import PhotonCameraStream from "@/components/app/photon-camera-stream.vue";
|
||||
|
||||
defineProps<{
|
||||
// TODO fully update v-model usage in custom components on Vue3 update
|
||||
value: number[];
|
||||
}>();
|
||||
const value = defineModel<number[]>();
|
||||
|
||||
const driverMode = computed<boolean>({
|
||||
get: () => useCameraSettingsStore().isDriverMode,
|
||||
@@ -42,34 +39,33 @@ const performanceRecommendation = computed<string>(() => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<v-card color="primary" height="100%" class="d-flex flex-column" dark>
|
||||
<v-card-title class="justify-space-between align-center pt-3 pb-3">
|
||||
<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' : 'transparent'"
|
||||
:text-color="fpsTooLow ? '#C7EA46' : '#ff4d00'"
|
||||
style="font-size: 1rem; padding: 0; margin: 0"
|
||||
: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 color="transparent" text-color="red" style="font-size: 1rem; padding: 0; margin: 0">
|
||||
<v-chip v-else label variant="text" color="red" 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"
|
||||
color="accent"
|
||||
color="primary"
|
||||
hide-details="auto"
|
||||
/>
|
||||
</v-card-title>
|
||||
<v-divider class="ml-3 mr-3" />
|
||||
<v-row class="stream-viewer-container pa-3 align-center">
|
||||
<v-col v-if="value.includes(0)" class="stream-view">
|
||||
<v-col v-if="value?.includes(0)" class="stream-view">
|
||||
<photon-camera-stream
|
||||
id="input-camera-stream"
|
||||
:camera-settings="useCameraSettingsStore().currentCameraSettings"
|
||||
@@ -77,7 +73,7 @@ const performanceRecommendation = computed<string>(() => {
|
||||
style="width: 100%; height: auto"
|
||||
/>
|
||||
</v-col>
|
||||
<v-col v-if="value.includes(1)" class="stream-view">
|
||||
<v-col v-if="value?.includes(1)" class="stream-view">
|
||||
<photon-camera-stream
|
||||
id="output-camera-stream"
|
||||
:camera-settings="useCameraSettingsStore().currentCameraSettings"
|
||||
@@ -90,9 +86,6 @@ const performanceRecommendation = computed<string>(() => {
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.v-input--switch {
|
||||
margin-top: 0;
|
||||
}
|
||||
.stream-viewer-container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import type { Component } from "vue";
|
||||
import { computed, getCurrentInstance, onBeforeUpdate, ref } from "vue";
|
||||
import { computed, ref } from "vue";
|
||||
import { useCameraSettingsStore } from "@/stores/settings/CameraSettingsStore";
|
||||
import { useStateStore } from "@/stores/StateStore";
|
||||
import InputTab from "@/components/dashboard/tabs/InputTab.vue";
|
||||
@@ -14,6 +14,10 @@ import TargetsTab from "@/components/dashboard/tabs/TargetsTab.vue";
|
||||
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;
|
||||
@@ -21,58 +25,25 @@ 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]);
|
||||
const getTabGroups = (): ConfigOption[][] => {
|
||||
const smAndDown = getCurrentInstance()?.proxy.$vuetify.breakpoint.smAndDown || false;
|
||||
const mdAndDown = getCurrentInstance()?.proxy.$vuetify.breakpoint.mdAndDown || false;
|
||||
const lgAndDown = getCurrentInstance()?.proxy.$vuetify.breakpoint.lgAndDown || false;
|
||||
const xl = getCurrentInstance()?.proxy.$vuetify.breakpoint.xl || false;
|
||||
const { smAndDown, mdAndDown, lgAndDown, xl } = useDisplay();
|
||||
|
||||
if (smAndDown || useCameraSettingsStore().isDriverMode || (mdAndDown && !useStateStore().sidebarFolded)) {
|
||||
const getTabGroups = (): ConfigOption[][] => {
|
||||
if (smAndDown.value || useCameraSettingsStore().isDriverMode) {
|
||||
return [Object.values(allTabs)];
|
||||
} else if (mdAndDown || !useStateStore().sidebarFolded) {
|
||||
} else if (mdAndDown.value || !useStateStore().sidebarFolded) {
|
||||
return [
|
||||
[
|
||||
allTabs.inputTab,
|
||||
@@ -85,7 +56,7 @@ const getTabGroups = (): ConfigOption[][] => {
|
||||
],
|
||||
[allTabs.targetsTab, allTabs.pnpTab, allTabs.map3dTab]
|
||||
];
|
||||
} else if (lgAndDown) {
|
||||
} else if (lgAndDown.value) {
|
||||
return [
|
||||
[allTabs.inputTab],
|
||||
[
|
||||
@@ -98,7 +69,7 @@ const getTabGroups = (): ConfigOption[][] => {
|
||||
],
|
||||
[allTabs.targetsTab, allTabs.pnpTab, allTabs.map3dTab]
|
||||
];
|
||||
} else if (xl) {
|
||||
} else if (xl.value) {
|
||||
return [
|
||||
[allTabs.inputTab],
|
||||
[allTabs.thresholdTab],
|
||||
@@ -135,45 +106,40 @@ const tabGroups = computed<ConfigOption[][]>(() => {
|
||||
.filter((it) => it.length); // Remove empty tab groups
|
||||
});
|
||||
|
||||
onBeforeUpdate(() => {
|
||||
const onBeforeTabUpdate = () => {
|
||||
// Force the current tab to the input tab on driver mode change
|
||||
if (useCameraSettingsStore().isDriverMode) {
|
||||
selectedTabs.value[0] = 0;
|
||||
}
|
||||
});
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<v-row no-gutters class="tabGroups">
|
||||
<template v-if="!useCameraSettingsStore().hasConnected">
|
||||
<v-col cols="12">
|
||||
<v-card color="error">
|
||||
<v-card-title class="white--text">
|
||||
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
|
||||
v-for="(tabGroupData, tabGroupIndex) in tabGroups"
|
||||
:key="tabGroupIndex"
|
||||
:cols="tabGroupIndex == 1 && useCameraSettingsStore().currentPipelineSettings.doMultiTarget ? 7 : ''"
|
||||
: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
|
||||
background-color="primary"
|
||||
dark
|
||||
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>
|
||||
@@ -185,6 +151,11 @@ onBeforeUpdate(() => {
|
||||
</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,20 +2,12 @@
|
||||
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 props = defineProps<{
|
||||
// TODO fully update v-model usage in custom components on Vue3 update
|
||||
value: number[];
|
||||
}>();
|
||||
const theme = useTheme();
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: "input", value: number[]): void;
|
||||
}>();
|
||||
|
||||
const localValue = computed({
|
||||
get: () => props.value,
|
||||
set: (v) => emit("input", v)
|
||||
});
|
||||
const value = defineModel<number[]>();
|
||||
|
||||
const processingMode = computed<number>({
|
||||
get: () => (useCameraSettingsStore().currentPipelineSettings.solvePNPEnabled ? 1 : 0),
|
||||
@@ -30,25 +22,39 @@ const processingMode = computed<number>({
|
||||
<template>
|
||||
<v-card
|
||||
:disabled="useCameraSettingsStore().isDriverMode || useStateStore().colorPickingMode"
|
||||
class="mt-3"
|
||||
color="primary"
|
||||
style="height: 100%; display: flex; flex-direction: column"
|
||||
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 dark class="fill">
|
||||
<v-btn color="secondary" :disabled="!useCameraSettingsStore().hasConnected">
|
||||
<v-icon left>mdi-square-outline</v-icon>
|
||||
<v-btn-toggle v-model="processingMode" mandatory class="fill w-100">
|
||||
<v-btn
|
||||
color="buttonPassive"
|
||||
:disabled="!useCameraSettingsStore().hasConnected"
|
||||
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
|
||||
class="w-50"
|
||||
>
|
||||
<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"
|
||||
>
|
||||
<v-icon left>mdi-cube-outline</v-icon>
|
||||
<template #prepend>
|
||||
<v-icon size="large">mdi-cube-outline</v-icon>
|
||||
</template>
|
||||
<span>3D</span>
|
||||
</v-btn>
|
||||
</v-btn-toggle>
|
||||
@@ -57,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="localValue" :multiple="true" mandatory dark class="fill">
|
||||
<v-btn color="secondary" class="fill">
|
||||
<v-icon left 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">
|
||||
<v-icon left 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>
|
||||
@@ -73,14 +87,8 @@ const processingMode = computed<number>({
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.v-btn-toggle.fill {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.v-btn-toggle.fill > .v-btn {
|
||||
width: 50%;
|
||||
height: 100%;
|
||||
.v-btn--disabled {
|
||||
background-color: #191919 !important;
|
||||
}
|
||||
|
||||
th {
|
||||
|
||||
@@ -3,22 +3,20 @@ import { PipelineType } from "@/types/PipelineTypes";
|
||||
import PvSelect from "@/components/common/pv-select.vue";
|
||||
import PvSlider from "@/components/common/pv-slider.vue";
|
||||
import PvSwitch from "@/components/common/pv-switch.vue";
|
||||
import { computed, getCurrentInstance } from "vue";
|
||||
import { computed } from "vue";
|
||||
import { useStateStore } from "@/stores/StateStore";
|
||||
import type { ActivePipelineSettings } from "@/types/PipelineTypes";
|
||||
import { useCameraSettingsStore } from "@/stores/settings/CameraSettingsStore";
|
||||
import { useDisplay } from "vuetify";
|
||||
|
||||
// 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
|
||||
const currentPipelineSettings = computed<ActivePipelineSettings>(
|
||||
() => useCameraSettingsStore().currentPipelineSettings
|
||||
);
|
||||
|
||||
const { mdAndDown } = useDisplay();
|
||||
const interactiveCols = computed(() =>
|
||||
(getCurrentInstance()?.proxy.$vuetify.breakpoint.mdAndDown || false) &&
|
||||
(!useStateStore().sidebarFolded || useCameraSettingsStore().isDriverMode)
|
||||
? 8
|
||||
: 7
|
||||
mdAndDown.value && (!useStateStore().sidebarFolded || useCameraSettingsStore().isDriverMode) ? 8 : 7
|
||||
);
|
||||
</script>
|
||||
|
||||
@@ -29,7 +27,7 @@ const interactiveCols = computed(() =>
|
||||
label="Target family"
|
||||
:items="['AprilTag 36h11 (6.5in)', 'AprilTag 16h5 (6in)']"
|
||||
:select-cols="interactiveCols"
|
||||
@input="(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ tagFamily: value }, false)"
|
||||
@update:modelValue="(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ tagFamily: value }, false)"
|
||||
/>
|
||||
<pv-slider
|
||||
v-model="currentPipelineSettings.decimate"
|
||||
@@ -38,7 +36,7 @@ const interactiveCols = computed(() =>
|
||||
tooltip="Increases FPS at the expense of range by reducing image resolution initially"
|
||||
:min="1"
|
||||
:max="8"
|
||||
@input="(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ decimate: value }, false)"
|
||||
@update:modelValue="(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ decimate: value }, false)"
|
||||
/>
|
||||
<pv-slider
|
||||
v-model="currentPipelineSettings.blur"
|
||||
@@ -48,7 +46,7 @@ const interactiveCols = computed(() =>
|
||||
:min="0"
|
||||
:max="5"
|
||||
:step="0.1"
|
||||
@input="(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ blur: value }, false)"
|
||||
@update:modelValue="(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ blur: value }, false)"
|
||||
/>
|
||||
<pv-slider
|
||||
v-model="currentPipelineSettings.threads"
|
||||
@@ -57,7 +55,7 @@ const interactiveCols = computed(() =>
|
||||
tooltip="Number of threads spawned by the AprilTag detector"
|
||||
:min="1"
|
||||
:max="8"
|
||||
@input="(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ threads: value }, false)"
|
||||
@update:modelValue="(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ threads: value }, false)"
|
||||
/>
|
||||
<pv-slider
|
||||
v-model="currentPipelineSettings.decisionMargin"
|
||||
@@ -66,7 +64,9 @@ const interactiveCols = computed(() =>
|
||||
tooltip="Tags with a 'margin' (decoding quality score) less than this wil be rejected. Increase this to reduce the number of false positive detections"
|
||||
:min="0"
|
||||
:max="250"
|
||||
@input="(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ decisionMargin: value }, false)"
|
||||
@update:modelValue="
|
||||
(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ decisionMargin: value }, false)
|
||||
"
|
||||
/>
|
||||
<pv-slider
|
||||
v-model="currentPipelineSettings.numIterations"
|
||||
@@ -75,14 +75,18 @@ const interactiveCols = computed(() =>
|
||||
tooltip="Number of iterations the pose estimation algorithm will run, 50-100 is a good starting point"
|
||||
:min="0"
|
||||
:max="500"
|
||||
@input="(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ numIterations: value }, false)"
|
||||
@update:modelValue="
|
||||
(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ numIterations: value }, false)
|
||||
"
|
||||
/>
|
||||
<pv-switch
|
||||
v-model="currentPipelineSettings.refineEdges"
|
||||
:switch-cols="interactiveCols"
|
||||
label="Refine Edges"
|
||||
tooltip="Further refines the AprilTag corner position initial estimate, suggested left on"
|
||||
@input="(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ refineEdges: value }, false)"
|
||||
@update:modelValue="
|
||||
(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ refineEdges: value }, false)
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -5,20 +5,18 @@ import PvSlider from "@/components/common/pv-slider.vue";
|
||||
import PvSwitch from "@/components/common/pv-switch.vue";
|
||||
import PvRangeSlider from "@/components/common/pv-range-slider.vue";
|
||||
import PvSelect from "@/components/common/pv-select.vue";
|
||||
import { computed, getCurrentInstance } from "vue";
|
||||
import { computed } from "vue";
|
||||
import { useStateStore } from "@/stores/StateStore";
|
||||
import { useDisplay } from "vuetify";
|
||||
|
||||
// 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
|
||||
const currentPipelineSettings = computed<ActivePipelineSettings>(
|
||||
() => useCameraSettingsStore().currentPipelineSettings
|
||||
);
|
||||
|
||||
const { mdAndDown } = useDisplay();
|
||||
const interactiveCols = computed(() =>
|
||||
(getCurrentInstance()?.proxy.$vuetify.breakpoint.mdAndDown || false) &&
|
||||
(!useStateStore().sidebarFolded || useCameraSettingsStore().isDriverMode)
|
||||
? 8
|
||||
: 7
|
||||
mdAndDown.value && (!useStateStore().sidebarFolded || useCameraSettingsStore().isDriverMode) ? 8 : 7
|
||||
);
|
||||
</script>
|
||||
|
||||
@@ -29,7 +27,7 @@ const interactiveCols = computed(() =>
|
||||
label="Target family"
|
||||
:items="['AprilTag Family 36h11', 'AprilTag Family 16h5']"
|
||||
:select-cols="interactiveCols"
|
||||
@input="(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ tagFamily: value }, false)"
|
||||
@update:modelValue="(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ tagFamily: value }, false)"
|
||||
/>
|
||||
<pv-range-slider
|
||||
v-model="currentPipelineSettings.threshWinSizes"
|
||||
@@ -39,7 +37,9 @@ const interactiveCols = computed(() =>
|
||||
:max="255"
|
||||
:slider-cols="interactiveCols"
|
||||
:step="2"
|
||||
@input="(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ threshWinSizes: value }, false)"
|
||||
@update:modelValue="
|
||||
(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ threshWinSizes: value }, false)
|
||||
"
|
||||
/>
|
||||
<pv-slider
|
||||
v-model="currentPipelineSettings.threshStepSize"
|
||||
@@ -49,7 +49,9 @@ const interactiveCols = computed(() =>
|
||||
:min="2"
|
||||
:max="128"
|
||||
:step="1"
|
||||
@input="(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ threshStepSize: value }, false)"
|
||||
@update:modelValue="
|
||||
(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ threshStepSize: value }, false)
|
||||
"
|
||||
/>
|
||||
<pv-slider
|
||||
v-model="currentPipelineSettings.threshConstant"
|
||||
@@ -59,21 +61,27 @@ const interactiveCols = computed(() =>
|
||||
:min="0"
|
||||
:max="128"
|
||||
:step="1"
|
||||
@input="(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ threshConstant: value }, false)"
|
||||
@update:modelValue="
|
||||
(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ threshConstant: value }, false)
|
||||
"
|
||||
/>
|
||||
<pv-switch
|
||||
v-model="currentPipelineSettings.useCornerRefinement"
|
||||
label="Refine Corners"
|
||||
tooltip="Further refine the initial corners with subpixel accuracy."
|
||||
:switch-cols="interactiveCols"
|
||||
@input="(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ useCornerRefinement: value }, false)"
|
||||
@update:modelValue="
|
||||
(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ useCornerRefinement: value }, false)
|
||||
"
|
||||
/>
|
||||
<pv-switch
|
||||
v-model="currentPipelineSettings.debugThreshold"
|
||||
label="Debug Threshold"
|
||||
tooltip="Display the first threshold step to the color stream."
|
||||
:switch-cols="interactiveCols"
|
||||
@input="(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ debugThreshold: value }, false)"
|
||||
@update:modelValue="
|
||||
(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ debugThreshold: value }, false)
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -4,8 +4,9 @@ import { type ActivePipelineSettings, PipelineType } from "@/types/PipelineTypes
|
||||
import PvRangeSlider from "@/components/common/pv-range-slider.vue";
|
||||
import PvSelect from "@/components/common/pv-select.vue";
|
||||
import PvSlider from "@/components/common/pv-slider.vue";
|
||||
import { computed, getCurrentInstance } from "vue";
|
||||
import { computed } from "vue";
|
||||
import { useStateStore } from "@/stores/StateStore";
|
||||
import { useDisplay } from "vuetify";
|
||||
|
||||
// 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
|
||||
@@ -48,12 +49,9 @@ const contourRadius = computed<[number, number]>({
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const { mdAndDown } = useDisplay();
|
||||
const interactiveCols = computed(() =>
|
||||
(getCurrentInstance()?.proxy.$vuetify.breakpoint.mdAndDown || false) &&
|
||||
(!useStateStore().sidebarFolded || useCameraSettingsStore().isDriverMode)
|
||||
? 8
|
||||
: 7
|
||||
mdAndDown.value && (!useStateStore().sidebarFolded || useCameraSettingsStore().isDriverMode) ? 8 : 7
|
||||
);
|
||||
</script>
|
||||
|
||||
@@ -65,7 +63,7 @@ const interactiveCols = computed(() =>
|
||||
tooltip="Used to determine how to calculate target landmarks, as well as aspect ratio"
|
||||
:items="['Portrait', 'Landscape']"
|
||||
:select-cols="interactiveCols"
|
||||
@input="
|
||||
@update:modelValue="
|
||||
(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ contourTargetOrientation: value }, false)
|
||||
"
|
||||
/>
|
||||
@@ -75,7 +73,9 @@ const interactiveCols = computed(() =>
|
||||
tooltip="Chooses the sorting mode used to determine the 'best' targets to provide to user code"
|
||||
:select-cols="interactiveCols"
|
||||
:items="['Largest', 'Smallest', 'Highest', 'Lowest', 'Rightmost', 'Leftmost', 'Centermost']"
|
||||
@input="(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ contourSortMode: value }, false)"
|
||||
@update:modelValue="
|
||||
(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ contourSortMode: value }, false)
|
||||
"
|
||||
/>
|
||||
<pv-range-slider
|
||||
v-model="contourArea"
|
||||
@@ -84,7 +84,9 @@ const interactiveCols = computed(() =>
|
||||
:max="100"
|
||||
:slider-cols="interactiveCols"
|
||||
:step="0.01"
|
||||
@input="(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ contourArea: value }, false)"
|
||||
@update:modelValue="
|
||||
(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ contourArea: value }, false)
|
||||
"
|
||||
/>
|
||||
<pv-range-slider
|
||||
v-if="useCameraSettingsStore().currentPipelineType !== PipelineType.ColoredShape"
|
||||
@@ -95,7 +97,9 @@ const interactiveCols = computed(() =>
|
||||
:max="100"
|
||||
:slider-cols="interactiveCols"
|
||||
:step="0.1"
|
||||
@input="(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ contourRatio: value }, false)"
|
||||
@update:modelValue="
|
||||
(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ contourRatio: value }, false)
|
||||
"
|
||||
/>
|
||||
<pv-range-slider
|
||||
v-if="useCameraSettingsStore().currentPipelineType === PipelineType.ColoredShape"
|
||||
@@ -105,7 +109,9 @@ const interactiveCols = computed(() =>
|
||||
:min="0"
|
||||
:max="100"
|
||||
:slider-cols="interactiveCols"
|
||||
@input="(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ contourFullness: value }, false)"
|
||||
@update:modelValue="
|
||||
(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ contourFullness: value }, false)
|
||||
"
|
||||
/>
|
||||
<pv-range-slider
|
||||
v-if="currentPipelineSettings.pipelineType === PipelineType.ColoredShape"
|
||||
@@ -115,7 +121,9 @@ const interactiveCols = computed(() =>
|
||||
:min="0"
|
||||
:max="4000"
|
||||
:slider-cols="interactiveCols"
|
||||
@input="(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ contourPerimeter: value }, false)"
|
||||
@update:modelValue="
|
||||
(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ contourPerimeter: value }, false)
|
||||
"
|
||||
/>
|
||||
<pv-slider
|
||||
v-model="useCameraSettingsStore().currentPipelineSettings.contourSpecklePercentage"
|
||||
@@ -124,7 +132,7 @@ const interactiveCols = computed(() =>
|
||||
:min="0"
|
||||
:max="100"
|
||||
:slider-cols="interactiveCols"
|
||||
@input="
|
||||
@update:modelValue="
|
||||
(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ contourSpecklePercentage: value }, false)
|
||||
"
|
||||
/>
|
||||
@@ -137,7 +145,9 @@ const interactiveCols = computed(() =>
|
||||
:max="4"
|
||||
:step="0.1"
|
||||
:slider-cols="interactiveCols"
|
||||
@input="(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ contourFilterRangeX: value }, false)"
|
||||
@update:modelValue="
|
||||
(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ contourFilterRangeX: value }, false)
|
||||
"
|
||||
/>
|
||||
<pv-slider
|
||||
v-model="currentPipelineSettings.contourFilterRangeY"
|
||||
@@ -147,7 +157,9 @@ const interactiveCols = computed(() =>
|
||||
:max="4"
|
||||
:step="0.1"
|
||||
:slider-cols="interactiveCols"
|
||||
@input="(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ contourFilterRangeY: value }, false)"
|
||||
@update:modelValue="
|
||||
(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ contourFilterRangeY: value }, false)
|
||||
"
|
||||
/>
|
||||
<pv-select
|
||||
v-model="useCameraSettingsStore().currentPipelineSettings.contourGroupingMode"
|
||||
@@ -155,7 +167,9 @@ const interactiveCols = computed(() =>
|
||||
tooltip="Whether or not every two targets are paired with each other (good for e.g. 2019 targets)"
|
||||
:select-cols="interactiveCols"
|
||||
:items="['Single', 'Dual', 'Two or More']"
|
||||
@input="(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ contourGroupingMode: value }, false)"
|
||||
@update:modelValue="
|
||||
(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ contourGroupingMode: value }, false)
|
||||
"
|
||||
/>
|
||||
<pv-select
|
||||
v-model="useCameraSettingsStore().currentPipelineSettings.contourIntersection"
|
||||
@@ -164,7 +178,9 @@ const interactiveCols = computed(() =>
|
||||
:select-cols="interactiveCols"
|
||||
:items="['None', 'Up', 'Down', 'Left', 'Right']"
|
||||
:disabled="useCameraSettingsStore().currentPipelineSettings.contourGroupingMode === 0"
|
||||
@input="(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ contourIntersection: value }, false)"
|
||||
@update:modelValue="
|
||||
(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ contourIntersection: value }, false)
|
||||
"
|
||||
/>
|
||||
</template>
|
||||
<template v-else-if="currentPipelineSettings.pipelineType === PipelineType.ColoredShape">
|
||||
@@ -174,7 +190,9 @@ const interactiveCols = computed(() =>
|
||||
tooltip="The shape of targets to look for"
|
||||
:select-cols="interactiveCols"
|
||||
:items="['Circle', 'Polygon', 'Triangle', 'Quadrilateral']"
|
||||
@input="(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ contourShape: value }, false)"
|
||||
@update:modelValue="
|
||||
(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ contourShape: value }, false)
|
||||
"
|
||||
/>
|
||||
<pv-slider
|
||||
v-if="currentPipelineSettings.contourShape >= 1"
|
||||
@@ -185,7 +203,9 @@ const interactiveCols = computed(() =>
|
||||
:min="0"
|
||||
:max="100"
|
||||
:slider-cols="interactiveCols"
|
||||
@input="(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ accuracyPercentage: value }, false)"
|
||||
@update:modelValue="
|
||||
(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ accuracyPercentage: value }, false)
|
||||
"
|
||||
/>
|
||||
<pv-slider
|
||||
v-if="currentPipelineSettings.contourShape === 0"
|
||||
@@ -196,7 +216,7 @@ const interactiveCols = computed(() =>
|
||||
:min="1"
|
||||
:max="100"
|
||||
:slider-cols="interactiveCols"
|
||||
@input="
|
||||
@update:modelValue="
|
||||
(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ circleDetectThreshold: value }, false)
|
||||
"
|
||||
/>
|
||||
@@ -208,7 +228,9 @@ const interactiveCols = computed(() =>
|
||||
:min="1"
|
||||
:max="100"
|
||||
:slider-cols="interactiveCols"
|
||||
@input="(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ maxCannyThresh: value }, false)"
|
||||
@update:modelValue="
|
||||
(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ maxCannyThresh: value }, false)
|
||||
"
|
||||
/>
|
||||
<pv-slider
|
||||
v-if="currentPipelineSettings.contourShape === 0"
|
||||
@@ -218,7 +240,9 @@ const interactiveCols = computed(() =>
|
||||
:min="1"
|
||||
:max="100"
|
||||
:slider-cols="interactiveCols"
|
||||
@input="(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ circleAccuracy: value }, false)"
|
||||
@update:modelValue="
|
||||
(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ circleAccuracy: value }, false)
|
||||
"
|
||||
/>
|
||||
<pv-range-slider
|
||||
v-if="currentPipelineSettings.contourShape === 0"
|
||||
@@ -228,7 +252,9 @@ const interactiveCols = computed(() =>
|
||||
:min="0"
|
||||
:max="100"
|
||||
:slider-cols="interactiveCols"
|
||||
@input="(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ contourRadius: value }, false)"
|
||||
@update:modelValue="
|
||||
(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ contourRadius: value }, false)
|
||||
"
|
||||
/>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
@@ -3,10 +3,11 @@ import PvSlider from "@/components/common/pv-slider.vue";
|
||||
import { useCameraSettingsStore } from "@/stores/settings/CameraSettingsStore";
|
||||
import PvSwitch from "@/components/common/pv-switch.vue";
|
||||
import PvSelect from "@/components/common/pv-select.vue";
|
||||
import { computed, getCurrentInstance } from "vue";
|
||||
import { computed } from "vue";
|
||||
import { useSettingsStore } from "@/stores/settings/GeneralSettingsStore";
|
||||
import { useStateStore } from "@/stores/StateStore";
|
||||
import { getResolutionString } from "@/lib/PhotonUtils";
|
||||
import { useDisplay } from "vuetify";
|
||||
|
||||
// Due to something with libcamera or something else IDK much about, the 90° rotations need to be disabled if the libcamera drivers are being used.
|
||||
const cameraRotations = computed(() =>
|
||||
@@ -62,12 +63,10 @@ const handleStreamResolutionChange = (value: number) => {
|
||||
false
|
||||
);
|
||||
};
|
||||
const { mdAndDown } = useDisplay();
|
||||
|
||||
const interactiveCols = computed(() =>
|
||||
(getCurrentInstance()?.proxy.$vuetify.breakpoint.mdAndDown || false) &&
|
||||
(!useStateStore().sidebarFolded || useCameraSettingsStore().isDriverMode)
|
||||
? 8
|
||||
: 7
|
||||
mdAndDown.value && (!useStateStore().sidebarFolded || useCameraSettingsStore().isDriverMode) ? 8 : 7
|
||||
);
|
||||
</script>
|
||||
|
||||
@@ -75,11 +74,12 @@ const interactiveCols = computed(() =>
|
||||
<div>
|
||||
<pv-switch
|
||||
v-model="useCameraSettingsStore().currentPipelineSettings.cameraAutoExposure"
|
||||
class="pt-2"
|
||||
label="Auto Exposure"
|
||||
:switch-cols="interactiveCols"
|
||||
tooltip="Enables or Disables camera automatic adjustment for current lighting conditions"
|
||||
@input="(args) => useCameraSettingsStore().changeCurrentPipelineSetting({ cameraAutoExposure: args }, false)"
|
||||
@update:modelValue="
|
||||
(args) => useCameraSettingsStore().changeCurrentPipelineSetting({ cameraAutoExposure: args }, false)
|
||||
"
|
||||
/>
|
||||
<pv-slider
|
||||
v-model="useCameraSettingsStore().currentPipelineSettings.cameraExposureRaw"
|
||||
@@ -90,7 +90,9 @@ const interactiveCols = computed(() =>
|
||||
:max="useCameraSettingsStore().maxExposureRaw"
|
||||
:slider-cols="interactiveCols"
|
||||
:step="1"
|
||||
@input="(args) => useCameraSettingsStore().changeCurrentPipelineSetting({ cameraExposureRaw: args }, false)"
|
||||
@update:modelValue="
|
||||
(args) => useCameraSettingsStore().changeCurrentPipelineSetting({ cameraExposureRaw: args }, false)
|
||||
"
|
||||
/>
|
||||
<pv-slider
|
||||
v-model="useCameraSettingsStore().currentPipelineSettings.cameraBrightness"
|
||||
@@ -98,7 +100,9 @@ const interactiveCols = computed(() =>
|
||||
:min="0"
|
||||
:max="100"
|
||||
:slider-cols="interactiveCols"
|
||||
@input="(args) => useCameraSettingsStore().changeCurrentPipelineSetting({ cameraBrightness: args }, false)"
|
||||
@update:modelValue="
|
||||
(args) => useCameraSettingsStore().changeCurrentPipelineSetting({ cameraBrightness: args }, false)
|
||||
"
|
||||
/>
|
||||
<pv-slider
|
||||
v-if="useCameraSettingsStore().currentPipelineSettings.cameraGain >= 0"
|
||||
@@ -108,7 +112,7 @@ const interactiveCols = computed(() =>
|
||||
:min="0"
|
||||
:max="100"
|
||||
:slider-cols="interactiveCols"
|
||||
@input="(args) => useCameraSettingsStore().changeCurrentPipelineSetting({ cameraGain: args }, false)"
|
||||
@update:modelValue="(args) => useCameraSettingsStore().changeCurrentPipelineSetting({ cameraGain: args }, false)"
|
||||
/>
|
||||
<pv-slider
|
||||
v-if="useCameraSettingsStore().currentPipelineSettings.cameraRedGain !== -1"
|
||||
@@ -118,7 +122,9 @@ const interactiveCols = computed(() =>
|
||||
:max="100"
|
||||
:slider-cols="interactiveCols"
|
||||
tooltip="Controls red automatic white balance gain, which affects how the camera captures colors in different conditions"
|
||||
@input="(args) => useCameraSettingsStore().changeCurrentPipelineSetting({ cameraRedGain: args }, false)"
|
||||
@update:modelValue="
|
||||
(args) => useCameraSettingsStore().changeCurrentPipelineSetting({ cameraRedGain: args }, false)
|
||||
"
|
||||
/>
|
||||
<pv-slider
|
||||
v-if="useCameraSettingsStore().currentPipelineSettings.cameraBlueGain !== -1"
|
||||
@@ -128,14 +134,18 @@ const interactiveCols = computed(() =>
|
||||
:max="100"
|
||||
:slider-cols="interactiveCols"
|
||||
tooltip="Controls blue automatic white balance gain, which affects how the camera captures colors in different conditions"
|
||||
@input="(args) => useCameraSettingsStore().changeCurrentPipelineSetting({ cameraBlueGain: args }, false)"
|
||||
@update:modelValue="
|
||||
(args) => useCameraSettingsStore().changeCurrentPipelineSetting({ cameraBlueGain: args }, false)
|
||||
"
|
||||
/>
|
||||
<pv-switch
|
||||
v-model="useCameraSettingsStore().currentPipelineSettings.cameraAutoWhiteBalance"
|
||||
label="Auto White Balance"
|
||||
:switch-cols="interactiveCols"
|
||||
tooltip="Enables or Disables camera automatic adjustment for current lighting conditions"
|
||||
@input="(args) => useCameraSettingsStore().changeCurrentPipelineSetting({ cameraAutoWhiteBalance: args }, false)"
|
||||
@update:modelValue="
|
||||
(args) => useCameraSettingsStore().changeCurrentPipelineSetting({ cameraAutoWhiteBalance: args }, false)
|
||||
"
|
||||
/>
|
||||
<pv-slider
|
||||
v-model="useCameraSettingsStore().currentPipelineSettings.cameraWhiteBalanceTemp"
|
||||
@@ -144,7 +154,9 @@ const interactiveCols = computed(() =>
|
||||
:min="useCameraSettingsStore().minWhiteBalanceTemp"
|
||||
:max="useCameraSettingsStore().maxWhiteBalanceTemp"
|
||||
:slider-cols="interactiveCols"
|
||||
@input="(args) => useCameraSettingsStore().changeCurrentPipelineSetting({ cameraWhiteBalanceTemp: args }, false)"
|
||||
@update:modelValue="
|
||||
(args) => useCameraSettingsStore().changeCurrentPipelineSetting({ cameraWhiteBalanceTemp: args }, false)
|
||||
"
|
||||
/>
|
||||
<pv-select
|
||||
v-model="useCameraSettingsStore().currentPipelineSettings.inputImageRotationMode"
|
||||
@@ -152,7 +164,9 @@ const interactiveCols = computed(() =>
|
||||
tooltip="Rotates the camera stream. Rotation not available when camera has been calibrated."
|
||||
:items="cameraRotations"
|
||||
:select-cols="interactiveCols"
|
||||
@input="(args) => useCameraSettingsStore().changeCurrentPipelineSetting({ inputImageRotationMode: args }, false)"
|
||||
@update:modelValue="
|
||||
(args) => useCameraSettingsStore().changeCurrentPipelineSetting({ inputImageRotationMode: args }, false)
|
||||
"
|
||||
/>
|
||||
<pv-select
|
||||
v-model="useCameraSettingsStore().currentPipelineSettings.cameraVideoModeIndex"
|
||||
@@ -160,7 +174,7 @@ const interactiveCols = computed(() =>
|
||||
tooltip="Resolution and FPS the camera should directly capture at"
|
||||
:items="cameraResolutions"
|
||||
:select-cols="interactiveCols"
|
||||
@input="(args) => handleResolutionChange(args)"
|
||||
@update:modelValue="(args) => handleResolutionChange(args)"
|
||||
/>
|
||||
<pv-select
|
||||
v-model="useCameraSettingsStore().currentPipelineSettings.streamingFrameDivisor"
|
||||
@@ -168,7 +182,7 @@ const interactiveCols = computed(() =>
|
||||
tooltip="Resolution to which camera frames are downscaled for streaming to the dashboard"
|
||||
:items="streamResolutions"
|
||||
:select-cols="interactiveCols"
|
||||
@input="(args) => handleStreamResolutionChange(args)"
|
||||
@update:modelValue="(args) => handleStreamResolutionChange(args)"
|
||||
/>
|
||||
<pv-switch
|
||||
v-if="useCameraSettingsStore().isDriverMode"
|
||||
@@ -176,7 +190,7 @@ const interactiveCols = computed(() =>
|
||||
label="Crosshair"
|
||||
:switch-cols="interactiveCols"
|
||||
tooltip="Enables or disables a crosshair overlay on the camera stream"
|
||||
@input="(args) => useCameraSettingsStore().changeCurrentPipelineSetting({ crosshair: args }, false)"
|
||||
@update:modelValue="(args) => useCameraSettingsStore().changeCurrentPipelineSetting({ crosshair: args }, false)"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -11,12 +11,17 @@ const trackedTargets = computed<PhotonTarget[]>(() => useStateStore().currentPip
|
||||
<div>
|
||||
<v-row style="width: 100%">
|
||||
<v-col>
|
||||
<span class="white--text">Target Visualization</span>
|
||||
<span class="text-white">Target Visualization</span>
|
||||
</v-col>
|
||||
</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>
|
||||
|
||||
@@ -2,9 +2,13 @@
|
||||
import { useCameraSettingsStore } from "@/stores/settings/CameraSettingsStore";
|
||||
import { type ObjectDetectionPipelineSettings, PipelineType } from "@/types/PipelineTypes";
|
||||
import PvSlider from "@/components/common/pv-slider.vue";
|
||||
import { computed, getCurrentInstance } from "vue";
|
||||
import PvSelect from "@/components/common/pv-select.vue";
|
||||
import PvRangeSlider from "@/components/common/pv-range-slider.vue";
|
||||
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
|
||||
@@ -22,23 +26,38 @@ const contourRatio = computed<[number, number]>({
|
||||
set: (v) => (useCameraSettingsStore().currentPipelineSettings.contourRatio = v)
|
||||
});
|
||||
|
||||
const { mdAndDown } = useDisplay();
|
||||
|
||||
const interactiveCols = computed(() =>
|
||||
(getCurrentInstance()?.proxy.$vuetify.breakpoint.mdAndDown || false) &&
|
||||
(!useStateStore().sidebarFolded || useCameraSettingsStore().isDriverMode)
|
||||
? 9
|
||||
: 8
|
||||
mdAndDown.value && (!useStateStore().sidebarFolded || useCameraSettingsStore().isDriverMode) ? 9 : 8
|
||||
);
|
||||
|
||||
// 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: () => supportedModels.value.indexOf(currentPipelineSettings.value.model),
|
||||
get: () => {
|
||||
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) => {
|
||||
useCameraSettingsStore().changeCurrentPipelineSetting({ model: supportedModels.value[v] }, false);
|
||||
if (v !== undefined && v >= 0 && v < supportedModels.value.length) {
|
||||
const newModel = supportedModels.value[v];
|
||||
useCameraSettingsStore().changeCurrentPipelineSetting({ model: newModel }, true);
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
@@ -50,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"
|
||||
@@ -61,7 +81,20 @@ const selectedModel = computed({
|
||||
:min="0"
|
||||
:max="1"
|
||||
:step="0.01"
|
||||
@input="(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ confidence: value }, false)"
|
||||
@update:modelValue="
|
||||
(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"
|
||||
@@ -70,7 +103,9 @@ const selectedModel = computed({
|
||||
:max="100"
|
||||
:slider-cols="interactiveCols"
|
||||
:step="0.01"
|
||||
@input="(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ contourArea: value }, false)"
|
||||
@update:modelValue="
|
||||
(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ contourArea: value }, false)
|
||||
"
|
||||
/>
|
||||
<pv-range-slider
|
||||
v-model="contourRatio"
|
||||
@@ -80,7 +115,9 @@ const selectedModel = computed({
|
||||
:max="100"
|
||||
:slider-cols="interactiveCols"
|
||||
:step="0.01"
|
||||
@input="(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ contourRatio: value }, false)"
|
||||
@update:modelValue="
|
||||
(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ contourRatio: value }, false)
|
||||
"
|
||||
/>
|
||||
<pv-select
|
||||
v-model="useCameraSettingsStore().currentPipelineSettings.contourTargetOrientation"
|
||||
@@ -88,8 +125,12 @@ const selectedModel = computed({
|
||||
tooltip="Used to determine how to calculate target landmarks, as well as aspect ratio"
|
||||
:items="['Portrait', 'Landscape']"
|
||||
:select-cols="interactiveCols"
|
||||
@input="
|
||||
(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ contourTargetOrientation: value }, false)
|
||||
@update:modelValue="
|
||||
(value) =>
|
||||
useCameraSettingsStore().changeCurrentPipelineSetting(
|
||||
{ contourTargetOrientation: typeof value === 'string' ? Number(value) : value },
|
||||
false
|
||||
)
|
||||
"
|
||||
/>
|
||||
<pv-select
|
||||
@@ -98,7 +139,13 @@ const selectedModel = computed({
|
||||
tooltip="Chooses the sorting mode used to determine the 'best' targets to provide to user code"
|
||||
:select-cols="interactiveCols"
|
||||
:items="['Largest', 'Smallest', 'Highest', 'Lowest', 'Rightmost', 'Leftmost', 'Centermost']"
|
||||
@input="(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ contourSortMode: value }, false)"
|
||||
@update:modelValue="
|
||||
(value) =>
|
||||
useCameraSettingsStore().changeCurrentPipelineSetting(
|
||||
{ contourSortMode: typeof value === 'string' ? Number(value) : value },
|
||||
false
|
||||
)
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -3,9 +3,13 @@ import PvSelect from "@/components/common/pv-select.vue";
|
||||
import { useCameraSettingsStore } from "@/stores/settings/CameraSettingsStore";
|
||||
import { type ActivePipelineSettings, PipelineType, RobotOffsetPointMode } from "@/types/PipelineTypes";
|
||||
import PvSwitch from "@/components/common/pv-switch.vue";
|
||||
import { computed, getCurrentInstance } from "vue";
|
||||
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(
|
||||
() =>
|
||||
@@ -45,12 +49,10 @@ const offsetPoints = computed<MetricItem[]>(() => {
|
||||
const currentPipelineSettings = computed<ActivePipelineSettings>(
|
||||
() => useCameraSettingsStore().currentPipelineSettings
|
||||
);
|
||||
const { mdAndDown } = useDisplay();
|
||||
|
||||
const interactiveCols = computed(() =>
|
||||
(getCurrentInstance()?.proxy.$vuetify.breakpoint.mdAndDown || false) &&
|
||||
(!useStateStore().sidebarFolded || useCameraSettingsStore().isDriverMode)
|
||||
? 8
|
||||
: 7
|
||||
mdAndDown.value && (!useStateStore().sidebarFolded || useCameraSettingsStore().isDriverMode) ? 8 : 7
|
||||
);
|
||||
</script>
|
||||
|
||||
@@ -62,7 +64,7 @@ const interactiveCols = computed(() =>
|
||||
tooltip="If enabled, up to five targets will be displayed and sent via PhotonLib, instead of just one"
|
||||
:disabled="isTagPipeline"
|
||||
:switch-cols="interactiveCols"
|
||||
@input="
|
||||
@update:modelValue="
|
||||
(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ outputShowMultipleTargets: value }, false)
|
||||
"
|
||||
/>
|
||||
@@ -78,7 +80,9 @@ const interactiveCols = computed(() =>
|
||||
tooltip="If enabled, all visible fiducial targets will be used to provide a single pose estimate from their combined model."
|
||||
:switch-cols="interactiveCols"
|
||||
:disabled="!isTagPipeline"
|
||||
@input="(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ doMultiTarget: value }, false)"
|
||||
@update:modelValue="
|
||||
(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ doMultiTarget: value }, false)
|
||||
"
|
||||
/>
|
||||
<pv-switch
|
||||
v-if="
|
||||
@@ -92,7 +96,9 @@ const interactiveCols = computed(() =>
|
||||
tooltip="If disabled, visible fiducial targets used for multi-target estimation will not also be used for single-target estimation."
|
||||
:switch-cols="interactiveCols"
|
||||
:disabled="!isTagPipeline || !currentPipelineSettings.doMultiTarget"
|
||||
@input="(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ doSingleTargetAlways: value }, false)"
|
||||
@update:modelValue="
|
||||
(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ doSingleTargetAlways: value }, false)
|
||||
"
|
||||
/>
|
||||
<pv-select
|
||||
v-model="useCameraSettingsStore().currentPipelineSettings.contourTargetOffsetPointEdge"
|
||||
@@ -100,7 +106,7 @@ const interactiveCols = computed(() =>
|
||||
tooltip="Changes where the 'center' of the target is (used for calculating e.g. pitch and yaw)"
|
||||
:items="['Center', 'Top', 'Bottom', 'Left', 'Right']"
|
||||
:select-cols="interactiveCols"
|
||||
@input="
|
||||
@update:modelValue="
|
||||
(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ contourTargetOffsetPointEdge: value }, false)
|
||||
"
|
||||
/>
|
||||
@@ -111,7 +117,7 @@ const interactiveCols = computed(() =>
|
||||
tooltip="Used to determine how to calculate target landmarks (e.g. the top, left, or bottom of the target)"
|
||||
:items="['Portrait', 'Landscape']"
|
||||
:select-cols="interactiveCols"
|
||||
@input="
|
||||
@update:modelValue="
|
||||
(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ contourTargetOrientation: value }, false)
|
||||
"
|
||||
/>
|
||||
@@ -121,22 +127,28 @@ const interactiveCols = computed(() =>
|
||||
tooltip="Used to add an arbitrary offset to the location of the targeting crosshair"
|
||||
:items="['None', 'Single Point', 'Dual Point']"
|
||||
:select-cols="interactiveCols"
|
||||
@input="(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ offsetRobotOffsetMode: value }, false)"
|
||||
@update:modelValue="
|
||||
(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ offsetRobotOffsetMode: value }, false)
|
||||
"
|
||||
/>
|
||||
<table
|
||||
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"
|
||||
@@ -148,10 +160,11 @@ const interactiveCols = computed(() =>
|
||||
>
|
||||
<v-col cols="6" class="pl-0">
|
||||
<v-btn
|
||||
small
|
||||
size="small"
|
||||
block
|
||||
color="accent"
|
||||
class="black--text"
|
||||
color="primary"
|
||||
class="text-black"
|
||||
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
|
||||
@click="useCameraSettingsStore().takeRobotOffsetPoint(RobotOffsetType.Single)"
|
||||
>
|
||||
Take Point
|
||||
@@ -159,9 +172,10 @@ const interactiveCols = computed(() =>
|
||||
</v-col>
|
||||
<v-col cols="6" class="pr-0">
|
||||
<v-btn
|
||||
small
|
||||
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
|
||||
@@ -174,10 +188,11 @@ const interactiveCols = computed(() =>
|
||||
>
|
||||
<v-col cols="6" lg="4" class="pl-0 pr-2">
|
||||
<v-btn
|
||||
small
|
||||
size="small"
|
||||
block
|
||||
color="accent"
|
||||
class="black--text"
|
||||
color="primary"
|
||||
class="text-black"
|
||||
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
|
||||
@click="useCameraSettingsStore().takeRobotOffsetPoint(RobotOffsetType.DualFirst)"
|
||||
>
|
||||
Take First Point
|
||||
@@ -185,10 +200,11 @@ const interactiveCols = computed(() =>
|
||||
</v-col>
|
||||
<v-col cols="6" lg="4" class="pl-2 pr-0 pr-lg-2">
|
||||
<v-btn
|
||||
small
|
||||
size="small"
|
||||
block
|
||||
color="accent"
|
||||
class="black--text"
|
||||
color="primary"
|
||||
class="text-black"
|
||||
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
|
||||
@click="useCameraSettingsStore().takeRobotOffsetPoint(RobotOffsetType.DualSecond)"
|
||||
>
|
||||
Take Second Point
|
||||
@@ -196,9 +212,10 @@ const interactiveCols = computed(() =>
|
||||
</v-col>
|
||||
<v-col cols="12" lg="4" class="pl-0 pl-lg-2 pr-0">
|
||||
<v-btn
|
||||
small
|
||||
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
|
||||
@@ -229,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>
|
||||
|
||||
@@ -3,14 +3,13 @@ import PvSelect from "@/components/common/pv-select.vue";
|
||||
import { useCameraSettingsStore } from "@/stores/settings/CameraSettingsStore";
|
||||
import { TargetModel } from "@/types/PipelineTypes";
|
||||
import PvSlider from "@/components/common/pv-slider.vue";
|
||||
import { computed, getCurrentInstance } from "vue";
|
||||
import { computed } from "vue";
|
||||
import { useStateStore } from "@/stores/StateStore";
|
||||
import { useDisplay } from "vuetify";
|
||||
const { mdAndDown } = useDisplay();
|
||||
|
||||
const interactiveCols = computed(() =>
|
||||
(getCurrentInstance()?.proxy.$vuetify.breakpoint.mdAndDown || false) &&
|
||||
(!useStateStore().sidebarFolded || useCameraSettingsStore().isDriverMode)
|
||||
? 9
|
||||
: 8
|
||||
mdAndDown.value && (!useStateStore().sidebarFolded || useCameraSettingsStore().isDriverMode) ? 9 : 8
|
||||
);
|
||||
</script>
|
||||
|
||||
@@ -30,7 +29,9 @@ const interactiveCols = computed(() =>
|
||||
{ name: '2025 Algae (16.25in)', value: TargetModel.ReefscapeAlgae }
|
||||
]"
|
||||
:select-cols="interactiveCols"
|
||||
@input="(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ targetModel: value }, false)"
|
||||
@update:modelValue="
|
||||
(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ targetModel: value }, false)
|
||||
"
|
||||
/>
|
||||
<pv-slider
|
||||
v-model="useCameraSettingsStore().currentPipelineSettings.cornerDetectionAccuracyPercentage"
|
||||
@@ -39,7 +40,7 @@ const interactiveCols = computed(() =>
|
||||
label="Contour simplification Percentage"
|
||||
:min="0"
|
||||
:max="100"
|
||||
@input="
|
||||
@update:modelValue="
|
||||
(value) =>
|
||||
useCameraSettingsStore().changeCurrentPipelineSetting({ cornerDetectionAccuracyPercentage: value }, false)
|
||||
"
|
||||
|
||||
@@ -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
|
||||
@@ -34,8 +37,8 @@ const resetCurrentBuffer = () => {
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<v-row align="start" class="pb-4">
|
||||
<v-simple-table dense class="pt-2 pb-12">
|
||||
<v-row class="pb-4">
|
||||
<v-table density="compact" class="pt-2 pb-12 pl-3 pr-3">
|
||||
<template #default>
|
||||
<thead>
|
||||
<tr>
|
||||
@@ -44,24 +47,24 @@ const resetCurrentBuffer = () => {
|
||||
currentPipelineSettings.pipelineType === PipelineType.AprilTag ||
|
||||
currentPipelineSettings.pipelineType === PipelineType.Aruco
|
||||
"
|
||||
class="text-center white--text"
|
||||
class="text-center text-white"
|
||||
>
|
||||
Fiducial ID
|
||||
</th>
|
||||
<template v-if="currentPipelineSettings.pipelineType === PipelineType.ObjectDetection">
|
||||
<th class="text-center white--text">Class</th>
|
||||
<th class="text-center white--text">Confidence</th>
|
||||
<th class="text-center text-white">Class</th>
|
||||
<th class="text-center text-white">Confidence</th>
|
||||
</template>
|
||||
<template v-if="!useCameraSettingsStore().currentPipelineSettings.solvePNPEnabled">
|
||||
<th class="text-center white--text">Pitch θ°</th>
|
||||
<th class="text-center white--text">Yaw θ°</th>
|
||||
<th class="text-center white--text">Skew θ°</th>
|
||||
<th class="text-center white--text">Area %</th>
|
||||
<th class="text-center text-white">Pitch θ°</th>
|
||||
<th class="text-center text-white">Yaw θ°</th>
|
||||
<th class="text-center text-white">Skew θ°</th>
|
||||
<th class="text-center text-white">Area %</th>
|
||||
</template>
|
||||
<template v-else>
|
||||
<th class="text-center white--text">X meters</th>
|
||||
<th class="text-center white--text">Y meters</th>
|
||||
<th class="text-center white--text">Z Angle θ°</th>
|
||||
<th class="text-center text-white">X meters</th>
|
||||
<th class="text-center text-white">Y meters</th>
|
||||
<th class="text-center text-white">Z Angle θ°</th>
|
||||
</template>
|
||||
<template
|
||||
v-if="
|
||||
@@ -70,7 +73,7 @@ const resetCurrentBuffer = () => {
|
||||
useCameraSettingsStore().currentPipelineSettings.solvePNPEnabled
|
||||
"
|
||||
>
|
||||
<th class="text-center white--text">Ambiguity Ratio</th>
|
||||
<th class="text-center text-white">Ambiguity Ratio</th>
|
||||
</template>
|
||||
</tr>
|
||||
</thead>
|
||||
@@ -78,7 +81,7 @@ const resetCurrentBuffer = () => {
|
||||
<tr
|
||||
v-for="(target, index) in useStateStore().currentPipelineResults?.targets"
|
||||
:key="index"
|
||||
class="white--text"
|
||||
class="text-white"
|
||||
>
|
||||
<td
|
||||
v-if="
|
||||
@@ -91,13 +94,13 @@ const resetCurrentBuffer = () => {
|
||||
</td>
|
||||
<td
|
||||
v-if="currentPipelineSettings.pipelineType === PipelineType.ObjectDetection"
|
||||
class="text-center white--text"
|
||||
class="text-center text-white"
|
||||
>
|
||||
{{ useStateStore().currentPipelineResults?.classNames[target.classId] }}
|
||||
</td>
|
||||
<td
|
||||
v-if="currentPipelineSettings.pipelineType === PipelineType.ObjectDetection"
|
||||
class="text-center white--text"
|
||||
class="text-center text-white"
|
||||
>
|
||||
{{ target.confidence.toFixed(2) }}
|
||||
</td>
|
||||
@@ -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>
|
||||
@@ -126,7 +129,7 @@ const resetCurrentBuffer = () => {
|
||||
</tr>
|
||||
</tbody>
|
||||
</template>
|
||||
</v-simple-table>
|
||||
</v-table>
|
||||
</v-row>
|
||||
<v-container
|
||||
v-if="
|
||||
@@ -136,122 +139,128 @@ const resetCurrentBuffer = () => {
|
||||
useCameraSettingsStore().isCurrentVideoFormatCalibrated &&
|
||||
useCameraSettingsStore().currentPipelineSettings.solvePNPEnabled
|
||||
"
|
||||
class="pl-3 pr-3"
|
||||
>
|
||||
<v-row class="pb-4 white--text">
|
||||
<v-row class="pb-4 text-white">
|
||||
<v-card-subtitle class="ma-0 pa-0 pb-4" style="font-size: 16px"
|
||||
>Multi-tag pose, field-to-camera</v-card-subtitle
|
||||
>
|
||||
<v-simple-table dense>
|
||||
<v-table density="compact">
|
||||
<template #default>
|
||||
<thead>
|
||||
<tr class="white--text">
|
||||
<th class="text-center white--text">X meters</th>
|
||||
<th class="text-center white--text">Y meters</th>
|
||||
<th class="text-center white--text">Z meters</th>
|
||||
<th class="text-center white--text">X Angle θ°</th>
|
||||
<th class="text-center white--text">Y Angle θ°</th>
|
||||
<th class="text-center white--text">Z Angle θ°</th>
|
||||
<th class="text-center white--text">Tags</th>
|
||||
<tr class="text-white">
|
||||
<th class="text-center text-white">X meters</th>
|
||||
<th class="text-center text-white">Y meters</th>
|
||||
<th class="text-center text-white">Z meters</th>
|
||||
<th class="text-center text-white">X Angle θ°</th>
|
||||
<th class="text-center text-white">Y Angle θ°</th>
|
||||
<th class="text-center text-white">Z Angle θ°</th>
|
||||
<th class="text-center text-white">Tags</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody v-show="useStateStore().currentPipelineResults?.multitagResult">
|
||||
<tr>
|
||||
<td class="text-center white--text">
|
||||
<td class="text-center text-white">
|
||||
{{ useStateStore().currentPipelineResults?.multitagResult?.bestTransform.x.toFixed(3) }} m
|
||||
</td>
|
||||
<td class="text-center white--text">
|
||||
<td class="text-center text-white">
|
||||
{{ useStateStore().currentPipelineResults?.multitagResult?.bestTransform.y.toFixed(3) }} m
|
||||
</td>
|
||||
<td class="text-center white--text">
|
||||
<td class="text-center text-white">
|
||||
{{ useStateStore().currentPipelineResults?.multitagResult?.bestTransform.z.toFixed(3) }} m
|
||||
</td>
|
||||
<td class="text-center white--text">
|
||||
<td class="text-center text-white">
|
||||
{{
|
||||
toDeg(useStateStore().currentPipelineResults?.multitagResult?.bestTransform.angle_x || 0).toFixed(
|
||||
2
|
||||
)
|
||||
}}°
|
||||
</td>
|
||||
<td class="text-center white--text">
|
||||
<td class="text-center text-white">
|
||||
{{
|
||||
toDeg(useStateStore().currentPipelineResults?.multitagResult?.bestTransform.angle_y || 0).toFixed(
|
||||
2
|
||||
)
|
||||
}}°
|
||||
</td>
|
||||
<td class="text-center white--text">
|
||||
<td class="text-center text-white">
|
||||
{{
|
||||
toDeg(useStateStore().currentPipelineResults?.multitagResult?.bestTransform.angle_z || 0).toFixed(
|
||||
2
|
||||
)
|
||||
}}°
|
||||
</td>
|
||||
<td class="text-center white--text">
|
||||
<td class="text-center text-white">
|
||||
{{ useStateStore().currentPipelineResults?.multitagResult?.fiducialIDsUsed }}
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</template>
|
||||
</v-simple-table>
|
||||
</v-table>
|
||||
</v-row>
|
||||
<v-row class="pb-4 white--text" style="display: flex; flex-direction: column">
|
||||
<v-row class="pb-4 text-white" style="display: flex; flex-direction: column">
|
||||
<v-card-subtitle class="ma-0 pa-0 pb-4 pr-4" style="font-size: 16px"
|
||||
>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" depressed @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-simple-table dense>
|
||||
<v-table density="compact">
|
||||
<template #default>
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="text-center white--text">X meters</th>
|
||||
<th class="text-center white--text">Y meters</th>
|
||||
<th class="text-center white--text">Z meters</th>
|
||||
<th class="text-center white--text">X Angle θ°</th>
|
||||
<th class="text-center white--text">Y Angle θ°</th>
|
||||
<th class="text-center white--text">Z Angle θ°</th>
|
||||
<th class="text-center text-white">X meters</th>
|
||||
<th class="text-center text-white">Y meters</th>
|
||||
<th class="text-center text-white">Z meters</th>
|
||||
<th class="text-center text-white">X Angle θ°</th>
|
||||
<th class="text-center text-white">Y Angle θ°</th>
|
||||
<th class="text-center text-white">Z Angle θ°</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody v-show="useStateStore().currentPipelineResults?.multitagResult">
|
||||
<tr>
|
||||
<td class="text-center white--text">
|
||||
<td class="text-center text-white">
|
||||
{{
|
||||
calculateStdDev(useStateStore().currentMultitagBuffer?.map((v) => v.bestTransform.x) || []).toFixed(
|
||||
5
|
||||
)
|
||||
}} m
|
||||
</td>
|
||||
<td class="text-center white--text">
|
||||
<td class="text-center text-white">
|
||||
{{
|
||||
calculateStdDev(useStateStore().currentMultitagBuffer?.map((v) => v.bestTransform.y) || []).toFixed(
|
||||
5
|
||||
)
|
||||
}} m
|
||||
</td>
|
||||
<td class="text-center white--text">
|
||||
<td class="text-center text-white">
|
||||
{{
|
||||
calculateStdDev(useStateStore().currentMultitagBuffer?.map((v) => v.bestTransform.z) || []).toFixed(
|
||||
5
|
||||
)
|
||||
}} m
|
||||
</td>
|
||||
<td class="text-center white--text">
|
||||
<td class="text-center text-white">
|
||||
{{
|
||||
calculateStdDev(
|
||||
useStateStore().currentMultitagBuffer?.map((v) => toDeg(v.bestTransform.angle_x)) || []
|
||||
).toFixed(5)
|
||||
}}°
|
||||
</td>
|
||||
<td class="text-center white--text">
|
||||
<td class="text-center text-white">
|
||||
{{
|
||||
calculateStdDev(
|
||||
useStateStore().currentMultitagBuffer?.map((v) => toDeg(v.bestTransform.angle_y)) || []
|
||||
).toFixed(5)
|
||||
}}°
|
||||
</td>
|
||||
<td class="text-center white--text">
|
||||
<td class="text-center text-white">
|
||||
{{
|
||||
calculateStdDev(
|
||||
useStateStore().currentMultitagBuffer?.map((v) => toDeg(v.bestTransform.angle_z)) || []
|
||||
@@ -261,15 +270,18 @@ const resetCurrentBuffer = () => {
|
||||
</tr>
|
||||
</tbody>
|
||||
</template>
|
||||
</v-simple-table>
|
||||
</v-table>
|
||||
</v-row>
|
||||
</v-container>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.v-data-table {
|
||||
background-color: #006492 !important;
|
||||
th {
|
||||
padding-left: 8px !important;
|
||||
padding-right: 8px !important;
|
||||
}
|
||||
.v-table {
|
||||
width: 100%;
|
||||
font-size: 1rem !important;
|
||||
|
||||
@@ -282,13 +294,9 @@ const resetCurrentBuffer = () => {
|
||||
}
|
||||
}
|
||||
tbody {
|
||||
:hover {
|
||||
td {
|
||||
background-color: #005281 !important;
|
||||
}
|
||||
}
|
||||
tr {
|
||||
td {
|
||||
padding: 0 !important;
|
||||
font-size: 1rem !important;
|
||||
color: white !important;
|
||||
}
|
||||
@@ -307,7 +315,7 @@ const resetCurrentBuffer = () => {
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background-color: #ffd843;
|
||||
background-color: rgb(var(--v-theme-accent));
|
||||
border-radius: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
<script setup lang="ts">
|
||||
import { useCameraSettingsStore } from "@/stores/settings/CameraSettingsStore";
|
||||
import { computed, getCurrentInstance, onBeforeUnmount, onMounted } from "vue";
|
||||
import { computed, onBeforeUnmount, onMounted } from "vue";
|
||||
import PvRangeSlider from "@/components/common/pv-range-slider.vue";
|
||||
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;
|
||||
@@ -123,12 +127,10 @@ onBeforeUnmount(() => {
|
||||
|
||||
cameraStream.removeEventListener("click", handleStreamClick);
|
||||
});
|
||||
const { mdAndDown } = useDisplay();
|
||||
|
||||
const interactiveCols = computed(() =>
|
||||
(getCurrentInstance()?.proxy.$vuetify.breakpoint.mdAndDown || false) &&
|
||||
(!useStateStore().sidebarFolded || useCameraSettingsStore().isDriverMode)
|
||||
? 9
|
||||
: 8
|
||||
mdAndDown.value && (!useStateStore().sidebarFolded || useCameraSettingsStore().isDriverMode) ? 9 : 8
|
||||
);
|
||||
</script>
|
||||
|
||||
@@ -144,7 +146,7 @@ const interactiveCols = computed(() =>
|
||||
:max="180"
|
||||
:slider-cols="interactiveCols"
|
||||
:inverted="useCameraSettingsStore().currentPipelineSettings.hueInverted"
|
||||
@input="(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ hsvHue: value }, false)"
|
||||
@update:modelValue="(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ hsvHue: value }, false)"
|
||||
/>
|
||||
<pv-range-slider
|
||||
id="sat-slider"
|
||||
@@ -155,7 +157,9 @@ const interactiveCols = computed(() =>
|
||||
:min="0"
|
||||
:max="255"
|
||||
:slider-cols="interactiveCols"
|
||||
@input="(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ hsvSaturation: value }, false)"
|
||||
@update:modelValue="
|
||||
(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ hsvSaturation: value }, false)
|
||||
"
|
||||
/>
|
||||
<pv-range-slider
|
||||
id="value-slider"
|
||||
@@ -166,53 +170,73 @@ const interactiveCols = computed(() =>
|
||||
:min="0"
|
||||
:max="255"
|
||||
:slider-cols="interactiveCols"
|
||||
@input="(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ hsvValue: value }, false)"
|
||||
@update:modelValue="(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ hsvValue: value }, false)"
|
||||
/>
|
||||
<pv-switch
|
||||
v-model="useCameraSettingsStore().currentPipelineSettings.hueInverted"
|
||||
label="Invert Hue"
|
||||
:switch-cols="interactiveCols"
|
||||
tooltip="Selects the hue range outside of the hue slider bounds instead of inside"
|
||||
@input="(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ hueInverted: value }, false)"
|
||||
@update:modelValue="
|
||||
(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ hueInverted: value }, false)
|
||||
"
|
||||
/>
|
||||
<div>
|
||||
<div class="white--text pt-3">Color Picker</div>
|
||||
<div class="text-white pt-3">Color Picker</div>
|
||||
<div class="d-flex pt-3">
|
||||
<template v-if="!useStateStore().colorPickingMode">
|
||||
<v-col cols="4" class="pl-0 pr-2">
|
||||
<v-btn
|
||||
small
|
||||
size="small"
|
||||
block
|
||||
color="accent"
|
||||
class="black--text"
|
||||
color="primary"
|
||||
class="text-black"
|
||||
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
|
||||
@click="enableColorPicking(useCameraSettingsStore().currentPipelineSettings.hueInverted ? 2 : 3)"
|
||||
>
|
||||
<v-icon left> 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="black--text" small block @click="enableColorPicking(1)">
|
||||
<v-icon left> 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>
|
||||
<v-col cols="4" class="pl-2 pr-0">
|
||||
<v-btn
|
||||
small
|
||||
size="small"
|
||||
block
|
||||
color="accent"
|
||||
class="black--text"
|
||||
color="primary"
|
||||
class="text-black"
|
||||
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
|
||||
@click="enableColorPicking(useCameraSettingsStore().currentPipelineSettings.hueInverted ? 3 : 2)"
|
||||
>
|
||||
<v-icon left> 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="black--text" 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>
|
||||
@@ -224,32 +248,32 @@ const interactiveCols = computed(() =>
|
||||
.threshold-modifiers {
|
||||
--averageHue: 0;
|
||||
}
|
||||
#hue-slider >>> .v-slider {
|
||||
#hue-slider:deep(.v-slider__container) {
|
||||
background: linear-gradient(to right, #f00 0%, #ff0 17%, #0f0 33%, #0ff 50%, #00f 67%, #f0f 83%, #f00 100%);
|
||||
border-radius: 10px;
|
||||
/* prettier-ignore */
|
||||
box-shadow: 0 0 5px #333, inset 0 0 3px #333;
|
||||
}
|
||||
#sat-slider >>> .v-slider {
|
||||
#sat-slider:deep(.v-slider__container) {
|
||||
background: linear-gradient(to right, #fff 0%, hsl(var(--averageHue), 100%, 50%) 100%);
|
||||
border-radius: 10px;
|
||||
/* prettier-ignore */
|
||||
box-shadow: 0 0 5px #333, inset 0 0 3px #333;
|
||||
}
|
||||
#value-slider >>> .v-slider {
|
||||
#value-slider:deep(.v-slider__container) {
|
||||
background: linear-gradient(to right, #000 0%, hsl(var(--averageHue), 100%, 50%) 100%);
|
||||
border-radius: 10px;
|
||||
/* prettier-ignore */
|
||||
box-shadow: 0 0 5px #333, inset 0 0 3px #333;
|
||||
}
|
||||
>>> .v-slider__thumb {
|
||||
:deep(.v-slider__thumb) {
|
||||
outline: black solid thin;
|
||||
}
|
||||
.normal-slider >>> .v-slider__track-fill {
|
||||
.normal-slider:deep(.v-slider__track-fill) {
|
||||
outline: black solid thin;
|
||||
}
|
||||
|
||||
.inverted-slider >>> .v-slider__track-background {
|
||||
.inverted-slider:deep(.v-slider__track-background) {
|
||||
outline: black solid thin;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -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 dark 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-simple-table fixed-header height="100%" dense dark>
|
||||
<v-table fixed-header height="100%" density="compact">
|
||||
<template #default>
|
||||
<thead style="font-size: 1.25rem">
|
||||
<tr>
|
||||
@@ -47,21 +43,19 @@ const quaternionToEuler = (rot_quat: Quaternion): { x: number; y: number; z: num
|
||||
</tr>
|
||||
</tbody>
|
||||
</template>
|
||||
</v-simple-table>
|
||||
</v-table>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.v-data-table {
|
||||
.v-table {
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,16 +2,17 @@
|
||||
import { inject, ref } from "vue";
|
||||
import { useStateStore } from "@/stores/StateStore";
|
||||
import PvSelect from "@/components/common/pv-select.vue";
|
||||
import PvInput from "@/components/common/pv-input.vue";
|
||||
import axios from "axios";
|
||||
import { 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
|
||||
@@ -97,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) {
|
||||
@@ -140,10 +138,10 @@ enum ImportType {
|
||||
ApriltagFieldLayout
|
||||
}
|
||||
const showImportDialog = ref(false);
|
||||
const importType = ref<ImportType | number>(-1);
|
||||
const importType = ref<ImportType | undefined>(undefined);
|
||||
const importFile = ref<File | null>(null);
|
||||
const handleSettingsImport = () => {
|
||||
if (importType.value === -1 || importFile.value === null) return;
|
||||
if (importType.value === undefined || importFile.value === null) return;
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append("data", importFile.value);
|
||||
@@ -169,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) {
|
||||
@@ -198,7 +191,7 @@ const handleSettingsImport = () => {
|
||||
});
|
||||
|
||||
showImportDialog.value = false;
|
||||
importType.value = -1;
|
||||
importType.value = undefined;
|
||||
importFile.value = null;
|
||||
};
|
||||
|
||||
@@ -217,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) {
|
||||
@@ -237,52 +230,67 @@ const nukePhotonConfigDirectory = () => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<v-card dark 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 left 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 left 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 left 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 left 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
|
||||
v-model="showImportDialog"
|
||||
width="600"
|
||||
@input="
|
||||
@update:modelValue="
|
||||
() => {
|
||||
importType = -1;
|
||||
importType = undefined;
|
||||
importFile = null;
|
||||
}
|
||||
"
|
||||
>
|
||||
<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"
|
||||
@@ -297,32 +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"
|
||||
:disabled="importType === -1"
|
||||
:error-messages="importType === -1 ? 'Settings type not selected' : ''"
|
||||
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"
|
||||
align="center"
|
||||
>
|
||||
<v-btn color="secondary" :disabled="importFile === null" @click="handleSettingsImport">
|
||||
<v-icon left class="open-icon"> mdi-import </v-icon>
|
||||
<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 left 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
|
||||
@@ -334,8 +345,12 @@ const nukePhotonConfigDirectory = () => {
|
||||
/>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6">
|
||||
<v-btn color="secondary" @click="openExportLogsPrompt">
|
||||
<v-icon left 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 -->
|
||||
@@ -349,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 left 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 left class="open-icon"> mdi-skull-crossbones </v-icon>
|
||||
<span class="open-icon">
|
||||
{{
|
||||
$vuetify.breakpoint.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 dark 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 right 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 right 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-row class="align-center white--text">
|
||||
<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 left 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"
|
||||
@@ -401,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 + '":'"
|
||||
@@ -409,17 +429,16 @@ 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 left 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.breakpoint.mdAndUp ? "Delete everything, I have backed up what I need" : "Delete Everything"
|
||||
}}
|
||||
{{ $vuetify.display.mdAndUp ? "Delete everything, I have backed up what I need" : "Delete Everything" }}
|
||||
</span>
|
||||
</v-btn>
|
||||
</v-card-text>
|
||||
|
||||
@@ -4,18 +4,16 @@ import { useSettingsStore } from "@/stores/settings/GeneralSettingsStore";
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<v-card dark 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"
|
||||
@input="(args) => useSettingsStore().changeLEDBrightness(args)"
|
||||
@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,17 +125,17 @@ onBeforeMount(() => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<v-card dark 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-btn text @click="fetchMetrics">
|
||||
<v-icon left class="open-icon">mdi-reload</v-icon>
|
||||
<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" 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-simple-table class="metrics-table mt-3">
|
||||
<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>
|
||||
<th
|
||||
@@ -163,11 +168,11 @@ onBeforeMount(() => {
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</v-simple-table>
|
||||
</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-simple-table class="metrics-table mt-3">
|
||||
<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>
|
||||
<th
|
||||
@@ -201,7 +206,7 @@ onBeforeMount(() => {
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</v-simple-table>
|
||||
</v-table>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</template>
|
||||
@@ -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-data-table {
|
||||
thead,
|
||||
tbody {
|
||||
background-color: #006492;
|
||||
}
|
||||
|
||||
:hover {
|
||||
tbody > tr {
|
||||
background-color: #005281 !important;
|
||||
}
|
||||
}
|
||||
|
||||
.v-table {
|
||||
::-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();
|
||||
@@ -124,9 +135,14 @@ const saveGeneralSettings = () => {
|
||||
});
|
||||
};
|
||||
|
||||
const currentNetworkInterfaceIndex = computed<number>({
|
||||
get: () => useSettingsStore().networkInterfaceNames.indexOf(useSettingsStore().network.networkManagerIface || ""),
|
||||
set: (v) => (tempSettingsStruct.value.networkManagerIface = useSettingsStore().networkInterfaceNames[v])
|
||||
const currentNetworkInterfaceIndex = computed<number | undefined>({
|
||||
get: () => {
|
||||
const index = useSettingsStore().networkInterfaceNames.indexOf(
|
||||
useSettingsStore().network.networkManagerIface || ""
|
||||
);
|
||||
return index === -1 ? undefined : index;
|
||||
},
|
||||
set: (v) => v && (tempSettingsStruct.value.networkManagerIface = useSettingsStore().networkInterfaceNames[v])
|
||||
});
|
||||
|
||||
watchEffect(() => {
|
||||
@@ -136,11 +152,24 @@ watchEffect(() => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<v-card dark 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"
|
||||
@@ -154,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
|
||||
class="pt-3 pb-3"
|
||||
color="error"
|
||||
text-color="white"
|
||||
style="margin: 10px 0"
|
||||
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"
|
||||
@@ -202,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"
|
||||
@@ -225,57 +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
|
||||
class="pt-3 pb-3"
|
||||
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>
|
||||
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
|
||||
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
|
||||
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"
|
||||
color="primary"
|
||||
class="mt-3"
|
||||
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
|
||||
style="color: black; width: 100%"
|
||||
:disabled="!settingsValid || !settingsHaveChanged()"
|
||||
@click="saveGeneralSettings"
|
||||
@@ -283,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>
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user