Compare commits

..

9 Commits
Dev ... 2026

Author SHA1 Message Date
samfreund
ac8cccaa2f lint 2026-03-31 22:38:08 -05:00
samfreund
98fee3bd1f CI changes 2026-03-31 22:38:04 -05:00
samfreund
d7bac45e76 quarky, don't run away! 2026-03-31 19:27:48 -05:00
Matt Morley
9883008ed3 more generic quotes 2026-03-31 18:43:18 -05:00
Matt Morley
01b8b8ccb3 Mini-quarky spawn 2026-03-31 18:43:13 -05:00
Matt Morley
fd3d9f6ccc Mini-quarkies that bounce of edges 2026-03-31 18:43:10 -05:00
Matt Morley
d7f0e17dda Even more phrases 2026-03-31 18:43:06 -05:00
Chris Gerth
966071ae2d bad timeouts, more phrases 2026-03-31 18:43:03 -05:00
Chris Gerth
502ae644a4 look ma no inter 2026-03-31 18:42:57 -05:00
492 changed files with 9464 additions and 7668 deletions

View File

@@ -10,7 +10,7 @@ concurrency:
cancel-in-progress: true cancel-in-progress: true
env: env:
IMAGE_VERSION: v2027.0.0 IMAGE_VERSION: v2026.1.3
jobs: jobs:
@@ -21,116 +21,102 @@ jobs:
- uses: actions/checkout@v6 - uses: actions/checkout@v6
- uses: gradle/actions/wrapper-validation@v5 - uses: gradle/actions/wrapper-validation@v5
# build-examples: build-examples:
# strategy: strategy:
# fail-fast: false fail-fast: false
# matrix: matrix:
# include: include:
# - os: windows-2022 - os: windows-2022
# artifact-name: Win64 artifact-name: Win64
# - os: macos-14 - os: macos-14
# artifact-name: macOS artifact-name: macOS
# - os: ubuntu-24.04 - os: ubuntu-24.04
# artifact-name: Linux artifact-name: Linux
# name: "Photonlib - Build Examples - ${{ matrix.os }}" name: "Photonlib - Build Examples - ${{ matrix.os }}"
# runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
# needs: [build-photonlib-host, build-photonlib-docker] needs: [build-photonlib-host, build-photonlib-docker]
# steps:
# - name: Checkout code
# uses: actions/checkout@v6
# with:
# fetch-depth: 0
# - name: Fetch tags
# run: git fetch --tags --force
# - uses: actions/setup-java@v5
# with:
# java-version: 25
# distribution: temurin
# - name: Install SystemCore Toolchain
# run: ./gradlew installSystemCoreToolchain
# - name: Delete duplicate toolchains
# run: |
# find ~/.gradle/cache/ -name *bookworm* -exec rm -rf {} +
# du -h . | sort -h
# if: matrix.os == 'ubuntu-24.04'
# # Download prebuilt photonlib artifacts
# - uses: actions/download-artifact@v7
# with:
# name: maven-${{ matrix.artifact-name }}
# - uses: actions/download-artifact@v7
# with:
# name: maven-SystemCore
# - name: Move to maven local
# run: |
# mkdir -p ~/.m2/repository/
# mv maven/org ~/.m2/repository/
# - name: Copy vendordeps
# shell: bash
# run: |
# for vendordep_folder in photonlib-*-examples/*/; do
# # Remove trailing slash for cross-platform compatibility
# vendordep_folder="${vendordep_folder%/}"
# # Filter for projects only
# if [ -e "$vendordep_folder/build.gradle" ]; then
# mkdir -p "$vendordep_folder/vendordeps/"
# cp vendordeps/photonlib-json-1.0.json "$vendordep_folder/vendordeps/"
# fi
# done
# - name: Build Java examples
# working-directory: photonlib-java-examples
# run: |
# ./gradlew build
# ./gradlew clean
# - name: Build C++ examples
# working-directory: photonlib-cpp-examples
# run: |
# ./gradlew build
# ./gradlew clean
typecheck-client:
name: "Typecheck Client"
runs-on: ubuntu-24.04
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@v6 uses: actions/checkout@v6
- name: Install pnpm
uses: pnpm/action-setup@v5
with: with:
version: 11 fetch-depth: 0
- name: Setup Node.js - name: Fetch tags
uses: actions/setup-node@v6 run: git fetch --tags --force
- name: Install Java 17
uses: actions/setup-java@v5
with: with:
node-version: 24 java-version: 17
cache: pnpm distribution: temurin
cache-dependency-path: photon-client/pnpm-lock.yaml - name: Install RoboRIO Toolchain
- name: Typecheck Client run: ./gradlew installRoboRioToolchain
working-directory: photon-client - name: Delete duplicate toolchains
run: | run: |
pnpm install --frozen-lockfile find ~/.gradle/cache/ -name *roborio-academic* -exec rm -rf {} +
pnpm type-check du -h . | sort -h
if: matrix.os == 'ubuntu-24.04'
# Download prebuilt photonlib artifacts
- uses: actions/download-artifact@v7
with:
name: maven-${{ matrix.artifact-name }}
- uses: actions/download-artifact@v7
with:
name: maven-Athena
- name: Move to maven local
run: |
mkdir -p ~/.m2/repository/
mv maven/org ~/.m2/repository/
- name: Copy vendordeps
shell: bash
run: |
for vendordep_folder in photonlib-*-examples/*/; do
# Remove trailing slash for cross-platform compatibility
vendordep_folder="${vendordep_folder%/}"
# Filter for projects only
if [ -e "$vendordep_folder/build.gradle" ]; then
mkdir -p "$vendordep_folder/vendordeps/"
cp vendordeps/photonlib-json-1.0.json "$vendordep_folder/vendordeps/"
fi
done
- name: Build Java examples
working-directory: photonlib-java-examples
run: |
./gradlew build
./gradlew clean
- name: Build C++ examples
working-directory: photonlib-cpp-examples
run: |
./gradlew build
./gradlew clean
playwright-tests: playwright-tests:
name: "Playwright E2E tests" name: "Playwright E2E tests"
runs-on: ubuntu-24.04 runs-on: ubuntu-24.04
needs: [validation] needs: [validation]
steps: steps:
# Checkout code.
- name: Checkout code - name: Checkout code
uses: actions/checkout@v6 uses: actions/checkout@v6
- uses: actions/setup-java@v5
with: with:
java-version: 25 fetch-depth: 0
- name: Fetch tags
run: git fetch --tags --force
- name: Install Java 17
uses: actions/setup-java@v5
with:
java-version: 17
distribution: temurin distribution: temurin
- uses: pnpm/action-setup@v5 - name: Install pnpm
uses: pnpm/action-setup@v4
with: with:
version: 11 version: 10
- uses: actions/setup-node@v6 - name: Setup Node.js
uses: actions/setup-node@v6
with: with:
cache: pnpm node-version: 22
cache-dependency-path: photon-client/pnpm-lock.yaml
node-version: 24
- name: Setup tests - name: Setup tests
working-directory: photon-client working-directory: photon-client
run: | run: |
@@ -141,10 +127,10 @@ jobs:
- name: Run Playwright tests - name: Run Playwright tests
working-directory: photon-client working-directory: photon-client
run: pnpm test run: pnpm test
- uses: actions/upload-artifact@v7 - uses: actions/upload-artifact@v6
if: ${{ !cancelled() }} if: ${{ !cancelled() }}
with: with:
name: "Playwright Report" name: playwright-report
path: photon-client/playwright-report/ path: photon-client/playwright-report/
retention-days: 30 retention-days: 30
build-gradle: build-gradle:
@@ -152,24 +138,26 @@ jobs:
runs-on: ubuntu-24.04 runs-on: ubuntu-24.04
needs: [validation] needs: [validation]
steps: steps:
# Checkout code.
- name: Checkout code - name: Checkout code
uses: actions/checkout@v6 uses: actions/checkout@v6
with: with:
fetch-depth: 0 fetch-depth: 0
- name: Fetch tags - name: Fetch tags
run: git fetch --tags --force run: git fetch --tags --force
- uses: actions/setup-java@v5 - name: Install Java 17
uses: actions/setup-java@v5
with: with:
java-version: 25 java-version: 17
distribution: temurin distribution: temurin
- uses: pnpm/action-setup@v5 - name: Install pnpm
uses: pnpm/action-setup@v4
with: with:
version: 11 version: 10
- uses: actions/setup-node@v6 - name: Setup Node.js
uses: actions/setup-node@v6
with: with:
cache: pnpm node-version: 22
cache-dependency-path: photon-client/pnpm-lock.yaml
node-version: 24
- name: Gradle Build - name: Gradle Build
run: ./gradlew photon-targeting:build photon-core:build photon-server:build -x check run: ./gradlew photon-targeting:build photon-core:build photon-server:build -x check
- name: Gradle Tests and Coverage - name: Gradle Tests and Coverage
@@ -196,7 +184,7 @@ jobs:
working-directory: docs working-directory: docs
run: | run: |
make html make html
- uses: actions/upload-artifact@v7 - uses: actions/upload-artifact@v6
with: with:
name: built-docs name: built-docs
path: docs/build/html path: docs/build/html
@@ -210,9 +198,10 @@ jobs:
with: with:
fetch-depth: 0 fetch-depth: 0
- uses: actions/setup-java@v5 - name: Install Java 17
uses: actions/setup-java@v5
with: with:
java-version: 25 java-version: 17
distribution: temurin distribution: temurin
# grab all tags # grab all tags
@@ -225,9 +214,9 @@ jobs:
mv photon-lib/build/generated/vendordeps/photonlib.json photon-lib/build/generated/vendordeps/photonlib-$(git describe --tags --match=v*).json mv photon-lib/build/generated/vendordeps/photonlib.json photon-lib/build/generated/vendordeps/photonlib-$(git describe --tags --match=v*).json
# Upload it here so it shows up in releases # Upload it here so it shows up in releases
- uses: actions/upload-artifact@v7 - uses: actions/upload-artifact@v6
with: with:
archive: false name: photonlib-vendor-json
path: photon-lib/build/generated/vendordeps/photonlib-*.json path: photon-lib/build/generated/vendordeps/photonlib-*.json
build-photonlib-host: build-photonlib-host:
@@ -237,12 +226,12 @@ jobs:
fail-fast: false fail-fast: false
matrix: matrix:
include: include:
- os: windows-2022 - os: windows-2022
artifact-name: Win64 artifact-name: Win64
- os: macos-26 - os: macos-26
artifact-name: macOS artifact-name: macOS
- os: ubuntu-24.04 - os: ubuntu-24.04
artifact-name: Linux artifact-name: Linux
name: "Photonlib - Build Host - ${{ matrix.artifact-name }}" name: "Photonlib - Build Host - ${{ matrix.artifact-name }}"
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
@@ -251,21 +240,22 @@ jobs:
- uses: actions/checkout@v6 - uses: actions/checkout@v6
with: with:
fetch-depth: 0 fetch-depth: 0
- uses: actions/setup-java@v5 - name: Install Java 17
uses: actions/setup-java@v5
with: with:
java-version: 25 java-version: 17
distribution: temurin distribution: temurin
- run: git fetch --tags --force - run: git fetch --tags --force
- run: ./gradlew photon-targeting:build photon-lib:build - run: ./gradlew photon-targeting:build photon-lib:build
name: Build with Gradle name: Build with Gradle
- run: ./gradlew photon-lib:publish photon-targeting:publish # - run: ./gradlew photon-lib:publish photon-targeting:publish
name: Publish # name: Publish
env: # env:
ARTIFACTORY_API_KEY: ${{ secrets.ARTIFACTORY_API_KEY }} # ARTIFACTORY_API_KEY: ${{ secrets.ARTIFACTORY_API_KEY }}
if: github.event_name == 'push' && github.repository_owner == 'photonvision' && (github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/v')) # if: github.event_name == 'push' && github.repository_owner == 'photonvision'
# Copy artifacts to build/outputs/maven # Copy artifacts to build/outputs/maven
- run: ./gradlew photon-lib:publish photon-targeting:publish -PcopyOfflineArtifacts - run: ./gradlew photon-lib:publish photon-targeting:publish -PcopyOfflineArtifacts
- uses: actions/upload-artifact@v7 - uses: actions/upload-artifact@v6
with: with:
name: maven-${{ matrix.artifact-name }} name: maven-${{ matrix.artifact-name }}
path: build/outputs path: build/outputs
@@ -275,13 +265,13 @@ jobs:
fail-fast: false fail-fast: false
matrix: matrix:
include: include:
- container: wpilib/systemcore-cross-ubuntu:2027-24.04 - container: wpilib/roborio-cross-ubuntu:2025-24.04
artifact-name: SystemCore artifact-name: Athena
build-options: "-Ponlylinuxsystemcore" build-options: "-Ponlylinuxathena"
- container: wpilib/raspbian-cross-ubuntu:2027-bookworm-24.04 - container: wpilib/raspbian-cross-ubuntu:bullseye-22.04
artifact-name: Raspbian artifact-name: Raspbian
build-options: "-Ponlylinuxarm32" build-options: "-Ponlylinuxarm32"
- container: wpilib/aarch64-cross-ubuntu:2027-bookworm-24.04 - container: wpilib/aarch64-cross-ubuntu:bullseye-22.04
artifact-name: Aarch64 artifact-name: Aarch64
build-options: "-Ponlylinuxarm64" build-options: "-Ponlylinuxarm64"
@@ -299,14 +289,14 @@ jobs:
- name: Build PhotonLib - name: Build PhotonLib
# We don't need to run tests, since we specify only non-native platforms # 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 }} -x test run: ./gradlew photon-targeting:build photon-lib:build ${{ matrix.build-options }} -x test
- name: Publish # - name: Publish
run: ./gradlew photon-lib:publish photon-targeting:publish ${{ matrix.build-options }} # run: ./gradlew photon-lib:publish photon-targeting:publish ${{ matrix.build-options }}
env: # env:
ARTIFACTORY_API_KEY: ${{ secrets.ARTIFACTORY_API_KEY }} # ARTIFACTORY_API_KEY: ${{ secrets.ARTIFACTORY_API_KEY }}
if: github.event_name == 'push' && github.repository_owner == 'photonvision' && (github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/v')) # if: github.event_name == 'push' && github.repository_owner == 'photonvision'
# Copy artifacts to build/outputs/maven # Copy artifacts to build/outputs/maven
- run: ./gradlew photon-lib:publish photon-targeting:publish -PcopyOfflineArtifacts ${{ matrix.build-options }} - run: ./gradlew photon-lib:publish photon-targeting:publish -PcopyOfflineArtifacts ${{ matrix.build-options }}
- uses: actions/upload-artifact@v7 - uses: actions/upload-artifact@v6
with: with:
name: maven-${{ matrix.artifact-name }} name: maven-${{ matrix.artifact-name }}
path: build/outputs path: build/outputs
@@ -321,7 +311,7 @@ jobs:
fetch-depth: 0 fetch-depth: 0
- run: git fetch --tags --force - run: git fetch --tags --force
# download all maven-* artifacts to outputs/ # download all maven-* artifacts to outputs/
- uses: actions/download-artifact@v8 - uses: actions/download-artifact@v7
with: with:
merge-multiple: true merge-multiple: true
path: output path: output
@@ -331,7 +321,7 @@ jobs:
name: ZIP stuff up name: ZIP stuff up
working-directory: output working-directory: output
- run: ls output - run: ls output
- uses: actions/upload-artifact@v7 - uses: actions/upload-artifact@v6
with: with:
name: photonlib-offline name: photonlib-offline
path: output/*.zip path: output/*.zip
@@ -345,7 +335,7 @@ jobs:
include: include:
- os: ubuntu-24.04 - os: ubuntu-24.04
artifact-name: Linux artifact-name: Linux
arch-override: linuxx86-64 arch-override: linuxx64
- os: ubuntu-24.04 - os: ubuntu-24.04
artifact-name: LinuxArm64 artifact-name: LinuxArm64
arch-override: linuxarm64 arch-override: linuxarm64
@@ -357,22 +347,25 @@ jobs:
- uses: actions/checkout@v6 - uses: actions/checkout@v6
with: with:
fetch-depth: 0 fetch-depth: 0
- uses: actions/setup-java@v5 - name: Install Java 17
uses: actions/setup-java@v5
with: with:
java-version: 25 java-version: 17
distribution: temurin distribution: temurin
- uses: pnpm/action-setup@v5 - name: Install pnpm
uses: pnpm/action-setup@v4
with: with:
version: 11 version: 10
- uses: actions/setup-node@v6 - name: Setup Node.js
uses: actions/setup-node@v6
with: with:
node-version: 24 node-version: 22
cache: pnpm cache: pnpm
cache-dependency-path: photon-client/pnpm-lock.yaml cache-dependency-path: photon-client/pnpm-lock.yaml
- name: Install Arm64 Toolchain - name: Install Arm64 Toolchain
run: ./gradlew installArm64Toolchain run: ./gradlew installArm64Toolchain
if: ${{ (matrix.artifact-name) == 'LinuxArm64' }} if: ${{ (matrix.artifact-name) == 'LinuxArm64' }}
- uses: actions/download-artifact@v8 - uses: actions/download-artifact@v7
with: with:
name: built-docs name: built-docs
path: photon-server/src/main/resources/web/docs path: photon-server/src/main/resources/web/docs
@@ -380,11 +373,11 @@ jobs:
if: ${{ (matrix.arch-override != 'none') }} if: ${{ (matrix.arch-override != 'none') }}
- run: ./gradlew photon-server:shadowJar - run: ./gradlew photon-server:shadowJar
if: ${{ (matrix.arch-override == 'none') }} if: ${{ (matrix.arch-override == 'none') }}
- uses: actions/upload-artifact@v7 - uses: actions/upload-artifact@v6
with: with:
archive: false name: jar-${{ matrix.artifact-name }}
path: photon-server/build/libs path: photon-server/build/libs
- uses: actions/upload-artifact@v7 - uses: actions/upload-artifact@v6
with: with:
name: photon-targeting_jar-${{ matrix.artifact-name }} name: photon-targeting_jar-${{ matrix.artifact-name }}
path: photon-targeting/build/libs path: photon-targeting/build/libs
@@ -401,7 +394,7 @@ jobs:
arch-override: macarm64 arch-override: macarm64
- os: macos-latest - os: macos-latest
artifact-name: macOS artifact-name: macOS
arch-override: macx86-64 arch-override: macx64
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
name: "Build fat JAR - ${{ matrix.artifact-name }}" name: "Build fat JAR - ${{ matrix.artifact-name }}"
@@ -415,9 +408,9 @@ jobs:
fail-fast: false fail-fast: false
matrix: matrix:
include: include:
- os: windows-2022 - os: windows-latest
artifact-name: Win artifact-name: Win64
arch-override: winx86-64 arch-override: winx64
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
name: "Build fat JAR - ${{ matrix.artifact-name }}" name: "Build fat JAR - ${{ matrix.artifact-name }}"
@@ -432,26 +425,24 @@ jobs:
matrix: matrix:
include: include:
- os: ubuntu-24.04 - os: ubuntu-24.04
artifact-name: photonvision-*-linuxx86-64.jar artifact-name: jar-Linux
extraOpts: -Djdk.lang.Process.launchMechanism=vfork extraOpts: -Djdk.lang.Process.launchMechanism=vfork
- os: windows-2022 - os: windows-latest
artifact-name: photonvision-*-winx86-64.jar artifact-name: jar-Win64
- os: macos-latest - os: macos-latest
artifact-name: photonvision-*-macarm64.jar artifact-name: jar-macOS
- os: ubuntu-24.04-arm
artifact-name: photonvision-*-linuxarm64.jar
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
steps: steps:
- uses: actions/setup-java@v5 - name: Install Java 17
uses: actions/setup-java@v5
with: with:
java-version: 25 java-version: 17
distribution: temurin distribution: temurin
- uses: actions/download-artifact@v8 - uses: actions/download-artifact@v7
with: with:
pattern: ${{ matrix.artifact-name }} name: ${{ matrix.artifact-name }}
# The jar is run twice to exercise different code paths. # The jar is run twice to exercise different code paths.
- run: | - run: |
echo "=== First run ===" echo "=== First run ==="
@@ -474,11 +465,11 @@ jobs:
fi fi
echo "=== Second run ===" echo "=== Second run ==="
java -jar ${{ matrix.extraOpts }} *.jar --smoketest java -jar ${{ matrix.extraOpts }} *.jar --smoketest
if: ${{ (matrix.os) != 'windows-2022' }} if: ${{ (matrix.os) != 'windows-latest' }}
- run: | - run: |
ls *.jar | %{ Write-Host "Running $($_.Name)"; Start-Process "java" -ArgumentList "-jar `"$($_.FullName)`" --smoketest" -NoNewWindow -Wait; break } ls *.jar | %{ Write-Host "Running $($_.Name)"; Start-Process "java" -ArgumentList "-jar `"$($_.FullName)`" --smoketest" -NoNewWindow -Wait; break }
ls *.jar | %{ Write-Host "Running $($_.Name)"; Start-Process "java" -ArgumentList "-jar `"$($_.FullName)`" --smoketest" -NoNewWindow -Wait; break } ls *.jar | %{ Write-Host "Running $($_.Name)"; Start-Process "java" -ArgumentList "-jar `"$($_.FullName)`" --smoketest" -NoNewWindow -Wait; break }
if: ${{ (matrix.os) == 'windows-2022' }} if: ${{ (matrix.os) == 'windows-latest' }}
build-image: build-image:
needs: [build-package-linux] needs: [build-package-linux]
@@ -488,68 +479,86 @@ jobs:
matrix: matrix:
include: include:
- os: ubuntu-24.04-arm - os: ubuntu-24.04-arm
artifact-name: LinuxArm64
image_suffix: RaspberryPi image_suffix: RaspberryPi
plat_override: LINUX_RASPBIAN64 plat_override: LINUX_RASPBIAN64
image_url: https://github.com/PhotonVision/photon-image-modifier/releases/download/$IMAGE_VERSION/photonvision_raspi.img.xz image_url: https://github.com/PhotonVision/photon-image-modifier/releases/download/$IMAGE_VERSION/photonvision_raspi.img.xz
minimum_free_mb: 100 minimum_free_mb: 100
- os: ubuntu-24.04-arm - os: ubuntu-24.04-arm
artifact-name: LinuxArm64
image_suffix: limelight2 image_suffix: limelight2
plat_override: LINUX_RASPBIAN64 plat_override: LINUX_RASPBIAN64
image_url: https://github.com/PhotonVision/photon-image-modifier/releases/download/$IMAGE_VERSION/photonvision_limelight.img.xz image_url: https://github.com/PhotonVision/photon-image-modifier/releases/download/$IMAGE_VERSION/photonvision_limelight.img.xz
minimum_free_mb: 100 minimum_free_mb: 100
- os: ubuntu-24.04-arm - os: ubuntu-24.04-arm
artifact-name: LinuxArm64
image_suffix: limelight3 image_suffix: limelight3
plat_override: LINUX_RASPBIAN64 plat_override: LINUX_RASPBIAN64
image_url: https://github.com/PhotonVision/photon-image-modifier/releases/download/$IMAGE_VERSION/photonvision_limelight3.img.xz image_url: https://github.com/PhotonVision/photon-image-modifier/releases/download/$IMAGE_VERSION/photonvision_limelight3.img.xz
minimum_free_mb: 100 minimum_free_mb: 100
- os: ubuntu-24.04-arm - os: ubuntu-24.04-arm
artifact-name: LinuxArm64
image_suffix: limelight3G image_suffix: limelight3G
plat_override: LINUX_RASPBIAN64 plat_override: LINUX_RASPBIAN64
image_url: https://github.com/PhotonVision/photon-image-modifier/releases/download/$IMAGE_VERSION/photonvision_limelight3g.img.xz image_url: https://github.com/PhotonVision/photon-image-modifier/releases/download/$IMAGE_VERSION/photonvision_limelight3g.img.xz
minimum_free_mb: 100 minimum_free_mb: 100
- os: ubuntu-24.04-arm - os: ubuntu-24.04-arm
artifact-name: LinuxArm64
image_suffix: limelight4 image_suffix: limelight4
plat_override: LINUX_RASPBIAN64 plat_override: LINUX_RASPBIAN64
image_url: https://github.com/PhotonVision/photon-image-modifier/releases/download/$IMAGE_VERSION/photonvision_limelight4.img.xz image_url: https://github.com/PhotonVision/photon-image-modifier/releases/download/$IMAGE_VERSION/photonvision_limelight4.img.xz
minimum_free_mb: 100 minimum_free_mb: 100
- os: ubuntu-24.04-arm - os: ubuntu-24.04-arm
artifact-name: LinuxArm64
image_suffix: luma_p1 image_suffix: luma_p1
plat_override: LINUX_RASPBIAN64 plat_override: LINUX_RASPBIAN64
image_url: https://github.com/PhotonVision/photon-image-modifier/releases/download/$IMAGE_VERSION/photonvision_luma_p1.img.xz image_url: https://github.com/PhotonVision/photon-image-modifier/releases/download/$IMAGE_VERSION/photonvision_luma_p1.img.xz
minimum_free_mb: 100 minimum_free_mb: 100
- os: ubuntu-24.04-arm - os: ubuntu-24.04-arm
artifact-name: LinuxArm64
image_suffix: orangepi5 image_suffix: orangepi5
plat_override: LINUX_RK3588_64 plat_override: LINUX_RK3588_64
image_url: https://github.com/PhotonVision/photon-image-modifier/releases/download/$IMAGE_VERSION/photonvision_opi5.img.xz image_url: https://github.com/PhotonVision/photon-image-modifier/releases/download/$IMAGE_VERSION/photonvision_opi5.img.xz
minimum_free_mb: 1024 minimum_free_mb: 1024
- os: ubuntu-24.04-arm - os: ubuntu-24.04-arm
artifact-name: LinuxArm64
image_suffix: orangepi5b image_suffix: orangepi5b
plat_override: LINUX_RK3588_64 plat_override: LINUX_RK3588_64
image_url: https://github.com/PhotonVision/photon-image-modifier/releases/download/$IMAGE_VERSION/photonvision_opi5b.img.xz image_url: https://github.com/PhotonVision/photon-image-modifier/releases/download/$IMAGE_VERSION/photonvision_opi5b.img.xz
minimum_free_mb: 1024 minimum_free_mb: 1024
- os: ubuntu-24.04-arm - os: ubuntu-24.04-arm
artifact-name: LinuxArm64
image_suffix: orangepi5plus image_suffix: orangepi5plus
plat_override: LINUX_RK3588_64 plat_override: LINUX_RK3588_64
image_url: https://github.com/PhotonVision/photon-image-modifier/releases/download/$IMAGE_VERSION/photonvision_opi5plus.img.xz image_url: https://github.com/PhotonVision/photon-image-modifier/releases/download/$IMAGE_VERSION/photonvision_opi5plus.img.xz
minimum_free_mb: 1024 minimum_free_mb: 1024
- os: ubuntu-24.04-arm - os: ubuntu-24.04-arm
artifact-name: LinuxArm64
image_suffix: orangepi5pro image_suffix: orangepi5pro
plat_override: LINUX_RK3588_64 plat_override: LINUX_RK3588_64
image_url: https://github.com/PhotonVision/photon-image-modifier/releases/download/$IMAGE_VERSION/photonvision_opi5pro.img.xz image_url: https://github.com/PhotonVision/photon-image-modifier/releases/download/$IMAGE_VERSION/photonvision_opi5pro.img.xz
minimum_free_mb: 1024 minimum_free_mb: 1024
- os: ubuntu-24.04-arm - os: ubuntu-24.04-arm
artifact-name: LinuxArm64
image_suffix: orangepi5max image_suffix: orangepi5max
plat_override: LINUX_RK3588_64 plat_override: LINUX_RK3588_64
image_url: https://github.com/PhotonVision/photon-image-modifier/releases/download/$IMAGE_VERSION/photonvision_opi5max.img.xz image_url: https://github.com/PhotonVision/photon-image-modifier/releases/download/$IMAGE_VERSION/photonvision_opi5max.img.xz
minimum_free_mb: 1024 minimum_free_mb: 1024
- os: ubuntu-24.04-arm - os: ubuntu-24.04-arm
artifact-name: LinuxArm64
image_suffix: rock5c image_suffix: rock5c
plat_override: LINUX_RK3588_64 plat_override: LINUX_RK3588_64
image_url: https://github.com/PhotonVision/photon-image-modifier/releases/download/$IMAGE_VERSION/photonvision_rock5c.img.xz image_url: https://github.com/PhotonVision/photon-image-modifier/releases/download/$IMAGE_VERSION/photonvision_rock5c.img.xz
minimum_free_mb: 1024 minimum_free_mb: 1024
- os: ubuntu-24.04-arm - os: ubuntu-24.04-arm
artifact-name: LinuxArm64
image_suffix: orangepi6plus
plat_override: LINUX_AARCH64
image_url: https://github.com/PhotonVision/photon-image-modifier/releases/download/$IMAGE_VERSION/photonvision_opi6plus.img.xz
minimum_free_mb: 1024
- os: ubuntu-24.04-arm
artifact-name: LinuxArm64
image_suffix: rubikpi3 image_suffix: rubikpi3
cache: 'yes'
plat_override: LINUX_QCS6490 plat_override: LINUX_QCS6490
image_url: https://github.com/PhotonVision/photon-image-modifier/releases/download/$IMAGE_VERSION/photonvision_rubikpi3.tar.xz image_url: https://github.com/PhotonVision/photon-image-modifier/releases/download/$IMAGE_VERSION/photonvision_rubikpi3.tar.xz
minimum_free_mb: 1024 minimum_free_mb: 1024
@@ -564,15 +573,14 @@ jobs:
uses: actions/checkout@v6 uses: actions/checkout@v6
with: with:
fetch-depth: 0 fetch-depth: 0
- uses: actions/download-artifact@v8 - uses: actions/download-artifact@v7
with: with:
pattern: photonvision-*-linuxarm64.jar name: jar-${{ matrix.artifact-name }}
- uses: photonvision/photon-image-runner@v2.0.0 - uses: photonvision/photon-image-runner@HEAD
name: Generate image name: Generate image
id: generate_image id: generate_image
with: with:
image_url: ${{ matrix.image_url }} image_url: ${{ matrix.image_url }}
use_cache: ${{ matrix.cache || 'no' }}
minimum_free_mb: ${{ matrix.minimum_free_mb }} minimum_free_mb: ${{ matrix.minimum_free_mb }}
root_location: ${{ matrix.root_location || 'partition=2' }} root_location: ${{ matrix.root_location || 'partition=2' }}
shrink_image: ${{ matrix.shrink_image || 'yes' }} shrink_image: ${{ matrix.shrink_image || 'yes' }}
@@ -603,9 +611,10 @@ jobs:
# Point smoketest to the old image # Point smoketest to the old image
echo "smoketest_image_loc=${{ steps.generate_image.outputs.image }}" >> $GITHUB_ENV echo "smoketest_image_loc=${{ steps.generate_image.outputs.image }}" >> $GITHUB_ENV
- uses: actions/upload-artifact@v7 - uses: actions/upload-artifact@v6
name: Upload image
with: with:
archive: false name: image-${{ matrix.image_suffix }}
path: photonvision*.xz path: photonvision*.xz
# This is done after uploading the image to avoid contaminating the image with logs, caches, etc. # This is done after uploading the image to avoid contaminating the image with logs, caches, etc.
@@ -634,32 +643,30 @@ jobs:
runs-on: ubuntu-24.04 runs-on: ubuntu-24.04
steps: steps:
# Download all fat JARs # Download all fat JARs
- uses: actions/download-artifact@v8 - uses: actions/download-artifact@v7
with: with:
merge-multiple: true merge-multiple: true
pattern: photonvision-*.jar pattern: jar-*
# Download offline photonlib # Download offline photonlib
- uses: actions/download-artifact@v8 - uses: actions/download-artifact@v7
with: with:
merge-multiple: true merge-multiple: true
pattern: photonlib-offline pattern: photonlib-offline
# Download vendor json # Download vendor json
- uses: actions/download-artifact@v8 - uses: actions/download-artifact@v7
with:
pattern: photonlib-*.json
# Download all images
- uses: actions/download-artifact@v8
with: with:
merge-multiple: true merge-multiple: true
pattern: photonvision-*.xz pattern: photonlib-vendor-json
# Download all images
- uses: actions/download-artifact@v7
with:
merge-multiple: true
pattern: image-*
- run: find - run: find
# Push to dev release
- uses: pyTooling/Actions/releaser@r6 - uses: pyTooling/Actions/releaser@r6
with: with:
token: ${{ secrets.GITHUB_TOKEN }} token: ${{ secrets.GITHUB_TOKEN }}
tag: 'Dev'
rm: true
snapshots: false snapshots: false
files: | files: |
**/*.xz **/*.xz
@@ -667,13 +674,13 @@ jobs:
**/*win*.jar **/*win*.jar
**/photonlib*.json **/photonlib*.json
**/photonlib*.zip **/photonlib*.zip
if: github.event_name == 'push' if: startsWith(github.ref, 'refs/tags/v')
- name: Create Vendor JSON Repo PR # - name: Create Vendor JSON Repo PR
uses: wpilibsuite/vendor-json-repo/.github/actions/add_vendordep@HEAD # uses: wpilibsuite/vendor-json-repo/.github/actions/add_vendordep@HEAD
with: # with:
repo: PhotonVision/vendor-json-repo # repo: PhotonVision/vendor-json-repo
token: ${{ secrets.VENDOR_JSON_REPO_PUSH_TOKEN }} # token: ${{ secrets.VENDOR_JSON_REPO_PUSH_TOKEN }}
vendordep_file: ${{ github.workspace }}/photonlib-${{ github.ref_name }}.json # vendordep_file: ${{ github.workspace }}/photonlib-${{ github.ref_name }}.json
pr_title: Update photonlib to ${{ github.ref_name }} # pr_title: Update photonlib to ${{ github.ref_name }}
pr_branch: photonlib-${{ github.ref_name }} # pr_branch: photonlib-${{ github.ref_name }}
if: github.repository == 'PhotonVision/photonvision' && startsWith(github.ref, 'refs/tags/v') # if: github.repository == 'PhotonVision/photonvision' && startsWith(github.ref, 'refs/tags/v')

View File

@@ -13,9 +13,10 @@ jobs:
steps: steps:
- name: Checkout sources - name: Checkout sources
uses: actions/checkout@v6 uses: actions/checkout@v6
- uses: actions/setup-java@v5 - name: Setup Java
uses: actions/setup-java@v5
with: with:
distribution: 'temurin' distribution: 'temurin'
java-version: 25 java-version: 17
- name: Generate and submit dependency graph - name: Generate and submit dependency graph
uses: gradle/actions/dependency-submission@v5 uses: gradle/actions/dependency-submission@v5

View File

@@ -46,9 +46,9 @@ jobs:
- name: Generate diff - name: Generate diff
run: git diff HEAD > wpiformat-fixes.patch run: git diff HEAD > wpiformat-fixes.patch
if: ${{ failure() }} if: ${{ failure() }}
- uses: actions/upload-artifact@v7 - uses: actions/upload-artifact@v6
with: with:
archive: false name: wpiformat fixes
path: wpiformat-fixes.patch path: wpiformat-fixes.patch
if: ${{ failure() }} if: ${{ failure() }}
javaformat: javaformat:
@@ -61,7 +61,7 @@ jobs:
fetch-depth: 0 fetch-depth: 0
- uses: actions/setup-java@v5 - uses: actions/setup-java@v5
with: with:
java-version: 25 java-version: 17
distribution: temurin distribution: temurin
- run: | - run: |
set +e set +e
@@ -81,12 +81,13 @@ jobs:
steps: steps:
- uses: actions/checkout@v6 - uses: actions/checkout@v6
- name: Install pnpm - name: Install pnpm
uses: pnpm/action-setup@v5 uses: pnpm/action-setup@v4
with: with:
version: 10 version: 10
- uses: actions/setup-node@v6 - name: Setup Node.js
uses: actions/setup-node@v6
with: with:
node-version: 24 node-version: 22
cache: pnpm cache: pnpm
cache-dependency-path: photon-client/pnpm-lock.yaml cache-dependency-path: photon-client/pnpm-lock.yaml
- name: Install Dependencies - name: Install Dependencies

View File

@@ -30,19 +30,21 @@ jobs:
runs-on: ubuntu-24.04 runs-on: ubuntu-24.04
steps: steps:
- uses: actions/checkout@v6 - uses: actions/checkout@v6
- uses: pnpm/action-setup@v5 - name: Install pnpm
uses: pnpm/action-setup@v4
with: with:
version: 10 version: 10
- uses: actions/setup-node@v6 - name: Setup Node.js
uses: actions/setup-node@v6
with: with:
node-version: 24 node-version: 22
cache: pnpm cache: pnpm
cache-dependency-path: photon-client/pnpm-lock.yaml cache-dependency-path: photon-client/pnpm-lock.yaml
- name: Install Dependencies - name: Install Dependencies
run: pnpm i --frozen-lockfile run: pnpm i --frozen-lockfile
- name: Build Production Client - name: Build Production Client
run: pnpm run build-demo run: pnpm run build-demo
- uses: actions/upload-artifact@v7 - uses: actions/upload-artifact@v6
with: with:
name: built-demo name: built-demo
path: photon-client/dist/ path: photon-client/dist/
@@ -58,15 +60,16 @@ jobs:
fetch-depth: 0 fetch-depth: 0
- name: Fetch tags - name: Fetch tags
run: git fetch --tags --force run: git fetch --tags --force
- uses: actions/setup-java@v5 - name: Install Java 17
uses: actions/setup-java@v5
with: with:
java-version: 25 java-version: 17
distribution: temurin distribution: temurin
- name: Build javadocs/doxygen - name: Build javadocs/doxygen
run: | run: |
chmod +x gradlew chmod +x gradlew
./gradlew photon-docs:generateJavaDocs photon-docs:doxygen ./gradlew photon-docs:generateJavaDocs photon-docs:doxygen
- uses: actions/upload-artifact@v7 - uses: actions/upload-artifact@v6
with: with:
name: docs-java-cpp name: docs-java-cpp
path: photon-docs/build/docs path: photon-docs/build/docs
@@ -77,38 +80,37 @@ jobs:
runs-on: ubuntu-24.04 runs-on: ubuntu-24.04
steps: steps:
# Download docs artifact # Download docs artifact
- uses: actions/download-artifact@v8 - uses: actions/download-artifact@v7
with: with:
pattern: docs-* pattern: docs-*
- run: find . - run: find .
- name: Publish Docs To Development # - name: Publish Docs To Development
if: github.ref == 'refs/heads/main' # if: github.ref == 'refs/heads/main'
uses: up9cloud/action-rsync@v1.4 # uses: up9cloud/action-rsync@v1.4
env: # env:
HOST: ${{ secrets.WEBMASTER_SSH_HOST }} # HOST: ${{ secrets.WEBMASTER_SSH_HOST }}
USER: ${{ secrets.WEBMASTER_SSH_USERNAME }} # USER: ${{ secrets.WEBMASTER_SSH_USERNAME }}
KEY: ${{secrets.WEBMASTER_SSH_KEY}} # KEY: ${{secrets.WEBMASTER_SSH_KEY}}
TARGET: /var/www/html/photonvision-docs/development/ # TARGET: /var/www/html/photonvision-docs/development/
- name: Publish Docs To Release # - name: Publish Docs To Release
if: startsWith(github.ref, 'refs/tags/v') # if: startsWith(github.ref, 'refs/tags/v')
uses: up9cloud/action-rsync@v1.4 # uses: up9cloud/action-rsync@v1.4
env: # env:
HOST: ${{ secrets.WEBMASTER_SSH_HOST }} # HOST: ${{ secrets.WEBMASTER_SSH_HOST }}
USER: ${{ secrets.WEBMASTER_SSH_USERNAME }} # USER: ${{ secrets.WEBMASTER_SSH_USERNAME }}
KEY: ${{ secrets.WEBMASTER_SSH_KEY }} # KEY: ${{ secrets.WEBMASTER_SSH_KEY }}
TARGET: /var/www/html/photonvision-docs/release/ # TARGET: /var/www/html/photonvision-docs/release/
publish_demo: publish_demo:
name: Publish PhotonClient Demo name: Publish PhotonClient Demo
needs: [build_demo] needs: [build_demo]
runs-on: ubuntu-24.04 runs-on: ubuntu-24.04
steps: steps:
- uses: actions/download-artifact@v8 - uses: actions/download-artifact@v7
with: with:
name: built-demo name: built-demo
- run: find . - run: find .
- name: Publish demo - name: Publish demo
if: github.ref == 'refs/heads/main'
uses: up9cloud/action-rsync@v1.4 uses: up9cloud/action-rsync@v1.4
env: env:
HOST: ${{ secrets.WEBMASTER_SSH_HOST }} HOST: ${{ secrets.WEBMASTER_SSH_HOST }}

View File

@@ -36,7 +36,7 @@ jobs:
run: python setup.py sdist bdist_wheel run: python setup.py sdist bdist_wheel
- name: Upload artifacts - name: Upload artifacts
uses: actions/upload-artifact@v7 uses: actions/upload-artifact@v6
with: with:
name: dist name: dist
path: ./photon-lib/py/dist/ path: ./photon-lib/py/dist/
@@ -62,7 +62,7 @@ jobs:
pip install pytest mypy pip install pytest mypy
- name: Download artifacts - name: Download artifacts
uses: actions/download-artifact@v8 uses: actions/download-artifact@v6
with: with:
name: dist name: dist
path: dist/ path: dist/
@@ -100,10 +100,9 @@ jobs:
- name: Install dependencies - name: Install dependencies
run: | run: |
python -m pip install --upgrade pip python -m pip install --upgrade pip
python -m pip install mypy
- name: Download artifacts - name: Download artifacts
uses: actions/download-artifact@v8 uses: actions/download-artifact@v6
with: with:
name: dist name: dist
path: ./photon-lib/py/dist/ path: ./photon-lib/py/dist/
@@ -121,26 +120,26 @@ jobs:
for folder in */; for folder in */;
do do
echo $folder echo $folder
./test.sh $folder ./run.sh $folder
done done
deploy: # deploy:
needs: [test-py, build-python-examples] # needs: [test-py, build-python-examples]
runs-on: ubuntu-24.04 # runs-on: ubuntu-24.04
# Only upload on tags # # Only upload on tags
if: startsWith(github.ref, 'refs/tags/v') # if: startsWith(github.ref, 'refs/tags/v')
steps: # steps:
- name: Download artifacts # - name: Download artifacts
uses: actions/download-artifact@v8 # uses: actions/download-artifact@v6
with: # with:
name: dist # name: dist
path: dist/ # path: dist/
- name: Publish package distributions to PyPI # - name: Publish package distributions to PyPI
uses: pypa/gh-action-pypi-publish@release/v1 # uses: pypa/gh-action-pypi-publish@release/v1
with: # with:
packages-dir: ./dist/ # packages-dir: ./dist/
permissions: # permissions:
id-token: write # IMPORTANT: this permission is mandatory for trusted publishing # id-token: write # IMPORTANT: this permission is mandatory for trusted publishing

View File

@@ -11,12 +11,13 @@ jobs:
steps: steps:
- uses: actions/checkout@v6 - uses: actions/checkout@v6
- name: Install pnpm - name: Install pnpm
uses: pnpm/action-setup@v5 uses: pnpm/action-setup@v4
with: with:
version: 10 version: 10
- uses: actions/setup-node@v6 - name: Setup Node
uses: actions/setup-node@v6
with: with:
node-version: 24 node-version: 22
cache: pnpm cache: pnpm
cache-dependency-path: website/pnpm-lock.yaml cache-dependency-path: website/pnpm-lock.yaml
- name: Install packages - name: Install packages
@@ -36,27 +37,22 @@ jobs:
format-check: format-check:
name: Check Formatting name: Check Formatting
defaults:
run:
working-directory: website
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v6 - uses: actions/checkout@v6
- uses: pnpm/action-setup@v5 - name: Install pnpm
uses: pnpm/action-setup@v4
with: with:
version: 10 version: 10
- uses: actions/setup-node@v6 - name: Setup Node
uses: actions/setup-node@v6
with: with:
node-version: 24 node-version: 22
cache: pnpm cache: pnpm
cache-dependency-path: website/pnpm-lock.yaml cache-dependency-path: website/pnpm-lock.yaml
- name: Install Packages - name: Install Packages
run: pnpm i --frozen-lockfile run: pnpm i --frozen-lockfile
- run: | working-directory: website
set +e - name: Run Formatting Check
pnpm run format-ci run: pnpm prettier -c .
exit_code=$? working-directory: website
if test "$exit_code" -ne "0"; then
echo "::error ::Linting failed. See https://docs.photonvision.org/en/latest/docs/contributing/linting.html"
fi
exit $exit_code

2
.gitignore vendored
View File

@@ -161,5 +161,3 @@ photon-client/playwright/.auth/
shell.nix shell.nix
flake.nix flake.nix
flake.lock flake.lock
/workspace

View File

@@ -2,7 +2,7 @@ cppHeaderFileInclude {
\.h$ \.h$
} }
generatedFileExclude { modifiableFileExclude {
photon-lib/py/photonlibpy/generated/ photon-lib/py/photonlibpy/generated/
photon-targeting/src/generated/ photon-targeting/src/generated/
photon-targeting/src/main/native/cpp/photon/constrained_solvepnp/generate/ photon-targeting/src/main/native/cpp/photon/constrained_solvepnp/generate/

View File

@@ -32,11 +32,11 @@ You can run one of the many built in examples straight from the command line, to
Note that these are case sensitive! Note that these are case sensitive!
* `-PArchOverride=foobar`: builds for a target system other than your current architecture. [Valid overrides](https://github.com/wpilibsuite/wpilib-tool-plugin/blob/main/src/main/java/edu/wpi/first/tools/NativePlatforms.java) are: * `-PArchOverride=foobar`: builds for a target system other than your current architecture. [Valid overrides](https://github.com/wpilibsuite/wpilib-tool-plugin/blob/main/src/main/java/edu/wpi/first/tools/NativePlatforms.java) are:
* winx86-64 * winx64
* winarm64 * winarm64
* macx86-64 * macx64
* macarm64 * macarm64
* linuxx86-64 * linuxx64
* linuxarm64 * linuxarm64
* linuxathena * linuxathena
- `-PtgtIP`: Specifies where `./gradlew deploy` should try to copy the fat JAR to - `-PtgtIP`: Specifies where `./gradlew deploy` should try to copy the fat JAR to
@@ -45,7 +45,7 @@ Note that these are case sensitive!
- `-Pprofile`: enables JVM profiling - `-Pprofile`: enables JVM profiling
- `-PwithSanitizers`: On Linux, enables `-fsanitize=address,undefined,leak` - `-PwithSanitizers`: On Linux, enables `-fsanitize=address,undefined,leak`
If you're cross-compiling, you'll need the WPILib toolchain installed. This must be done via Gradle: for example `./gradlew installArm64Toolchain` or `./gradlew installSystemCoreToolchain` If you're cross-compiling, you'll need the WPILib toolchain installed. This must be done via Gradle: for example `./gradlew installArm64Toolchain` or `./gradlew installRoboRioToolchain`
## Out-of-Source Dependencies ## Out-of-Source Dependencies
@@ -67,7 +67,7 @@ PhotonVision was forked from [Chameleon Vision](https://github.com/Chameleon-Vis
* [EJML](https://github.com/lessthanoptimal/ejml) * [EJML](https://github.com/lessthanoptimal/ejml)
* [Javalin](https://javalin.io/) * [Javalin](https://javalin.io/)
* [JSON](https://json.org) * [JSON](https://json.org)
* [Avaje](https://avaje.io) - Specifically [jsonb](https://avaje.io/jsonb/) * [FasterXML](https://github.com/FasterXML) - Specifically [jackson](https://github.com/FasterXML/jackson)
* [MessagePack for Java](https://github.com/msgpack/msgpack-java) * [MessagePack for Java](https://github.com/msgpack/msgpack-java)
* [OSHI](https://github.com/oshi/oshi) * [OSHI](https://github.com/oshi/oshi)
* [QuickBuffers](https://github.com/HebiRobotics/QuickBuffers) * [QuickBuffers](https://github.com/HebiRobotics/QuickBuffers)

View File

@@ -1,17 +1,16 @@
import org.wpilib.toolchain.* import edu.wpi.first.toolchain.*
plugins { plugins {
id "cpp" id "cpp"
id "com.diffplug.spotless" version "8.1.0" id "com.diffplug.spotless" version "8.1.0"
id "org.wpilib.WPILibRepositoriesPlugin" version "2027.0.0" id "edu.wpi.first.wpilib.repositories.WPILibRepositoriesPlugin" version "2020.2"
id 'org.wpilib.NativeUtils' version '2027.7.1' apply false id "edu.wpi.first.GradleRIO" version "2026.2.1"
id 'org.wpilib.DeployUtils' version '2027.1.0' apply false id 'org.photonvision.tools.WpilibTools' version '2.4.1-photon'
id 'org.photonvision.tools.WpilibTools' version 'v5.0.1' id 'com.google.protobuf' version '0.9.3' apply false
id 'com.google.protobuf' version '0.9.5' apply false id 'edu.wpi.first.GradleJni' version '1.1.0'
id 'org.wpilib.GradleJni' version '2027.0.0'
id "org.ysb33r.doxygen" version "2.0.0" apply false id "org.ysb33r.doxygen" version "2.0.0" apply false
id 'com.gradleup.shadow' version '9.0.0' apply false id 'com.gradleup.shadow' version '8.3.4' apply false
id "com.github.node-gradle.node" version "7.1.0" apply false id "com.github.node-gradle.node" version "7.0.1" apply false
} }
allprojects { allprojects {
@@ -22,7 +21,6 @@ allprojects {
maven { url = "https://maven.photonvision.org/releases" } maven { url = "https://maven.photonvision.org/releases" }
maven { url = "https://maven.photonvision.org/snapshots" } maven { url = "https://maven.photonvision.org/snapshots" }
} }
wpilibRepositories.use2027Repos()
wpilibRepositories.addAllReleaseRepositories(it) wpilibRepositories.addAllReleaseRepositories(it)
wpilibRepositories.addAllDevelopmentRepositories(it) wpilibRepositories.addAllDevelopmentRepositories(it)
} }
@@ -30,21 +28,20 @@ allprojects {
ext.localMavenURL = file("$project.buildDir/outputs/maven") ext.localMavenURL = file("$project.buildDir/outputs/maven")
ext.allOutputsFolder = file("$project.buildDir/outputs") ext.allOutputsFolder = file("$project.buildDir/outputs")
ext { // Configure the version number.
wpilibVersion = "2027.0.0-alpha-6" apply from: "versioningHelper.gradle"
openCVversion = "2027-4.13.0-3"
ejmlVersion = "0.43.1";
avajeJsonbVersion = "3.14";
msgpackVersion = "0.9.0";
quickbufVersion = "1.3.3";
jacocoVersion = "0.8.14";
ext {
wpilibVersion = "2026.2.1"
wpimathVersion = wpilibVersion
openCVYear = "2025"
openCVversion = "4.10.0-3"
javalinVersion = "6.7.0" javalinVersion = "6.7.0"
libcameraDriverVersion = "v2027.0.0-alpha-1" libcameraDriverVersion = "v2026.0.0"
rknnVersion = "dev-v2026.0.1-3-g14c3ecb" rknnVersion = "dev-v2026.0.1-1-g89b2888"
tfliteVersion = "v2027.0.2-alpha-1" rubikVersion = "dev-v2026.0.1-4-g13d6279"
wpilibYear = "2027_alpha5" frcYear = "2026"
mrcalVersion = "v2027.0.2"; mrcalVersion = "dev-v2026.0.0-1-g239d80e";
pubVersion = versionString pubVersion = versionString
isDev = pubVersion.startsWith("dev") isDev = pubVersion.startsWith("dev")
@@ -65,7 +62,7 @@ spotless {
java { java {
target fileTree('.') { target fileTree('.') {
include '**/*.java' include '**/*.java'
exclude '**/build/**', '**/build-*/**', '**/src/generated/**', "**/bin/generated-sources/**" exclude '**/build/**', '**/build-*/**', '**/src/generated/**'
} }
toggleOffOn() toggleOffOn()
googleJavaFormat() googleJavaFormat()
@@ -97,7 +94,7 @@ spotless {
} }
wrapper { wrapper {
gradleVersion = '9.4.0' gradleVersion = '8.14.3'
} }
ext.getCurrentArch = { ext.getCurrentArch = {

View File

@@ -36,6 +36,7 @@ sphinx-autobuild==2024.10.3
sphinx-basic-ng==1.0.0b2 sphinx-basic-ng==1.0.0b2
sphinx-notfound-page==1.1.0 sphinx-notfound-page==1.1.0
sphinx-rtd-theme==3.0.2 sphinx-rtd-theme==3.0.2
sphinx-tabs==3.4.7
sphinx_design==0.6.1 sphinx_design==0.6.1
sphinxcontrib-applehelp==2.0.0 sphinxcontrib-applehelp==2.0.0
sphinxcontrib-devhelp==2.0.0 sphinxcontrib-devhelp==2.0.0

View File

@@ -181,4 +181,4 @@ if token:
linkcheck_auth = [(R"https://github.com/.+", token)] linkcheck_auth = [(R"https://github.com/.+", token)]
# MyST configuration (https://myst-parser.readthedocs.io/en/latest/configuration.html) # MyST configuration (https://myst-parser.readthedocs.io/en/latest/configuration.html)
myst_enable_extensions = ["colon_fence", "substitution", "attrs_inline"] myst_enable_extensions = ["colon_fence", "substitution"]

View File

@@ -1,17 +1,19 @@
# NetworkTables API # NetworkTables API
## Usage ## About
:::{warning} :::{warning}
PhotonVision's NetworkTables API is not designed for robot consumption, only internal at-a-glance debugging.** PhotonVision interfaces with PhotonLib, our vendor dependency, using NetworkTables. If you are running PhotonVision on a robot (ie. with a RoboRIO), you should **turn the NetworkTables server switch (in the settings tab) off** in order to get PhotonLib to work. Also ensure that you set your team number. **The NetworkTables server should only be enabled if you know what you're doing!**
::: :::
**Use PhotonLib instead, as the NetworkTables API is not supported for robot consumption.**
## API ## API
:::{warning}
NetworkTables is not a supported setup/viable option when using PhotonVision as we only send one target at a time (this is problematic when using AprilTags, which will return data from multiple tags at once).
**We strongly recommend using PhotonLib instead, as the NetworkTables API will most likely be removed in 2027.**
:::
The tables below contain the the name of the key for each entry that PhotonVision sends over the network and a short description of the key. The entries should be extracted from a subtable with your camera's nickname (visible in the PhotonVision UI) under the main `photonvision` table. The tables below contain the the name of the key for each entry that PhotonVision sends over the network and a short description of the key. The entries should be extracted from a subtable with your camera's nickname (visible in the PhotonVision UI) under the main `photonvision` table.
### Getting Target Information ### Getting Target Information
@@ -62,3 +64,9 @@ These entries are global, meaning that they should be called on the main `photon
| Key | Type | Description | | Key | Type | Description |
| --------- | ----- | -------------------------------------------------------- | | --------- | ----- | -------------------------------------------------------- |
| `ledMode` | `int` | Sets the LED Mode (-1: default, 0: off, 1: on, 2: blink) | | `ledMode` | `int` | Sets the LED Mode (-1: default, 0: off, 1: on, 2: blink) |
:::{warning}
Setting the LED mode to -1 (default) when `multiple` cameras are connected may result in unexpected behavior. {ref}`This is a known limitation of PhotonVision. <docs/troubleshooting/common-errors:LED Control>`
Single camera operation should work without issue.
:::

View File

@@ -3,8 +3,5 @@
"ledPins" : [ 13, 18 ], "ledPins" : [ 13, 18 ],
"ledsCanDim" : true, "ledsCanDim" : true,
"ledPWMFrequency" : 1000, "ledPWMFrequency" : 1000,
"statusLEDType": "GreenYellow",
"statusLEDPins": [ 5, 4 ],
"statusLEDActiveHigh": false,
"vendorFOV" : 75.76079874010732 "vendorFOV" : 75.76079874010732
} }

View File

@@ -2,8 +2,5 @@
"deviceName" : "Limelight 2", "deviceName" : "Limelight 2",
"ledPins" : [ 17, 18 ], "ledPins" : [ 17, 18 ],
"ledsCanDim" : false, "ledsCanDim" : false,
"statusLEDType": "GreenYellow",
"statusLEDPins": [ 5, 4 ],
"statusLEDActiveHigh": false,
"vendorFOV" : 75.76079874010732 "vendorFOV" : 75.76079874010732
} }

View File

@@ -25,7 +25,7 @@ Go to the [GitHub releases page](https://github.com/PhotonVision/photonvision/re
:::{note} :::{note}
If your coprocessor has a 64 bit ARM based CPU architecture (OrangePi, Raspberry Pi, etc.), download the LinuxArm64.jar file. If your coprocessor has a 64 bit ARM based CPU architecture (OrangePi, Raspberry Pi, etc.), download the LinuxArm64.jar file.
If your coprocessor has an 64 bit x86 based CPU architecture (Mini PC, laptop, etc.), download the Linuxx86-64.jar file. If your coprocessor has an 64 bit x86 based CPU architecture (Mini PC, laptop, etc.), download the Linuxx64.jar file.
::: :::
:::{warning} :::{warning}

View File

@@ -1,9 +1,53 @@
# MacOS Installation # Mac OS Installation
## Builds :::{warning}
Due to current [cscore](https://github.com/wpilibsuite/allwpilib/tree/main/cscore) restrictions, the PhotonVision server backend may have issues running macOS.
:::
Builds for macOS are currently generated in CI for testing compatibility. This allows us to ensure that macOS remains a viable development platform. :::{note}
You do not need to install PhotonVision on a Mac in order to access the webdashboard (assuming you are using an external coprocessor like a Raspberry Pi).
:::
## Use as a coprocessor VERY Limited macOS support is available.
This is not a recommended path. MacOS is not officially supported, and your mileage may vary. ## Installing Java
PhotonVision requires a JDK installed and on the system path. JDK 17 is needed (different versions will not work). You may already have this if you have installed WPILib 2026+. If not, [download and install it from here](https://adoptium.net/temurin/releases?version=17).
:::{warning}
Using a JDK other than JDK17 will cause issues when running PhotonVision and is not supported.
:::
## Downloading the Latest Stable Release of PhotonVision
Go to the [GitHub releases page](https://github.com/PhotonVision/photonvision/releases) and download the relevant .jar file for your coprocessor.
:::{note}
If you have an M Series Mac, download the macarm64.jar file.
If you have an Intel based Mac, download the macx64.jar file.
:::
:::{warning}
Be careful to pick the latest stable release. "Draft" or "Pre-Release" versions are not stable and often have bugs.
:::
## Running PhotonVision
To run PhotonVision, open a terminal window of your choice and run the following command:
```
$ java -jar /path/to/photonvision/photonvision-xxx.jar
```
:::{warning}
Due to current [cscore](https://github.com/wpilibsuite/allwpilib/tree/main/cscore) restrictions, the PhotonVision using test mode is all that is known to work currently.
:::
## Accessing the PhotonVision Interface
Once the Java backend is up and running, you can access the main vision interface by navigating to `localhost:5800` inside your browser.
:::{warning}
Due to current [cscore](https://github.com/wpilibsuite/allwpilib/tree/main/cscore) restrictions, it is unlikely any streams will open from real webcams.
:::

View File

@@ -16,7 +16,7 @@ PhotonVision requires a JDK installed and on the system path. **JDK 17 is needed
## Downloading the Latest Stable Release of PhotonVision ## Downloading the Latest Stable Release of PhotonVision
Go to the [GitHub releases page](https://github.com/PhotonVision/photonvision/releases) and download the winx86-64.jar file. Go to the [GitHub releases page](https://github.com/PhotonVision/photonvision/releases) and download the winx64.jar file.
## Running PhotonVision ## Running PhotonVision

View File

@@ -23,7 +23,7 @@ Ensure that your camera is calibrated and 3D mode is enabled. Navigate to the Ou
By default, enabling multi-target will disable calculating camera-to-target transforms for each observed AprilTag target to increase performance; the X/Y/angle numbers shown in the target table of the UI are instead calculated using the tag's expected location (per the field layout JSON) and the field-to-camera transform calculated using MultiTag. If you additionally want the individual camera-to-target transform calculated using SolvePNP for each target, enable "Always Do Single-Target Estimation". By default, enabling multi-target will disable calculating camera-to-target transforms for each observed AprilTag target to increase performance; the X/Y/angle numbers shown in the target table of the UI are instead calculated using the tag's expected location (per the field layout JSON) and the field-to-camera transform calculated using MultiTag. If you additionally want the individual camera-to-target transform calculated using SolvePNP for each target, enable "Always Do Single-Target Estimation".
::: :::
This multi-target pose estimate can be accessed using PhotonLib. We suggest using {ref}`the PhotonPoseEstimator class <docs/programming/photonlib/robot-pose-estimator:AprilTags and PhotonPoseEstimator>` and calling `estimateCoprocMultiTagPose` (with an optional fallback like `estimateLowestAmbiguityPose`) to simplify code, but the transform can be directly accessed using `getMultiTagResult`/`MultiTagResult()`/`multitagResult` (Java/C++/Python). This multi-target pose estimate can be accessed using PhotonLib. We suggest using {ref}`the PhotonPoseEstimator class <docs/programming/photonlib/robot-pose-estimator:AprilTags and PhotonPoseEstimator>` with the `MULTI_TAG_PNP_ON_COPROCESSOR` strategy to simplify code, but the transform can be directly accessed using `getMultiTagResult`/`MultiTagResult()`/`multitagResult` (Java/C++/Python).
```{eval-rst} ```{eval-rst}
.. tab-set-code:: .. tab-set-code::
@@ -46,7 +46,7 @@ This multi-target pose estimate can be accessed using PhotonLib. We suggest usin
{ {
auto multiTagResult = result.MultiTagResult(); auto multiTagResult = result.MultiTagResult();
if (multiTagResult.has_value()) { if (multiTagResult.has_value()) {
wpi::math::Transform3d fieldToCamera = multiTagResult->estimatedPose.best; frc::Transform3d fieldToCamera = multiTagResult->estimatedPose.best;
} }
} }

View File

@@ -7,7 +7,7 @@ In order to detect AprilTags and use 3D mode, your camera must be calibrated at
To calibrate a camera, images of a ChArUco board (or chessboard) are taken. By comparing where the grid corners should be in object space (for example, a corner once every inch in an 8x6 grid) with where they appear in the camera image, we can find a least-squares estimate for intrinsic camera properties like focal lengths, center point, and distortion coefficients. For more on camera calibration, please review the [OpenCV documentation](https://docs.opencv.org/4.x/dc/dbb/tutorial_py_calibration.html). To calibrate a camera, images of a ChArUco board (or chessboard) are taken. By comparing where the grid corners should be in object space (for example, a corner once every inch in an 8x6 grid) with where they appear in the camera image, we can find a least-squares estimate for intrinsic camera properties like focal lengths, center point, and distortion coefficients. For more on camera calibration, please review the [OpenCV documentation](https://docs.opencv.org/4.x/dc/dbb/tutorial_py_calibration.html).
:::{warning} :::{warning}
PhotonVision supports many resolution configuration options, with only a minimum of 640x480 required. However, higher resolutions may be too performance-intensive for some coprocessors to handle. Therefore, we recommend experimenting to see what works best for your coprocessor. While any resolution can be calibrated, higher resolutions may be too performance-intensive for some coprocessors to handle. Therefore, we recommend experimenting to see what works best for your coprocessor.
::: :::
:::{note} :::{note}

View File

@@ -8,11 +8,11 @@ This section contains the build instructions from the source code available at [
**Java Development Kit:** **Java Development Kit:**
This project requires Java Development Kit (JDK) 25 to be compiled. This is the same Java version that comes with WPILib for 2027. **Windows Users must use the JDK that ships with WPILib.** For other platforms, you can follow the instructions to install JDK 25 for your platform [here](https://bell-sw.com/pages/downloads/#jdk-25-lts). This project requires Java Development Kit (JDK) 17 to be compiled. This is the same Java version that comes with WPILib for 2026+. **Windows Users must use the JDK that ships with WPILib.** For other platforms, you can follow the instructions to install JDK 17 for your platform [here](https://bell-sw.com/pages/downloads/#jdk-17-lts).
**Node JS:** **Node JS:**
The UI is written in Node JS. To compile the UI, Node 24 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/). The UI is written in Node JS. To compile the UI, Node 22 or later is required. To install Node JS, follow the instructions for your platform [on the official Node JS website](https://nodejs.org/en/download/).
**pnpm:** **pnpm:**
@@ -47,30 +47,6 @@ In the photon-client directory:
pnpm install pnpm install
``` ```
### Building the UI
In order to properly build UI changes before running the project, run the following `gradlew` command in the project's root directory:
```{eval-rst}
.. tab-set::
.. tab-item:: Linux
:sync: linux
``./gradlew buildAndCopyUI``
.. tab-item:: macOS
:sync: macos
``./gradlew buildAndCopyUI``
.. tab-item:: Windows (cmd)
:sync: windows
``gradlew buildAndCopyUI``
```
### Using hot reload on the UI ### Using hot reload on the UI
In the photon-client directory: In the photon-client directory:

View File

@@ -79,9 +79,9 @@ public class Robot extends TimedRobot {
camera.getAllUnreadResults(); camera.getAllUnreadResults();
} }
var t1 = Timer.getMonotonicTimestamp(); var t1 = Timer.getFPGATimestamp();
light.set(true); light.set(true);
var t2 = Timer.getMonotonicTimestamp(); var t2 = Timer.getFPGATimestamp();
for (int i = 0; i < 100; i++) { for (int i = 0; i < 100; i++) {

View File

@@ -34,16 +34,10 @@ To install *doc8*, the python tool we use to lint our documentation, run `pipx i
To lint the documentation, run `doc8 docs` from the root level of the docs. To lint the documentation, run `doc8 docs` from the root level of the docs.
## Website
### Formatting the website
To format the website, run `pnpm -C website run format`.
## Alias ## Alias
The following [alias](https://www.computerworld.com/article/1373210/how-to-use-aliases-in-linux-shell-commands.html) can be added to your shell config, which will allow you to lint the entirety of the PhotonVision project by running `pvLint`. The alias will work on Linux, macOS, Git Bash on Windows, and WSL. The following [alias](https://www.computerworld.com/article/1373210/how-to-use-aliases-in-linux-shell-commands.html) can be added to your shell config, which will allow you to lint the entirety of the PhotonVision project by running `pvLint`. The alias will work on Linux, macOS, Git Bash on Windows, and WSL.
```sh ```sh
alias pvLint='wpiformat -v && ./gradlew spotlessApply && pnpm -C photon-client lint && pnpm -C photon-client format && doc8 docs && pnpm -C website format' alias pvLint='wpiformat -v && ./gradlew spotlessApply && pnpm -C photon-client lint && pnpm -C photon-client format && doc8 docs'
``` ```

View File

@@ -19,18 +19,14 @@ When running on Linux, PhotonVision can use [diozero](https://www.diozero.com) t
"ledsCanDim" : true, "ledsCanDim" : true,
"ledBrightnessRange" : [ 0, 100 ], "ledBrightnessRange" : [ 0, 100 ],
"ledPWMFrequency" : 0, "ledPWMFrequency" : 0,
"statusLEDType": "RGB", "statusRGBPins" : [ ],
"statusLEDPins" : [ ], "statusRGBActiveHigh" : false,
"statusLEDActiveHigh" : false,
} }
``` ```
There are currently two types of status LEDs supported: :::{note}
No hardware boards with status RGB LED pins or non-dimming LED's have been tested yet. Please reach out to the development team if these features are desired, they can assist with configuration and testing.
* `RGB` (default): A singular LED mixing separate red, green, and blue inputs :::
* `GreenYellow`: A pair of independent green and yellow LEDs
For an explanation of the colors used for status LEDs, see {ref}`Status LEDs<docs/troubleshooting/status-leds:Status LEDs>`
### GPIO Pinout ### GPIO Pinout
@@ -138,9 +134,8 @@ Here is a complete example `hardwareConfig.json`:
"ledsCanDim" : true, "ledsCanDim" : true,
"ledBrightnessRange" : [ 0, 100 ], "ledBrightnessRange" : [ 0, 100 ],
"ledPWMFrequency" : 0, "ledPWMFrequency" : 0,
"statusLEDType": "RGB", "statusRGBPins" : [ ],
"statusLEDPins" : [ ], "statusRGBActiveHigh" : false,
"statusLEDActiveHigh" : false,
"getGPIOCommand" : "getGPIO {p}", "getGPIOCommand" : "getGPIO {p}",
"setGPIOCommand" : "setGPIO {p} {s}", "setGPIOCommand" : "setGPIO {p} {s}",
"setPWMCommand" : "setPWM {p} {v}", "setPWMCommand" : "setPWM {p} {v}",

View File

@@ -10,7 +10,7 @@ In order to use PhotonVision, you need a coprocessor and a camera. This page dis
### Minimum System Requirements ### Minimum System Requirements
- Ubuntu 24.04 LTS or Windows 10/11 - Ubuntu 22.04 LTS or Windows 10/11
- We don't recommend using Windows for anything except testing out the system on a local machine. - We don't recommend using Windows for anything except testing out the system on a local machine.
- CPU: ARM Cortex-A53 (the CPU on Raspberry Pi 3) or better - CPU: ARM Cortex-A53 (the CPU on Raspberry Pi 3) or better
- At least 8GB of storage - At least 8GB of storage

View File

@@ -8,8 +8,6 @@ PhotonVision runs object detection on the Orange Pi 5 by use of the RKNN model a
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. 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.
If you have a YOLO-Pro model from Edge Impulse, it can be converted using [this notebook](https://github.com/ramalamadingdong/yolo-pro-to-yolo11/tree/main).
## Converting Custom Models ## Converting Custom Models
:::{warning} :::{warning}

View File

@@ -8,8 +8,6 @@ PhotonVision runs object detection on the Rubik Pi 3 by use of [TensorflowLite](
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. 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.
If you have a YOLO-Pro model from Edge Impulse, it can be converted using [this notebook](https://github.com/ramalamadingdong/yolo-pro-to-yolo11/tree/main).
## Converting Custom Models ## Converting Custom Models
:::{warning} :::{warning}

View File

@@ -60,7 +60,7 @@ You can also get the pipeline latency from a pipeline result using the `getLaten
.. code-block:: c++ .. code-block:: c++
// Get the pipeline latency. // Get the pipeline latency.
wpi::units::second_t latency = result.GetLatency(); units::second_t latency = result.GetLatency();
.. code-block:: python .. code-block:: python

View File

@@ -107,7 +107,7 @@ You can get a list of tracked targets using the `getTargets()`/`GetTargets()` (J
.. code-block:: c++ .. code-block:: c++
// Get a list of currently tracked targets. // Get a list of currently tracked targets.
std::span<photonlib::PhotonTrackedTarget> targets = result.GetTargets(); wpi::ArrayRef<photonlib::PhotonTrackedTarget> targets = result.GetTargets();
.. code-block:: python .. code-block:: python
@@ -166,8 +166,8 @@ You can get the {ref}`best target <docs/reflectiveAndShape/contour-filtering:Con
double pitch = target.GetPitch(); double pitch = target.GetPitch();
double area = target.GetArea(); double area = target.GetArea();
double skew = target.GetSkew(); double skew = target.GetSkew();
wpi::math::Transform2d pose = target.GetCameraToTarget(); frc::Transform2d pose = target.GetCameraToTarget();
wpi::util::SmallVector<std::pair<double, double>, 4> corners = target.GetCorners(); wpi::SmallVector<std::pair<double, double>, 4> corners = target.GetCorners();
.. code-block:: python .. code-block:: python
@@ -206,8 +206,8 @@ All of the data above (**except skew**) is available when using AprilTags.
// Get information from target. // Get information from target.
int targetID = target.GetFiducialId(); int targetID = target.GetFiducialId();
double poseAmbiguity = target.GetPoseAmbiguity(); double poseAmbiguity = target.GetPoseAmbiguity();
wpi::math::Transform3d bestCameraToTarget = target.getBestCameraToTarget(); frc::Transform3d bestCameraToTarget = target.getBestCameraToTarget();
wpi::math::Transform3d alternateCameraToTarget = target.getAlternateCameraToTarget(); frc::Transform3d alternateCameraToTarget = target.getAlternateCameraToTarget();
.. code-block:: python .. code-block:: python

View File

@@ -38,8 +38,8 @@ You can get your robot's `Pose2D` on the field using various camera data, target
.. code-block:: c++ .. code-block:: c++
// Calculate robot's field relative pose // Calculate robot's field relative pose
wpi::math::Pose2d robotPose = photonlib::EstimateFieldToRobot( frc::Pose2D robotPose = photonlib::EstimateFieldToRobot(
kCameraHeight, kTargetHeight, kCameraPitch, kTargetPitch, wpi::math::Rotation2d(wpi::units::degree_t(-target.GetYaw())), wpi::math::Rotation2d(wpi::units::degree_t(gyro.GetRotation2d)), targetPose, cameraToRobot); 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
@@ -106,8 +106,8 @@ You can get a [translation](https://docs.wpilib.org/en/latest/docs/software/adva
.. code-block:: c++ .. code-block:: c++
// Calculate a translation from the camera to the target. // Calculate a translation from the camera to the target.
wpi::math::Translation2d translation = photonlib::PhotonUtils::EstimateCameraToTargetTranslation( frc::Translation2d translation = photonlib::PhotonUtils::EstimateCameraToTargetTranslation(
distance, wpi::math::Rotation2d(wpi::units::degree_t(-target.GetYaw()))); distance, frc::Rotation2d(units::degree_t(-target.GetYaw())));
.. code-block:: python .. code-block:: python

View File

@@ -81,7 +81,7 @@ If you would like to access your Ethernet-connected vision device from a compute
.. code-block:: c++ .. code-block:: c++
wpi::net::PortForwarder::GetInstance().Add(5800, "photonvision.local", 5800); wpi::PortForwarder::GetInstance().Add(5800, "photonvision.local", 5800);
.. code-block:: python .. code-block:: python

View File

@@ -1,10 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" id="led" width="30" height="30" viewBox="0 0 100 100" style="color-scheme: dark light">
<circle cx="50" cy="50" r="20" fill="dimgrey"/>
<circle cx="50" cy="50" r="20" stroke="black" stroke-width="5" fill="currentColor"/>
<line x1="5" y1="50" x2="20" y2="50" stroke="currentColor" stroke-width="5" stroke-linecap="round"/>
<line x1="27.5" y1="11.03" x2="35" y2="24.02" stroke="currentColor" stroke-width="5" stroke-linecap="round"/>
<line x1="72.5" y1="11.03" x2="65" y2="24.02" stroke="currentColor" stroke-width="5" stroke-linecap="round"/>
<line x1="80" y1="50" x2="95" y2="50" stroke="currentColor" stroke-width="5" stroke-linecap="round"/>
<line x1="72.5" y1="88.97" x2="65" y2="75.98" stroke="currentColor" stroke-width="5" stroke-linecap="round"/>
<line x1="27.5" y1="88.97" x2="35" y2="75.98" stroke="currentColor" stroke-width="5" stroke-linecap="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 927 B

View File

@@ -5,7 +5,6 @@
common-errors common-errors
logging logging
status-leds
camera-troubleshooting camera-troubleshooting
networking-troubleshooting networking-troubleshooting
unix-commands unix-commands

View File

@@ -1,101 +0,0 @@
---
myst:
substitutions:
led_loader: |
```{image} images/led.svg
:height: 0
```
led: |
```{raw} html
<svg class="led" height="30" width="30">
<use href="../../_images/led.svg#led"/>
</svg>
```
---
<!-- markdownlint-disable-next-line MD033 MD041 -->
<style>
svg.led {
--off-color: transparent;
color: var(--on-color);
}
@keyframes led-blink {
66% {
color: var(--off-color);
}
}
:not(.solid) > svg.led {
animation: led-blink 0.45s steps(1) infinite;
}
@keyframes led-even-blink {
50% {
color: var(--off-color);
}
}
:not(.solid).fast > svg.led {
animation-name: led-even-blink;
animation-duration: 150ms;
}
:not(.solid).error > svg.led {
animation-name: led-even-blink;
animation-duration: 0.90s;
}
.green > svg.led {
--on-color: limegreen;
}
.blue > svg.led {
--on-color: blue;
}
.yellow > svg.led {
--on-color: yellow;
}
.red > svg.led {
--on-color: red;
}
.anti-yellow > svg.led {
--on-color: transparent;
--off-color: yellow;
}
.off > svg.led {
color: var(--off-color);
}
</style>
# Status LEDs
PhotonVision has support for multiple kinds of status LEDs. Make sure you reference the correct table for the type present on your hardware.
## RGB LED
Color | Flashing | Preview | Status
--------|----------|:-------------------------:|-----------------------------------------------
Green | Yes | [{{ led }}]{.green} | Running normally, no targets visible
Blue | No | [{{ led }}]{.solid .blue} | Running normally, targets visible
Yellow | Yes | [{{ led }}]{.yellow} | NT Disconnected, no targets visible
Blue | Yes | [{{ led }}]{.blue} | NT Disconnected, targets visible
Red | Yes | [{{ led }}]{.red} | Initializing or faulted, not running
Off | No | [{{ led }}]{.off} | No power or initialization fault, not running
## Green and Yellow LEDs
Used on Limelight 1, 2, 2+, 3, 3G, and 3A
Green and Yellow LED patterns may be active at the same time
Color | Pattern | Preview | Status
--------|----------------|:-----------------------------------------------------------:|-------------------------------------------------
Green | Slow Flashing | [{{ led }}]{.green} [{{ led }}]{.off} | No targets visible
Green | Quick Flashing | [{{ led }}]{.fast .green} [{{ led }}]{.off} | Targets visible
Yellow | Flashing | [{{ led }}]{.off} [{{ led }}]{.yellow} | NT Disconnected
Yellow | Solid | [{{ led }}]{.off} [{{ led }}]{.solid .yellow} | NT Connected
Both | Alternating | [{{ led }}]{.green .error} [{{ led }}]{.anti-yellow .error} | Initializing or faulted, not running
Both | Off | [{{ led }}]{.off} [{{ led }}]{.off} | No power or initialization fault, not running
{{ led_loader }}

View File

@@ -13,10 +13,10 @@ You may see a warning similar to `The authenticity of host 'xxx' can't be establ
Example: Example:
``` ```
ssh photon@hostname ssh pi@hostname
``` ```
For PhotonVision images, the username will be `photon` and the password will be `vision`. For PhotonVision, the username will be `pi` and the password will be `raspberry`.
### ip ### ip

View File

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

View File

@@ -1,3 +1 @@
/// <reference types="vite/client" /> /// <reference types="vite/client" />
declare module "vue3-virtual-scroll-list";

View File

@@ -5,7 +5,7 @@ import skipFormattingConfig from "@vue/eslint-config-prettier/skip-formatting";
export default defineConfigWithVueTs( export default defineConfigWithVueTs(
pluginVue.configs["flat/recommended-error"], pluginVue.configs["flat/recommended-error"],
vueTsConfigs.recommendedTypeChecked, vueTsConfigs.recommended,
skipFormattingConfig, skipFormattingConfig,
{ {
ignores: ["**/dist/**", "playwright-report"] ignores: ["**/dist/**", "playwright-report"]
@@ -42,13 +42,10 @@ export default defineConfigWithVueTs(
"vue/no-use-v-else-with-v-for": "error", "vue/no-use-v-else-with-v-for": "error",
"vue/no-useless-mustaches": "error", "vue/no-useless-mustaches": "error",
"vue/no-useless-v-bind": "error", "vue/no-useless-v-bind": "error",
"vue/prefer-use-template-ref": "error",
"vue/require-default-prop": "off", "vue/require-default-prop": "off",
"vue/require-typed-ref": "error",
"vue/v-for-delimiter-style": "error", "vue/v-for-delimiter-style": "error",
"vue/v-on-event-hyphenation": "off", "vue/v-on-event-hyphenation": "off",
"@typescript-eslint/no-empty-object-type": "error", "@typescript-eslint/no-explicit-any": "off",
"@typescript-eslint/no-explicit-any": "error",
"vue/valid-v-slot": ["error", { allowModifiers: true }] "vue/valid-v-slot": ["error", { allowModifiers: true }]
} }
} }

View File

@@ -3,9 +3,6 @@
"version": "0.0.0", "version": "0.0.0",
"private": true, "private": true,
"type": "module", "type": "module",
"engines": {
"node": "24.x"
},
"scripts": { "scripts": {
"dev": "vite", "dev": "vite",
"preview": "vite preview --port 4173", "preview": "vite preview --port 4173",
@@ -17,11 +14,10 @@
"format-ci": "prettier --check src/", "format-ci": "prettier --check src/",
"test": "playwright test", "test": "playwright test",
"test-ui": "playwright test --ui", "test-ui": "playwright test --ui",
"test-setup": "playwright install --with-deps", "test-setup": "playwright install --with-deps"
"type-check": "vue-tsc --noEmit"
}, },
"dependencies": { "dependencies": {
"@adam-rocska/units-and-measurement": "^1.2.0",
"@fontsource/prompt": "^5.2.6", "@fontsource/prompt": "^5.2.6",
"@mdi/font": "^7.4.47", "@mdi/font": "^7.4.47",
"@msgpack/msgpack": "^3.1.2", "@msgpack/msgpack": "^3.1.2",
@@ -38,10 +34,9 @@
"devDependencies": { "devDependencies": {
"@eslint/js": "^9.31.0", "@eslint/js": "^9.31.0",
"@playwright/test": "^1.56.1", "@playwright/test": "^1.56.1",
"@types/node": "^24.0.0", "@types/node": "^22.15.14",
"@types/three": "^0.178.0", "@types/three": "^0.178.0",
"@vitejs/plugin-vue": "^6.0.6", "@vitejs/plugin-vue": "^6.0.0",
"vue-tsc": "^3.2.5",
"@vue/eslint-config-prettier": "^10.2.0", "@vue/eslint-config-prettier": "^10.2.0",
"@vue/eslint-config-typescript": "^14.5.0", "@vue/eslint-config-typescript": "^14.5.0",
"@vue/tsconfig": "^0.7.0", "@vue/tsconfig": "^0.7.0",
@@ -50,7 +45,7 @@
"prettier": "^3.6.2", "prettier": "^3.6.2",
"sass": "^1.89.2", "sass": "^1.89.2",
"typescript": "^5.8.3", "typescript": "^5.8.3",
"vite": "^8.0.10", "vite": "^8.0.2",
"vite-plugin-vuetify": "^2.1.1" "vite-plugin-vuetify": "^2.1.1"
} }
} }

View File

@@ -8,9 +8,6 @@ importers:
.: .:
dependencies: dependencies:
'@adam-rocska/units-and-measurement':
specifier: ^1.2.0
version: 1.2.0
'@fontsource/prompt': '@fontsource/prompt':
specifier: ^5.2.6 specifier: ^5.2.6
version: 5.2.6 version: 5.2.6
@@ -55,14 +52,14 @@ importers:
specifier: ^1.56.1 specifier: ^1.56.1
version: 1.56.1 version: 1.56.1
'@types/node': '@types/node':
specifier: ^24.0.0 specifier: ^22.15.14
version: 24.12.2 version: 22.15.14
'@types/three': '@types/three':
specifier: ^0.178.0 specifier: ^0.178.0
version: 0.178.1 version: 0.178.1
'@vitejs/plugin-vue': '@vitejs/plugin-vue':
specifier: ^6.0.6 specifier: ^6.0.0
version: 6.0.6(vite@8.0.10(@types/node@24.12.2)(sass@1.89.2))(vue@3.5.13(typescript@5.8.3)) version: 6.0.0(vite@8.0.2(@types/node@22.15.14)(sass@1.89.2))(vue@3.5.13(typescript@5.8.3))
'@vue/eslint-config-prettier': '@vue/eslint-config-prettier':
specifier: ^10.2.0 specifier: ^10.2.0
version: 10.2.0(eslint@9.31.0)(prettier@3.6.2) version: 10.2.0(eslint@9.31.0)(prettier@3.6.2)
@@ -88,21 +85,14 @@ importers:
specifier: ^5.8.3 specifier: ^5.8.3
version: 5.8.3 version: 5.8.3
vite: vite:
specifier: ^8.0.10 specifier: ^8.0.2
version: 8.0.10(@types/node@24.12.2)(sass@1.89.2) version: 8.0.2(@types/node@22.15.14)(sass@1.89.2)
vite-plugin-vuetify: vite-plugin-vuetify:
specifier: ^2.1.1 specifier: ^2.1.1
version: 2.1.1(vite@8.0.10(@types/node@24.12.2)(sass@1.89.2))(vue@3.5.13(typescript@5.8.3))(vuetify@3.8.3) version: 2.1.1(vite@8.0.2(@types/node@22.15.14)(sass@1.89.2))(vue@3.5.13(typescript@5.8.3))(vuetify@3.8.3)
vue-tsc:
specifier: ^3.2.5
version: 3.2.5(typescript@5.8.3)
packages: packages:
'@adam-rocska/units-and-measurement@1.2.0':
resolution: {integrity: sha512-mBnZ8/STbztVec+Mz9DH932z0gny52SebtSJ/y3n+IVtuF7KqbtQ3t1u1lpFSkLFU1msaNGzFgqsW7Emj0lrXA==}
engines: {node: '>=20.0.0'}
'@babel/helper-string-parser@7.27.1': '@babel/helper-string-parser@7.27.1':
resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==} resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==}
engines: {node: '>=6.9.0'} engines: {node: '>=6.9.0'}
@@ -127,14 +117,14 @@ packages:
'@dimforge/rapier3d-compat@0.12.0': '@dimforge/rapier3d-compat@0.12.0':
resolution: {integrity: sha512-uekIGetywIgopfD97oDL5PfeezkFpNhwlzlaEYNOA0N6ghdsOvh/HYjSMek5Q2O1PYvRSDFcqFVJl4r4ZBwOow==} resolution: {integrity: sha512-uekIGetywIgopfD97oDL5PfeezkFpNhwlzlaEYNOA0N6ghdsOvh/HYjSMek5Q2O1PYvRSDFcqFVJl4r4ZBwOow==}
'@emnapi/core@1.10.0': '@emnapi/core@1.9.1':
resolution: {integrity: sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==} resolution: {integrity: sha512-mukuNALVsoix/w1BJwFzwXBN/dHeejQtuVzcDsfOEsdpCumXb/E9j8w11h5S54tT1xhifGfbbSm/ICrObRb3KA==}
'@emnapi/runtime@1.10.0': '@emnapi/runtime@1.9.1':
resolution: {integrity: sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==} resolution: {integrity: sha512-VYi5+ZVLhpgK4hQ0TAjiQiZ6ol0oe4mBx7mVv7IflsiEp0OWoVsp/+f9Vc1hOhE0TtkORVrI1GvzyreqpgWtkA==}
'@emnapi/wasi-threads@1.2.1': '@emnapi/wasi-threads@1.2.0':
resolution: {integrity: sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==} resolution: {integrity: sha512-N10dEJNSsUx41Z6pZsXU8FjPjpBEplgH24sfkmITrBED1/U2Esum9F3lfLrMjKHHjmi557zQn7kR9R+XWXu5Rg==}
'@eslint-community/eslint-utils@4.7.0': '@eslint-community/eslint-utils@4.7.0':
resolution: {integrity: sha512-dyybb3AcajC7uha6CvhdVRJqaKyn7w2YKqKyAN37NKYgZT36w+iRb0Dymmc5qEJ549c/S31cMMSFd75bteCpCw==} resolution: {integrity: sha512-dyybb3AcajC7uha6CvhdVRJqaKyn7w2YKqKyAN37NKYgZT36w+iRb0Dymmc5qEJ549c/S31cMMSFd75bteCpCw==}
@@ -207,11 +197,8 @@ packages:
resolution: {integrity: sha512-JEW4DEtBzfe8HvUYecLU9e6+XJnKDlUAIve8FvPzF3Kzs6Xo/KuZkZJsDH0wJXl/qEZbeeE7edxDNY3kMs39hQ==} resolution: {integrity: sha512-JEW4DEtBzfe8HvUYecLU9e6+XJnKDlUAIve8FvPzF3Kzs6Xo/KuZkZJsDH0wJXl/qEZbeeE7edxDNY3kMs39hQ==}
engines: {node: '>= 18'} engines: {node: '>= 18'}
'@napi-rs/wasm-runtime@1.1.4': '@napi-rs/wasm-runtime@1.1.1':
resolution: {integrity: sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow==} resolution: {integrity: sha512-p64ah1M1ld8xjWv3qbvFwHiFVWrq1yFvV4f7w+mzaqiR4IlSgkqhcRdHwsGgomwzBH51sRY4NEowLxnaBjcW/A==}
peerDependencies:
'@emnapi/core': ^1.7.1
'@emnapi/runtime': ^1.7.1
'@nodelib/fs.scandir@2.1.5': '@nodelib/fs.scandir@2.1.5':
resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==}
@@ -225,8 +212,8 @@ packages:
resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==}
engines: {node: '>= 8'} engines: {node: '>= 8'}
'@oxc-project/types@0.127.0': '@oxc-project/types@0.122.0':
resolution: {integrity: sha512-aIYXQBo4lCbO4z0R3FHeucQHpF46l2LbMdxRvqvuRuW2OxdnSkcng5B8+K12spgLDj93rtN3+J2Vac/TIO+ciQ==} resolution: {integrity: sha512-oLAl5kBpV4w69UtFZ9xqcmTi+GENWOcPF7FCrczTiBbmC0ibXxCwyvZGbO39rCVEuLGAZM84DH0pUIyyv/YJzA==}
'@parcel/watcher-android-arm64@2.5.1': '@parcel/watcher-android-arm64@2.5.1':
resolution: {integrity: sha512-KF8+j9nNbUN8vzOFDpRMsaKBHZ/mcjEjMToVMJOhTozkDonQFFrRcfdLWn6yWKCmJKmdVxSgHiYvTCef4/qcBA==} resolution: {integrity: sha512-KF8+j9nNbUN8vzOFDpRMsaKBHZ/mcjEjMToVMJOhTozkDonQFFrRcfdLWn6yWKCmJKmdVxSgHiYvTCef4/qcBA==}
@@ -319,100 +306,100 @@ packages:
engines: {node: '>=18'} engines: {node: '>=18'}
hasBin: true hasBin: true
'@rolldown/binding-android-arm64@1.0.0-rc.17': '@rolldown/binding-android-arm64@1.0.0-rc.11':
resolution: {integrity: sha512-s70pVGhw4zqGeFnXWvAzJDlvxhlRollagdCCKRgOsgUOH3N1l0LIxf83AtGzmb5SiVM4Hjl5HyarMRfdfj3DaQ==} resolution: {integrity: sha512-SJ+/g+xNnOh6NqYxD0V3uVN4W3VfnrGsC9/hoglicgTNfABFG9JjISvkkU0dNY84MNHLWyOgxP9v9Y9pX4S7+A==}
engines: {node: ^20.19.0 || >=22.12.0} engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm64] cpu: [arm64]
os: [android] os: [android]
'@rolldown/binding-darwin-arm64@1.0.0-rc.17': '@rolldown/binding-darwin-arm64@1.0.0-rc.11':
resolution: {integrity: sha512-4ksWc9n0mhlZpZ9PMZgTGjeOPRu8MB1Z3Tz0Mo02eWfWCHMW1zN82Qz/pL/rC+yQa+8ZnutMF0JjJe7PjwasYw==} resolution: {integrity: sha512-7WQgR8SfOPwmDZGFkThUvsmd/nwAWv91oCO4I5LS7RKrssPZmOt7jONN0cW17ydGC1n/+puol1IpoieKqQidmg==}
engines: {node: ^20.19.0 || >=22.12.0} engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm64] cpu: [arm64]
os: [darwin] os: [darwin]
'@rolldown/binding-darwin-x64@1.0.0-rc.17': '@rolldown/binding-darwin-x64@1.0.0-rc.11':
resolution: {integrity: sha512-SUSDOI6WwUVNcWxd02QEBjLdY1VPHvlEkw6T/8nYG322iYWCTxRb1vzk4E+mWWYehTp7ERibq54LSJGjmouOsw==} resolution: {integrity: sha512-39Ks6UvIHq4rEogIfQBoBRusj0Q0nPVWIvqmwBLaT6aqQGIakHdESBVOPRRLacy4WwUPIx4ZKzfZ9PMW+IeyUQ==}
engines: {node: ^20.19.0 || >=22.12.0} engines: {node: ^20.19.0 || >=22.12.0}
cpu: [x64] cpu: [x64]
os: [darwin] os: [darwin]
'@rolldown/binding-freebsd-x64@1.0.0-rc.17': '@rolldown/binding-freebsd-x64@1.0.0-rc.11':
resolution: {integrity: sha512-hwnz3nw9dbJ05EDO/PvcjaaewqqDy7Y1rn1UO81l8iIK1GjenME75dl16ajbvSSMfv66WXSRCYKIqfgq2KCfxw==} resolution: {integrity: sha512-jfsm0ZHfhiqrvWjJAmzsqiIFPz5e7mAoCOPBNTcNgkiid/LaFKiq92+0ojH+nmJmKYkre4t71BWXUZDNp7vsag==}
engines: {node: ^20.19.0 || >=22.12.0} engines: {node: ^20.19.0 || >=22.12.0}
cpu: [x64] cpu: [x64]
os: [freebsd] os: [freebsd]
'@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.17': '@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.11':
resolution: {integrity: sha512-IS+W7epTcwANmFSQFrS1SivEXHtl1JtuQA9wlxrZTcNi6mx+FDOYrakGevvvTwgj2JvWiK8B29/qD9BELZPyXQ==} resolution: {integrity: sha512-zjQaUtSyq1nVe3nxmlSCuR96T1LPlpvmJ0SZy0WJFEsV4kFbXcq2u68L4E6O0XeFj4aex9bEauqjW8UQBeAvfQ==}
engines: {node: ^20.19.0 || >=22.12.0} engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm] cpu: [arm]
os: [linux] os: [linux]
'@rolldown/binding-linux-arm64-gnu@1.0.0-rc.17': '@rolldown/binding-linux-arm64-gnu@1.0.0-rc.11':
resolution: {integrity: sha512-e6usGaHKW5BMNZOymS1UcEYGowQMWcgZ71Z17Sl/h2+ZziNJ1a9n3Zvcz6LdRyIW5572wBCTH/Z+bKuZouGk9Q==} resolution: {integrity: sha512-WMW1yE6IOnehTcFE9eipFkm3XN63zypWlrJQ2iF7NrQ9b2LDRjumFoOGJE8RJJTJCTBAdmLMnJ8uVitACUUo1Q==}
engines: {node: ^20.19.0 || >=22.12.0} engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm64] cpu: [arm64]
os: [linux] os: [linux]
'@rolldown/binding-linux-arm64-musl@1.0.0-rc.17': '@rolldown/binding-linux-arm64-musl@1.0.0-rc.11':
resolution: {integrity: sha512-b/CgbwAJpmrRLp02RPfhbudf5tZnN9nsPWK82znefso832etkem8H7FSZwxrOI9djcdTP7U6YfNhbRnh7djErg==} resolution: {integrity: sha512-jfndI9tsfm4APzjNt6QdBkYwre5lRPUgHeDHoI7ydKUuJvz3lZeCfMsI56BZj+7BYqiKsJm7cfd/6KYV7ubrBg==}
engines: {node: ^20.19.0 || >=22.12.0} engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm64] cpu: [arm64]
os: [linux] os: [linux]
'@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.17': '@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.11':
resolution: {integrity: sha512-4EII1iNGRUN5WwGbF/kOh/EIkoDN9HsupgLQoXfY+D1oyJm7/F4t5PYU5n8SWZgG0FEwakyM8pGgwcBYruGTlA==} resolution: {integrity: sha512-ZlFgw46NOAGMgcdvdYwAGu2Q+SLFA9LzbJLW+iyMOJyhj5wk6P3KEE9Gct4xWwSzFoPI7JCdYmYMzVtlgQ+zfw==}
engines: {node: ^20.19.0 || >=22.12.0} engines: {node: ^20.19.0 || >=22.12.0}
cpu: [ppc64] cpu: [ppc64]
os: [linux] os: [linux]
'@rolldown/binding-linux-s390x-gnu@1.0.0-rc.17': '@rolldown/binding-linux-s390x-gnu@1.0.0-rc.11':
resolution: {integrity: sha512-AH8oq3XqQo4IibpVXvPeLDI5pzkpYn0WiZAfT05kFzoJ6tQNzwRdDYQ45M8I/gslbodRZwW8uxLhbSBbkv96rA==} resolution: {integrity: sha512-hIOYmuT6ofM4K04XAZd3OzMySEO4K0/nc9+jmNcxNAxRi6c5UWpqfw3KMFV4MVFWL+jQsSh+bGw2VqmaPMTLyw==}
engines: {node: ^20.19.0 || >=22.12.0} engines: {node: ^20.19.0 || >=22.12.0}
cpu: [s390x] cpu: [s390x]
os: [linux] os: [linux]
'@rolldown/binding-linux-x64-gnu@1.0.0-rc.17': '@rolldown/binding-linux-x64-gnu@1.0.0-rc.11':
resolution: {integrity: sha512-cLnjV3xfo7KslbU41Z7z8BH/E1y5mzUYzAqih1d1MDaIGZRCMqTijqLv76/P7fyHuvUcfGsIpqCdddbxLLK9rA==} resolution: {integrity: sha512-qXBQQO9OvkjjQPLdUVr7Nr2t3QTZI7s4KZtfw7HzBgjbmAPSFwSv4rmET9lLSgq3rH/ndA3ngv3Qb8l2njoPNA==}
engines: {node: ^20.19.0 || >=22.12.0} engines: {node: ^20.19.0 || >=22.12.0}
cpu: [x64] cpu: [x64]
os: [linux] os: [linux]
'@rolldown/binding-linux-x64-musl@1.0.0-rc.17': '@rolldown/binding-linux-x64-musl@1.0.0-rc.11':
resolution: {integrity: sha512-0phclDw1spsL7dUB37sIARuis2tAgomCJXAHZlpt8PXZ4Ba0dRP1e+66lsRqrfhISeN9bEGNjQs+T/Fbd7oYGw==} resolution: {integrity: sha512-/tpFfoSTzUkH9LPY+cYbqZBDyyX62w5fICq9qzsHLL8uTI6BHip3Q9Uzft0wylk/i8OOwKik8OxW+QAhDmzwmg==}
engines: {node: ^20.19.0 || >=22.12.0} engines: {node: ^20.19.0 || >=22.12.0}
cpu: [x64] cpu: [x64]
os: [linux] os: [linux]
'@rolldown/binding-openharmony-arm64@1.0.0-rc.17': '@rolldown/binding-openharmony-arm64@1.0.0-rc.11':
resolution: {integrity: sha512-0ag/hEgXOwgw4t8QyQvUCxvEg+V0KBcA6YuOx9g0r02MprutRF5dyljgm3EmR02O292UX7UeS6HzWHAl6KgyhA==} resolution: {integrity: sha512-mcp3Rio2w72IvdZG0oQ4bM2c2oumtwHfUfKncUM6zGgz0KgPz4YmDPQfnXEiY5t3+KD/i8HG2rOB/LxdmieK2g==}
engines: {node: ^20.19.0 || >=22.12.0} engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm64] cpu: [arm64]
os: [openharmony] os: [openharmony]
'@rolldown/binding-wasm32-wasi@1.0.0-rc.17': '@rolldown/binding-wasm32-wasi@1.0.0-rc.11':
resolution: {integrity: sha512-LEXei6vo0E5wTGwpkJ4KoT3OZJRnglwldt5ziLzOlc6qqb55z4tWNq2A+PFqCJuvWWdP53CVhG1Z9NtToDPJrA==} resolution: {integrity: sha512-LXk5Hii1Ph9asuGRjBuz8TUxdc1lWzB7nyfdoRgI0WGPZKmCxvlKk8KfYysqtr4MfGElu/f/pEQRh8fcEgkrWw==}
engines: {node: ^20.19.0 || >=22.12.0} engines: {node: '>=14.0.0'}
cpu: [wasm32] cpu: [wasm32]
'@rolldown/binding-win32-arm64-msvc@1.0.0-rc.17': '@rolldown/binding-win32-arm64-msvc@1.0.0-rc.11':
resolution: {integrity: sha512-gUmyzBl3SPMa6hrqFUth9sVfcLBlYsbMzBx5PlexMroZStgzGqlZ26pYG89rBb45Mnia+oil6YAIFeEWGWhoZA==} resolution: {integrity: sha512-dDwf5otnx0XgRY1yqxOC4ITizcdzS/8cQ3goOWv3jFAo4F+xQYni+hnMuO6+LssHHdJW7+OCVL3CoU4ycnh35Q==}
engines: {node: ^20.19.0 || >=22.12.0} engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm64] cpu: [arm64]
os: [win32] os: [win32]
'@rolldown/binding-win32-x64-msvc@1.0.0-rc.17': '@rolldown/binding-win32-x64-msvc@1.0.0-rc.11':
resolution: {integrity: sha512-3hkiolcUAvPB9FLb3UZdfjVVNWherN1f/skkGWJP/fgSQhYUZpSIRr0/I8ZK9TkF3F7kxvJAk0+IcKvPHk9qQg==} resolution: {integrity: sha512-LN4/skhSggybX71ews7dAj6r2geaMJfm3kMbK2KhFMg9B10AZXnKoLCVVgzhMHL0S+aKtr4p8QbAW8k+w95bAA==}
engines: {node: ^20.19.0 || >=22.12.0} engines: {node: ^20.19.0 || >=22.12.0}
cpu: [x64] cpu: [x64]
os: [win32] os: [win32]
'@rolldown/pluginutils@1.0.0-rc.13': '@rolldown/pluginutils@1.0.0-beta.19':
resolution: {integrity: sha512-3ngTAv6F/Py35BsYbeeLeecvhMKdsKm4AoOETVhAA+Qc8nrA2I0kF7oa93mE9qnIurngOSpMnQ0x2nQY2FPviA==} resolution: {integrity: sha512-3FL3mnMbPu0muGOCaKAhhFEYmqv9eTfPSJRJmANrCwtgK8VuxpsZDGK+m0LYAGoyO8+0j5uRe4PeyPDK1yA/hA==}
'@rolldown/pluginutils@1.0.0-rc.17': '@rolldown/pluginutils@1.0.0-rc.11':
resolution: {integrity: sha512-n8iosDOt6Ig1UhJ2AYqoIhHWh/isz0xpicHTzpKBeotdVsTEcxsSA/i3EVM7gQAj0rU27OLAxCjzlj15IWY7bg==} resolution: {integrity: sha512-xQO9vbwBecJRv9EUcQ/y0dzSTJgA7Q6UVN7xp6B81+tBGSLVAK03yJ9NkJaUA7JFD91kbjxRSC/mDnmvXzbHoQ==}
'@tweenjs/tween.js@23.1.3': '@tweenjs/tween.js@23.1.3':
resolution: {integrity: sha512-vJmvvwFxYuGnF2axRtPYocag6Clbb5YS7kLL+SO/TeVFzHqDIWrNKYtcsPMibjDx9O+bu+psAy9NKfWklassUA==} resolution: {integrity: sha512-vJmvvwFxYuGnF2axRtPYocag6Clbb5YS7kLL+SO/TeVFzHqDIWrNKYtcsPMibjDx9O+bu+psAy9NKfWklassUA==}
@@ -426,8 +413,8 @@ packages:
'@types/json-schema@7.0.15': '@types/json-schema@7.0.15':
resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==}
'@types/node@24.12.2': '@types/node@22.15.14':
resolution: {integrity: sha512-A1sre26ke7HDIuY/M23nd9gfB+nrmhtYyMINbjI1zHJxYteKR6qSMX56FsmjMcDb3SMcjJg5BiRRgOCC/yBD0g==} resolution: {integrity: sha512-BL1eyu/XWsFGTtDWOYULQEs4KR0qdtYfCxYAUYRoB7JP7h9ETYLgQTww6kH8Sj2C0pFGgrpM0XKv6/kbIzYJ1g==}
'@types/raf@3.4.3': '@types/raf@3.4.3':
resolution: {integrity: sha512-c4YAvMedbPZ5tEyxzQdMoOhhJ4RD3rngZIdwC2/qDN3d7JpEhB6fiBRKVY1lg5B7Wk+uPBjn5f39j1/2MY1oOw==} resolution: {integrity: sha512-c4YAvMedbPZ5tEyxzQdMoOhhJ4RD3rngZIdwC2/qDN3d7JpEhB6fiBRKVY1lg5B7Wk+uPBjn5f39j1/2MY1oOw==}
@@ -491,22 +478,13 @@ packages:
resolution: {integrity: sha512-1rYQTCLFFzOI5Nl0c8LUpJT8HxpwVRn9E4CkMsYfuN6ctmQqExjSTzzSk0Tz2apmXy7WU6/6fyaZVVA/thPN+w==} resolution: {integrity: sha512-1rYQTCLFFzOI5Nl0c8LUpJT8HxpwVRn9E4CkMsYfuN6ctmQqExjSTzzSk0Tz2apmXy7WU6/6fyaZVVA/thPN+w==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
'@vitejs/plugin-vue@6.0.6': '@vitejs/plugin-vue@6.0.0':
resolution: {integrity: sha512-u9HHgfrq3AjXlysn0eINFnWQOJQLO9WN6VprZ8FXl7A2bYisv3Hui9Ij+7QZ41F/WYWarHjwBbXtD7dKg3uxbg==} resolution: {integrity: sha512-iAliE72WsdhjzTOp2DtvKThq1VBC4REhwRcaA+zPAAph6I+OQhUXv+Xu2KS7ElxYtb7Zc/3R30Hwv1DxEo7NXQ==}
engines: {node: ^20.19.0 || >=22.12.0} engines: {node: ^20.19.0 || >=22.12.0}
peerDependencies: peerDependencies:
vite: ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0 vite: ^5.0.0 || ^6.0.0 || ^7.0.0
vue: ^3.2.25 vue: ^3.2.25
'@volar/language-core@2.4.28':
resolution: {integrity: sha512-w4qhIJ8ZSitgLAkVay6AbcnC7gP3glYM3fYwKV3srj8m494E3xtrCv6E+bWviiK/8hs6e6t1ij1s2Endql7vzQ==}
'@volar/source-map@2.4.28':
resolution: {integrity: sha512-yX2BDBqJkRXfKw8my8VarTyjv48QwxdJtvRgUpNE5erCsgEUdI2DsLbpa+rOQVAJYshY99szEcRDmyHbF10ggQ==}
'@volar/typescript@2.4.28':
resolution: {integrity: sha512-Ja6yvWrbis2QtN4ClAKreeUZPVYMARDYZl9LMEv1iQ1QdepB6wn0jTRxA9MftYmYa4DQ4k/DaSZpFPUfxl8giw==}
'@vue/compiler-core@3.5.13': '@vue/compiler-core@3.5.13':
resolution: {integrity: sha512-oOdAkwqUfW1WqpwSYJce06wvt6HljgY3fGeM9NcVA1HaYOij3mZG9Rkysn0OHuyUAGMbEbARIpsG+LPVlBJ5/Q==} resolution: {integrity: sha512-oOdAkwqUfW1WqpwSYJce06wvt6HljgY3fGeM9NcVA1HaYOij3mZG9Rkysn0OHuyUAGMbEbARIpsG+LPVlBJ5/Q==}
@@ -548,9 +526,6 @@ packages:
typescript: typescript:
optional: true optional: true
'@vue/language-core@3.2.5':
resolution: {integrity: sha512-d3OIxN/+KRedeM5wQ6H6NIpwS3P5gC9nmyaHgBk+rO6dIsjY+tOh4UlPpiZbAh3YtLdCGEX4M16RmsBqPmJV+g==}
'@vue/reactivity@3.5.13': '@vue/reactivity@3.5.13':
resolution: {integrity: sha512-NaCwtw8o48B9I6L1zl2p41OHo/2Z4wqYGGIK1Khu5T7yxrn+ATOixn/Udn2m+6kZKB/J7cuT9DbWWhRxqixACg==} resolution: {integrity: sha512-NaCwtw8o48B9I6L1zl2p41OHo/2Z4wqYGGIK1Khu5T7yxrn+ATOixn/Udn2m+6kZKB/J7cuT9DbWWhRxqixACg==}
@@ -606,9 +581,6 @@ packages:
ajv@6.12.6: ajv@6.12.6:
resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==} resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==}
alien-signals@3.1.2:
resolution: {integrity: sha512-d9dYqZTS90WLiU0I5c6DHj/HcKkF8ZyGN3G5x8wSbslulz70KOxaqCT0hQCo9KOyhVqzqGojvNdJXoTumZOtcw==}
ansi-styles@4.3.0: ansi-styles@4.3.0:
resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==}
engines: {node: '>=8'} engines: {node: '>=8'}
@@ -1163,9 +1135,6 @@ packages:
ms@2.1.3: ms@2.1.3:
resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
muggle-string@0.4.1:
resolution: {integrity: sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ==}
nanoid@3.3.11: nanoid@3.3.11:
resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==} resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==}
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
@@ -1196,9 +1165,6 @@ packages:
resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==}
engines: {node: '>=6'} engines: {node: '>=6'}
path-browserify@1.0.1:
resolution: {integrity: sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==}
path-exists@4.0.0: path-exists@4.0.0:
resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==}
engines: {node: '>=8'} engines: {node: '>=8'}
@@ -1247,14 +1213,14 @@ packages:
resolution: {integrity: sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==} resolution: {integrity: sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==}
engines: {node: '>=4'} engines: {node: '>=4'}
postcss@8.5.13:
resolution: {integrity: sha512-qif0+jGGZoLWdHey3UFHHWP0H7Gbmsk8T5VEqyYFbWqPr1XqvLGBbk/sl8V5exGmcYJklJOhOQq1pV9IcsiFag==}
engines: {node: ^10 || ^12 || >=14}
postcss@8.5.3: postcss@8.5.3:
resolution: {integrity: sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A==} resolution: {integrity: sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A==}
engines: {node: ^10 || ^12 || >=14} engines: {node: ^10 || ^12 || >=14}
postcss@8.5.8:
resolution: {integrity: sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==}
engines: {node: ^10 || ^12 || >=14}
prelude-ls@1.2.1: prelude-ls@1.2.1:
resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==}
engines: {node: '>= 0.8.0'} engines: {node: '>= 0.8.0'}
@@ -1303,8 +1269,8 @@ packages:
resolution: {integrity: sha512-9aZLIrhRaD97sgVhtJOW6ckOEh6/GnvQtdVNfdZ6s67+3/XwLS9lBcQYzEEhYVeUowN7pRzMLsyGhK2i/xvWbw==} resolution: {integrity: sha512-9aZLIrhRaD97sgVhtJOW6ckOEh6/GnvQtdVNfdZ6s67+3/XwLS9lBcQYzEEhYVeUowN7pRzMLsyGhK2i/xvWbw==}
engines: {node: '>= 0.8.15'} engines: {node: '>= 0.8.15'}
rolldown@1.0.0-rc.17: rolldown@1.0.0-rc.11:
resolution: {integrity: sha512-ZrT53oAKrtA4+YtBWPQbtPOxIbVDbxT0orcYERKd63VJTF13zPcgXTvD4843L8pcsI7M6MErt8QtON6lrB9tyA==} resolution: {integrity: sha512-NRjoKMusSjfRbSYiH3VSumlkgFe7kYAa3pzVOsVYVFY3zb5d7nS+a3KGQ7hJKXuYWbzJKPVQ9Wxq2UvyK+ENpw==}
engines: {node: ^20.19.0 || >=22.12.0} engines: {node: ^20.19.0 || >=22.12.0}
hasBin: true hasBin: true
@@ -1367,8 +1333,8 @@ packages:
three@0.178.0: three@0.178.0:
resolution: {integrity: sha512-ybFIB0+x8mz0wnZgSGy2MO/WCO6xZhQSZnmfytSPyNpM0sBafGRVhdaj+erYh5U+RhQOAg/eXqw5uVDiM2BjhQ==} resolution: {integrity: sha512-ybFIB0+x8mz0wnZgSGy2MO/WCO6xZhQSZnmfytSPyNpM0sBafGRVhdaj+erYh5U+RhQOAg/eXqw5uVDiM2BjhQ==}
tinyglobby@0.2.16: tinyglobby@0.2.15:
resolution: {integrity: sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==} resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==}
engines: {node: '>=12.0.0'} engines: {node: '>=12.0.0'}
to-regex-range@5.0.1: to-regex-range@5.0.1:
@@ -1403,8 +1369,8 @@ packages:
engines: {node: '>=14.17'} engines: {node: '>=14.17'}
hasBin: true hasBin: true
undici-types@7.16.0: undici-types@6.21.0:
resolution: {integrity: sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==} resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==}
upath@2.0.1: upath@2.0.1:
resolution: {integrity: sha512-1uEe95xksV1O0CYKXo8vQvN1JEbtJp7lb7C5U9HMsIp6IVwntkH/oNUzyVNQSd4S1sYk2FpSSW44FqMc8qee5w==} resolution: {integrity: sha512-1uEe95xksV1O0CYKXo8vQvN1JEbtJp7lb7C5U9HMsIp6IVwntkH/oNUzyVNQSd4S1sYk2FpSSW44FqMc8qee5w==}
@@ -1427,14 +1393,14 @@ packages:
vue: ^3.0.0 vue: ^3.0.0
vuetify: ^3.0.0 vuetify: ^3.0.0
vite@8.0.10: vite@8.0.2:
resolution: {integrity: sha512-rZuUu9j6J5uotLDs+cAA4O5H4K1SfPliUlQwqa6YEwSrWDZzP4rhm00oJR5snMewjxF5V/K3D4kctsUTsIU9Mw==} resolution: {integrity: sha512-1gFhNi+bHhRE/qKZOJXACm6tX4bA3Isy9KuKF15AgSRuRazNBOJfdDemPBU16/mpMxApDPrWvZ08DcLPEoRnuA==}
engines: {node: ^20.19.0 || >=22.12.0} engines: {node: ^20.19.0 || >=22.12.0}
hasBin: true hasBin: true
peerDependencies: peerDependencies:
'@types/node': ^20.19.0 || >=22.12.0 '@types/node': ^20.19.0 || >=22.12.0
'@vitejs/devtools': ^0.1.0 '@vitejs/devtools': ^0.1.0
esbuild: ^0.27.0 || ^0.28.0 esbuild: ^0.27.0
jiti: '>=1.21.0' jiti: '>=1.21.0'
less: ^4.0.0 less: ^4.0.0
sass: ^1.70.0 sass: ^1.70.0
@@ -1470,9 +1436,6 @@ packages:
yaml: yaml:
optional: true optional: true
vscode-uri@3.1.0:
resolution: {integrity: sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ==}
vue-eslint-parser@10.1.3: vue-eslint-parser@10.1.3:
resolution: {integrity: sha512-dbCBnd2e02dYWsXoqX5yKUZlOt+ExIpq7hmHKPb5ZqKcjf++Eo0hMseFTZMLKThrUk61m+Uv6A2YSBve6ZvuDQ==} resolution: {integrity: sha512-dbCBnd2e02dYWsXoqX5yKUZlOt+ExIpq7hmHKPb5ZqKcjf++Eo0hMseFTZMLKThrUk61m+Uv6A2YSBve6ZvuDQ==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
@@ -1484,12 +1447,6 @@ packages:
peerDependencies: peerDependencies:
vue: ^3.2.0 vue: ^3.2.0
vue-tsc@3.2.5:
resolution: {integrity: sha512-/htfTCMluQ+P2FISGAooul8kO4JMheOTCbCy4M6dYnYYjqLe3BExZudAua6MSIKSFYQtFOYAll7XobYwcpokGA==}
hasBin: true
peerDependencies:
typescript: '>=5.0.0'
vue3-virtual-scroll-list@0.2.1: vue3-virtual-scroll-list@0.2.1:
resolution: {integrity: sha512-G4KxITUOy9D4ro15zOp40D6ogmMefzjIyMsBKqN3xGbV1P6dlKYMx+BBXCKm3Nr/6iipcUKM272Sh2AJRyWMyQ==} resolution: {integrity: sha512-G4KxITUOy9D4ro15zOp40D6ogmMefzjIyMsBKqN3xGbV1P6dlKYMx+BBXCKm3Nr/6iipcUKM272Sh2AJRyWMyQ==}
peerDependencies: peerDependencies:
@@ -1541,8 +1498,6 @@ packages:
snapshots: snapshots:
'@adam-rocska/units-and-measurement@1.2.0': {}
'@babel/helper-string-parser@7.27.1': {} '@babel/helper-string-parser@7.27.1': {}
'@babel/helper-validator-identifier@7.27.1': {} '@babel/helper-validator-identifier@7.27.1': {}
@@ -1560,18 +1515,18 @@ snapshots:
'@dimforge/rapier3d-compat@0.12.0': {} '@dimforge/rapier3d-compat@0.12.0': {}
'@emnapi/core@1.10.0': '@emnapi/core@1.9.1':
dependencies: dependencies:
'@emnapi/wasi-threads': 1.2.1 '@emnapi/wasi-threads': 1.2.0
tslib: 2.8.1 tslib: 2.8.1
optional: true optional: true
'@emnapi/runtime@1.10.0': '@emnapi/runtime@1.9.1':
dependencies: dependencies:
tslib: 2.8.1 tslib: 2.8.1
optional: true optional: true
'@emnapi/wasi-threads@1.2.1': '@emnapi/wasi-threads@1.2.0':
dependencies: dependencies:
tslib: 2.8.1 tslib: 2.8.1
optional: true optional: true
@@ -1641,10 +1596,10 @@ snapshots:
'@msgpack/msgpack@3.1.2': {} '@msgpack/msgpack@3.1.2': {}
'@napi-rs/wasm-runtime@1.1.4(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)': '@napi-rs/wasm-runtime@1.1.1':
dependencies: dependencies:
'@emnapi/core': 1.10.0 '@emnapi/core': 1.9.1
'@emnapi/runtime': 1.10.0 '@emnapi/runtime': 1.9.1
'@tybys/wasm-util': 0.10.1 '@tybys/wasm-util': 0.10.1
optional: true optional: true
@@ -1660,7 +1615,7 @@ snapshots:
'@nodelib/fs.scandir': 2.1.5 '@nodelib/fs.scandir': 2.1.5
fastq: 1.19.1 fastq: 1.19.1
'@oxc-project/types@0.127.0': {} '@oxc-project/types@0.122.0': {}
'@parcel/watcher-android-arm64@2.5.1': '@parcel/watcher-android-arm64@2.5.1':
optional: true optional: true
@@ -1729,58 +1684,56 @@ snapshots:
dependencies: dependencies:
playwright: 1.56.1 playwright: 1.56.1
'@rolldown/binding-android-arm64@1.0.0-rc.17': '@rolldown/binding-android-arm64@1.0.0-rc.11':
optional: true optional: true
'@rolldown/binding-darwin-arm64@1.0.0-rc.17': '@rolldown/binding-darwin-arm64@1.0.0-rc.11':
optional: true optional: true
'@rolldown/binding-darwin-x64@1.0.0-rc.17': '@rolldown/binding-darwin-x64@1.0.0-rc.11':
optional: true optional: true
'@rolldown/binding-freebsd-x64@1.0.0-rc.17': '@rolldown/binding-freebsd-x64@1.0.0-rc.11':
optional: true optional: true
'@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.17': '@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.11':
optional: true optional: true
'@rolldown/binding-linux-arm64-gnu@1.0.0-rc.17': '@rolldown/binding-linux-arm64-gnu@1.0.0-rc.11':
optional: true optional: true
'@rolldown/binding-linux-arm64-musl@1.0.0-rc.17': '@rolldown/binding-linux-arm64-musl@1.0.0-rc.11':
optional: true optional: true
'@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.17': '@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.11':
optional: true optional: true
'@rolldown/binding-linux-s390x-gnu@1.0.0-rc.17': '@rolldown/binding-linux-s390x-gnu@1.0.0-rc.11':
optional: true optional: true
'@rolldown/binding-linux-x64-gnu@1.0.0-rc.17': '@rolldown/binding-linux-x64-gnu@1.0.0-rc.11':
optional: true optional: true
'@rolldown/binding-linux-x64-musl@1.0.0-rc.17': '@rolldown/binding-linux-x64-musl@1.0.0-rc.11':
optional: true optional: true
'@rolldown/binding-openharmony-arm64@1.0.0-rc.17': '@rolldown/binding-openharmony-arm64@1.0.0-rc.11':
optional: true optional: true
'@rolldown/binding-wasm32-wasi@1.0.0-rc.17': '@rolldown/binding-wasm32-wasi@1.0.0-rc.11':
dependencies: dependencies:
'@emnapi/core': 1.10.0 '@napi-rs/wasm-runtime': 1.1.1
'@emnapi/runtime': 1.10.0
'@napi-rs/wasm-runtime': 1.1.4(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)
optional: true optional: true
'@rolldown/binding-win32-arm64-msvc@1.0.0-rc.17': '@rolldown/binding-win32-arm64-msvc@1.0.0-rc.11':
optional: true optional: true
'@rolldown/binding-win32-x64-msvc@1.0.0-rc.17': '@rolldown/binding-win32-x64-msvc@1.0.0-rc.11':
optional: true optional: true
'@rolldown/pluginutils@1.0.0-rc.13': {} '@rolldown/pluginutils@1.0.0-beta.19': {}
'@rolldown/pluginutils@1.0.0-rc.17': {} '@rolldown/pluginutils@1.0.0-rc.11': {}
'@tweenjs/tween.js@23.1.3': {} '@tweenjs/tween.js@23.1.3': {}
@@ -1793,9 +1746,9 @@ snapshots:
'@types/json-schema@7.0.15': {} '@types/json-schema@7.0.15': {}
'@types/node@24.12.2': '@types/node@22.15.14':
dependencies: dependencies:
undici-types: 7.16.0 undici-types: 6.21.0
'@types/raf@3.4.3': '@types/raf@3.4.3':
optional: true optional: true
@@ -1894,24 +1847,12 @@ snapshots:
'@typescript-eslint/types': 8.32.0 '@typescript-eslint/types': 8.32.0
eslint-visitor-keys: 4.2.0 eslint-visitor-keys: 4.2.0
'@vitejs/plugin-vue@6.0.6(vite@8.0.10(@types/node@24.12.2)(sass@1.89.2))(vue@3.5.13(typescript@5.8.3))': '@vitejs/plugin-vue@6.0.0(vite@8.0.2(@types/node@22.15.14)(sass@1.89.2))(vue@3.5.13(typescript@5.8.3))':
dependencies: dependencies:
'@rolldown/pluginutils': 1.0.0-rc.13 '@rolldown/pluginutils': 1.0.0-beta.19
vite: 8.0.10(@types/node@24.12.2)(sass@1.89.2) vite: 8.0.2(@types/node@22.15.14)(sass@1.89.2)
vue: 3.5.13(typescript@5.8.3) vue: 3.5.13(typescript@5.8.3)
'@volar/language-core@2.4.28':
dependencies:
'@volar/source-map': 2.4.28
'@volar/source-map@2.4.28': {}
'@volar/typescript@2.4.28':
dependencies:
'@volar/language-core': 2.4.28
path-browserify: 1.0.1
vscode-uri: 3.1.0
'@vue/compiler-core@3.5.13': '@vue/compiler-core@3.5.13':
dependencies: dependencies:
'@babel/parser': 7.27.2 '@babel/parser': 7.27.2
@@ -1984,16 +1925,6 @@ snapshots:
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
'@vue/language-core@3.2.5':
dependencies:
'@volar/language-core': 2.4.28
'@vue/compiler-dom': 3.5.13
'@vue/shared': 3.5.13
alien-signals: 3.1.2
muggle-string: 0.4.1
path-browserify: 1.0.1
picomatch: 4.0.4
'@vue/reactivity@3.5.13': '@vue/reactivity@3.5.13':
dependencies: dependencies:
'@vue/shared': 3.5.13 '@vue/shared': 3.5.13
@@ -2050,8 +1981,6 @@ snapshots:
json-schema-traverse: 0.4.1 json-schema-traverse: 0.4.1
uri-js: 4.4.1 uri-js: 4.4.1
alien-signals@3.1.2: {}
ansi-styles@4.3.0: ansi-styles@4.3.0:
dependencies: dependencies:
color-convert: 2.0.1 color-convert: 2.0.1
@@ -2572,8 +2501,6 @@ snapshots:
ms@2.1.3: {} ms@2.1.3: {}
muggle-string@0.4.1: {}
nanoid@3.3.11: {} nanoid@3.3.11: {}
natural-compare@1.4.0: {} natural-compare@1.4.0: {}
@@ -2606,8 +2533,6 @@ snapshots:
dependencies: dependencies:
callsites: 3.1.0 callsites: 3.1.0
path-browserify@1.0.1: {}
path-exists@4.0.0: {} path-exists@4.0.0: {}
path-key@3.1.1: {} path-key@3.1.1: {}
@@ -2643,13 +2568,13 @@ snapshots:
cssesc: 3.0.0 cssesc: 3.0.0
util-deprecate: 1.0.2 util-deprecate: 1.0.2
postcss@8.5.13: postcss@8.5.3:
dependencies: dependencies:
nanoid: 3.3.11 nanoid: 3.3.11
picocolors: 1.1.1 picocolors: 1.1.1
source-map-js: 1.2.1 source-map-js: 1.2.1
postcss@8.5.3: postcss@8.5.8:
dependencies: dependencies:
nanoid: 3.3.11 nanoid: 3.3.11
picocolors: 1.1.1 picocolors: 1.1.1
@@ -2688,26 +2613,26 @@ snapshots:
rgbcolor@1.0.1: rgbcolor@1.0.1:
optional: true optional: true
rolldown@1.0.0-rc.17: rolldown@1.0.0-rc.11:
dependencies: dependencies:
'@oxc-project/types': 0.127.0 '@oxc-project/types': 0.122.0
'@rolldown/pluginutils': 1.0.0-rc.17 '@rolldown/pluginutils': 1.0.0-rc.11
optionalDependencies: optionalDependencies:
'@rolldown/binding-android-arm64': 1.0.0-rc.17 '@rolldown/binding-android-arm64': 1.0.0-rc.11
'@rolldown/binding-darwin-arm64': 1.0.0-rc.17 '@rolldown/binding-darwin-arm64': 1.0.0-rc.11
'@rolldown/binding-darwin-x64': 1.0.0-rc.17 '@rolldown/binding-darwin-x64': 1.0.0-rc.11
'@rolldown/binding-freebsd-x64': 1.0.0-rc.17 '@rolldown/binding-freebsd-x64': 1.0.0-rc.11
'@rolldown/binding-linux-arm-gnueabihf': 1.0.0-rc.17 '@rolldown/binding-linux-arm-gnueabihf': 1.0.0-rc.11
'@rolldown/binding-linux-arm64-gnu': 1.0.0-rc.17 '@rolldown/binding-linux-arm64-gnu': 1.0.0-rc.11
'@rolldown/binding-linux-arm64-musl': 1.0.0-rc.17 '@rolldown/binding-linux-arm64-musl': 1.0.0-rc.11
'@rolldown/binding-linux-ppc64-gnu': 1.0.0-rc.17 '@rolldown/binding-linux-ppc64-gnu': 1.0.0-rc.11
'@rolldown/binding-linux-s390x-gnu': 1.0.0-rc.17 '@rolldown/binding-linux-s390x-gnu': 1.0.0-rc.11
'@rolldown/binding-linux-x64-gnu': 1.0.0-rc.17 '@rolldown/binding-linux-x64-gnu': 1.0.0-rc.11
'@rolldown/binding-linux-x64-musl': 1.0.0-rc.17 '@rolldown/binding-linux-x64-musl': 1.0.0-rc.11
'@rolldown/binding-openharmony-arm64': 1.0.0-rc.17 '@rolldown/binding-openharmony-arm64': 1.0.0-rc.11
'@rolldown/binding-wasm32-wasi': 1.0.0-rc.17 '@rolldown/binding-wasm32-wasi': 1.0.0-rc.11
'@rolldown/binding-win32-arm64-msvc': 1.0.0-rc.17 '@rolldown/binding-win32-arm64-msvc': 1.0.0-rc.11
'@rolldown/binding-win32-x64-msvc': 1.0.0-rc.17 '@rolldown/binding-win32-x64-msvc': 1.0.0-rc.11
run-parallel@1.2.0: run-parallel@1.2.0:
dependencies: dependencies:
@@ -2761,7 +2686,7 @@ snapshots:
three@0.178.0: {} three@0.178.0: {}
tinyglobby@0.2.16: tinyglobby@0.2.15:
dependencies: dependencies:
fdir: 6.5.0(picomatch@4.0.4) fdir: 6.5.0(picomatch@4.0.4)
picomatch: 4.0.4 picomatch: 4.0.4
@@ -2794,7 +2719,7 @@ snapshots:
typescript@5.8.3: {} typescript@5.8.3: {}
undici-types@7.16.0: {} undici-types@6.21.0: {}
upath@2.0.1: {} upath@2.0.1: {}
@@ -2809,31 +2734,29 @@ snapshots:
base64-arraybuffer: 1.0.2 base64-arraybuffer: 1.0.2
optional: true optional: true
vite-plugin-vuetify@2.1.1(vite@8.0.10(@types/node@24.12.2)(sass@1.89.2))(vue@3.5.13(typescript@5.8.3))(vuetify@3.8.3): vite-plugin-vuetify@2.1.1(vite@8.0.2(@types/node@22.15.14)(sass@1.89.2))(vue@3.5.13(typescript@5.8.3))(vuetify@3.8.3):
dependencies: dependencies:
'@vuetify/loader-shared': 2.1.0(vue@3.5.13(typescript@5.8.3))(vuetify@3.8.3) '@vuetify/loader-shared': 2.1.0(vue@3.5.13(typescript@5.8.3))(vuetify@3.8.3)
debug: 4.4.0 debug: 4.4.0
upath: 2.0.1 upath: 2.0.1
vite: 8.0.10(@types/node@24.12.2)(sass@1.89.2) vite: 8.0.2(@types/node@22.15.14)(sass@1.89.2)
vue: 3.5.13(typescript@5.8.3) vue: 3.5.13(typescript@5.8.3)
vuetify: 3.8.3(typescript@5.8.3)(vite-plugin-vuetify@2.1.1)(vue@3.5.13(typescript@5.8.3)) vuetify: 3.8.3(typescript@5.8.3)(vite-plugin-vuetify@2.1.1)(vue@3.5.13(typescript@5.8.3))
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
vite@8.0.10(@types/node@24.12.2)(sass@1.89.2): vite@8.0.2(@types/node@22.15.14)(sass@1.89.2):
dependencies: dependencies:
lightningcss: 1.32.0 lightningcss: 1.32.0
picomatch: 4.0.4 picomatch: 4.0.4
postcss: 8.5.13 postcss: 8.5.8
rolldown: 1.0.0-rc.17 rolldown: 1.0.0-rc.11
tinyglobby: 0.2.16 tinyglobby: 0.2.15
optionalDependencies: optionalDependencies:
'@types/node': 24.12.2 '@types/node': 22.15.14
fsevents: 2.3.3 fsevents: 2.3.3
sass: 1.89.2 sass: 1.89.2
vscode-uri@3.1.0: {}
vue-eslint-parser@10.1.3(eslint@9.31.0): vue-eslint-parser@10.1.3(eslint@9.31.0):
dependencies: dependencies:
debug: 4.4.0 debug: 4.4.0
@@ -2852,12 +2775,6 @@ snapshots:
'@vue/devtools-api': 6.6.4 '@vue/devtools-api': 6.6.4
vue: 3.5.13(typescript@5.8.3) vue: 3.5.13(typescript@5.8.3)
vue-tsc@3.2.5(typescript@5.8.3):
dependencies:
'@volar/typescript': 2.4.28
'@vue/language-core': 3.2.5
typescript: 5.8.3
vue3-virtual-scroll-list@0.2.1(vue@3.5.13(typescript@5.8.3)): vue3-virtual-scroll-list@0.2.1(vue@3.5.13(typescript@5.8.3)):
dependencies: dependencies:
vue: 3.5.13(typescript@5.8.3) vue: 3.5.13(typescript@5.8.3)
@@ -2877,7 +2794,7 @@ snapshots:
vue: 3.5.13(typescript@5.8.3) vue: 3.5.13(typescript@5.8.3)
optionalDependencies: optionalDependencies:
typescript: 5.8.3 typescript: 5.8.3
vite-plugin-vuetify: 2.1.1(vite@8.0.10(@types/node@24.12.2)(sass@1.89.2))(vue@3.5.13(typescript@5.8.3))(vuetify@3.8.3) vite-plugin-vuetify: 2.1.1(vite@8.0.2(@types/node@22.15.14)(sass@1.89.2))(vue@3.5.13(typescript@5.8.3))(vuetify@3.8.3)
which@2.0.2: which@2.0.2:
dependencies: dependencies:

View File

@@ -1,3 +0,0 @@
allowBuilds:
'@parcel/watcher': true
core-js: true

View File

@@ -11,10 +11,9 @@ import { useTheme } from "vuetify";
import { restoreThemeConfig } from "@/lib/ThemeManager"; import { restoreThemeConfig } from "@/lib/ThemeManager";
const is_demo = import.meta.env.MODE === "demo"; const is_demo = import.meta.env.MODE === "demo";
const backendHost = inject<string>("backendHost");
if (!is_demo) { if (!is_demo) {
const websocket = new AutoReconnectingWebsocket( const websocket = new AutoReconnectingWebsocket(
`ws://${backendHost}/websocket_data`, `ws://${inject("backendHost")}/websocket_data`,
() => { () => {
useStateStore().$patch({ backendConnected: true }); useStateStore().$patch({ backendConnected: true });
}, },
@@ -76,6 +75,36 @@ onBeforeMount(() => {
<photon-log-view /> <photon-log-view />
<photon-error-snackbar /> <photon-error-snackbar />
</v-app> </v-app>
<!-- Quarky overlay -->
<div class="quarky-overlay">
<div id="quarkyContainer" class="quarky-container" style="left: calc(100vw - 550px); top: calc(100vh - 550px)">
<img id="quarkyImage" src="" alt="Quarky" />
<div
id="quarkySpeechBubble"
style="
display: none;
position: absolute;
left: 50%;
top: 0;
transform: translateX(-50%);
min-width: 120px;
max-width: 320px;
padding: 16px 24px;
background: #fff;
color: #222;
border-radius: 16px;
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15);
font-size: 1.2em;
font-family: sans-serif;
opacity: 0;
transition: opacity 0.7s;
pointer-events: none;
z-index: 10;
"
></div>
</div>
</div>
</template> </template>
<style lang="scss"> <style lang="scss">
@@ -118,4 +147,33 @@ onBeforeMount(() => {
div.v-layout { div.v-layout {
overflow: unset !important; overflow: unset !important;
} }
/* Overlay container for Quarky */
.quarky-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
pointer-events: none;
z-index: 1000;
}
/* Quarky animation container */
.quarky-container {
position: absolute;
width: 500px;
height: 500px;
background-color: transparent;
transition:
left 1s cubic-bezier(0.42, 0, 0.58, 1),
top 1s cubic-bezier(0.42, 0, 0.58, 1);
}
.quarky-container img {
width: 100%;
height: 100%;
object-fit: contain;
display: block;
}
</style> </style>

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 101 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 82 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 102 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 113 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 99 KiB

View File

@@ -3,7 +3,7 @@ import type { PhotonTarget } from "@/types/PhotonTrackingTypes";
// @ts-expect-error Intellisense says these conflict with the dynamic imports below // @ts-expect-error Intellisense says these conflict with the dynamic imports below
import type { Mesh, Object3D, PerspectiveCamera, Scene, WebGLRenderer } from "three"; import type { Mesh, Object3D, PerspectiveCamera, Scene, WebGLRenderer } from "three";
// @ts-expect-error Intellisense says these conflict with the dynamic imports below // @ts-expect-error Intellisense says these conflict with the dynamic imports below
import type { TrackballControls } from "three/examples/jsm/controls/TrackballControls.js"; import type { TrackballControls } from "three/examples/jsm/controls/TrackballControls";
import { onBeforeUnmount, onMounted, watchEffect } from "vue"; import { onBeforeUnmount, onMounted, watchEffect } from "vue";
const { const {
ArrowHelper, ArrowHelper,
@@ -20,7 +20,7 @@ const {
Scene, Scene,
WebGLRenderer WebGLRenderer
} = await import("three"); } = await import("three");
const { TrackballControls } = await import("three/examples/jsm/controls/TrackballControls.js"); const { TrackballControls } = await import("three/examples/jsm/controls/TrackballControls");
import { useCameraSettingsStore } from "@/stores/settings/CameraSettingsStore"; import { useCameraSettingsStore } from "@/stores/settings/CameraSettingsStore";
import { createPerspectiveCamera } from "@/lib/ThreeUtils"; import { createPerspectiveCamera } from "@/lib/ThreeUtils";
@@ -213,14 +213,14 @@ onMounted(async () => {
renderer.render(scene, camera); renderer.render(scene, camera);
}; };
await drawTargets(props.targets); drawTargets(props.targets);
animate(); animate();
}); });
onBeforeUnmount(() => { onBeforeUnmount(() => {
window.removeEventListener("resize", onWindowResize); window.removeEventListener("resize", onWindowResize);
}); });
watchEffect(() => { watchEffect(() => {
void drawTargets(props.targets); drawTargets(props.targets);
}); });
</script> </script>

View File

@@ -1,13 +1,5 @@
<script setup lang="ts"> <script setup lang="ts">
import { onBeforeUnmount, onMounted, ref, watch, watchEffect, type Ref } from "vue"; import { onBeforeUnmount, onMounted, ref, watch, watchEffect, type Ref } from "vue";
import type {
Scene as SceneType,
PerspectiveCamera as PerspectiveCameraType,
WebGLRenderer as WebGLRendererType,
Group as GroupType,
Object3D
} from "three";
import type { TrackballControls as TrackballControlsType } from "three/examples/jsm/controls/TrackballControls.js";
const { const {
AmbientLight, AmbientLight,
AxesHelper, AxesHelper,
@@ -24,7 +16,7 @@ const {
SphereGeometry, SphereGeometry,
WebGLRenderer WebGLRenderer
} = await import("three"); } = await import("three");
const { TrackballControls } = await import("three/examples/jsm/controls/TrackballControls.js"); const { TrackballControls } = await import("three/examples/jsm/controls/TrackballControls");
import type { BoardObservation, CameraCalibrationResult } from "@/types/SettingTypes"; import type { BoardObservation, CameraCalibrationResult } from "@/types/SettingTypes";
import axios from "axios"; import axios from "axios";
import { useCameraSettingsStore } from "@/stores/settings/CameraSettingsStore"; import { useCameraSettingsStore } from "@/stores/settings/CameraSettingsStore";
@@ -39,12 +31,12 @@ const props = defineProps<{
title: string; title: string;
}>(); }>();
let scene: SceneType | undefined; let scene: Scene | undefined;
let camera: PerspectiveCameraType | undefined; let camera: PerspectiveCamera | undefined;
let renderer: WebGLRendererType | undefined; let renderer: WebGLRenderer | undefined;
let controls: TrackballControlsType | undefined; let controls: TrackballControls | undefined;
const createChessboard = (obs: BoardObservation, cal: CameraCalibrationResult): GroupType => { const createChessboard = (obs: BoardObservation, cal: CameraCalibrationResult): Group => {
const group = new Group(); const group = new Group();
if (obs.locationInImageSpace.length === 0) return group; if (obs.locationInImageSpace.length === 0) return group;
@@ -202,6 +194,9 @@ const resetCamThirdPerson = () => {
let animationFrameId: number | null = null; let animationFrameId: number | null = null;
onMounted(async () => { onMounted(async () => {
// Grab data first off
fetchCalibrationData();
scene = new Scene(); scene = new Scene();
camera = new PerspectiveCamera(75, 800 / 800, 0.1, 1000); camera = new PerspectiveCamera(75, 800 / 800, 0.1, 1000);
@@ -261,10 +256,6 @@ onMounted(async () => {
controls.update(); controls.update();
// Fetch calibration only after the scene is ready so the initial draw
// can happen immediately when the data arrives.
await fetchCalibrationData();
const animate = () => { const animate = () => {
if (!scene || !camera || !renderer || !controls) { if (!scene || !camera || !renderer || !controls) {
return; return;
@@ -327,7 +318,7 @@ if (import.meta.hot) {
} }
watchEffect(() => { watchEffect(() => {
void drawCalibration(calibrationData.value); drawCalibration(calibrationData.value);
}); });
watch( watch(
@@ -337,9 +328,9 @@ watch(
props.resolution.height, props.resolution.height,
useCameraSettingsStore().getCalibrationCoeffs(props.resolution) useCameraSettingsStore().getCalibrationCoeffs(props.resolution)
], ],
async () => { () => {
console.log("Camera or resolution changed, refetching calibration"); console.log("Camera or resolution changed, refetching calibration");
await fetchCalibrationData(); fetchCalibrationData();
} }
); );
</script> </script>

View File

@@ -1,5 +1,5 @@
<script setup lang="ts"> <script setup lang="ts">
import { computed, inject, onBeforeUnmount, useTemplateRef } from "vue"; import { computed, inject, ref, onBeforeUnmount } from "vue";
import { useStateStore } from "@/stores/StateStore"; import { useStateStore } from "@/stores/StateStore";
import { useCameraSettingsStore } from "@/stores/settings/CameraSettingsStore"; import { useCameraSettingsStore } from "@/stores/settings/CameraSettingsStore";
import type { StyleValue } from "vue"; import type { StyleValue } from "vue";
@@ -13,7 +13,6 @@ const props = defineProps<{
cameraSettings: UiCameraConfiguration; cameraSettings: UiCameraConfiguration;
}>(); }>();
const backendHostname = inject<string>("backendHostname");
const emptyStreamSrc = "//:0"; const emptyStreamSrc = "//:0";
const streamSrc = computed<string>(() => { const streamSrc = computed<string>(() => {
const port = props.cameraSettings.stream[props.streamType === "Raw" ? "inputPort" : "outputPort"]; const port = props.cameraSettings.stream[props.streamType === "Raw" ? "inputPort" : "outputPort"];
@@ -22,7 +21,7 @@ const streamSrc = computed<string>(() => {
return emptyStreamSrc; return emptyStreamSrc;
} }
return `http://${backendHostname}:${port}/stream.mjpg`; return `http://${inject("backendHostname")}:${port}/stream.mjpg`;
}); });
const streamDesc = computed<string>(() => `${props.streamType} Stream View`); const streamDesc = computed<string>(() => `${props.streamType} Stream View`);
const streamStyle = computed<StyleValue>(() => { const streamStyle = computed<StyleValue>(() => {
@@ -68,26 +67,26 @@ const handleCaptureClick = () => {
const handlePopoutClick = () => { const handlePopoutClick = () => {
window.open(streamSrc.value); window.open(streamSrc.value);
}; };
const handleFullscreenRequest = async () => { const handleFullscreenRequest = () => {
const stream = document.getElementById(props.id); const stream = document.getElementById(props.id);
if (!stream) return; if (!stream) return;
await stream.requestFullscreen(); stream.requestFullscreen();
}; };
const mjpgStream = useTemplateRef("mjpgStream"); const mjpgStream: any = ref(null);
const handleStreamError = () => { const handleStreamError = () => {
if (streamSrc.value && streamSrc.value !== emptyStreamSrc) { if (streamSrc.value && streamSrc.value !== emptyStreamSrc) {
console.error("Error loading stream:", streamSrc.value, " Trying again."); console.error("Error loading stream:", streamSrc.value, " Trying again.");
setTimeout(() => { setTimeout(() => {
mjpgStream.value!.src = streamSrc.value; mjpgStream.value.src = streamSrc.value;
}, 100); }, 100);
} }
}; };
onBeforeUnmount(() => { onBeforeUnmount(() => {
if (!mjpgStream.value) return; if (!mjpgStream.value) return;
mjpgStream.value.src = emptyStreamSrc; mjpgStream.value["src"] = emptyStreamSrc;
}); });
</script> </script>

View File

@@ -1,5 +1,5 @@
<script setup lang="ts"> <script setup lang="ts">
import { computed, inject, ref, useTemplateRef, watch } from "vue"; import { computed, inject, ref, watch } from "vue";
import { LogLevel, type LogMessage } from "@/types/SettingTypes"; import { LogLevel, type LogMessage } from "@/types/SettingTypes";
import { useStateStore } from "@/stores/StateStore"; import { useStateStore } from "@/stores/StateStore";
import LogEntry from "@/components/app/photon-log-entry.vue"; import LogEntry from "@/components/app/photon-log-entry.vue";
@@ -10,10 +10,10 @@ const backendHost = inject<string>("backendHost");
const searchQuery = ref(""); const searchQuery = ref("");
const timeInput = ref<string>(); const timeInput = ref<string>();
const autoScroll = ref(true); const autoScroll = ref(true);
const logList = useTemplateRef<InstanceType<typeof VirtualList>>("logList"); // this needs to be typed in the definition since vue has trouble inferring it const logList = ref();
const logKeeps = ref(40); const logKeeps = ref(40);
const exportLogFile = useTemplateRef("exportLogFile"); const exportLogFile = ref();
const selectedLogLevels = ref<Record<number, boolean>>({ const selectedLogLevels = ref({
[LogLevel.ERROR]: true, [LogLevel.ERROR]: true,
[LogLevel.WARN]: true, [LogLevel.WARN]: true,
[LogLevel.INFO]: true, [LogLevel.INFO]: true,
@@ -48,7 +48,7 @@ watch(logs, () => {
); );
autoScroll.value = bottomOffset < 50; autoScroll.value = bottomOffset < 50;
if (autoScroll.value) logList.value?.scrollToBottom(); if (autoScroll.value) logList.value.scrollToBottom();
}); });
const getLogLevelFromIndex = (index: number): string => { const getLogLevelFromIndex = (index: number): string => {
@@ -56,7 +56,7 @@ const getLogLevelFromIndex = (index: number): string => {
}; };
const handleLogExport = () => { const handleLogExport = () => {
exportLogFile.value?.click(); exportLogFile.value.click();
}; };
const handleLogClear = () => { const handleLogClear = () => {

View File

@@ -1,5 +1,5 @@
<script setup lang="ts"> <script setup lang="ts">
import { computed, ref, watch, watchEffect } from "vue"; import { computed, ref, watchEffect } from "vue";
import { useCameraSettingsStore } from "@/stores/settings/CameraSettingsStore"; import { useCameraSettingsStore } from "@/stores/settings/CameraSettingsStore";
import { CalibrationBoardTypes, CalibrationTagFamilies, type VideoFormat } from "@/types/SettingTypes"; import { CalibrationBoardTypes, CalibrationTagFamilies, type VideoFormat } from "@/types/SettingTypes";
import MonoLogo from "@/assets/images/logoMono.png"; import MonoLogo from "@/assets/images/logoMono.png";
@@ -15,7 +15,6 @@ import CameraCalibrationInfoCard from "@/components/cameras/CameraCalibrationInf
import { useSettingsStore } from "@/stores/settings/GeneralSettingsStore"; import { useSettingsStore } from "@/stores/settings/GeneralSettingsStore";
import { useTheme } from "vuetify"; import { useTheme } from "vuetify";
import TooltippedLabel from "@/components/common/pv-tooltipped-label.vue"; import TooltippedLabel from "@/components/common/pv-tooltipped-label.vue";
import { length } from "@adam-rocska/units-and-measurement/length";
const PromptRegular = import("@/assets/fonts/PromptRegular"); const PromptRegular = import("@/assets/fonts/PromptRegular");
const jspdf = import("jspdf"); const jspdf = import("jspdf");
@@ -39,11 +38,6 @@ const getUniqueVideoFormatsByResolution = (): VideoFormat[] => {
if (!skip) { if (!skip) {
const calib = useCameraSettingsStore().getCalibrationCoeffs(format.resolution); const calib = useCameraSettingsStore().getCalibrationCoeffs(format.resolution);
// minPixelCount is the multiplied area of a 640x480 (the minimum for proper calibration) resolution
const minPixelCount = 640 * 480;
const resArea = format.resolution.width * format.resolution.height;
if (calib !== undefined) { if (calib !== undefined) {
// Mean overall reprojection error // Mean overall reprojection error
// Calculated as average of each observation's mean error // Calculated as average of each observation's mean error
@@ -66,10 +60,7 @@ const getUniqueVideoFormatsByResolution = (): VideoFormat[] => {
) * ) *
(180 / Math.PI); (180 / Math.PI);
} }
uniqueResolutions.push(format);
if (resArea >= minPixelCount) {
uniqueResolutions.push(format);
}
} }
}); });
uniqueResolutions.sort( uniqueResolutions.sort(
@@ -90,28 +81,20 @@ const calibrationDivisors = computed(() =>
}) })
); );
const uniqueVideoResolutionIndex = ref(getUniqueVideoResolutionStrings()?.[0]?.value); const uniqueVideoResolutionString = ref("");
// Use a watchEffect so the value is populated/reacts when the stores become available or update. // 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. // This avoids trying to index into an array that may be empty during page reload.
watchEffect(() => { watchEffect(() => {
const currentIndex = useCameraSettingsStore().currentVideoFormat.index ?? 0;
useStateStore().calibrationData.videoFormatIndex = currentIndex;
const names = useCameraSettingsStore().currentCameraSettings.validVideoFormats.map((f) => const names = useCameraSettingsStore().currentCameraSettings.validVideoFormats.map((f) =>
getResolutionString(f.resolution) getResolutionString(f.resolution)
); );
const currentFormatIndex = useCameraSettingsStore().currentVideoFormat.index ?? 0; uniqueVideoResolutionString.value = names[currentIndex] ?? names[0] ?? "";
// Checks if the current resolution is present in the list of valid formats, if not defaults to the last index (which is usually the highest resolution)
const currentIndex =
getUniqueVideoResolutionStrings()
.map((x) => x.name)
.find((n) => n === names[currentFormatIndex]) !== undefined
? currentFormatIndex
: names.length - 1;
useStateStore().calibrationData.videoFormatIndex = currentIndex;
uniqueVideoResolutionIndex.value = currentIndex;
}); });
const dimensionUnit = ref<"in" | "mm">("in"); const squareSizeIn = ref(1);
const squareSize = ref(30); const markerSizeIn = ref(0.75);
const markerSize = ref(22);
const patternWidth = ref(8); const patternWidth = ref(8);
const patternHeight = ref(8); const patternHeight = ref(8);
const boardType = ref<CalibrationBoardTypes>(CalibrationBoardTypes.Charuco); const boardType = ref<CalibrationBoardTypes>(CalibrationBoardTypes.Charuco);
@@ -119,13 +102,6 @@ const useOldPattern = ref(false);
const tagFamily = ref<CalibrationTagFamilies>(CalibrationTagFamilies.Dict_4X4_1000); const tagFamily = ref<CalibrationTagFamilies>(CalibrationTagFamilies.Dict_4X4_1000);
const requestedVideoFormatIndex = ref(0); const requestedVideoFormatIndex = ref(0);
watch(dimensionUnit, (value, oldValue) => {
squareSize.value = length[oldValue](squareSize.value)[value].value;
markerSize.value = length[oldValue](markerSize.value)[value].value;
});
const dimensionStep = computed(() => (dimensionUnit.value === "mm" ? 0.1 : 0.01));
// Emperical testing - with stack size limit of 1MB, we can handle at -least- 700k points // Emperical testing - with stack size limit of 1MB, we can handle at -least- 700k points
const tooManyPoints = computed( const tooManyPoints = computed(
() => useStateStore().calibrationData.imageCount * patternWidth.value * patternHeight.value > 700000 () => useStateStore().calibrationData.imageCount * patternWidth.value * patternHeight.value > 700000
@@ -146,31 +122,25 @@ const downloadCalibBoard = async () => {
switch (boardType.value) { switch (boardType.value) {
case CalibrationBoardTypes.Chessboard: case CalibrationBoardTypes.Chessboard:
const squareSizeIn = length[dimensionUnit.value](squareSize.value).in.value; const chessboardStartX = (paperWidth - patternWidth.value * squareSizeIn.value) / 2;
const chessboardStartX = (paperWidth - patternWidth.value * squareSizeIn) / 2;
const chessboardStartY = (paperHeight - patternHeight.value * squareSizeIn) / 2; const chessboardStartY = (paperHeight - patternWidth.value * squareSizeIn.value) / 2;
for (let squareY = 0; squareY < patternHeight.value; squareY++) { for (let squareY = 0; squareY < patternHeight.value; squareY++) {
for (let squareX = 0; squareX < patternWidth.value; squareX++) { for (let squareX = 0; squareX < patternWidth.value; squareX++) {
const xPos = chessboardStartX + squareX * squareSizeIn; const xPos = chessboardStartX + squareX * squareSizeIn.value;
const yPos = chessboardStartY + squareY * squareSizeIn; const yPos = chessboardStartY + squareY * squareSizeIn.value;
// Only draw the odd squares to create the chessboard pattern // Only draw the odd squares to create the chessboard pattern
if (squareY % 2 !== squareX % 2) { if (squareY % 2 !== squareX % 2) {
doc.rect(xPos, yPos, squareSizeIn, squareSizeIn, "F"); doc.rect(xPos, yPos, squareSizeIn.value, squareSizeIn.value, "F");
} }
} }
} }
doc.text( doc.text(`${patternWidth.value} x ${patternHeight.value} | ${squareSizeIn.value}in`, paperWidth - 1, 1.0, {
`${patternWidth.value} x ${patternHeight.value} | ${squareSize.value}${dimensionUnit.value}`, maxWidth: (paperWidth - 2.0) / 2,
paperWidth - 1, align: "right"
1.0, });
{
maxWidth: (paperWidth - 2.0) / 2,
align: "right"
}
);
break; break;
case CalibrationBoardTypes.Charuco: case CalibrationBoardTypes.Charuco:
@@ -206,13 +176,13 @@ const downloadCalibBoard = async () => {
}; };
const isCalibrating = computed( const isCalibrating = computed(
() => useCameraSettingsStore().currentCameraSettings.currentPipelineIndex === WebsocketPipelineType.Calib3d.valueOf() () => useCameraSettingsStore().currentCameraSettings.currentPipelineIndex === WebsocketPipelineType.Calib3d
); );
const startCalibration = () => { const startCalibration = () => {
useCameraSettingsStore().startPnPCalibration({ useCameraSettingsStore().startPnPCalibration({
squareSizeMeters: length[dimensionUnit.value](squareSize.value).m.value, squareSizeIn: squareSizeIn.value,
markerSizeMeters: length[dimensionUnit.value](markerSize.value).m.value, markerSizeIn: markerSizeIn.value,
patternHeight: patternHeight.value, patternHeight: patternHeight.value,
patternWidth: patternWidth.value, patternWidth: patternWidth.value,
boardType: boardType.value, boardType: boardType.value,
@@ -233,7 +203,7 @@ const endCalibration = () => {
calibSuccess.value = undefined; calibSuccess.value = undefined;
calibEndpointFail.value = false; calibEndpointFail.value = false;
if (!hasEnoughImages.value) { if (!useStateStore().calibrationData.hasEnoughImages) {
calibCanceled.value = true; calibCanceled.value = true;
} }
@@ -261,10 +231,6 @@ const endCalibration = () => {
const drawAllSnapshots = ref(true); const drawAllSnapshots = ref(true);
const bypassVal = ref(false);
const minCount = computed(() => (bypassVal.value ? 10 : 100));
const hasEnoughImages = computed(() => useStateStore().calibrationData.imageCount >= minCount.value);
const showCalDialog = ref(false); const showCalDialog = ref(false);
const selectedVideoFormat = ref<VideoFormat | undefined>(undefined); const selectedVideoFormat = ref<VideoFormat | undefined>(undefined);
const setSelectedVideoFormat = (format: VideoFormat) => { const setSelectedVideoFormat = (format: VideoFormat) => {
@@ -329,23 +295,23 @@ const setSelectedVideoFormat = (format: VideoFormat) => {
> >
<v-form v-model="settingsValid"> <v-form v-model="settingsValid">
<pv-select <pv-select
v-model="uniqueVideoResolutionIndex" v-model="uniqueVideoResolutionString"
label="Resolution" label="Resolution"
:select-cols="8" :select-cols="8"
:disabled="isCalibrating" :disabled="isCalibrating"
tooltip="Resolution to calibrate at (you will have to calibrate every resolution you use 3D mode on)" tooltip="Resolution to calibrate at (you will have to calibrate every resolution you use 3D mode on)"
:items="getUniqueVideoResolutionStrings()" :items="getUniqueVideoResolutionStrings()"
@update:model-value="(value) => (useStateStore().calibrationData.videoFormatIndex = value)" @update:model-value="
useStateStore().calibrationData.videoFormatIndex =
getUniqueVideoResolutionStrings().find((v) => v.value === $event)?.value || 0
"
/> />
<pv-select <pv-select
v-model="boardType" v-model="boardType"
label="Board Type" label="Board Type"
tooltip="Calibration board pattern to use" tooltip="Calibration board pattern to use"
:select-cols="8" :select-cols="8"
:items="[ :items="['Chessboard', 'ChArUco']"
{ value: CalibrationBoardTypes.Charuco, name: 'ChArUco' },
{ value: CalibrationBoardTypes.Chessboard, name: 'Chessboard' }
]"
:disabled="isCalibrating" :disabled="isCalibrating"
/> />
<v-alert <v-alert
@@ -375,43 +341,25 @@ const setSelectedVideoFormat = (format: VideoFormat) => {
label="Tag Family" label="Tag Family"
tooltip="Dictionary of ArUco markers on the ChArUco board" tooltip="Dictionary of ArUco markers on the ChArUco board"
:select-cols="8" :select-cols="8"
:items="[ :items="['Dict_4X4_1000', 'Dict_5X5_1000', 'Dict_6X6_1000', 'Dict_7X7_1000']"
{ value: CalibrationTagFamilies.Dict_4X4_1000, name: 'Dict_4X4_1000' },
{ value: CalibrationTagFamilies.Dict_5X5_1000, name: 'Dict_5X5_1000' },
{ value: CalibrationTagFamilies.Dict_6X6_1000, name: 'Dict_6X6_1000' },
{ value: CalibrationTagFamilies.Dict_7X7_1000, name: 'Dict_7X7_1000' }
]"
:disabled="isCalibrating"
/>
<pv-select
v-model="dimensionUnit"
label="Dimension Unit"
tooltip="Units used for pattern spacing and marker size inputs"
:select-cols="8"
:items="[
{ value: 'in', name: 'Inches' },
{ value: 'mm', name: 'Millimeters' }
]"
:disabled="isCalibrating" :disabled="isCalibrating"
/> />
<pv-number-input <pv-number-input
v-model="squareSize" v-model="squareSizeIn"
:label="`Pattern Spacing (${dimensionUnit})`" label="Pattern Spacing (in)"
:tooltip="`Spacing between pattern features in ${dimensionUnit === 'mm' ? 'millimeters' : 'inches'}`" tooltip="Spacing between pattern features in inches"
:disabled="isCalibrating" :disabled="isCalibrating"
:rules="[(v) => v > 0 || 'Size must be positive']" :rules="[(v) => v > 0 || 'Size must be positive']"
:label-cols="4" :label-cols="4"
:step="dimensionStep"
/> />
<pv-number-input <pv-number-input
v-if="boardType === CalibrationBoardTypes.Charuco" v-if="boardType === CalibrationBoardTypes.Charuco"
v-model="markerSize" v-model="markerSizeIn"
:label="`Marker Size (${dimensionUnit})`" label="Marker Size (in)"
:tooltip="`Size of the tag markers in ${dimensionUnit === 'mm' ? 'millimeters' : 'inches'}; must be smaller than pattern spacing`" tooltip="Size of the tag markers in inches must be smaller than pattern spacing"
:disabled="isCalibrating" :disabled="isCalibrating"
:rules="[(v) => v > 0 || 'Size must be positive']" :rules="[(v) => v > 0 || 'Size must be positive']"
:label-cols="4" :label-cols="4"
:step="dimensionStep"
/> />
<pv-number-input <pv-number-input
v-model="patternWidth" v-model="patternWidth"
@@ -535,22 +483,11 @@ const setSelectedVideoFormat = (format: VideoFormat) => {
<v-chip <v-chip
:variant="theme.global.current.value.dark ? 'tonal' : 'elevated'" :variant="theme.global.current.value.dark ? 'tonal' : 'elevated'"
label label
:color="hasEnoughImages ? 'buttonPassive' : 'light-grey'" :color="useStateStore().calibrationData.hasEnoughImages ? 'buttonPassive' : 'light-grey'"
> >
Snapshots: {{ useStateStore().calibrationData.imageCount }} of at least Snapshots: {{ useStateStore().calibrationData.imageCount }} of at least
{{ minCount }} {{ useStateStore().calibrationData.minimumImageCount }}
</v-chip> </v-chip>
<v-spacer />
<pv-switch
v-model="bypassVal"
color="error"
hide-details
class="ml-4"
label="Bypass minimum"
:label-cols="6"
:switch-cols="6"
tooltip="Bypass the minimum recommended amount of snapshots for a calibration. Should only be used for dev work or temporary tests not competitions. Still requires 10 images to calibrate."
/>
</div> </div>
<div> <div>
<v-btn <v-btn
@@ -595,14 +532,16 @@ const setSelectedVideoFormat = (format: VideoFormat) => {
size="small" size="small"
block block
:variant="theme.global.current.value.dark ? 'outlined' : 'elevated'" :variant="theme.global.current.value.dark ? 'outlined' : 'elevated'"
:color="hasEnoughImages ? 'buttonActive' : 'error'" :color="useStateStore().calibrationData.hasEnoughImages ? 'buttonActive' : 'error'"
:disabled="!isCalibrating || !settingsValid" :disabled="!isCalibrating || !settingsValid"
@click="endCalibration" @click="endCalibration"
> >
<v-icon start class="calib-btn-icon" size="large"> <v-icon start class="calib-btn-icon" size="large">
{{ hasEnoughImages ? "mdi-flag-checkered" : "mdi-flag-off-outline" }} {{ useStateStore().calibrationData.hasEnoughImages ? "mdi-flag-checkered" : "mdi-flag-off-outline" }}
</v-icon> </v-icon>
<span class="calib-btn-label">{{ hasEnoughImages ? "Finish Calibration" : "Cancel Calibration" }}</span> <span class="calib-btn-label">{{
useStateStore().calibrationData.hasEnoughImages ? "Finish Calibration" : "Cancel Calibration"
}}</span>
</v-btn> </v-btn>
</v-col> </v-col>
</div> </div>

View File

@@ -3,7 +3,7 @@ import PhotonCalibrationVisualizer from "@/components/app/photon-calibration-vis
import type { CameraCalibrationResult, VideoFormat } from "@/types/SettingTypes"; import type { CameraCalibrationResult, VideoFormat } from "@/types/SettingTypes";
import { useCameraSettingsStore } from "@/stores/settings/CameraSettingsStore"; import { useCameraSettingsStore } from "@/stores/settings/CameraSettingsStore";
import { useStateStore } from "@/stores/StateStore"; import { useStateStore } from "@/stores/StateStore";
import { computed, inject, ref, useTemplateRef } from "vue"; import { computed, inject, ref } from "vue";
import { axiosPost, getResolutionString, parseJsonFile } from "@/lib/PhotonUtils"; import { axiosPost, getResolutionString, parseJsonFile } from "@/lib/PhotonUtils";
import { useTheme } from "vuetify"; import { useTheme } from "vuetify";
import PvDeleteModal from "@/components/common/pv-delete-modal.vue"; import PvDeleteModal from "@/components/common/pv-delete-modal.vue";
@@ -13,28 +13,28 @@ const props = defineProps<{
videoFormat: VideoFormat; videoFormat: VideoFormat;
}>(); }>();
const confirmRemoveDialog = ref({ show: false, vf: props.videoFormat }); const confirmRemoveDialog = ref({ show: false, vf: props.videoFormat as VideoFormat });
const removeCalibration = async (vf: VideoFormat) => { const removeCalibration = (vf: VideoFormat) => {
await axiosPost("/calibration/remove", "delete a camera calibration", { axiosPost("/calibration/remove", "delete a camera calibration", {
cameraUniqueName: useCameraSettingsStore().currentCameraSettings.uniqueName, cameraUniqueName: useCameraSettingsStore().currentCameraSettings.uniqueName,
width: vf.resolution.width, width: vf.resolution.width,
height: vf.resolution.height height: vf.resolution.height
}); });
}; };
const exportCalibration = useTemplateRef("exportCalibration"); const exportCalibration = ref();
const openExportCalibrationPrompt = () => { const openExportCalibrationPrompt = () => {
exportCalibration.value?.click(); exportCalibration.value.click();
}; };
const importCalibrationFromPhotonJson = useTemplateRef("importCalibrationFromPhotonJson"); const importCalibrationFromPhotonJson = ref();
const openUploadPhotonCalibJsonPrompt = () => { const openUploadPhotonCalibJsonPrompt = () => {
importCalibrationFromPhotonJson.value?.click(); importCalibrationFromPhotonJson.value.click();
}; };
const importCalibration = async () => { const importCalibration = async () => {
const files = importCalibrationFromPhotonJson.value?.files; const files = importCalibrationFromPhotonJson.value.files;
if (!files?.length) return; if (files.length === 0) return;
const uploadedJson = files[0]; const uploadedJson = files[0];
const data = await parseJsonFile<CameraCalibrationResult>(uploadedJson); const data = await parseJsonFile<CameraCalibrationResult>(uploadedJson);

View File

@@ -53,7 +53,7 @@ const fetchSnapshots = () => {
.get("/utils/getImageSnapshots") .get("/utils/getImageSnapshots")
.then((response) => { .then((response) => {
imgData.value = response.data.map( imgData.value = response.data.map(
(snapshotData: { snapshotName: string; cameraUniqueName: string; snapshotData: string }, index: number) => { (snapshotData: { snapshotName: string; cameraUniqueName: string; snapshotData: string }, index) => {
const metadata = getSnapshotMetadataFromName(snapshotData.snapshotName); const metadata = getSnapshotMetadataFromName(snapshotData.snapshotName);
return { return {
@@ -99,7 +99,7 @@ const expanded = ref([]);
<v-card-text class="pt-0"> <v-card-text class="pt-0">
<v-btn <v-btn
color="buttonPassive" color="buttonPassive"
:variant="theme.global.current.value.dark ? 'outlined' : 'elevated'" :variant="theme.global.current.value.dark ? 'outlined' : 'tonal'"
@click="fetchSnapshots" @click="fetchSnapshots"
> >
<v-icon start class="open-icon" size="large"> mdi-folder </v-icon> <v-icon start class="open-icon" size="large"> mdi-folder </v-icon>

View File

@@ -9,7 +9,6 @@ import { computed, ref, watchEffect } from "vue";
import { type CameraSettingsChangeRequest, ValidQuirks } from "@/types/SettingTypes"; import { type CameraSettingsChangeRequest, ValidQuirks } from "@/types/SettingTypes";
import { useTheme } from "vuetify"; import { useTheme } from "vuetify";
import { axiosPost } from "@/lib/PhotonUtils"; import { axiosPost } from "@/lib/PhotonUtils";
import { WebsocketPipelineType } from "@/types/WebsocketDataTypes";
const theme = useTheme(); const theme = useTheme();
@@ -21,7 +20,7 @@ const focusMode = computed<boolean>({
get: () => useCameraSettingsStore().isFocusMode, get: () => useCameraSettingsStore().isFocusMode,
set: (v) => set: (v) =>
useCameraSettingsStore().changeCurrentPipelineIndex( useCameraSettingsStore().changeCurrentPipelineIndex(
v ? WebsocketPipelineType.FocusCamera : useCameraSettingsStore().currentCameraSettings.lastPipelineIndex || 0, v ? -3 : useCameraSettingsStore().currentCameraSettings.lastPipelineIndex || 0,
true true
) )
}); });
@@ -66,8 +65,8 @@ const settingsHaveChanged = (): boolean => {
const a = tempSettingsStruct.value; const a = tempSettingsStruct.value;
const b = useCameraSettingsStore().currentCameraSettings; const b = useCameraSettingsStore().currentCameraSettings;
for (const quirk of Object.values(ValidQuirks)) { for (const q in ValidQuirks) {
if (a.quirksToChange[quirk] !== b.cameraQuirks.quirks[quirk]) return true; if (a.quirksToChange[q] !== b.cameraQuirks.quirks[q]) return true;
} }
return a.fov !== b.fov.value; return a.fov !== b.fov.value;
@@ -121,12 +120,12 @@ watchEffect(() => {
}); });
const showDeleteCamera = ref(false); const showDeleteCamera = ref(false);
const deleteThisCamera = async () => { const deleteThisCamera = () => {
await axiosPost("/utils/nukeOneCamera", "delete this camera", { axiosPost("/utils/nukeOneCamera", "delete this camera", {
cameraUniqueName: useStateStore().currentCameraUniqueName cameraUniqueName: useStateStore().currentCameraUniqueName
}); });
}; };
const wrappedCameras = computed<SelectItem<string>[]>(() => const wrappedCameras = computed<SelectItem[]>(() =>
Object.keys(useCameraSettingsStore().cameras).map((cameraUniqueName) => ({ Object.keys(useCameraSettingsStore().cameras).map((cameraUniqueName) => ({
name: useCameraSettingsStore().cameras[cameraUniqueName].nickname, name: useCameraSettingsStore().cameras[cameraUniqueName].nickname,
value: cameraUniqueName value: cameraUniqueName
@@ -160,7 +159,7 @@ const wrappedCameras = computed<SelectItem<string>[]>(() =>
v-model="arducamSelectWrapper" v-model="arducamSelectWrapper"
label="Arducam Model" label="Arducam Model"
:items="[ :items="[
{ name: 'None', value: 0 }, { name: 'None', value: 0, disabled: true },
{ name: 'OV9281', value: 1 }, { name: 'OV9281', value: 1 },
{ name: 'OV2311', value: 2 }, { name: 'OV2311', value: 2 },
{ name: 'OV9782', value: 3 } { name: 'OV9782', value: 3 }

View File

@@ -6,7 +6,6 @@ import { PipelineType } from "@/types/PipelineTypes";
import { useStateStore } from "@/stores/StateStore"; import { useStateStore } from "@/stores/StateStore";
import { useSettingsStore } from "@/stores/settings/GeneralSettingsStore"; import { useSettingsStore } from "@/stores/settings/GeneralSettingsStore";
import { useTheme } from "vuetify"; import { useTheme } from "vuetify";
import { WebsocketPipelineType } from "@/types/WebsocketDataTypes";
const theme = useTheme(); const theme = useTheme();
@@ -16,7 +15,7 @@ const driverMode = computed<boolean>({
get: () => useCameraSettingsStore().isDriverMode, get: () => useCameraSettingsStore().isDriverMode,
set: (v) => set: (v) =>
useCameraSettingsStore().changeCurrentPipelineIndex( useCameraSettingsStore().changeCurrentPipelineIndex(
v ? WebsocketPipelineType.DriverMode : useCameraSettingsStore().currentCameraSettings.lastPipelineIndex || 0, v ? -1 : useCameraSettingsStore().currentCameraSettings.lastPipelineIndex || 0,
true true
) )
}); });

View File

@@ -1,51 +1,69 @@
<script setup lang="ts"> <script setup lang="ts">
import type { PVCameraInfo } from "@/types/SettingTypes"; import { PVCameraInfo } from "@/types/SettingTypes";
const { camera } = defineProps<{ camera: PVCameraInfo }>(); const { camera } = defineProps({
camera: {
type: PVCameraInfo,
required: true
}
});
const cameraInfoFor: any = (camera: PVCameraInfo) => {
if (camera.PVUsbCameraInfo) {
return camera.PVUsbCameraInfo;
}
if (camera.PVCSICameraInfo) {
return camera.PVCSICameraInfo;
}
if (camera.PVFileCameraInfo) {
return camera.PVFileCameraInfo;
}
return {};
};
</script> </script>
<template> <template>
<div> <div>
<v-table density="compact" :style="{ backgroundColor: 'var(--v-primary-base)' }"> <v-table density="compact" :style="{ backgroundColor: 'var(--v-primary-base)' }">
<tbody> <tbody>
<tr v-if="'dev' in camera && camera.dev !== null"> <tr v-if="cameraInfoFor(camera).dev !== undefined && cameraInfoFor(camera).dev !== null">
<td>Device Number:</td> <td>Device Number:</td>
<td>{{ camera.dev }}</td> <td>{{ cameraInfoFor(camera).dev }}</td>
</tr> </tr>
<tr v-if="'name' in camera && camera.name !== null"> <tr v-if="cameraInfoFor(camera).name !== undefined && cameraInfoFor(camera).name !== null">
<td>Name:</td> <td>Name:</td>
<td>{{ camera.name }}</td> <td>{{ cameraInfoFor(camera).name }}</td>
</tr> </tr>
<tr> <tr>
<td>Type:</td> <td>Type:</td>
<td v-if="camera.type === 'PVUsbCameraInfo'" class="mb-3">USB Camera</td> <td v-if="camera.PVUsbCameraInfo" class="mb-3">USB Camera</td>
<td v-else-if="camera.type === 'PVCSICameraInfo'" class="mb-3">CSI Camera</td> <td v-else-if="camera.PVCSICameraInfo" class="mb-3">CSI Camera</td>
<td v-else-if="camera.type === 'PVFileCameraInfo'" class="mb-3">File Camera</td> <td v-else-if="camera.PVFileCameraInfo" class="mb-3">File Camera</td>
<td v-else>Unidentified Camera Type</td> <td v-else>Unidentified Camera Type</td>
</tr> </tr>
<tr v-if="'baseName' in camera && camera.baseName !== null"> <tr v-if="cameraInfoFor(camera).baseName !== undefined && cameraInfoFor(camera).baseName !== null">
<td>Base Name:</td> <td>Base Name:</td>
<td>{{ camera.baseName }}</td> <td>{{ cameraInfoFor(camera).baseName }}</td>
</tr> </tr>
<tr v-if="'vendorId' in camera && camera.vendorId !== null"> <tr v-if="cameraInfoFor(camera).vendorId !== undefined && cameraInfoFor(camera).vendorId !== null">
<td>Vendor ID:</td> <td>Vendor ID:</td>
<td>{{ camera.vendorId }}</td> <td>{{ cameraInfoFor(camera).vendorId }}</td>
</tr> </tr>
<tr v-if="'productId' in camera && camera.productId !== null"> <tr v-if="cameraInfoFor(camera).productId !== undefined && cameraInfoFor(camera).productId !== null">
<td>Product ID:</td> <td>Product ID:</td>
<td>{{ camera.productId }}</td> <td>{{ cameraInfoFor(camera).productId }}</td>
</tr> </tr>
<tr v-if="'path' in camera && camera.path !== null"> <tr v-if="cameraInfoFor(camera).path !== undefined && cameraInfoFor(camera).path !== null">
<td>Path:</td> <td>Path:</td>
<td style="word-break: break-all">{{ camera.path }}</td> <td style="word-break: break-all">{{ cameraInfoFor(camera).path }}</td>
</tr> </tr>
<tr v-if="'uniquePath' in camera && camera.uniquePath !== null"> <tr v-if="cameraInfoFor(camera).uniquePath !== undefined && cameraInfoFor(camera).uniquePath !== null">
<td>Unique Path:</td> <td>Unique Path:</td>
<td style="word-break: break-all">{{ camera.uniquePath }}</td> <td style="word-break: break-all">{{ cameraInfoFor(camera).uniquePath }}</td>
</tr> </tr>
<tr v-if="'otherPaths' in camera && camera.otherPaths !== null"> <tr v-if="cameraInfoFor(camera).otherPaths !== undefined && cameraInfoFor(camera).otherPaths !== null">
<td>Other Paths:</td> <td>Other Paths:</td>
<td>{{ camera.otherPaths }}</td> <td>{{ cameraInfoFor(camera).otherPaths }}</td>
</tr> </tr>
</tbody> </tbody>
</v-table> </v-table>

View File

@@ -1,5 +1,5 @@
<script setup lang="ts"> <script setup lang="ts">
import type { PVCameraInfo } from "@/types/SettingTypes"; import { PVCameraInfo } from "@/types/SettingTypes";
function isEqual<T>(a: T, b: T): boolean { function isEqual<T>(a: T, b: T): boolean {
if (a === b) { if (a === b) {
@@ -15,7 +15,29 @@ function isEqual<T>(a: T, b: T): boolean {
); );
} }
const { saved, current } = defineProps<{ saved: PVCameraInfo; current: PVCameraInfo }>(); const { saved, current } = defineProps({
saved: {
type: PVCameraInfo,
required: true
},
current: {
type: PVCameraInfo,
required: true
}
});
const cameraInfoFor = (camera: PVCameraInfo): any => {
if (camera.PVUsbCameraInfo) {
return camera.PVUsbCameraInfo;
}
if (camera.PVCSICameraInfo) {
return camera.PVCSICameraInfo;
}
if (camera.PVFileCameraInfo) {
return camera.PVFileCameraInfo;
}
return {};
};
</script> </script>
<template> <template>
@@ -28,70 +50,79 @@ const { saved, current } = defineProps<{ saved: PVCameraInfo; current: PVCameraI
<th>Current</th> <th>Current</th>
</tr> </tr>
<tr <tr
v-if="'dev' in saved && 'dev' in current && saved.dev !== null" v-if="cameraInfoFor(saved).dev !== undefined && cameraInfoFor(saved).dev !== null"
:class="saved.dev !== current.dev ? 'mismatch' : ''" :class="cameraInfoFor(saved).dev !== cameraInfoFor(current).dev ? 'mismatch' : ''"
> >
<td>Device Number:</td> <td>Device Number:</td>
<td>{{ saved.dev }}</td> <td>{{ cameraInfoFor(saved).dev }}</td>
<td>{{ current.dev }}</td> <td>{{ cameraInfoFor(current).dev }}</td>
</tr>
<tr v-if="saved.name !== null" :class="saved.name !== current.name ? 'mismatch' : ''">
<td>Name:</td>
<td>{{ saved.name }}</td>
<td>{{ current.name }}</td>
</tr> </tr>
<tr <tr
v-if="'baseName' in saved && 'baseName' in current && saved.baseName !== null" v-if="cameraInfoFor(saved).name !== undefined && cameraInfoFor(saved).name !== null"
:class="saved.baseName !== current.baseName ? 'mismatch' : ''" :class="cameraInfoFor(saved).name !== cameraInfoFor(current).name ? 'mismatch' : ''"
>
<td>Name:</td>
<td>{{ cameraInfoFor(saved).name }}</td>
<td>{{ cameraInfoFor(current).name }}</td>
</tr>
<tr
v-if="cameraInfoFor(saved).baseName !== undefined && cameraInfoFor(saved).baseName !== null"
:class="cameraInfoFor(saved).baseName !== cameraInfoFor(current).baseName ? 'mismatch' : ''"
> >
<td>Base Name:</td> <td>Base Name:</td>
<td>{{ saved.baseName }}</td> <td>{{ cameraInfoFor(saved).baseName }}</td>
<td>{{ current.baseName }}</td> <td>{{ cameraInfoFor(current).baseName }}</td>
</tr> </tr>
<tr> <tr>
<td>Type:</td> <td>Type:</td>
<td v-if="saved.type === 'PVUsbCameraInfo'" class="mb-3">USB Camera</td> <td v-if="saved.PVUsbCameraInfo" class="mb-3">USB Camera</td>
<td v-else-if="saved.type === 'PVCSICameraInfo'" class="mb-3">CSI Camera</td> <td v-else-if="saved.PVCSICameraInfo" class="mb-3">CSI Camera</td>
<td v-else-if="saved.type === 'PVFileCameraInfo'" class="mb-3">File Camera</td> <td v-else-if="saved.PVFileCameraInfo" class="mb-3">File Camera</td>
<td v-else>Unidentified Camera Type</td> <td v-else>Unidentified Camera Type</td>
<td v-if="current.type === 'PVUsbCameraInfo'" class="mb-3">USB Camera</td> <td v-if="current.PVUsbCameraInfo" class="mb-3">USB Camera</td>
<td v-else-if="current.type === 'PVCSICameraInfo'" class="mb-3">CSI Camera</td> <td v-else-if="current.PVCSICameraInfo" class="mb-3">CSI Camera</td>
<td v-else-if="current.type === 'PVFileCameraInfo'" class="mb-3">File Camera</td> <td v-else-if="current.PVFileCameraInfo" class="mb-3">File Camera</td>
<td v-else>Unidentified Camera Type</td> <td v-else>Unidentified Camera Type</td>
</tr> </tr>
<tr <tr
v-if="'vendorId' in saved && 'vendorId' in current && saved.vendorId !== null" v-if="cameraInfoFor(saved).vendorId !== undefined && cameraInfoFor(saved).vendorId !== null"
:class="saved.vendorId !== current.vendorId ? 'mismatch' : ''" :class="cameraInfoFor(saved).vendorId !== cameraInfoFor(current).vendorId ? 'mismatch' : ''"
> >
<td>Vendor ID:</td> <td>Vendor ID:</td>
<td>{{ saved.vendorId }}</td> <td>{{ cameraInfoFor(saved).vendorId }}</td>
<td>{{ current.vendorId }}</td> <td>{{ cameraInfoFor(current).vendorId }}</td>
</tr> </tr>
<tr <tr
v-if="'productId' in saved && 'productId' in current && saved.productId !== null" v-if="cameraInfoFor(saved).productId !== undefined && cameraInfoFor(saved).productId !== null"
:class="saved.productId !== current.productId ? 'mismatch' : ''" :class="cameraInfoFor(saved).productId !== cameraInfoFor(current).productId ? 'mismatch' : ''"
> >
<td>Product ID:</td> <td>Product ID:</td>
<td>{{ saved.productId }}</td> <td>{{ cameraInfoFor(saved).productId }}</td>
<td>{{ current.productId }}</td> <td>{{ cameraInfoFor(current).productId }}</td>
</tr>
<tr v-if="saved.path !== null" :class="saved.path !== current.path ? 'mismatch' : ''">
<td>Path:</td>
<td style="word-break: break-all">{{ saved.path }}</td>
<td style="word-break: break-all">{{ current.path }}</td>
</tr>
<tr v-if="saved.uniquePath !== null" :class="saved.uniquePath !== current.uniquePath ? 'mismatch' : ''">
<td>Unique Path:</td>
<td style="word-break: break-all">{{ saved.uniquePath }}</td>
<td style="word-break: break-all">{{ current.uniquePath }}</td>
</tr> </tr>
<tr <tr
v-if="'otherPaths' in saved && 'otherPaths' in current && saved.otherPaths !== null" v-if="cameraInfoFor(saved).path !== undefined && cameraInfoFor(saved).path !== null"
:class="isEqual(saved.otherPaths, current.otherPaths) ? '' : 'mismatch'" :class="cameraInfoFor(saved).path !== cameraInfoFor(current).path ? 'mismatch' : ''"
>
<td>Path:</td>
<td style="word-break: break-all">{{ cameraInfoFor(saved).path }}</td>
<td style="word-break: break-all">{{ cameraInfoFor(current).path }}</td>
</tr>
<tr
v-if="cameraInfoFor(saved).uniquePath !== undefined && cameraInfoFor(saved).uniquePath !== null"
:class="cameraInfoFor(saved).uniquePath !== cameraInfoFor(current).uniquePath ? 'mismatch' : ''"
>
<td>Unique Path:</td>
<td style="word-break: break-all">{{ cameraInfoFor(saved).uniquePath }}</td>
<td style="word-break: break-all">{{ cameraInfoFor(current).uniquePath }}</td>
</tr>
<tr
v-if="cameraInfoFor(saved).otherPaths !== undefined && cameraInfoFor(saved).otherPaths !== null"
:class="isEqual(cameraInfoFor(saved).otherPaths, cameraInfoFor(current).otherPaths) ? '' : 'mismatch'"
> >
<td>Other Paths:</td> <td>Other Paths:</td>
<td>{{ saved.otherPaths }}</td> <td>{{ cameraInfoFor(saved).otherPaths }}</td>
<td>{{ current.otherPaths }}</td> <td>{{ cameraInfoFor(current).otherPaths }}</td>
</tr> </tr>
</tbody> </tbody>
</v-table> </v-table>

View File

@@ -25,7 +25,7 @@ const emit = defineEmits<{
(e: "onEscape"): void; (e: "onEscape"): void;
}>(); }>();
const handleKeydown = ({ key }: KeyboardEvent) => { const handleKeydown = ({ key }) => {
switch (key) { switch (key) {
case "Enter": case "Enter":
// Explicitly check that all rule props return true // Explicitly check that all rule props return true

View File

@@ -1,15 +1,13 @@
<script setup lang="ts" generic="T extends string | number"> <script setup lang="ts">
import { computed } from "vue"; import { computed } from "vue";
import TooltippedLabel from "@/components/common/pv-tooltipped-label.vue"; import TooltippedLabel from "@/components/common/pv-tooltipped-label.vue";
export interface SelectItem<TValue extends string | number> { export interface SelectItem {
name: string | number; name: string | number;
value: TValue; value: string | number;
disabled?: boolean; disabled?: boolean;
} }
const value = defineModel<string | number | undefined>({ required: true });
type SelectItems = SelectItem<T>[] | ReadonlyArray<T>;
const value = defineModel<T>({ required: true });
const props = withDefaults( const props = withDefaults(
defineProps<{ defineProps<{
@@ -17,7 +15,7 @@ const props = withDefaults(
tooltip?: string; tooltip?: string;
selectCols?: number; selectCols?: number;
disabled?: boolean; disabled?: boolean;
items: SelectItems; items: string[] | number[] | SelectItem[];
}>(), }>(),
{ {
selectCols: 9, selectCols: 9,
@@ -25,20 +23,18 @@ const props = withDefaults(
} }
); );
const areSelectItems = (items: SelectItems): items is SelectItem<T>[] => typeof items[0] === "object";
// Computed in case items changes // Computed in case items changes
const items = computed<SelectItem<T>[]>(() => { const items = computed<SelectItem[]>(() => {
// Trivial case for empty list; we have no data // Trivial case for empty list; we have no data
if (!props.items.length) { if (!props.items.length) {
return []; return [];
} }
if (areSelectItems(props.items)) { // Check if the prop exists on the object to infer object type
return props.items; if ((props.items[0] as SelectItem).name) {
return props.items as SelectItem[];
} }
return props.items.map((v, i) => ({ name: v, value: i }));
return props.items.map((item) => ({ name: item, value: item }));
}); });
</script> </script>
@@ -53,7 +49,7 @@ const items = computed<SelectItem<T>[]>(() => {
:items="items" :items="items"
item-title="name" item-title="name"
item-value="value" item-value="value"
item-props item-props.disabled="disabled"
:disabled="disabled" :disabled="disabled"
hide-details="auto" hide-details="auto"
variant="underlined" variant="underlined"

View File

@@ -18,11 +18,11 @@ const props = withDefaults(
const emit = defineEmits<{ (e: "update:modelValue", value: number): void }>(); const emit = defineEmits<{ (e: "update:modelValue", value: number): void }>();
// Debounce function // Debounce function
function debounce(func: (...args: number[]) => void, wait: number) { function debounce(func: (...args: any[]) => void, wait: number) {
let timeout: ReturnType<typeof setTimeout>; let timeout: ReturnType<typeof setTimeout>;
return function (...args: number[]) { return function (...args: any[]) {
clearTimeout(timeout); clearTimeout(timeout);
timeout = setTimeout(() => func(...args), wait); timeout = setTimeout(() => func.apply(this, args), wait);
}; };
} }

View File

@@ -13,6 +13,28 @@ import PvDeleteModal from "@/components/common/pv-delete-modal.vue";
const theme = useTheme(); const theme = useTheme();
const changeCurrentCameraUniqueName = (cameraUniqueName: string) => {
useCameraSettingsStore().setCurrentCameraUniqueName(cameraUniqueName, true);
switch (useCameraSettingsStore().cameras[cameraUniqueName].pipelineSettings.pipelineType) {
case PipelineType.Reflective:
pipelineType.value = WebsocketPipelineType.Reflective;
break;
case PipelineType.ColoredShape:
pipelineType.value = WebsocketPipelineType.ColoredShape;
break;
case PipelineType.AprilTag:
pipelineType.value = WebsocketPipelineType.AprilTag;
break;
case PipelineType.Aruco:
pipelineType.value = WebsocketPipelineType.Aruco;
break;
case PipelineType.ObjectDetection:
pipelineType.value = WebsocketPipelineType.ObjectDetection;
break;
}
};
// Common RegEx used for naming both pipelines and cameras // Common RegEx used for naming both pipelines and cameras
const nameChangeRegex = /^[A-Za-z0-9_ \-)(]*[A-Za-z0-9][A-Za-z0-9_ \-)(.]*$/; const nameChangeRegex = /^[A-Za-z0-9_ \-)(]*[A-Za-z0-9][A-Za-z0-9_ \-)(.]*$/;
@@ -65,17 +87,17 @@ const cancelCameraNameEdit = () => {
}; };
// Pipeline Name Edit // Pipeline Name Edit
const pipelineNamesWrapper = computed(() => { const pipelineNamesWrapper = computed<SelectItem[]>(() => {
const pipelineNames = useCameraSettingsStore().pipelineNames.map((name, index) => ({ name: name, value: index })); const pipelineNames = useCameraSettingsStore().pipelineNames.map((name, index) => ({ name: name, value: index }));
if (useCameraSettingsStore().isDriverMode) { if (useCameraSettingsStore().isDriverMode) {
pipelineNames.push({ name: "Driver Mode", value: WebsocketPipelineType.DriverMode.valueOf() }); pipelineNames.push({ name: "Driver Mode", value: WebsocketPipelineType.DriverMode });
} }
if (useCameraSettingsStore().isFocusMode) { if (useCameraSettingsStore().isFocusMode) {
pipelineNames.push({ name: "Focus Mode", value: WebsocketPipelineType.FocusCamera.valueOf() }); pipelineNames.push({ name: "Focus Mode", value: WebsocketPipelineType.FocusCamera });
} }
if (useCameraSettingsStore().isCalibrationMode) { if (useCameraSettingsStore().isCalibrationMode) {
pipelineNames.push({ name: "3D Calibration Mode", value: WebsocketPipelineType.Calib3d.valueOf() }); pipelineNames.push({ name: "3D Calibration Mode", value: WebsocketPipelineType.Calib3d });
} }
return pipelineNames; return pipelineNames;
@@ -218,7 +240,7 @@ useCameraSettingsStore().$subscribe((mutation, state) => {
break; break;
} }
}); });
const wrappedCameras = computed<SelectItem<string>[]>(() => const wrappedCameras = computed<SelectItem[]>(() =>
Object.keys(useCameraSettingsStore().cameras).map((cameraUniqueName) => ({ Object.keys(useCameraSettingsStore().cameras).map((cameraUniqueName) => ({
name: useCameraSettingsStore().cameras[cameraUniqueName].nickname, name: useCameraSettingsStore().cameras[cameraUniqueName].nickname,
value: cameraUniqueName value: cameraUniqueName
@@ -235,7 +257,7 @@ const wrappedCameras = computed<SelectItem<string>[]>(() =>
v-model="useStateStore().currentCameraUniqueName" v-model="useStateStore().currentCameraUniqueName"
label="Camera" label="Camera"
:items="wrappedCameras" :items="wrappedCameras"
@update:modelValue="pipelineType = useCameraSettingsStore().currentWebsocketPipelineType" @update:modelValue="changeCurrentCameraUniqueName"
/> />
<pv-input <pv-input
v-else v-else

View File

@@ -13,9 +13,9 @@ import OutputTab from "@/components/dashboard/tabs/OutputTab.vue";
import TargetsTab from "@/components/dashboard/tabs/TargetsTab.vue"; import TargetsTab from "@/components/dashboard/tabs/TargetsTab.vue";
import PnPTab from "@/components/dashboard/tabs/PnPTab.vue"; import PnPTab from "@/components/dashboard/tabs/PnPTab.vue";
import Map3DTab from "@/components/dashboard/tabs/Map3DTab.vue"; import Map3DTab from "@/components/dashboard/tabs/Map3DTab.vue";
import { PipelineType } from "@/types/PipelineTypes";
import { WebsocketPipelineType } from "@/types/WebsocketDataTypes"; import { WebsocketPipelineType } from "@/types/WebsocketDataTypes";
import { useDisplay, useTheme } from "vuetify"; import { useDisplay } from "vuetify/lib/composables/display";
import { useTheme } from "vuetify";
const theme = useTheme(); const theme = useTheme();
@@ -106,17 +106,6 @@ const tabGroups = computed<ConfigOption[][]>(() => {
.filter((it) => it.length); // Remove empty tab groups .filter((it) => it.length); // Remove empty tab groups
}); });
// This boolean is used to satisfy type-checking requirements.
const shouldUseWideSecondTabGroup = computed(() => {
const currentPipelineSettings = useCameraSettingsStore().currentPipelineSettings;
return (
(currentPipelineSettings.pipelineType === PipelineType.AprilTag ||
currentPipelineSettings.pipelineType === PipelineType.Aruco) &&
currentPipelineSettings.doMultiTarget
);
});
const onBeforeTabUpdate = () => { const onBeforeTabUpdate = () => {
// Force the current tab to the input tab on driver mode change // Force the current tab to the input tab on driver mode change
if (useCameraSettingsStore().isDriverMode) { if (useCameraSettingsStore().isDriverMode) {
@@ -140,7 +129,7 @@ const onBeforeTabUpdate = () => {
<v-col <v-col
v-for="(tabGroupData, tabGroupIndex) in tabGroups" v-for="(tabGroupData, tabGroupIndex) in tabGroups"
:key="tabGroupIndex" :key="tabGroupIndex"
:cols="tabGroupIndex === 1 && shouldUseWideSecondTabGroup ? 7 : ''" :cols="tabGroupIndex === 1 && useCameraSettingsStore().currentPipelineSettings.doMultiTarget ? 7 : ''"
:class="tabGroupIndex !== tabGroups.length - 1 && 'pr-3'" :class="tabGroupIndex !== tabGroups.length - 1 && 'pr-3'"
@vue:before-update="onBeforeTabUpdate" @vue:before-update="onBeforeTabUpdate"
> >

View File

@@ -1,17 +1,18 @@
<script setup lang="ts"> <script setup lang="ts">
import { PipelineType, type AprilTagPipelineSettings, AprilTagFamily } from "@/types/PipelineTypes"; import { PipelineType } from "@/types/PipelineTypes";
import PvSelect from "@/components/common/pv-select.vue"; import PvSelect from "@/components/common/pv-select.vue";
import PvSlider from "@/components/common/pv-slider.vue"; import PvSlider from "@/components/common/pv-slider.vue";
import PvSwitch from "@/components/common/pv-switch.vue"; import PvSwitch from "@/components/common/pv-switch.vue";
import { computed } from "vue"; import { computed } from "vue";
import { useStateStore } from "@/stores/StateStore"; import { useStateStore } from "@/stores/StateStore";
import type { ActivePipelineSettings } from "@/types/PipelineTypes";
import { useCameraSettingsStore } from "@/stores/settings/CameraSettingsStore"; import { useCameraSettingsStore } from "@/stores/settings/CameraSettingsStore";
import { useDisplay } from "vuetify"; 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 // 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 // Defer reference to store access method
const currentPipelineSettings = computed<AprilTagPipelineSettings>( const currentPipelineSettings = computed<ActivePipelineSettings>(
() => useCameraSettingsStore().currentPipelineSettings as AprilTagPipelineSettings () => useCameraSettingsStore().currentPipelineSettings
); );
const { mdAndDown } = useDisplay(); const { mdAndDown } = useDisplay();
const interactiveCols = computed(() => const interactiveCols = computed(() =>
@@ -24,10 +25,7 @@ const interactiveCols = computed(() =>
<pv-select <pv-select
v-model="currentPipelineSettings.tagFamily" v-model="currentPipelineSettings.tagFamily"
label="Target family" label="Target family"
:items="[ :items="['AprilTag 36h11 (6.5in)', 'AprilTag 16h5 (6in)']"
{ value: AprilTagFamily.Family36h11, name: 'AprilTag 36h11 (6.5in)' },
{ value: AprilTagFamily.Family16h5, name: 'AprilTag 16h5 (6in)' }
]"
:select-cols="interactiveCols" :select-cols="interactiveCols"
@update:modelValue="(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ tagFamily: value }, false)" @update:modelValue="(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ tagFamily: value }, false)"
/> />

View File

@@ -1,6 +1,6 @@
<script setup lang="ts"> <script setup lang="ts">
import { useCameraSettingsStore } from "@/stores/settings/CameraSettingsStore"; import { useCameraSettingsStore } from "@/stores/settings/CameraSettingsStore";
import { PipelineType, type ArucoPipelineSettings, AprilTagFamily } from "@/types/PipelineTypes"; import { PipelineType, type ActivePipelineSettings } from "@/types/PipelineTypes";
import PvSlider from "@/components/common/pv-slider.vue"; import PvSlider from "@/components/common/pv-slider.vue";
import PvSwitch from "@/components/common/pv-switch.vue"; import PvSwitch from "@/components/common/pv-switch.vue";
import PvRangeSlider from "@/components/common/pv-range-slider.vue"; import PvRangeSlider from "@/components/common/pv-range-slider.vue";
@@ -11,8 +11,8 @@ 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 // 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 // Defer reference to store access method
const currentPipelineSettings = computed<ArucoPipelineSettings>( const currentPipelineSettings = computed<ActivePipelineSettings>(
() => useCameraSettingsStore().currentPipelineSettings as ArucoPipelineSettings () => useCameraSettingsStore().currentPipelineSettings
); );
const { mdAndDown } = useDisplay(); const { mdAndDown } = useDisplay();
const interactiveCols = computed(() => const interactiveCols = computed(() =>
@@ -25,10 +25,7 @@ const interactiveCols = computed(() =>
<pv-select <pv-select
v-model="currentPipelineSettings.tagFamily" v-model="currentPipelineSettings.tagFamily"
label="Target family" label="Target family"
:items="[ :items="['AprilTag Family 36h11', 'AprilTag Family 16h5']"
{ value: AprilTagFamily.Family36h11, name: 'AprilTag 36h11 (6.5in)' },
{ value: AprilTagFamily.Family16h5, name: 'AprilTag 16h5 (6in)' }
]"
:select-cols="interactiveCols" :select-cols="interactiveCols"
@update:modelValue="(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ tagFamily: value }, false)" @update:modelValue="(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ tagFamily: value }, false)"
/> />

View File

@@ -1,14 +1,6 @@
<script setup lang="ts"> <script setup lang="ts">
import { useCameraSettingsStore } from "@/stores/settings/CameraSettingsStore"; import { useCameraSettingsStore } from "@/stores/settings/CameraSettingsStore";
import { import { type ActivePipelineSettings, PipelineType } from "@/types/PipelineTypes";
type ActivePipelineSettings,
PipelineType,
ContourSortMode,
ContourTargetOrientation,
ContourGroupingMode,
ContourIntersection,
ContourShape
} from "@/types/PipelineTypes";
import PvRangeSlider from "@/components/common/pv-range-slider.vue"; import PvRangeSlider from "@/components/common/pv-range-slider.vue";
import PvSelect from "@/components/common/pv-select.vue"; import PvSelect from "@/components/common/pv-select.vue";
import PvSlider from "@/components/common/pv-slider.vue"; import PvSlider from "@/components/common/pv-slider.vue";
@@ -69,10 +61,7 @@ const interactiveCols = computed(() =>
v-model="useCameraSettingsStore().currentPipelineSettings.contourTargetOrientation" v-model="useCameraSettingsStore().currentPipelineSettings.contourTargetOrientation"
label="Target Orientation" label="Target Orientation"
tooltip="Used to determine how to calculate target landmarks, as well as aspect ratio" tooltip="Used to determine how to calculate target landmarks, as well as aspect ratio"
:items="[ :items="['Portrait', 'Landscape']"
{ value: ContourTargetOrientation.Portrait, name: 'Portrait' },
{ value: ContourTargetOrientation.Landscape, name: 'Landscape' }
]"
:select-cols="interactiveCols" :select-cols="interactiveCols"
@update:modelValue=" @update:modelValue="
(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ contourTargetOrientation: value }, false) (value) => useCameraSettingsStore().changeCurrentPipelineSetting({ contourTargetOrientation: value }, false)
@@ -83,15 +72,7 @@ const interactiveCols = computed(() =>
label="Target Sort" label="Target Sort"
tooltip="Chooses the sorting mode used to determine the 'best' targets to provide to user code" tooltip="Chooses the sorting mode used to determine the 'best' targets to provide to user code"
:select-cols="interactiveCols" :select-cols="interactiveCols"
:items="[ :items="['Largest', 'Smallest', 'Highest', 'Lowest', 'Rightmost', 'Leftmost', 'Centermost']"
{ value: ContourSortMode.Largest, name: 'Largest' },
{ value: ContourSortMode.Smallest, name: 'Smallest' },
{ value: ContourSortMode.Highest, name: 'Highest' },
{ value: ContourSortMode.Lowest, name: 'Lowest' },
{ value: ContourSortMode.Rightmost, name: 'Rightmost' },
{ value: ContourSortMode.Leftmost, name: 'Leftmost' },
{ value: ContourSortMode.Centermost, name: 'Centermost' }
]"
@update:modelValue=" @update:modelValue="
(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ contourSortMode: value }, false) (value) => useCameraSettingsStore().changeCurrentPipelineSetting({ contourSortMode: value }, false)
" "
@@ -185,11 +166,7 @@ const interactiveCols = computed(() =>
label="Target Grouping" label="Target Grouping"
tooltip="Whether or not every two targets are paired with each other (good for e.g. 2019 targets)" tooltip="Whether or not every two targets are paired with each other (good for e.g. 2019 targets)"
:select-cols="interactiveCols" :select-cols="interactiveCols"
:items="[ :items="['Single', 'Dual', 'Two or More']"
{ value: ContourGroupingMode.Single, name: 'Single' },
{ value: ContourGroupingMode.Dual, name: 'Dual' },
{ value: ContourGroupingMode.TwoOrMore, name: 'Two or More' }
]"
@update:modelValue=" @update:modelValue="
(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ contourGroupingMode: value }, false) (value) => useCameraSettingsStore().changeCurrentPipelineSetting({ contourGroupingMode: value }, false)
" "
@@ -199,13 +176,7 @@ const interactiveCols = computed(() =>
label="Target Intersection" label="Target Intersection"
tooltip="If target grouping is in dual mode it will use this dropdown to decide how targets are grouped with adjacent targets" tooltip="If target grouping is in dual mode it will use this dropdown to decide how targets are grouped with adjacent targets"
:select-cols="interactiveCols" :select-cols="interactiveCols"
:items="[ :items="['None', 'Up', 'Down', 'Left', 'Right']"
{ value: ContourIntersection.None, name: 'None' },
{ value: ContourIntersection.Up, name: 'Up' },
{ value: ContourIntersection.Down, name: 'Down' },
{ value: ContourIntersection.Left, name: 'Left' },
{ value: ContourIntersection.Right, name: 'Right' }
]"
:disabled="useCameraSettingsStore().currentPipelineSettings.contourGroupingMode === 0" :disabled="useCameraSettingsStore().currentPipelineSettings.contourGroupingMode === 0"
@update:modelValue=" @update:modelValue="
(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ contourIntersection: value }, false) (value) => useCameraSettingsStore().changeCurrentPipelineSetting({ contourIntersection: value }, false)
@@ -218,12 +189,7 @@ const interactiveCols = computed(() =>
label="Target Shape" label="Target Shape"
tooltip="The shape of targets to look for" tooltip="The shape of targets to look for"
:select-cols="interactiveCols" :select-cols="interactiveCols"
:items="[ :items="['Circle', 'Polygon', 'Triangle', 'Quadrilateral']"
{ value: ContourShape.Circle, name: 'Circle' },
{ value: ContourShape.Polygon, name: 'Polygon' },
{ value: ContourShape.Triangle, name: 'Triangle' },
{ value: ContourShape.Quadrilateral, name: 'Quadrilateral' }
]"
@update:modelValue=" @update:modelValue="
(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ contourShape: value }, false) (value) => useCameraSettingsStore().changeCurrentPipelineSetting({ contourShape: value }, false)
" "

View File

@@ -30,11 +30,11 @@ const getFilteredStreamDivisors = (): number[] => {
}; };
const getNumberOfSkippedDivisors = () => streamDivisors.length - getFilteredStreamDivisors().length; const getNumberOfSkippedDivisors = () => streamDivisors.length - getFilteredStreamDivisors().length;
const cameraResolutions = (): { name: string; value: number }[] => const cameraResolutions = computed(() =>
useCameraSettingsStore().currentCameraSettings.validVideoFormats.map<{ name: string; value: number }>((f) => ({ useCameraSettingsStore().currentCameraSettings.validVideoFormats.map(
name: `${getResolutionString(f.resolution)} at ${f.fps} FPS, ${f.pixelFormat}`, (f) => `${getResolutionString(f.resolution)} at ${f.fps} FPS, ${f.pixelFormat}`
value: f.index || 0 // Index won't ever be undefined )
})); );
const handleResolutionChange = (value: number) => { const handleResolutionChange = (value: number) => {
useCameraSettingsStore().changeCurrentPipelineSetting({ cameraVideoModeIndex: value }, false); useCameraSettingsStore().changeCurrentPipelineSetting({ cameraVideoModeIndex: value }, false);
@@ -49,23 +49,20 @@ const handleResolutionChange = (value: number) => {
const streamResolutions = computed(() => { const streamResolutions = computed(() => {
const streamDivisors = getFilteredStreamDivisors(); const streamDivisors = getFilteredStreamDivisors();
const currentResolution = useCameraSettingsStore().currentVideoFormat.resolution; const currentResolution = useCameraSettingsStore().currentVideoFormat.resolution;
return streamDivisors.map((x, i) => ({ return streamDivisors.map(
name: `${Math.floor(currentResolution.width / x)}x${Math.floor(currentResolution.height / x)}`, (x) =>
value: i `${getResolutionString({
})); width: Math.floor(currentResolution.width / x),
}); height: Math.floor(currentResolution.height / x)
const currentStreamResolutionIndex = computed<number>({ })}`
get: () => { );
const stored = useCameraSettingsStore().currentPipelineSettings.streamingFrameDivisor;
const skipped = getNumberOfSkippedDivisors();
return stored - skipped;
},
set: (index) => {
useCameraSettingsStore().changeCurrentPipelineSetting({
streamingFrameDivisor: index + getNumberOfSkippedDivisors()
});
}
}); });
const handleStreamResolutionChange = (value: number) => {
useCameraSettingsStore().changeCurrentPipelineSetting(
{ streamingFrameDivisor: value + getNumberOfSkippedDivisors() },
false
);
};
const { mdAndDown } = useDisplay(); const { mdAndDown } = useDisplay();
const interactiveCols = computed(() => const interactiveCols = computed(() =>
@@ -163,7 +160,7 @@ const interactiveCols = computed(() =>
/> />
<pv-switch <pv-switch
v-model="useCameraSettingsStore().currentPipelineSettings.blockForFrames" v-model="useCameraSettingsStore().currentPipelineSettings.blockForFrames"
:disabled="useCameraSettingsStore().currentCameraSettings.matchedCameraInfo.type !== 'PVUsbCameraInfo'" :disabled="!useCameraSettingsStore().currentCameraSettings.matchedCameraInfo.PVUsbCameraInfo"
label="Low Latency Mode" label="Low Latency Mode"
:switch-cols="interactiveCols" :switch-cols="interactiveCols"
tooltip="When enabled, USB cameras wait for the next camera frame for lowest latency. When disabled, uses the most recent available frame for higher FPS." tooltip="When enabled, USB cameras wait for the next camera frame for lowest latency. When disabled, uses the most recent available frame for higher FPS."
@@ -185,16 +182,17 @@ const interactiveCols = computed(() =>
v-model="useCameraSettingsStore().currentPipelineSettings.cameraVideoModeIndex" v-model="useCameraSettingsStore().currentPipelineSettings.cameraVideoModeIndex"
label="Resolution" label="Resolution"
tooltip="Resolution and FPS the camera should directly capture at" tooltip="Resolution and FPS the camera should directly capture at"
:items="cameraResolutions()" :items="cameraResolutions"
:select-cols="interactiveCols" :select-cols="interactiveCols"
@update:modelValue="(args) => handleResolutionChange(args)" @update:modelValue="(args) => handleResolutionChange(args)"
/> />
<pv-select <pv-select
v-model="currentStreamResolutionIndex" v-model="useCameraSettingsStore().currentPipelineSettings.streamingFrameDivisor"
label="Stream Resolution" label="Stream Resolution"
tooltip="Resolution to which camera frames are downscaled for streaming to the dashboard" tooltip="Resolution to which camera frames are downscaled for streaming to the dashboard"
:items="streamResolutions" :items="streamResolutions"
:select-cols="interactiveCols" :select-cols="interactiveCols"
@update:modelValue="(args) => handleStreamResolutionChange(args)"
/> />
<pv-switch <pv-switch
v-if="useCameraSettingsStore().isDriverMode" v-if="useCameraSettingsStore().isDriverMode"

View File

@@ -1,13 +1,8 @@
<script setup lang="ts"> <script setup lang="ts">
import { useCameraSettingsStore } from "@/stores/settings/CameraSettingsStore"; import { useCameraSettingsStore } from "@/stores/settings/CameraSettingsStore";
import { import { type ObjectDetectionPipelineSettings, PipelineType } from "@/types/PipelineTypes";
type ObjectDetectionPipelineSettings,
PipelineType,
ContourSortMode,
ContourTargetOrientation
} from "@/types/PipelineTypes";
import PvSlider from "@/components/common/pv-slider.vue"; import PvSlider from "@/components/common/pv-slider.vue";
import PvSelect, { type SelectItem } from "@/components/common/pv-select.vue"; import PvSelect from "@/components/common/pv-select.vue";
import PvRangeSlider from "@/components/common/pv-range-slider.vue"; import PvRangeSlider from "@/components/common/pv-range-slider.vue";
import { computed } from "vue"; import { computed } from "vue";
import { useStateStore } from "@/stores/StateStore"; import { useStateStore } from "@/stores/StateStore";
@@ -49,19 +44,19 @@ const supportedModels = computed<ObjectDetectionModelProperties[]>(() => {
return availableModels.filter(isSupported); return availableModels.filter(isSupported);
}); });
const modelWrapper = computed<SelectItem<string>[]>(() => const selectedModel = computed({
supportedModels.value.map((model) => ({ get: () => {
name: model.nickname, const currentModel = currentPipelineSettings.value.model;
value: model.modelPath if (!currentModel) return undefined;
}))
);
const selectedModel = computed<string>({ const index = supportedModels.value.findIndex((model) => model.modelPath === currentModel.modelPath);
get: () => currentPipelineSettings.value.model?.modelPath ?? "", return index === -1 ? undefined : index;
set: (value) => { },
const model = supportedModels.value.find((supportedModel) => supportedModel.modelPath === value);
if (model) { set: (v) => {
useCameraSettingsStore().changeCurrentPipelineSetting({ model }, true); if (v !== undefined && v >= 0 && v < supportedModels.value.length) {
const newModel = supportedModels.value[v];
useCameraSettingsStore().changeCurrentPipelineSetting({ model: newModel }, true);
} }
} }
}); });
@@ -74,7 +69,7 @@ const selectedModel = computed<string>({
label="Model" label="Model"
tooltip="The model used to detect objects in the camera feed" tooltip="The model used to detect objects in the camera feed"
:select-cols="interactiveCols" :select-cols="interactiveCols"
:items="modelWrapper" :items="supportedModels.map((model) => model.nickname)"
/> />
<pv-slider <pv-slider
@@ -128,13 +123,14 @@ const selectedModel = computed<string>({
v-model="useCameraSettingsStore().currentPipelineSettings.contourTargetOrientation" v-model="useCameraSettingsStore().currentPipelineSettings.contourTargetOrientation"
label="Target Orientation" label="Target Orientation"
tooltip="Used to determine how to calculate target landmarks, as well as aspect ratio" tooltip="Used to determine how to calculate target landmarks, as well as aspect ratio"
:items="[ :items="['Portrait', 'Landscape']"
{ value: ContourTargetOrientation.Portrait, name: 'Portrait' },
{ value: ContourTargetOrientation.Landscape, name: 'Landscape' }
]"
:select-cols="interactiveCols" :select-cols="interactiveCols"
@update:modelValue=" @update:modelValue="
(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ contourTargetOrientation: value }, false) (value) =>
useCameraSettingsStore().changeCurrentPipelineSetting(
{ contourTargetOrientation: typeof value === 'string' ? Number(value) : value },
false
)
" "
/> />
<pv-select <pv-select
@@ -142,17 +138,13 @@ const selectedModel = computed<string>({
label="Target Sort" label="Target Sort"
tooltip="Chooses the sorting mode used to determine the 'best' targets to provide to user code" tooltip="Chooses the sorting mode used to determine the 'best' targets to provide to user code"
:select-cols="interactiveCols" :select-cols="interactiveCols"
:items="[ :items="['Largest', 'Smallest', 'Highest', 'Lowest', 'Rightmost', 'Leftmost', 'Centermost']"
{ value: ContourSortMode.Largest, name: 'Largest' },
{ value: ContourSortMode.Smallest, name: 'Smallest' },
{ value: ContourSortMode.Highest, name: 'Highest' },
{ value: ContourSortMode.Lowest, name: 'Lowest' },
{ value: ContourSortMode.Rightmost, name: 'Rightmost' },
{ value: ContourSortMode.Leftmost, name: 'Leftmost' },
{ value: ContourSortMode.Centermost, name: 'Centermost' }
]"
@update:modelValue=" @update:modelValue="
(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ contourSortMode: value }, false) (value) =>
useCameraSettingsStore().changeCurrentPipelineSetting(
{ contourSortMode: typeof value === 'string' ? Number(value) : value },
false
)
" "
/> />
</div> </div>

View File

@@ -1,13 +1,7 @@
<script setup lang="ts"> <script setup lang="ts">
import PvSelect from "@/components/common/pv-select.vue"; import PvSelect from "@/components/common/pv-select.vue";
import { useCameraSettingsStore } from "@/stores/settings/CameraSettingsStore"; import { useCameraSettingsStore } from "@/stores/settings/CameraSettingsStore";
import { import { type ActivePipelineSettings, PipelineType, RobotOffsetPointMode } from "@/types/PipelineTypes";
type ActivePipelineSettings,
PipelineType,
RobotOffsetPointMode,
ContourTargetOrientation,
ContourTargetOffsetPointEdge
} from "@/types/PipelineTypes";
import PvSwitch from "@/components/common/pv-switch.vue"; import PvSwitch from "@/components/common/pv-switch.vue";
import PvSlider from "@/components/common/pv-slider.vue"; import PvSlider from "@/components/common/pv-slider.vue";
import { computed } from "vue"; import { computed } from "vue";
@@ -114,13 +108,7 @@ const interactiveCols = computed(() =>
v-model="useCameraSettingsStore().currentPipelineSettings.contourTargetOffsetPointEdge" v-model="useCameraSettingsStore().currentPipelineSettings.contourTargetOffsetPointEdge"
label="Target Offset Point" label="Target Offset Point"
tooltip="Changes where the 'center' of the target is (used for calculating e.g. pitch and yaw)" tooltip="Changes where the 'center' of the target is (used for calculating e.g. pitch and yaw)"
:items="[ :items="['Center', 'Top', 'Bottom', 'Left', 'Right']"
{ value: ContourTargetOffsetPointEdge.Center, name: 'Center' },
{ value: ContourTargetOffsetPointEdge.Top, name: 'Top' },
{ value: ContourTargetOffsetPointEdge.Bottom, name: 'Bottom' },
{ value: ContourTargetOffsetPointEdge.Left, name: 'Left' },
{ value: ContourTargetOffsetPointEdge.Right, name: 'Right' }
]"
:select-cols="interactiveCols" :select-cols="interactiveCols"
@update:modelValue=" @update:modelValue="
(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ contourTargetOffsetPointEdge: value }, false) (value) => useCameraSettingsStore().changeCurrentPipelineSetting({ contourTargetOffsetPointEdge: value }, false)
@@ -131,10 +119,7 @@ const interactiveCols = computed(() =>
v-model="useCameraSettingsStore().currentPipelineSettings.contourTargetOrientation" v-model="useCameraSettingsStore().currentPipelineSettings.contourTargetOrientation"
label="Target Orientation" label="Target Orientation"
tooltip="Used to determine how to calculate target landmarks (e.g. the top, left, or bottom of the target)" tooltip="Used to determine how to calculate target landmarks (e.g. the top, left, or bottom of the target)"
:items="[ :items="['Portrait', 'Landscape']"
{ value: ContourTargetOrientation.Portrait, name: 'Portrait' },
{ value: ContourTargetOrientation.Landscape, name: 'Landscape' }
]"
:select-cols="interactiveCols" :select-cols="interactiveCols"
@update:modelValue=" @update:modelValue="
(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ contourTargetOrientation: value }, false) (value) => useCameraSettingsStore().changeCurrentPipelineSetting({ contourTargetOrientation: value }, false)
@@ -144,11 +129,7 @@ const interactiveCols = computed(() =>
v-model="useCameraSettingsStore().currentPipelineSettings.offsetRobotOffsetMode" v-model="useCameraSettingsStore().currentPipelineSettings.offsetRobotOffsetMode"
label="Robot Offset Mode" label="Robot Offset Mode"
tooltip="Used to add an arbitrary offset to the location of the targeting crosshair" tooltip="Used to add an arbitrary offset to the location of the targeting crosshair"
:items="[ :items="['None', 'Single Point', 'Dual Point']"
{ value: RobotOffsetPointMode.None, name: 'None' },
{ value: RobotOffsetPointMode.Single, name: 'Single Point' },
{ value: RobotOffsetPointMode.Dual, name: 'Dual Point' }
]"
:select-cols="interactiveCols" :select-cols="interactiveCols"
@update:modelValue=" @update:modelValue="
(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ offsetRobotOffsetMode: value }, false) (value) => useCameraSettingsStore().changeCurrentPipelineSetting({ offsetRobotOffsetMode: value }, false)

View File

@@ -1,5 +1,6 @@
@ -0,0 +1,565 @@
<script setup lang="ts"> <script setup lang="ts">
import { inject, computed, ref, watch, useTemplateRef } from "vue"; import { inject, computed, ref, watch } from "vue";
import { useStateStore } from "@/stores/StateStore"; import { useStateStore } from "@/stores/StateStore";
import { useSettingsStore } from "@/stores/settings/GeneralSettingsStore"; import { useSettingsStore } from "@/stores/settings/GeneralSettingsStore";
import PvSelect from "@/components/common/pv-select.vue"; import PvSelect from "@/components/common/pv-select.vue";
@@ -14,20 +15,20 @@ const theme = useTheme();
const restartProgram = async () => { const restartProgram = async () => {
if (await axiosPost("/utils/restartProgram", "restart PhotonVision")) { if (await axiosPost("/utils/restartProgram", "restart PhotonVision")) {
await forceReloadPage(); forceReloadPage();
} }
}; };
const restartDevice = async () => { const restartDevice = async () => {
if (await axiosPost("/utils/restartDevice", "restart the device")) { if (await axiosPost("/utils/restartDevice", "restart the device")) {
await forceReloadPage(); forceReloadPage();
} }
}; };
const address = inject<string>("backendHost"); const address = inject<string>("backendHost");
const offlineUpdate = useTemplateRef("offlineUpdate"); const offlineUpdate = ref();
const openOfflineUpdatePrompt = () => { const openOfflineUpdatePrompt = () => {
offlineUpdate.value?.click(); offlineUpdate.value.click();
}; };
const offlineUpdateRegex = new RegExp("photonvision-((?:dev-)?v[\\w.-]+)-((?:linux|win|mac)\\w+)\\.jar"); const offlineUpdateRegex = new RegExp("photonvision-((?:dev-)?v[\\w.-]+)-((?:linux|win|mac)\\w+)\\.jar");
@@ -36,8 +37,8 @@ const majorVersionRegex = new RegExp("(?:dev-)?(\\d+)\\.\\d+\\.\\d+");
const offlineUpdateDialog = ref({ show: false, confirmString: "" }); const offlineUpdateDialog = ref({ show: false, confirmString: "" });
const handleOfflineUpdateRequest = async () => { const handleOfflineUpdateRequest = async () => {
const files = offlineUpdate.value?.files; const files = offlineUpdate.value.files;
if (!files?.length) return; if (files.length === 0) return;
const match = files[0].name.match(offlineUpdateRegex); const match = files[0].name.match(offlineUpdateRegex);
if (!match) { if (!match) {
@@ -67,7 +68,7 @@ const handleOfflineUpdateRequest = async () => {
}); });
return; return;
} else if (versionMatch && !dev) { } else if (versionMatch && !dev) {
await handleOfflineUpdate(files[0]); handleOfflineUpdate(files[0]);
} else if (!versionMatch && !dev) { } else if (!versionMatch && !dev) {
offlineUpdateDialog.value = { offlineUpdateDialog.value = {
show: true, show: true,
@@ -98,7 +99,7 @@ const handleOfflineUpdate = async (file: File) => {
if ( if (
await axiosPost("/utils/offlineUpdate", "upload new software", formData, { await axiosPost("/utils/offlineUpdate", "upload new software", formData, {
headers: { "Content-Type": "multipart/form-data" }, headers: { "Content-Type": "multipart/form-data" },
onUploadProgress: ({ progress }: { progress?: number }) => { onUploadProgress: ({ progress }) => {
const uploadPercentage = (progress || 0) * 100.0; const uploadPercentage = (progress || 0) * 100.0;
if (uploadPercentage < 99.5) { if (uploadPercentage < 99.5) {
useStateStore().showSnackbarMessage({ useStateStore().showSnackbarMessage({
@@ -117,18 +118,18 @@ const handleOfflineUpdate = async (file: File) => {
color: "secondary", color: "secondary",
timeout: -1 timeout: -1
}); });
await forceReloadPage(); forceReloadPage();
} }
}; };
const exportLogFile = useTemplateRef("exportLogFile"); const exportLogFile = ref();
const openExportLogsPrompt = () => { const openExportLogsPrompt = () => {
exportLogFile.value?.click(); exportLogFile.value.click();
}; };
const exportSettings = useTemplateRef("exportSettings"); const exportSettings = ref();
const openExportSettingsPrompt = () => { const openExportSettingsPrompt = () => {
exportSettings.value?.click(); exportSettings.value.click();
}; };
enum ImportType { enum ImportType {
@@ -140,10 +141,10 @@ enum ImportType {
} }
const showImportDialog = ref(false); const showImportDialog = ref(false);
const importType = ref<ImportType>(ImportType.AllSettings); const importType = ref<ImportType | undefined>(undefined);
const importFile = ref<File | null>(null); const importFile = ref<File | null>(null);
const handleSettingsImport = async () => { const handleSettingsImport = () => {
if (importType.value === undefined || importFile.value === null) return; if (importType.value === undefined || importFile.value === null) return;
const formData = new FormData(); const formData = new FormData();
formData.append("data", importFile.value); formData.append("data", importFile.value);
@@ -166,18 +167,18 @@ const handleSettingsImport = async () => {
settingsEndpoint = ""; settingsEndpoint = "";
break; break;
} }
await axiosPost(`/settings${settingsEndpoint}`, "import settings", formData, { axiosPost(`/settings${settingsEndpoint}`, "import settings", formData, {
headers: { "Content-Type": "multipart/form-data" } headers: { "Content-Type": "multipart/form-data" }
}); });
showImportDialog.value = false; showImportDialog.value = false;
importType.value = ImportType.AllSettings; importType.value = undefined;
importFile.value = null; importFile.value = null;
}; };
const showFactoryReset = ref(false); const showFactoryReset = ref(false);
const nukePhotonConfigDirectory = async () => { const nukePhotonConfigDirectory = async () => {
if (await axiosPost("/utils/nukeConfigDirectory", "delete the config directory")) { if (await axiosPost("/utils/nukeConfigDirectory", "delete the config directory")) {
await forceReloadPage(); forceReloadPage();
} }
}; };
@@ -502,7 +503,7 @@ watch(metricsHistorySnapshot, () => {
width="600" width="600"
@update:modelValue=" @update:modelValue="
() => { () => {
importType = ImportType.AllSettings; importType = undefined;
importFile = null; importFile = null;
} }
" "
@@ -516,13 +517,7 @@ watch(metricsHistorySnapshot, () => {
v-model="importType" v-model="importType"
label="Type" label="Type"
tooltip="Select the type of settings file you are trying to upload" tooltip="Select the type of settings file you are trying to upload"
:items="[ :items="['All Settings', 'Hardware Config', 'Hardware Settings', 'Network Config', 'Apriltag Layout']"
{ value: ImportType.AllSettings, name: 'All Settings' },
{ value: ImportType.HardwareConfig, name: 'Hardware Config' },
{ value: ImportType.HardwareSettings, name: 'Hardware Settings' },
{ value: ImportType.NetworkConfig, name: 'Network Config' },
{ value: ImportType.ApriltagFieldLayout, name: 'AprilTag Field Layout' }
]"
:select-cols="10" :select-cols="10"
style="width: 100%" style="width: 100%"
/> />
@@ -563,9 +558,7 @@ watch(metricsHistorySnapshot, () => {
:variant="theme.global.current.value.dark ? 'outlined' : 'elevated'" :variant="theme.global.current.value.dark ? 'outlined' : 'elevated'"
@click=" @click="
offlineUpdateDialog.show = false; offlineUpdateDialog.show = false;
if (offlineUpdate?.files?.length) { handleOfflineUpdate(offlineUpdate.files[0]);
handleOfflineUpdate(offlineUpdate.files[0]);
}
" "
> >
<v-icon start class="open-icon" size="large"> mdi-upload </v-icon> <v-icon start class="open-icon" size="large"> mdi-upload </v-icon>

View File

@@ -106,7 +106,6 @@ const saveGeneralSettings = async () => {
// Update the local settings cause the backend checked their validity. Assign is to deref value // 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) };
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} catch (error: any) { } catch (error: any) {
resetTempSettingsStruct(); resetTempSettingsStruct();
if (error.response) { if (error.response) {
@@ -151,11 +150,14 @@ const saveGeneralSettings = async () => {
} }
}; };
const currentNetworkInterface = computed<string>({ const currentNetworkInterfaceIndex = computed<number | undefined>({
get: () => useSettingsStore().network.networkManagerIface || "", get: () => {
set: (v) => { const index = useSettingsStore().networkInterfaceNames.indexOf(
tempSettingsStruct.value.networkManagerIface = v; useSettingsStore().network.networkManagerIface || ""
} );
return index === -1 ? undefined : index;
},
set: (v) => v && (tempSettingsStruct.value.networkManagerIface = useSettingsStore().networkInterfaceNames[v])
}); });
watchEffect(() => { watchEffect(() => {
@@ -254,7 +256,7 @@ watchEffect(() => {
/> />
<pv-select <pv-select
v-show="!useSettingsStore().network.networkingDisabled" v-show="!useSettingsStore().network.networkingDisabled"
v-model="currentNetworkInterface" v-model="currentNetworkInterfaceIndex"
label="NetworkManager interface" label="NetworkManager interface"
:disabled=" :disabled="
!tempSettingsStruct.shouldManage || !tempSettingsStruct.shouldManage ||

View File

@@ -1,5 +1,5 @@
<script setup lang="ts"> <script setup lang="ts">
import { onMounted, onBeforeUnmount, watch, useTemplateRef } from "vue"; import { onMounted, ref, onBeforeUnmount, watch } from "vue";
import { useTheme } from "vuetify"; import { useTheme } from "vuetify";
// Color - original (adjusted) // Color - original (adjusted)
@@ -8,14 +8,14 @@ import { useTheme } from "vuetify";
// green - 65, 181, 127 (r: 75, g: 209, b: 147) // green - 65, 181, 127 (r: 75, g: 209, b: 147)
// red - 238, 102, 102 (r: 238, g: 102, b: 102) // red - 238, 102, 102 (r: 238, g: 102, b: 102)
const colors = { const colors = {
"blue-light": { r: 255, g: 216, b: 67 }, "blue-LightTheme": { r: 255, g: 216, b: 67 },
"blue-dark": { r: 92, g: 154, b: 255 }, "blue-DarkTheme": { r: 92, g: 154, b: 255 },
"purple-light": { r: 255, g: 216, b: 67 }, "purple-LightTheme": { r: 255, g: 216, b: 67 },
"purple-dark": { r: 167, g: 104, b: 196 }, "purple-DarkTheme": { r: 167, g: 104, b: 196 },
"red-light": { r: 255, g: 216, b: 67 }, "red-LightTheme": { r: 255, g: 216, b: 67 },
"red-dark": { r: 238, g: 102, b: 102 }, "red-DarkTheme": { r: 238, g: 102, b: 102 },
"green-light": { r: 255, g: 216, b: 67 }, "green-LightTheme": { r: 255, g: 216, b: 67 },
"green-dark": { r: 75, g: 209, b: 147 } "green-DarkTheme": { r: 75, g: 209, b: 147 }
}; };
const DEFAULT_COLOR = "blue"; const DEFAULT_COLOR = "blue";
@@ -26,13 +26,9 @@ const typeLabels = {
}; };
const theme = useTheme(); const theme = useTheme();
const chartRef = useTemplateRef("chartRef"); const chartRef = ref(null);
let chart: echarts.ECharts | null = null; let chart: echarts.ECharts | null = null;
interface TooltipSeriesParam {
value: [number, number];
}
const getOptions = (data: ChartData[] = []) => { const getOptions = (data: ChartData[] = []) => {
const now = Date.now(); const now = Date.now();
return { return {
@@ -41,7 +37,7 @@ const getOptions = (data: ChartData[] = []) => {
}, },
tooltip: { tooltip: {
trigger: "axis", trigger: "axis",
formatter: (params: TooltipSeriesParam[]) => { formatter: (params: any) => {
const p = params[0]; const p = params[0];
const append = typeLabels[props.type]; const append = typeLabels[props.type];
const fmsLimitLabel = "FMS Limit - 7.000 Mb/s"; const fmsLimitLabel = "FMS Limit - 7.000 Mb/s";
@@ -84,12 +80,12 @@ const getOptions = (data: ChartData[] = []) => {
min: now - 55 * 1000, min: now - 55 * 1000,
axisLine: { axisLine: {
lineStyle: { lineStyle: {
color: theme.global.current.value.dark ? "#777" : "#aaa" color: theme.global.name.value === "LightTheme" ? "#aaa" : "#777"
} }
}, },
axisLabel: { axisLabel: {
align: "left", align: "left",
color: theme.global.current.value.dark ? "#ddd" : "#fff", color: theme.global.name.value === "LightTheme" ? "#fff" : "#ddd",
formatter: (value: number) => { formatter: (value: number) => {
const date = new Date(value); const date = new Date(value);
return date.toLocaleTimeString([], { return date.toLocaleTimeString([], {
@@ -106,12 +102,12 @@ const getOptions = (data: ChartData[] = []) => {
position: "right", position: "right",
min: min:
props.min ?? props.min ??
function (value: { min: number; max: number }) { function (value) {
return Math.max(0, (value.min - 10) | 0); return Math.max(0, (value.min - 10) | 0);
}, },
max: max:
props.max ?? props.max ??
function (value: { min: number; max: number }) { function (value) {
return (value.max + 10) | 0; return (value.max + 10) | 0;
}, },
splitNumber: 2, splitNumber: 2,
@@ -122,7 +118,7 @@ const getOptions = (data: ChartData[] = []) => {
} }
}, },
axisLabel: { axisLabel: {
color: theme.global.current.value.dark ? "#ddd" : "#fff" color: theme.global.name.value === "LightTheme" ? "#fff" : "#ddd"
} }
}, },
series: getSeries(data), series: getSeries(data),
@@ -131,7 +127,7 @@ const getOptions = (data: ChartData[] = []) => {
}; };
const getSeries = (data: ChartData[] = []) => { const getSeries = (data: ChartData[] = []) => {
const color = colors[`${props.color ?? DEFAULT_COLOR}-${theme.global.current.value.dark ? "dark" : "light"}`]; const color = colors[`${props.color ?? DEFAULT_COLOR}-${theme.global.name.value}`];
return [ return [
{ {
type: "line", type: "line",
@@ -192,10 +188,10 @@ interface ChartData {
// Type options: "percentage", "temperature", "mb" // Type options: "percentage", "temperature", "mb"
const props = defineProps<{ const props = defineProps<{
data: ChartData[]; data: ChartData[];
type: keyof typeof typeLabels; type: string;
min?: number; min?: number;
max?: number; max?: number;
color?: "red" | "green" | "blue" | "purple"; color?: string;
}>(); }>();
onMounted(async () => { onMounted(async () => {

View File

@@ -1,5 +1,5 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref, computed, inject, useTemplateRef } from "vue"; import { ref, computed, inject } from "vue";
import { useStateStore } from "@/stores/StateStore"; import { useStateStore } from "@/stores/StateStore";
import { useSettingsStore } from "@/stores/settings/GeneralSettingsStore"; import { useSettingsStore } from "@/stores/settings/GeneralSettingsStore";
import { type ObjectDetectionModelProperties } from "@/types/SettingTypes"; import { type ObjectDetectionModelProperties } from "@/types/SettingTypes";
@@ -46,7 +46,7 @@ const handleImport = async () => {
if ( if (
await axiosPost("/objectdetection/import", "import an object detection model", formData, { await axiosPost("/objectdetection/import", "import an object detection model", formData, {
headers: { "Content-Type": "multipart/form-data" }, headers: { "Content-Type": "multipart/form-data" },
onUploadProgress: ({ progress }: { progress?: number }) => { onUploadProgress: ({ progress }) => {
const uploadPercentage = (progress || 0) * 100.0; const uploadPercentage = (progress || 0) * 100.0;
if (uploadPercentage < 99.5) { if (uploadPercentage < 99.5) {
useStateStore().showSnackbarMessage({ useStateStore().showSnackbarMessage({
@@ -74,20 +74,20 @@ const handleImport = async () => {
importVersion.value = null; importVersion.value = null;
}; };
const deleteModel = async (model: ObjectDetectionModelProperties) => { const deleteModel = (model: ObjectDetectionModelProperties) => {
await axiosPost("/objectdetection/delete", "delete an object detection model", { axiosPost("/objectdetection/delete", "delete an object detection model", {
modelPath: model.modelPath modelPath: model.modelPath
}); });
}; };
const renameModel = async (model: ObjectDetectionModelProperties, newName: string) => { const renameModel = (model: ObjectDetectionModelProperties, newName: string) => {
useStateStore().showSnackbarMessage({ useStateStore().showSnackbarMessage({
message: "Renaming Object Detection Model...", message: "Renaming Object Detection Model...",
color: "secondary", color: "secondary",
timeout: -1 timeout: -1
}); });
await axiosPost("/objectdetection/rename", "rename an object detection model", { axiosPost("/objectdetection/rename", "rename an object detection model", {
modelPath: model.modelPath, modelPath: model.modelPath,
newName: newName newName: newName
}); });
@@ -97,7 +97,7 @@ const renameModel = async (model: ObjectDetectionModelProperties, newName: strin
// Filters out models that are not supported by the current backend, and returns a flattened list. // Filters out models that are not supported by the current backend, and returns a flattened list.
const supportedModels = computed(() => { const supportedModels = computed(() => {
const { availableModels, supportedBackends } = useSettingsStore().general; const { availableModels, supportedBackends } = useSettingsStore().general;
const isSupported = (model: ObjectDetectionModelProperties) => { const isSupported = (model: any) => {
// Check if model's family is in the list of supported backends // Check if model's family is in the list of supported backends
return supportedBackends.some((backend: string) => backend.toLowerCase() === model.family.toLowerCase()); return supportedBackends.some((backend: string) => backend.toLowerCase() === model.family.toLowerCase());
}; };
@@ -106,19 +106,19 @@ const supportedModels = computed(() => {
return availableModels.filter(isSupported); return availableModels.filter(isSupported);
}); });
const exportModels = useTemplateRef("exportModels"); const exportModels = ref();
const openExportPrompt = () => { const openExportPrompt = () => {
exportModels.value?.click(); exportModels.value.click();
}; };
const exportIndividualModel = useTemplateRef("exportIndividualModel"); const exportIndividualModel = ref();
const openExportIndividualModelPrompt = () => { const openExportIndividualModelPrompt = () => {
exportIndividualModel.value?.click(); exportIndividualModel.value.click();
}; };
const showNukeDialog = ref(false); const showNukeDialog = ref(false);
const nukeModels = async () => { const nukeModels = () => {
await axiosPost("/objectdetection/nuke", "clear and reset object detection models"); axiosPost("/objectdetection/nuke", "clear and reset object detection models");
}; };
const showBulkImportDialog = ref(false); const showBulkImportDialog = ref(false);
@@ -132,7 +132,7 @@ const handleBulkImport = async () => {
if ( if (
await axiosPost("/objectdetection/bulkimport", "import object detection models", formData, { await axiosPost("/objectdetection/bulkimport", "import object detection models", formData, {
headers: { "Content-Type": "multipart/form-data" }, headers: { "Content-Type": "multipart/form-data" },
onUploadProgress: ({ progress }: { progress?: number }) => { onUploadProgress: ({ progress }) => {
const uploadPercentage = (progress || 0) * 100.0; const uploadPercentage = (progress || 0) * 100.0;
if (uploadPercentage < 99.5) { if (uploadPercentage < 99.5) {
useStateStore().showSnackbarMessage({ useStateStore().showSnackbarMessage({

View File

@@ -40,13 +40,18 @@ export class AutoReconnectingWebsocket {
* Send data over the websocket. This is a no-op if the websocket is not in the OPEN state. * Send data over the websocket. This is a no-op if the websocket is not in the OPEN state.
* *
* @param data data to send * @param data data to send
* @param encodeData whether or not to encode the data using msgpack (defaults to true)
* @see isConnected * @see isConnected
* *
*/ */
send(data: unknown) { send(data, encodeData = true) {
// Only send data if the websocket is open // Only send data if the websocket is open
if (this.isConnected()) { if (this.isConnected()) {
this.websocket?.send(encode(data)); if (encodeData) {
this.websocket?.send(encode(data));
} else {
this.websocket?.send(data);
}
} }
} }

View File

@@ -1,6 +1,6 @@
import { useStateStore } from "@/stores/StateStore"; import { useStateStore } from "@/stores/StateStore";
import type { Resolution } from "@/types/SettingTypes"; import type { Resolution } from "@/types/SettingTypes";
import axios, { type AxiosRequestConfig } from "axios"; import axios from "axios";
export const resolutionsAreEqual = (a: Resolution, b: Resolution) => { export const resolutionsAreEqual = (a: Resolution, b: Resolution) => {
return a.height === b.height && a.width === b.width; return a.height === b.height && a.width === b.width;
@@ -51,16 +51,15 @@ export const forceReloadPage = async () => {
export const getResolutionString = (resolution: Resolution): string => `${resolution.width}x${resolution.height}`; export const getResolutionString = (resolution: Resolution): string => `${resolution.width}x${resolution.height}`;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export const parseJsonFile = async <T extends Record<string, any>>(file: File): Promise<T> => { export const parseJsonFile = async <T extends Record<string, any>>(file: File): Promise<T> => {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const fileReader = new FileReader(); const fileReader = new FileReader();
fileReader.onload = (event) => { fileReader.onload = (event) => {
const target: FileReader | null = event.target; const target: FileReader | null = event.target;
if (target === null) reject(new Error("FileReader event target is null")); if (target === null) reject();
else resolve(JSON.parse(target.result as string) as T); else resolve(JSON.parse(target.result as string) as T);
}; };
fileReader.onerror = () => reject(new Error("Error reading file")); fileReader.onerror = (error) => reject(error);
fileReader.readAsText(file); fileReader.readAsText(file);
}); });
}; };
@@ -74,13 +73,7 @@ export const parseJsonFile = async <T extends Record<string, any>>(file: File):
* @param config Optional axios request configuration * @param config Optional axios request configuration
* @returns A promise that resolves to true if the POST request is successful, or false if an error occurs. * @returns A promise that resolves to true if the POST request is successful, or false if an error occurs.
*/ */
export const axiosPost = async ( export const axiosPost = async (url: string, description: string, data?: any, config?: any): Promise<boolean> => {
url: string,
description: string,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
data?: any,
config?: AxiosRequestConfig
): Promise<boolean> => {
try { try {
await axios.post(url, data, config); await axios.post(url, data, config);
useStateStore().showSnackbarMessage({ useStateStore().showSnackbarMessage({
@@ -88,7 +81,6 @@ export const axiosPost = async (
color: "success" color: "success"
}); });
return true; return true;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} catch (error: any) { } catch (error: any) {
if (error.response) { if (error.response) {
useStateStore().showSnackbarMessage({ useStateStore().showSnackbarMessage({

View File

@@ -0,0 +1,452 @@
import IDLEGIF from "@/assets/images/idle.gif";
import GROWGIF from "@/assets/images/grow.gif";
import BLINKGIF from "@/assets/images/blink.gif";
import WAVEGIF from "@/assets/images/wave.gif";
import SPEAKGIF from "@/assets/images/speak.gif";
import SHRINKGIF from "@/assets/images/shrink.gif";
import POINTGIF from "@/assets/images/point.gif";
import { useCameraSettingsStore } from "@/stores/settings/CameraSettingsStore";
import { useStateStore } from "@/stores/StateStore";
const ANIMATIONS = {
idle: IDLEGIF,
grow: GROWGIF,
blink: BLINKGIF,
wave: WAVEGIF,
speak: SPEAKGIF,
shrink: SHRINKGIF,
point: POINTGIF
};
// Extended animation sequence
const SEQUENCE = [
{ animation: "grow", loops: 1 },
{ animation: "idle", loops: 2 },
{ animation: "blink", loops: 1 },
{ animation: "idle", loops: 1 },
{ animation: "speak", loops: 1 },
{ animation: "idle", loops: 1 },
{ animation: "point", loops: 1 },
{ animation: "idle", loops: 1 },
{ animation: "wave", loops: 1 },
{ animation: "idle", loops: 1 }
];
let quarkyImage;
let quarkyContainer;
let speechBubble;
let currentSequenceIndex = 0;
let currentLoopCount = 0;
let isMovingDemo = false;
let hasPlayedGrow = false;
// Speech bubble text (configurable)
let quarkySpeechText = "Hello from Quarky!";
// Turbo-encabulator style nonsense phrases
const quarkyPhrases = [
"Reverse phase oscillation detected!",
"Initializing hyperflux capacitor...",
"Reticulating splines in progress.",
"Quantum entanglement buffer overflowzomg",
"Did you remember the turbo-encabulator?",
"Engaging magnetic flux inverter.",
"Calibrating photon resonance field.",
"Deploying recursive feedback loop.",
"wow.",
"I applaud your pseudo-random bitstream.",
"Rebooting quantum foam stabilizer.",
"Analyzing subspace harmonics.",
"Transmitting encrypted flux packets.",
"Verifying entropic phase alignment.",
"Reconfiguring nano-particle array.",
"pew pew. pew pew.",
"I can't parse the synthetic logic matrix.",
"Don't forget to make the holographic interface.",
"You should generate more stochastic resonance.",
"Greetings",
"You look like you need some help!",
"Set this slider to 25",
"Set this slider to 67. HAHA 67!!!!",
"That's a horrible choice!",
"Fun is a core value! Is that a fun choice?",
"If your grandma saw that choice, would she be proud?",
"Chute Door?",
"Yes, Chute Door!",
"That's a bold strategy, Cotton.",
"asdflkjaslkdflklnf2222",
"00110101? That's just gibberish!",
"Three is my favorite number too!",
"Robots should not quit, but yours did!",
"Dont forget to disable auto-exposure! Or enable it. I'm not sure. ",
"Have you glued your lenses to keep them in focus?",
"Dont put spaces in your camera names — it makes the robot very sad",
"Upgrade to Photon Pro for gtsam support 👍",
"Did you forget to take off the lense covers? Its dark in here…"
];
// State-specific humorous phrases
const cameraNeedsSetupPhrases = [
"These cameras are just standing there... menacingly",
"Are your cameras plugged in? Trick question -- they aren't!",
"Have you hot-glued your USB cameras?"
];
const backendNotConnectedPhrases = [
"Um, is this thing even on?",
"Anyone home? Bulldozer? Bulldozer?",
"Have you tried turning the NI™ RoboRIO™ off and on again?"
];
const ntDisconnectedPhrases = [
"NetworkTables? More like Network'(; DROP TABLE websockets;--",
"Robots shouldn't quit, but I sure can't talk to yours!",
"Are you an OM5P? Because I can't talk to you over the LAN!",
"I'm a sentient subatomic particle, not a networking engineer."
];
/**
* Get list of applicable phrase categories based on current UI state
*/
function getApplicablePhraseLists() {
const cameraStore = useCameraSettingsStore();
const stateStore = useStateStore();
// Build list of applicable phrase categories
const applicableLists = [quarkyPhrases];
// Add state-specific categories (additive, not replacing)
if (cameraStore?.needsCameraConfiguration) {
applicableLists.push(cameraNeedsSetupPhrases);
}
if (!stateStore?.backendConnected) {
applicableLists.push(backendNotConnectedPhrases);
}
if (!stateStore?.ntConnectionStatus?.connected) {
applicableLists.push(ntDisconnectedPhrases);
}
return applicableLists;
}
/**
* Pick a random phrase from applicable categories
*/
function pickRandomPhrase() {
const applicableLists = getApplicablePhraseLists();
const randomList = applicableLists[Math.floor(Math.random() * applicableLists.length)];
return randomList[Math.floor(Math.random() * randomList.length)];
}
/**
* Get the duration of an animation in milliseconds
*/
function getAnimationDuration(animation) {
if (!animation) return 500; // Default 0.5s for empty state
// Animation durations (in seconds) based on quarky_generator.py
const durations = {
idle: 0.5,
grow: 2.0,
blink: 0.3,
wave: 1.8,
speak: 2.0,
shrink: 2.0,
point: 1.5
};
return (durations[animation] || 1.0) * 1000; // Convert to ms
}
/**
* Play an animation
*/
function playAnimation(animation) {
if (!animation) {
quarkyImage.src = "";
quarkyImage.style.display = "none";
return;
}
quarkyImage.style.display = "block";
quarkyImage.src = ANIMATIONS[animation];
if (animation === "speak") {
// Pick random phrase from applicable categories
quarkySpeechText = pickRandomPhrase();
speechBubble.textContent = quarkySpeechText;
speechBubble.style.display = "block";
speechBubble.style.opacity = 1;
setTimeout(() => {
speechBubble.style.opacity = 0;
setTimeout(() => {
speechBubble.style.display = "none";
}, 700);
}, 3000);
}
}
// Mini Quarky management
let miniQuarkies = [];
const MAX_MINI_QUARKIES = 20;
const MINI_QUARKY_SIZE = 120;
const MINI_QUARKY_Z_INDEX = 999;
const MINI_QUARKY_VELOCITY_MULTIPLIER = 12;
const MINI_QUARKY_VELOCITY_CENTER = 0.5;
const DIRECTION_CHANGE_PROBABILITY = 0.02;
const DIRECTION_CHANGE_VELOCITY_MULTIPLIER = 4;
const MINI_QUARKY_ANIMATION_INTERVAL_MS = 50;
const MINI_QUARKY_SPAWN_BASE_DELAY_MS = 4000;
const MINI_QUARKY_SPAWN_DELAY_RANGE_MS = 8000;
// Random movement every few cycles
let mouseX = window.innerWidth / 2;
let mouseY = window.innerHeight / 2;
let mouseMoving = false;
let mouseMoveTimeout = null;
/**
* Clamp Quarky's position to stay within the viewport, accounting for the sidebar
*/
function clampQuarkyPosition(x, y) {
const rect = quarkyContainer.getBoundingClientRect();
// Get sidebar width (account for both expanded and compact modes)
const sidebar = document.querySelector(".v-navigation-drawer");
const sidebarWidth = sidebar ? sidebar.offsetWidth : 0;
// Clamp to viewport, starting after the sidebar
const clampedX = Math.max(sidebarWidth, Math.min(x, window.innerWidth - rect.width));
const clampedY = Math.max(0, Math.min(y, window.innerHeight - rect.height));
return { x: clampedX, y: clampedY };
}
function mouseMoveHandler(e) {
mouseX = e.clientX + window.scrollX;
mouseY = e.clientY + window.scrollY;
mouseMoving = true;
clearTimeout(mouseMoveTimeout);
// After 1.5s of no movement, return Quarky to home and resume nonsense
mouseMoveTimeout = setTimeout(() => {
mouseMoving = false;
quarkyContainer.style.left = "calc(100vw - 550px)";
quarkyContainer.style.top = "calc(100vh - 550px)";
isMovingDemo = false;
playNextAnimation();
}, 1500);
// Actively track mouse: update Quarky's position every mouse move (clamped to viewport)
const clamped = clampQuarkyPosition(mouseX, mouseY);
quarkyContainer.style.left = `${clamped.x}px`;
quarkyContainer.style.top = `${clamped.y}px`;
// Immediately trigger Quarky to point at cursor
if (!isMovingDemo) {
isMovingDemo = true;
playAnimation("point");
setTimeout(() => {
playAnimation("idle");
}, getAnimationDuration("point"));
}
}
/**
* Advance to the next animation in the sequence
*/
function playNextAnimation() {
if (isMovingDemo) return;
let currentStep = SEQUENCE[currentSequenceIndex];
// On loop, skip grow after first time
if (hasPlayedGrow && currentSequenceIndex === 0 && currentStep.animation === "grow") {
currentSequenceIndex = 1;
currentStep = SEQUENCE[currentSequenceIndex];
}
// If mouse is moving, don't do normal cycle
if (mouseMoving) {
// Quarky will point at cursor via mousemove handler
return;
}
// Show speech bubble before speak
if (currentStep.animation === "speak") {
quarkySpeechText = pickRandomPhrase();
speechBubble.textContent = quarkySpeechText;
speechBubble.style.display = "block";
speechBubble.style.opacity = 1;
setTimeout(() => {
speechBubble.style.opacity = 0;
setTimeout(() => {
speechBubble.style.display = "none";
}, 700);
}, 3000);
}
// Fade out speech bubble after speak (during idle)
if (SEQUENCE[currentSequenceIndex - 1]?.animation === "speak" && currentStep.animation === "idle") {
// Speech bubble already has its own timeout from above
}
// Always return to corner when idle
quarkyContainer.style.left = "calc(100vw - 550px)";
quarkyContainer.style.top = "calc(100vh - 550px)";
// Play the animation
playAnimation(currentStep.animation);
// Calculate duration and schedule next animation
const duration = getAnimationDuration(currentStep.animation);
setTimeout(() => {
currentLoopCount++;
// Check if we've completed all loops for this step
if (currentLoopCount >= currentStep.loops) {
// Move to next step
currentLoopCount = 0;
currentSequenceIndex++;
// Loop back to start if we've completed the sequence
if (currentSequenceIndex >= SEQUENCE.length) {
currentSequenceIndex = 0;
hasPlayedGrow = true;
}
}
// Play the next animation
playNextAnimation();
}, duration);
}
function clickToPoint(e) {
// Ignore clicks on the button
if (e.target.id === "moveDemoBtn") return;
isMovingDemo = true;
const clickX = e.clientX + window.scrollX;
const clickY = e.clientY + window.scrollY;
const clamped = clampQuarkyPosition(clickX, clickY);
quarkyContainer.style.left = `${clamped.x}px`;
quarkyContainer.style.top = `${clamped.y}px`;
setTimeout(() => {
playAnimation("point");
setTimeout(() => {
playAnimation("idle");
quarkyContainer.style.left = "calc(100vw - 550px)";
quarkyContainer.style.top = "calc(100vh - 550px)";
setTimeout(() => {
isMovingDemo = false;
playNextAnimation();
}, 1000);
}, getAnimationDuration("point"));
}, 1000);
}
/**
* Spawn a mini Quarky that moves randomly around the screen
*/
function spawnMiniQuarky() {
console.log("SPAWNING A QUARKY");
// If at max, don't spawn
if (miniQuarkies.length >= MAX_MINI_QUARKIES) {
return;
}
const miniContainer = document.createElement("div");
miniContainer.style.position = "fixed";
miniContainer.style.width = `${MINI_QUARKY_SIZE}px`;
miniContainer.style.height = `${MINI_QUARKY_SIZE}px`;
miniContainer.style.pointerEvents = "none";
miniContainer.style.zIndex = MINI_QUARKY_Z_INDEX.toString();
// Spawn from main quarky's actual position
const rect = quarkyContainer.getBoundingClientRect();
const startX = rect.left + window.scrollX + rect.width / 2;
const startY = rect.top + window.scrollY + rect.height / 2;
miniContainer.style.left = startX + "px";
miniContainer.style.top = startY + "px";
const miniImage = document.createElement("img");
miniImage.src = ANIMATIONS["idle"];
miniImage.style.width = "100%";
miniImage.style.height = "100%";
miniImage.style.objectFit = "contain";
miniContainer.appendChild(miniImage);
document.body.appendChild(miniContainer);
const miniQuarky = {
container: miniContainer,
image: miniImage,
x: startX,
y: startY,
vx: (Math.random() - MINI_QUARKY_VELOCITY_CENTER) * MINI_QUARKY_VELOCITY_MULTIPLIER,
vy: (Math.random() - MINI_QUARKY_VELOCITY_CENTER) * MINI_QUARKY_VELOCITY_MULTIPLIER,
animationInterval: null
};
miniQuarkies.push(miniQuarky);
// Start movement loop
miniQuarky.animationInterval = setInterval(() => {
miniQuarky.x += miniQuarky.vx;
miniQuarky.y += miniQuarky.vy;
// Bounce off edges
if (miniQuarky.x <= 0 || miniQuarky.x >= window.innerWidth - MINI_QUARKY_SIZE) {
miniQuarky.vx *= -1;
miniQuarky.x = Math.max(0, Math.min(miniQuarky.x, window.innerWidth - MINI_QUARKY_SIZE));
}
if (miniQuarky.y <= 0 || miniQuarky.y >= window.innerHeight - MINI_QUARKY_SIZE) {
miniQuarky.vy *= -1;
miniQuarky.y = Math.max(0, Math.min(miniQuarky.y, window.innerHeight - MINI_QUARKY_SIZE));
}
// Occasionally change direction randomly
if (Math.random() < DIRECTION_CHANGE_PROBABILITY) {
miniQuarky.vx = (Math.random() - MINI_QUARKY_VELOCITY_CENTER) * DIRECTION_CHANGE_VELOCITY_MULTIPLIER;
miniQuarky.vy = (Math.random() - MINI_QUARKY_VELOCITY_CENTER) * DIRECTION_CHANGE_VELOCITY_MULTIPLIER;
}
miniContainer.style.left = miniQuarky.x + "px";
miniContainer.style.top = miniQuarky.y + "px";
}, MINI_QUARKY_ANIMATION_INTERVAL_MS);
}
/**
* Clean up all mini Quarkies
*/
function cleanupMiniQuarkies() {
// eslint-disable-line @typescript-eslint/no-unused-vars
miniQuarkies.forEach((mini) => {
clearInterval(mini.animationInterval);
mini.container.remove();
});
miniQuarkies = [];
}
export default function setup() {
quarkyImage = document.getElementById("quarkyImage");
quarkyContainer = document.getElementById("quarkyContainer");
speechBubble = document.getElementById("quarkySpeechBubble");
// Start the animation sequence
playNextAnimation();
//Install mouse move handler
window.addEventListener("mousemove", mouseMoveHandler);
// Click-to-point feature installation
window.addEventListener("click", (e) => {
clickToPoint(e);
});
// Spawn mini Quarkies on a random timer
function scheduleNextMiniQuarkySpawn() {
const delayMs = MINI_QUARKY_SPAWN_BASE_DELAY_MS + Math.random() * MINI_QUARKY_SPAWN_DELAY_RANGE_MS;
setTimeout(() => {
spawnMiniQuarky();
scheduleNextMiniQuarkySpawn();
}, delayMs);
}
scheduleNextMiniQuarkySpawn();
}

View File

@@ -6,12 +6,14 @@ import router from "@/router";
import vuetify from "@/plugins/vuetify"; import vuetify from "@/plugins/vuetify";
import axios from "axios"; import axios from "axios";
import setup from "@/lib/quarky.js";
type PhotonClientRuntimeMode = "production" | "development" | "local-network-development"; type PhotonClientRuntimeMode = "production" | "development" | "local-network-development";
const runtimeMode: PhotonClientRuntimeMode = process.env.NODE_ENV as PhotonClientRuntimeMode; const runtimeMode: PhotonClientRuntimeMode = process.env.NODE_ENV as PhotonClientRuntimeMode;
let backendHost: string; let backendHost: string;
let backendHostname: string; let backendHostname: string;
switch (runtimeMode) { switch (runtimeMode as PhotonClientRuntimeMode) {
case "development": case "development":
backendHost = `${location.hostname}:5800`; backendHost = `${location.hostname}:5800`;
backendHostname = location.hostname; backendHostname = location.hostname;
@@ -45,3 +47,4 @@ app.use(pinia);
app.use(vuetify); app.use(vuetify);
app.use(router); app.use(router);
app.mount("#app"); app.mount("#app");
setup();

View File

@@ -1,6 +1,6 @@
import "vuetify/styles"; import "vuetify/styles";
void import("@mdi/font/css/materialdesignicons.css"); import("@mdi/font/css/materialdesignicons.css");
import type { ThemeDefinition } from "vuetify"; import type { ThemeDefinition } from "vuetify/lib/composables/theme";
import { createVuetify } from "vuetify"; import { createVuetify } from "vuetify";
const CommonColors = { const CommonColors = {

View File

@@ -31,8 +31,7 @@ interface StateStore {
currentCameraUniqueName: string; currentCameraUniqueName: string;
networkUsageHistory: NetworkUsageEntry[]; networkUsageHistory: NetworkUsageEntry[];
// Key is a string, although often used as an index, because we need to reference using the camera unique name at times. backendResults: Record<number, PipelineResult>;
backendResults: Record<string, PipelineResult>;
multitagResultBuffer: Record<string, MultitagResult[]>; multitagResultBuffer: Record<string, MultitagResult[]>;
colorPickingMode: boolean; colorPickingMode: boolean;
@@ -40,6 +39,8 @@ interface StateStore {
calibrationData: { calibrationData: {
imageCount: number; imageCount: number;
videoFormatIndex: number; videoFormatIndex: number;
minimumImageCount: number;
hasEnoughImages: boolean;
}; };
snackbarData: { snackbarData: {
@@ -87,7 +88,9 @@ export const useStateStore = defineStore("state", {
calibrationData: { calibrationData: {
imageCount: 0, imageCount: 0,
videoFormatIndex: 0 videoFormatIndex: 0,
minimumImageCount: 12,
hasEnoughImages: false
}, },
snackbarData: { snackbarData: {
@@ -158,7 +161,9 @@ export const useStateStore = defineStore("state", {
updateCalibrationStateValuesFromWebsocket(data: WebsocketCalibrationData) { updateCalibrationStateValuesFromWebsocket(data: WebsocketCalibrationData) {
this.calibrationData = { this.calibrationData = {
imageCount: data.count, imageCount: data.count,
videoFormatIndex: data.videoModeIndex videoFormatIndex: data.videoModeIndex,
minimumImageCount: data.minCount,
hasEnoughImages: data.hasEnough
}; };
}, },
updateDiscoveredCameras(data: VsmState) { updateDiscoveredCameras(data: VsmState) {

View File

@@ -64,14 +64,17 @@ export const useCameraSettingsStore = defineStore("cameraSettings", {
pipelineNames(): string[] { pipelineNames(): string[] {
return this.currentCameraSettings.pipelineNicknames; return this.currentCameraSettings.pipelineNicknames;
}, },
currentPipelineName(): string {
return this.pipelineNames[useStateStore().currentCameraUniqueName];
},
isDriverMode(): boolean { isDriverMode(): boolean {
return this.currentCameraSettings.currentPipelineIndex === WebsocketPipelineType.DriverMode.valueOf(); return this.currentCameraSettings.currentPipelineIndex === WebsocketPipelineType.DriverMode;
}, },
isCalibrationMode(): boolean { isCalibrationMode(): boolean {
return this.currentCameraSettings.currentPipelineIndex === WebsocketPipelineType.Calib3d.valueOf(); return this.currentCameraSettings.currentPipelineIndex === WebsocketPipelineType.Calib3d;
}, },
isFocusMode(): boolean { isFocusMode(): boolean {
return this.currentCameraSettings.currentPipelineIndex === WebsocketPipelineType.FocusCamera.valueOf(); return this.currentCameraSettings.currentPipelineIndex === WebsocketPipelineType.FocusCamera;
}, },
isCSICamera(): boolean { isCSICamera(): boolean {
return this.currentCameraSettings.isCSICamera; return this.currentCameraSettings.isCSICamera;
@@ -91,9 +94,6 @@ export const useCameraSettingsStore = defineStore("cameraSettings", {
fpsLimit(): number { fpsLimit(): number {
return this.currentCameraSettings.fpsLimit; return this.currentCameraSettings.fpsLimit;
}, },
isEnabled(): boolean {
return this.currentCameraSettings.isEnabled;
},
isConnected(): boolean { isConnected(): boolean {
return this.currentCameraSettings.isConnected; return this.currentCameraSettings.isConnected;
}, },
@@ -116,20 +116,23 @@ export const useCameraSettingsStore = defineStore("cameraSettings", {
inputPort: d.inputStreamPort, inputPort: d.inputStreamPort,
outputPort: d.outputStreamPort outputPort: d.outputStreamPort
}, },
validVideoFormats: d.videoFormatList.map((v, i) => ({ validVideoFormats: Object.entries(d.videoFormatList)
resolution: { .sort(([firstKey], [secondKey]) => parseInt(firstKey) - parseInt(secondKey))
width: v.width, // eslint-disable-next-line @typescript-eslint/no-unused-vars
height: v.height .map<VideoFormat>(([k, v], i) => ({
}, resolution: {
fps: v.fps, width: v.width,
pixelFormat: v.pixelFormat, height: v.height
index: v.index || i, },
diagonalFOV: v.diagonalFOV, fps: v.fps,
horizontalFOV: v.horizontalFOV, pixelFormat: v.pixelFormat,
verticalFOV: v.verticalFOV, index: v.index || i,
standardDeviation: v.standardDeviation, diagonalFOV: v.diagonalFOV,
mean: v.mean horizontalFOV: v.horizontalFOV,
})), verticalFOV: v.verticalFOV,
standardDeviation: v.standardDeviation,
mean: v.mean
})),
completeCalibrations: d.calibrations, completeCalibrations: d.calibrations,
isCSICamera: d.isCSICamera, isCSICamera: d.isCSICamera,
minExposureRaw: d.minExposureRaw, minExposureRaw: d.minExposureRaw,
@@ -142,7 +145,6 @@ export const useCameraSettingsStore = defineStore("cameraSettings", {
maxWhiteBalanceTemp: d.maxWhiteBalanceTemp, maxWhiteBalanceTemp: d.maxWhiteBalanceTemp,
matchedCameraInfo: d.matchedCameraInfo, matchedCameraInfo: d.matchedCameraInfo,
fpsLimit: d.fpsLimit, fpsLimit: d.fpsLimit,
isEnabled: d.isEnabled,
isConnected: d.isConnected, isConnected: d.isConnected,
hasConnected: d.hasConnected, hasConnected: d.hasConnected,
mismatch: d.mismatch mismatch: d.mismatch
@@ -194,7 +196,7 @@ export const useCameraSettingsStore = defineStore("cameraSettings", {
addNewPipeline: [newPipelineName, pipelineType], addNewPipeline: [newPipelineName, pipelineType],
cameraUniqueName: cameraUniqueName cameraUniqueName: cameraUniqueName
}; };
useStateStore().websocket?.send(payload); useStateStore().websocket?.send(payload, true);
}, },
/** /**
* Modify the settings of the currently selected pipeline of the provided camera. * Modify the settings of the currently selected pipeline of the provided camera.
@@ -218,13 +220,15 @@ export const useCameraSettingsStore = defineStore("cameraSettings", {
if (updateStore) { if (updateStore) {
this.changePipelineSettingsInStore(settings, cameraUniqueName); this.changePipelineSettingsInStore(settings, cameraUniqueName);
} }
useStateStore().websocket?.send(payload); useStateStore().websocket?.send(payload, true);
}, },
changePipelineSettingsInStore( changePipelineSettingsInStore(
settings: Partial<ActivePipelineSettings>, settings: Partial<ActivePipelineSettings>,
cameraUniqueName: string = useStateStore().currentCameraUniqueName cameraUniqueName: string = useStateStore().currentCameraUniqueName
) { ) {
Object.assign(this.cameras[cameraUniqueName].pipelineSettings, settings); Object.entries(settings).forEach(([k, v]) => {
this.cameras[cameraUniqueName].pipelineSettings[k] = v;
});
}, },
/** /**
* Change the nickname of the currently selected pipeline of the provided camera. * Change the nickname of the currently selected pipeline of the provided camera.
@@ -245,7 +249,7 @@ export const useCameraSettingsStore = defineStore("cameraSettings", {
if (updateStore) { if (updateStore) {
this.cameras[cameraUniqueName].pipelineSettings.pipelineNickname = newName; this.cameras[cameraUniqueName].pipelineSettings.pipelineNickname = newName;
} }
useStateStore().websocket?.send(payload); useStateStore().websocket?.send(payload, true);
}, },
/** /**
* Modify the Pipeline type of the currently selected pipeline of the provided camera. This overwrites the current pipeline's settings when the backend resets the current pipeline settings. * Modify the Pipeline type of the currently selected pipeline of the provided camera. This overwrites the current pipeline's settings when the backend resets the current pipeline settings.
@@ -261,7 +265,7 @@ export const useCameraSettingsStore = defineStore("cameraSettings", {
pipelineType: type, pipelineType: type,
cameraUniqueName: cameraUniqueName cameraUniqueName: cameraUniqueName
}; };
useStateStore().websocket?.send(payload); useStateStore().websocket?.send(payload, true);
}, },
/** /**
* Change the index of the pipeline of the currently selected camera. * Change the index of the pipeline of the currently selected camera.
@@ -281,22 +285,21 @@ export const useCameraSettingsStore = defineStore("cameraSettings", {
}; };
if (updateStore) { if (updateStore) {
if ( if (
this.cameras[cameraUniqueName].currentPipelineIndex !== WebsocketPipelineType.DriverMode.valueOf() && this.cameras[cameraUniqueName].currentPipelineIndex !== -1 &&
this.cameras[cameraUniqueName].currentPipelineIndex !== WebsocketPipelineType.Calib3d.valueOf() && this.cameras[cameraUniqueName].currentPipelineIndex !== -2
this.cameras[cameraUniqueName].currentPipelineIndex !== WebsocketPipelineType.FocusCamera.valueOf()
) { ) {
this.cameras[cameraUniqueName].lastPipelineIndex = this.cameras[cameraUniqueName].currentPipelineIndex; this.cameras[cameraUniqueName].lastPipelineIndex = this.cameras[cameraUniqueName].currentPipelineIndex;
} }
this.cameras[cameraUniqueName].currentPipelineIndex = index; this.cameras[cameraUniqueName].currentPipelineIndex = index;
} }
useStateStore().websocket?.send(payload); useStateStore().websocket?.send(payload, true);
}, },
setDriverMode(isDriverMode: boolean, cameraUniqueName: string = useStateStore().currentCameraUniqueName) { setDriverMode(isDriverMode: boolean, cameraUniqueName: string = useStateStore().currentCameraUniqueName) {
const payload = { const payload = {
driverMode: isDriverMode, driverMode: isDriverMode,
cameraUniqueName: cameraUniqueName cameraUniqueName: cameraUniqueName
}; };
useStateStore().websocket?.send(payload); useStateStore().websocket?.send(payload, true);
}, },
/** /**
* Change the currently selected pipeline of the provided camera. * Change the currently selected pipeline of the provided camera.
@@ -308,7 +311,7 @@ export const useCameraSettingsStore = defineStore("cameraSettings", {
deleteCurrentPipeline: {}, deleteCurrentPipeline: {},
cameraUniqueName: cameraUniqueName cameraUniqueName: cameraUniqueName
}; };
useStateStore().websocket?.send(payload); useStateStore().websocket?.send(payload, true);
}, },
/** /**
* Duplicate the pipeline at the provided index. * Duplicate the pipeline at the provided index.
@@ -321,7 +324,7 @@ export const useCameraSettingsStore = defineStore("cameraSettings", {
duplicatePipeline: pipelineIndex, duplicatePipeline: pipelineIndex,
cameraUniqueName: cameraUniqueName cameraUniqueName: cameraUniqueName
}; };
useStateStore().websocket?.send(payload); useStateStore().websocket?.send(payload, true);
}, },
/** /**
* Change the currently set camera * Change the currently set camera
@@ -336,7 +339,7 @@ export const useCameraSettingsStore = defineStore("cameraSettings", {
if (updateStore) { if (updateStore) {
useStateStore().currentCameraUniqueName = cameraUniqueName; useStateStore().currentCameraUniqueName = cameraUniqueName;
} }
useStateStore().websocket?.send(payload); useStateStore().websocket?.send(payload, true);
}, },
/** /**
* Change the nickname of the provided camera. * Change the nickname of the provided camera.
@@ -368,8 +371,8 @@ export const useCameraSettingsStore = defineStore("cameraSettings", {
*/ */
startPnPCalibration( startPnPCalibration(
calibrationInitData: { calibrationInitData: {
squareSizeMeters: number; squareSizeIn: number;
markerSizeMeters: number; markerSizeIn: number;
patternWidth: number; patternWidth: number;
patternHeight: number; patternHeight: number;
boardType: CalibrationBoardTypes; boardType: CalibrationBoardTypes;
@@ -382,12 +385,14 @@ export const useCameraSettingsStore = defineStore("cameraSettings", {
const payload = { const payload = {
startPnpCalibration: { startPnpCalibration: {
count: stateCalibData.imageCount, count: stateCalibData.imageCount,
minCount: stateCalibData.minimumImageCount,
hasEnough: stateCalibData.hasEnoughImages,
videoModeIndex: stateCalibData.videoFormatIndex, videoModeIndex: stateCalibData.videoFormatIndex,
...calibrationInitData ...calibrationInitData
}, },
cameraUniqueName: cameraUniqueName cameraUniqueName: cameraUniqueName
}; };
useStateStore().websocket?.send(payload); useStateStore().websocket?.send(payload, true);
}, },
/** /**
* End the 3D calibration process for the provided camera. * End the 3D calibration process for the provided camera.
@@ -419,7 +424,7 @@ export const useCameraSettingsStore = defineStore("cameraSettings", {
takeCalibrationSnapshot: true, takeCalibrationSnapshot: true,
cameraUniqueName: cameraUniqueName cameraUniqueName: cameraUniqueName
}; };
useStateStore().websocket?.send(payload); useStateStore().websocket?.send(payload, true);
}, },
/** /**
* Save a snapshot of the input frame of the camera. * Save a snapshot of the input frame of the camera.
@@ -431,7 +436,7 @@ export const useCameraSettingsStore = defineStore("cameraSettings", {
saveInputSnapshot: true, saveInputSnapshot: true,
cameraUniqueName: cameraUniqueName cameraUniqueName: cameraUniqueName
}; };
useStateStore().websocket?.send(payload); useStateStore().websocket?.send(payload, true);
}, },
/** /**
* Save a snapshot of the output frame of the camera. * Save a snapshot of the output frame of the camera.
@@ -443,7 +448,7 @@ export const useCameraSettingsStore = defineStore("cameraSettings", {
saveOutputSnapshot: true, saveOutputSnapshot: true,
cameraUniqueName: cameraUniqueName cameraUniqueName: cameraUniqueName
}; };
useStateStore().websocket?.send(payload); useStateStore().websocket?.send(payload, true);
}, },
/** /**
* Set the robot offset mode type. * Set the robot offset mode type.
@@ -456,7 +461,7 @@ export const useCameraSettingsStore = defineStore("cameraSettings", {
robotOffsetPoint: type, robotOffsetPoint: type,
cameraUniqueName: cameraUniqueName cameraUniqueName: cameraUniqueName
}; };
useStateStore().websocket?.send(payload); useStateStore().websocket?.send(payload, true);
}, },
getCalibrationCoeffs( getCalibrationCoeffs(
resolution: Resolution, resolution: Resolution,

View File

@@ -10,7 +10,6 @@ import { NetworkConnectionType } from "@/types/SettingTypes";
import { useStateStore } from "@/stores/StateStore"; import { useStateStore } from "@/stores/StateStore";
import axios from "axios"; import axios from "axios";
import type { WebsocketSettingsUpdate } from "@/types/WebsocketDataTypes"; import type { WebsocketSettingsUpdate } from "@/types/WebsocketDataTypes";
import type { AprilTagFieldLayout } from "@/types/PhotonTrackingTypes";
import { ref } from "vue"; import { ref } from "vue";
interface GeneralSettingsStore { interface GeneralSettingsStore {
@@ -18,7 +17,7 @@ interface GeneralSettingsStore {
network: NetworkSettings; network: NetworkSettings;
lighting: LightingSettings; lighting: LightingSettings;
metrics: MetricData; metrics: MetricData;
currentFieldLayout: AprilTagFieldLayout; currentFieldLayout;
} }
interface MetricsEntry { interface MetricsEntry {
@@ -185,7 +184,7 @@ export const useSettingsStore = defineStore("settings", {
const payload = { const payload = {
enabledLEDPercentage: brightness enabledLEDPercentage: brightness
}; };
useStateStore().websocket?.send(payload); useStateStore().websocket?.send(payload, true);
} }
} }
}); });

View File

@@ -5,7 +5,6 @@ import type { ObjectDetectionModelProperties } from "@/types/SettingTypes";
* The on-wire form of PipelineType.java (the enum is serialized with `ordinal()`) * The on-wire form of PipelineType.java (the enum is serialized with `ordinal()`)
*/ */
export enum PipelineType { export enum PipelineType {
Calibration3d = 1,
DriverMode = 2, DriverMode = 2,
Reflective = 3, Reflective = 3,
ColoredShape = 4, ColoredShape = 4,
@@ -36,62 +35,18 @@ export enum TargetModel {
ReefscapeAlgae = 7 ReefscapeAlgae = 7
} }
export enum ContourSortMode {
Largest = 0,
Smallest = 1,
Highest = 2,
Lowest = 3,
Leftmost = 4,
Rightmost = 5,
Centermost = 6
}
export enum ContourTargetOrientation {
Portrait = 0,
Landscape = 1
}
export enum ContourGroupingMode {
Single = 0,
Dual = 1,
TwoOrMore = 2
}
export enum ContourIntersection {
None = 0,
Up = 1,
Down = 2,
Left = 3,
Right = 4
}
export enum ContourShape {
Circle = 0,
Polygon = 1,
Triangle = 2,
Quadrilateral = 3
}
export enum ContourTargetOffsetPointEdge {
Center = 0,
Top = 1,
Bottom = 2,
Left = 3,
Right = 4
}
export interface PipelineSettings { export interface PipelineSettings {
offsetRobotOffsetMode: RobotOffsetPointMode; offsetRobotOffsetMode: RobotOffsetPointMode;
streamingFrameDivisor: number; streamingFrameDivisor: number;
offsetDualPointBArea: number; offsetDualPointBArea: number;
contourGroupingMode: ContourGroupingMode; contourGroupingMode: number;
hsvValue: WebsocketNumberPair | [number, number]; hsvValue: WebsocketNumberPair | [number, number];
cameraGain: number; cameraGain: number;
cameraBlueGain: number; cameraBlueGain: number;
cameraRedGain: number; cameraRedGain: number;
cornerDetectionSideCount: number; cornerDetectionSideCount: number;
contourRatio: WebsocketNumberPair | [number, number]; contourRatio: WebsocketNumberPair | [number, number];
contourTargetOffsetPointEdge: ContourTargetOffsetPointEdge; contourTargetOffsetPointEdge: number;
pipelineNickname: string; pipelineNickname: string;
inputImageRotationMode: number; inputImageRotationMode: number;
contourArea: WebsocketNumberPair | [number, number]; contourArea: WebsocketNumberPair | [number, number];
@@ -101,7 +56,7 @@ export interface PipelineSettings {
inputShouldShow: boolean; inputShouldShow: boolean;
cameraAutoExposure: boolean; cameraAutoExposure: boolean;
contourSpecklePercentage: number; contourSpecklePercentage: number;
contourTargetOrientation: ContourTargetOrientation; contourTargetOrientation: number;
targetModel: TargetModel; targetModel: TargetModel;
cornerDetectionUseConvexHulls: boolean; cornerDetectionUseConvexHulls: boolean;
outputShouldShow: boolean; outputShouldShow: boolean;
@@ -112,7 +67,7 @@ export interface PipelineSettings {
ledMode: boolean; ledMode: boolean;
hueInverted: boolean; hueInverted: boolean;
outputMaximumTargets: number; outputMaximumTargets: number;
contourSortMode: ContourSortMode; contourSortMode: number;
cameraExposureRaw: number; cameraExposureRaw: number;
cameraMinExposureRaw: number; cameraMinExposureRaw: number;
cameraMaxExposureRaw: number; cameraMaxExposureRaw: number;
@@ -125,13 +80,11 @@ export interface PipelineSettings {
cornerDetectionAccuracyPercentage: number; cornerDetectionAccuracyPercentage: number;
hsvSaturation: WebsocketNumberPair | [number, number]; hsvSaturation: WebsocketNumberPair | [number, number];
pipelineType: PipelineType; pipelineType: PipelineType;
contourIntersection: ContourIntersection; contourIntersection: number;
cameraAutoWhiteBalance: boolean; cameraAutoWhiteBalance: boolean;
cameraWhiteBalanceTemp: number; cameraWhiteBalanceTemp: number;
crosshair: boolean;
blockForFrames: boolean; blockForFrames: boolean;
} }
export type ConfigurablePipelineSettings = Partial< export type ConfigurablePipelineSettings = Partial<
@@ -160,13 +113,13 @@ export const DefaultPipelineSettings: Omit<
offsetRobotOffsetMode: RobotOffsetPointMode.None, offsetRobotOffsetMode: RobotOffsetPointMode.None,
streamingFrameDivisor: 0, streamingFrameDivisor: 0,
offsetDualPointBArea: 0, offsetDualPointBArea: 0,
contourGroupingMode: ContourGroupingMode.Single, contourGroupingMode: 0,
hsvValue: { first: 50, second: 255 }, hsvValue: { first: 50, second: 255 },
cameraBlueGain: 20, cameraBlueGain: 20,
cameraRedGain: 11, cameraRedGain: 11,
cornerDetectionSideCount: 4, cornerDetectionSideCount: 4,
contourRatio: { first: 0, second: 20 }, contourRatio: { first: 0, second: 20 },
contourTargetOffsetPointEdge: ContourTargetOffsetPointEdge.Center, contourTargetOffsetPointEdge: 0,
pipelineNickname: "Placeholder Pipeline", pipelineNickname: "Placeholder Pipeline",
inputImageRotationMode: 0, inputImageRotationMode: 0,
contourArea: { first: 0, second: 100 }, contourArea: { first: 0, second: 100 },
@@ -176,7 +129,7 @@ export const DefaultPipelineSettings: Omit<
inputShouldShow: false, inputShouldShow: false,
cameraAutoExposure: false, cameraAutoExposure: false,
contourSpecklePercentage: 5, contourSpecklePercentage: 5,
contourTargetOrientation: ContourTargetOrientation.Landscape, contourTargetOrientation: 1,
cornerDetectionUseConvexHulls: true, cornerDetectionUseConvexHulls: true,
outputShouldShow: true, outputShouldShow: true,
outputShouldDraw: true, outputShouldDraw: true,
@@ -185,7 +138,7 @@ export const DefaultPipelineSettings: Omit<
hsvHue: { first: 50, second: 180 }, hsvHue: { first: 50, second: 180 },
hueInverted: false, hueInverted: false,
outputMaximumTargets: 20, outputMaximumTargets: 20,
contourSortMode: ContourSortMode.Largest, contourSortMode: 0,
offsetSinglePoint: { x: 0, y: 0 }, offsetSinglePoint: { x: 0, y: 0 },
cameraBrightness: 50, cameraBrightness: 50,
offsetDualPointAArea: 0, offsetDualPointAArea: 0,
@@ -194,12 +147,11 @@ export const DefaultPipelineSettings: Omit<
cornerDetectionStrategy: 0, cornerDetectionStrategy: 0,
cornerDetectionAccuracyPercentage: 10, cornerDetectionAccuracyPercentage: 10,
hsvSaturation: { first: 50, second: 255 }, hsvSaturation: { first: 50, second: 255 },
contourIntersection: ContourIntersection.Up, contourIntersection: 1,
cameraAutoWhiteBalance: false, cameraAutoWhiteBalance: false,
cameraWhiteBalanceTemp: 4000, cameraWhiteBalanceTemp: 4000,
cameraMinExposureRaw: 1, cameraMinExposureRaw: 1,
cameraMaxExposureRaw: 2, cameraMaxExposureRaw: 2,
crosshair: true,
blockForFrames: true blockForFrames: true
}; };
@@ -232,7 +184,7 @@ export interface ColoredShapePipelineSettings extends PipelineSettings {
contourRadius: WebsocketNumberPair | [number, number]; contourRadius: WebsocketNumberPair | [number, number];
circleDetectThreshold: number; circleDetectThreshold: number;
accuracyPercentage: number; accuracyPercentage: number;
contourShape: ContourShape; contourShape: number;
contourPerimeter: WebsocketNumberPair | [number, number]; contourPerimeter: WebsocketNumberPair | [number, number];
minDist: number; minDist: number;
maxCannyThresh: number; maxCannyThresh: number;
@@ -257,7 +209,7 @@ export const DefaultColoredShapePipelineSettings: ColoredShapePipelineSettings =
contourRadius: { first: 0, second: 100 }, contourRadius: { first: 0, second: 100 },
circleDetectThreshold: 5, circleDetectThreshold: 5,
accuracyPercentage: 10, accuracyPercentage: 10,
contourShape: ContourShape.Triangle, contourShape: 2,
contourPerimeter: { first: 0, second: 1.7976931348623157e308 }, contourPerimeter: { first: 0, second: 1.7976931348623157e308 },
minDist: 20, minDist: 20,
maxCannyThresh: 90 maxCannyThresh: 90
@@ -372,14 +324,13 @@ export const DefaultObjectDetectionPipelineSettings: ObjectDetectionPipelineSett
}; };
export interface Calibration3dPipelineSettings extends PipelineSettings { export interface Calibration3dPipelineSettings extends PipelineSettings {
pipelineType: PipelineType.Calibration3d;
drawAllSnapshots: boolean; drawAllSnapshots: boolean;
} }
export type ConfigurableCalibration3dPipelineSettings = Partial<Omit<Calibration3dPipelineSettings, "pipelineType">> & export type ConfigurableCalibration3dPipelineSettings = Partial<Omit<Calibration3dPipelineSettings, "pipelineType">> &
ConfigurablePipelineSettings; ConfigurablePipelineSettings;
export const DefaultCalibration3dPipelineSettings: Calibration3dPipelineSettings = { export const DefaultCalibration3dPipelineSettings: Calibration3dPipelineSettings = {
...DefaultPipelineSettings, ...DefaultPipelineSettings,
pipelineType: PipelineType.Calibration3d, pipelineType: PipelineType.ObjectDetection,
cameraGain: 20, cameraGain: 20,
targetModel: TargetModel.InfiniteRechargeHighGoalOuter, targetModel: TargetModel.InfiniteRechargeHighGoalOuter,
ledMode: true, ledMode: true,

View File

@@ -75,29 +75,46 @@ export type ConfigurableNetworkSettings = Omit<
"canManage" | "networkInterfaceNames" | "networkingDisabled" "canManage" | "networkInterfaceNames" | "networkingDisabled"
>; >;
interface PVCameraInfoBase { export interface PVCameraInfoBase {
type: "PVUsbCameraInfo" | "PVCSICameraInfo" | "PVFileCameraInfo"; /*
Huge hack. In Jackson, this is set based on the underlying type -- this
then maps to one of the 3 subclasses here below. Not sure how to best deal with this.
*/
cameraTypename: "PVUsbCameraInfo" | "PVCSICameraInfo" | "PVFileCameraInfo";
}
export interface PVUsbCameraInfo {
dev: number;
name: string;
otherPaths: string[];
path: string;
vendorId: number;
productId: number;
// In Java, PVCameraInfo provides a uniquePath property so we can have one Source of Truth here
uniquePath: string;
}
export interface PVCSICameraInfo {
baseName: string;
path: string;
// In Java, PVCameraInfo provides a uniquePath property so we can have one Source of Truth here
uniquePath: string;
}
export interface PVFileCameraInfo {
path: string; path: string;
name: string; name: string;
// In Java, PVCameraInfo provides a uniquePath property so we can have one Source of Truth here
uniquePath: string; uniquePath: string;
} }
export interface PVUsbCameraInfo extends PVCameraInfoBase { // This camera info will only ever hold one of its members - the others should be undefined.
type: "PVUsbCameraInfo"; export class PVCameraInfo {
dev: number; PVUsbCameraInfo: PVUsbCameraInfo | undefined;
otherPaths: string[]; PVCSICameraInfo: PVCSICameraInfo | undefined;
vendorId: number; PVFileCameraInfo: PVFileCameraInfo | undefined;
productId: number;
} }
export interface PVCSICameraInfo extends PVCameraInfoBase {
type: "PVCSICameraInfo";
baseName: string;
}
export interface PVFileCameraInfo extends PVCameraInfoBase {
type: "PVFileCameraInfo";
}
export type PVCameraInfo = PVUsbCameraInfo | PVCSICameraInfo | PVFileCameraInfo;
export interface VsmState { export interface VsmState {
disabledConfigs: WebsocketCameraSettingsUpdate[]; disabledConfigs: WebsocketCameraSettingsUpdate[];
@@ -259,7 +276,6 @@ export interface UiCameraConfiguration {
maxWhiteBalanceTemp: number; maxWhiteBalanceTemp: number;
fpsLimit: number; fpsLimit: number;
isEnabled: boolean;
matchedCameraInfo: PVCameraInfo; matchedCameraInfo: PVCameraInfo;
isConnected: boolean; isConnected: boolean;
@@ -422,13 +438,15 @@ export const PlaceholderCameraSettings: UiCameraConfiguration = reactive({
minWhiteBalanceTemp: 2000, minWhiteBalanceTemp: 2000,
maxWhiteBalanceTemp: 10000, maxWhiteBalanceTemp: 10000,
matchedCameraInfo: { matchedCameraInfo: {
type: "PVFileCameraInfo", PVFileCameraInfo: {
name: "Foobar", name: "Foobar",
path: "/dev/foobar", path: "/dev/foobar",
uniquePath: "/dev/foobar2" uniquePath: "/dev/foobar2"
},
PVCSICameraInfo: undefined,
PVUsbCameraInfo: undefined
}, },
fpsLimit: -1, fpsLimit: -1,
isEnabled: true,
isConnected: true, isConnected: true,
hasConnected: true, hasConnected: true,
mismatch: false mismatch: false

View File

@@ -30,18 +30,21 @@ export interface WebsocketNumberPair {
second: number; second: number;
} }
export type WebsocketVideoFormat = { export type WebsocketVideoFormat = Record<
fps: number; number,
height: number; {
width: number; fps: number;
pixelFormat: string; height: number;
index?: number; width: number;
diagonalFOV?: number; pixelFormat: string;
horizontalFOV?: number; index?: number;
verticalFOV?: number; diagonalFOV?: number;
standardDeviation?: number; horizontalFOV?: number;
mean?: number; verticalFOV?: number;
}[]; standardDeviation?: number;
mean?: number;
}
>;
// Companion to UICameraConfiguration in Java // Companion to UICameraConfiguration in Java
export interface WebsocketCameraSettingsUpdate { export interface WebsocketCameraSettingsUpdate {
@@ -65,7 +68,6 @@ export interface WebsocketCameraSettingsUpdate {
maxWhiteBalanceTemp: number; maxWhiteBalanceTemp: number;
matchedCameraInfo: PVCameraInfo; matchedCameraInfo: PVCameraInfo;
fpsLimit: number; fpsLimit: number;
isEnabled: boolean;
isConnected: boolean; isConnected: boolean;
hasConnected: boolean; hasConnected: boolean;
mismatch: boolean; mismatch: boolean;
@@ -87,8 +89,8 @@ export interface WebsocketCalibrationData {
minCount: number; minCount: number;
videoModeIndex: number; videoModeIndex: number;
patternHeight: number; patternHeight: number;
squareSizeMm: number; squareSizeIn: number;
markerSizeMm: number; markerSizeIn: number;
} }
export interface IncomingWebsocketData { export interface IncomingWebsocketData {

View File

@@ -2,7 +2,13 @@
import { useCameraSettingsStore } from "@/stores/settings/CameraSettingsStore"; import { useCameraSettingsStore } from "@/stores/settings/CameraSettingsStore";
import { computed, inject, ref } from "vue"; import { computed, inject, ref } from "vue";
import { useStateStore } from "@/stores/StateStore"; import { useStateStore } from "@/stores/StateStore";
import { PlaceholderCameraSettings, type PVCameraInfo } from "@/types/SettingTypes"; import {
PlaceholderCameraSettings,
PVCameraInfo,
type PVCSICameraInfo,
type PVFileCameraInfo,
type PVUsbCameraInfo
} from "@/types/SettingTypes";
import { axiosPost, getResolutionString } from "@/lib/PhotonUtils"; import { axiosPost, getResolutionString } from "@/lib/PhotonUtils";
import PhotonCameraStream from "@/components/app/photon-camera-stream.vue"; import PhotonCameraStream from "@/components/app/photon-camera-stream.vue";
import PvDeleteModal from "@/components/common/pv-delete-modal.vue"; import PvDeleteModal from "@/components/common/pv-delete-modal.vue";
@@ -12,22 +18,20 @@ import { useTheme } from "vuetify";
const theme = useTheme(); const theme = useTheme();
const backendHostname = inject<string>("backendHostname"); const formatUrl = (port) => `http://${inject("backendHostname")}:${port}/stream.mjpg`;
const formatUrl = (port: number) => `http://${backendHostname}:${port}/stream.mjpg`;
const activatingModule = ref(false); const activatingModule = ref(false);
const activateModule = async (moduleUniqueName: string) => { const activateModule = (moduleUniqueName: string) => {
if (activatingModule.value) return; if (activatingModule.value) return;
activatingModule.value = true; activatingModule.value = true;
await axiosPost("/utils/activateMatchedCamera", "activate a matched camera", { axiosPost("/utils/activateMatchedCamera", "activate a matched camera", {
cameraUniqueName: moduleUniqueName cameraUniqueName: moduleUniqueName
}); }).finally(() => (activatingModule.value = false));
activatingModule.value = false;
}; };
const assigningCamera = ref(false); const assigningCamera = ref(false);
const assignCamera = async (cameraInfo: PVCameraInfo) => { const assignCamera = (cameraInfo: PVCameraInfo) => {
if (assigningCamera.value) return; if (assigningCamera.value) return;
assigningCamera.value = true; assigningCamera.value = true;
@@ -35,39 +39,48 @@ const assignCamera = async (cameraInfo: PVCameraInfo) => {
cameraInfo: cameraInfo cameraInfo: cameraInfo
}; };
await axiosPost("/utils/assignUnmatchedCamera", "assign an unmatched camera", payload); axiosPost("/utils/assignUnmatchedCamera", "assign an unmatched camera", payload).finally(
assigningCamera.value = false; () => (assigningCamera.value = false)
);
}; };
const deactivatingModule = ref(false); const deactivatingModule = ref(false);
const deactivateModule = async (cameraUniqueName: string) => { const deactivateModule = (cameraUniqueName: string) => {
if (deactivatingModule.value) return; if (deactivatingModule.value) return;
deactivatingModule.value = true; deactivatingModule.value = true;
await axiosPost("/utils/unassignCamera", "unassign a camera", { cameraUniqueName: cameraUniqueName }); axiosPost("/utils/unassignCamera", "unassign a camera", { cameraUniqueName: cameraUniqueName }).finally(
deactivatingModule.value = false; () => (deactivatingModule.value = false)
);
}; };
const confirmDeleteDialog = ref({ show: false, nickname: "", cameraUniqueName: "" }); const confirmDeleteDialog = ref({ show: false, nickname: "", cameraUniqueName: "" });
const deletingCamera = ref<string | null>(null); const deletingCamera = ref<string | null>(null);
const deleteThisCamera = async (cameraUniqueName: string) => { const deleteThisCamera = (cameraUniqueName: string) => {
if (deletingCamera.value) return; if (deletingCamera.value) return;
deletingCamera.value = cameraUniqueName; deletingCamera.value = cameraUniqueName;
await axiosPost("/utils/nukeOneCamera", "delete a camera", { cameraUniqueName: cameraUniqueName }); axiosPost("/utils/nukeOneCamera", "delete a camera", { cameraUniqueName: cameraUniqueName }).finally(() => {
deletingCamera.value = null; deletingCamera.value = null;
});
}; };
const cameraConnected = (uniquePath: string | undefined): boolean => { const cameraConnected = (uniquePath: string): boolean => {
if (!uniquePath) return false; return (
return useStateStore().vsmState.allConnectedCameras.find((it) => it.uniquePath === uniquePath) !== undefined; useStateStore().vsmState.allConnectedCameras.find((it) => cameraInfoFor(it).uniquePath === uniquePath) !== undefined
);
}; };
const unmatchedCameras = computed(() => { const unmatchedCameras = computed(() => {
const activeVmPaths = Object.values(useCameraSettingsStore().cameras).map((it) => it.matchedCameraInfo.uniquePath); const activeVmPaths = Object.values(useCameraSettingsStore().cameras).map(
const disabledVmPaths = useStateStore().vsmState.disabledConfigs.map((it) => it.matchedCameraInfo.uniquePath); (it) => cameraInfoFor(it.matchedCameraInfo).uniquePath
);
const disabledVmPaths = useStateStore().vsmState.disabledConfigs.map(
(it) => cameraInfoFor(it.matchedCameraInfo).uniquePath
);
return useStateStore().vsmState.allConnectedCameras.filter( return useStateStore().vsmState.allConnectedCameras.filter(
(it) => !activeVmPaths.includes(it.uniquePath) && !disabledVmPaths.includes(it.uniquePath) (it) =>
!activeVmPaths.includes(cameraInfoFor(it).uniquePath) && !disabledVmPaths.includes(cameraInfoFor(it).uniquePath)
); );
}); });
@@ -78,8 +91,8 @@ const activeVisionModules = computed(() =>
// Display connected cameras first // Display connected cameras first
.sort( .sort(
(first, second) => (first, second) =>
(cameraConnected(second.matchedCameraInfo.uniquePath) ? 1 : 0) - (cameraConnected(cameraInfoFor(second.matchedCameraInfo).uniquePath) ? 1 : 0) -
(cameraConnected(first.matchedCameraInfo.uniquePath) ? 1 : 0) (cameraConnected(cameraInfoFor(first.matchedCameraInfo).uniquePath) ? 1 : 0)
) )
); );
@@ -92,24 +105,41 @@ const setCameraView = (camera: PVCameraInfo | null, isConnected: boolean | null)
viewingCamera.value = [camera, isConnected]; viewingCamera.value = [camera, isConnected];
}; };
/**
* Get the connection-type-specific camera info from the given PVCameraInfo object.
*/
const cameraInfoFor = (camera: PVCameraInfo | null): PVUsbCameraInfo | PVCSICameraInfo | PVFileCameraInfo | any => {
if (!camera) return null;
if (camera.PVUsbCameraInfo) {
return camera.PVUsbCameraInfo;
}
if (camera.PVCSICameraInfo) {
return camera.PVCSICameraInfo;
}
if (camera.PVFileCameraInfo) {
return camera.PVFileCameraInfo;
}
return {};
};
/** /**
* Find the PVCameraInfo currently occupying the same uniquePath as the the given module * Find the PVCameraInfo currently occupying the same uniquePath as the the given module
*/ */
const getMatchedDevice = (info: PVCameraInfo | undefined): PVCameraInfo => { const getMatchedDevice = (info: PVCameraInfo | undefined): PVCameraInfo => {
if (!info) { if (!info) {
return { return {
type: "PVFileCameraInfo", PVFileCameraInfo: undefined,
path: "", PVCSICameraInfo: undefined,
name: "", PVUsbCameraInfo: undefined
uniquePath: ""
}; };
} }
return ( return (
useStateStore().vsmState.allConnectedCameras.find((it) => it.uniquePath === info.uniquePath) || { useStateStore().vsmState.allConnectedCameras.find(
type: "PVFileCameraInfo", (it) => cameraInfoFor(it).uniquePath === cameraInfoFor(info).uniquePath
path: "", ) || {
name: "", PVFileCameraInfo: undefined,
uniquePath: "" PVCSICameraInfo: undefined,
PVUsbCameraInfo: undefined
} }
); );
}; };
@@ -128,11 +158,12 @@ const getMatchedDevice = (info: PVCameraInfo | undefined): PVCameraInfo => {
class="pr-0" class="pr-0"
> >
<v-card color="surface" class="rounded-12"> <v-card color="surface" class="rounded-12">
<v-card-title>{{ module.matchedCameraInfo.name }}</v-card-title> <v-card-title>{{ cameraInfoFor(module.matchedCameraInfo).name }}</v-card-title>
<v-card-subtitle v-if="!cameraConnected(module.matchedCameraInfo.uniquePath)" <v-card-subtitle v-if="!cameraConnected(cameraInfoFor(module.matchedCameraInfo).uniquePath)"
>Status: <span class="inactive-status">Disconnected</span></v-card-subtitle >Status: <span class="inactive-status">Disconnected</span></v-card-subtitle
> >
<v-card-subtitle v-else-if="cameraConnected(module.matchedCameraInfo.uniquePath) && !module.mismatch" <v-card-subtitle
v-else-if="cameraConnected(cameraInfoFor(module.matchedCameraInfo).uniquePath) && !module.mismatch"
>Status: <span class="active-status">Active</span></v-card-subtitle >Status: <span class="active-status">Active</span></v-card-subtitle
> >
<v-card-subtitle v-else>Status: <span class="mismatch-status">Mismatch</span></v-card-subtitle> <v-card-subtitle v-else>Status: <span class="mismatch-status">Mismatch</span></v-card-subtitle>
@@ -141,7 +172,7 @@ const getMatchedDevice = (info: PVCameraInfo | undefined): PVCameraInfo => {
<tbody> <tbody>
<tr <tr
v-if=" v-if="
cameraConnected(module.matchedCameraInfo.uniquePath) && cameraConnected(cameraInfoFor(module.matchedCameraInfo).uniquePath) &&
useStateStore().backendResults[module.uniqueName] useStateStore().backendResults[module.uniqueName]
" "
> >
@@ -183,7 +214,7 @@ const getMatchedDevice = (info: PVCameraInfo | undefined): PVCameraInfo => {
</tbody> </tbody>
</v-table> </v-table>
<div <div
v-if="cameraConnected(module.matchedCameraInfo.uniquePath)" v-if="cameraConnected(cameraInfoFor(module.matchedCameraInfo).uniquePath)"
:id="`stream-container-${index}`" :id="`stream-container-${index}`"
class="d-flex flex-column justify-center align-center mt-3" class="d-flex flex-column justify-center align-center mt-3"
style="height: 250px" style="height: 250px"
@@ -202,7 +233,12 @@ const getMatchedDevice = (info: PVCameraInfo | undefined): PVCameraInfo => {
color="buttonPassive" color="buttonPassive"
style="width: 100%" style="width: 100%"
:variant="theme.global.current.value.dark ? 'outlined' : 'elevated'" :variant="theme.global.current.value.dark ? 'outlined' : 'elevated'"
@click="setCameraView(module.matchedCameraInfo, cameraConnected(module.matchedCameraInfo.uniquePath))" @click="
setCameraView(
module.matchedCameraInfo,
cameraConnected(cameraInfoFor(module.matchedCameraInfo).uniquePath)
)
"
> >
<span>Details</span> <span>Details</span>
</v-btn> </v-btn>
@@ -279,7 +315,7 @@ const getMatchedDevice = (info: PVCameraInfo | undefined): PVCameraInfo => {
</tr> </tr>
<tr> <tr>
<td>Connected</td> <td>Connected</td>
<td>{{ cameraConnected(module.matchedCameraInfo.uniquePath) }}</td> <td>{{ cameraConnected(cameraInfoFor(module.matchedCameraInfo).uniquePath) }}</td>
</tr> </tr>
</tbody> </tbody>
</v-table> </v-table>
@@ -291,7 +327,12 @@ const getMatchedDevice = (info: PVCameraInfo | undefined): PVCameraInfo => {
color="buttonPassive" color="buttonPassive"
style="width: 100%" style="width: 100%"
:variant="theme.global.current.value.dark ? 'outlined' : 'elevated'" :variant="theme.global.current.value.dark ? 'outlined' : 'elevated'"
@click="setCameraView(module.matchedCameraInfo, cameraConnected(module.matchedCameraInfo.uniquePath))" @click="
setCameraView(
module.matchedCameraInfo,
cameraConnected(cameraInfoFor(module.matchedCameraInfo).uniquePath)
)
"
> >
<span>Details</span> <span>Details</span>
</v-btn> </v-btn>
@@ -336,15 +377,15 @@ const getMatchedDevice = (info: PVCameraInfo | undefined): PVCameraInfo => {
<v-col v-for="(camera, index) in unmatchedCameras" :key="index" cols="12" sm="6" lg="4" class="pr-0"> <v-col v-for="(camera, index) in unmatchedCameras" :key="index" cols="12" sm="6" lg="4" class="pr-0">
<v-card class="pr-0 rounded-12" color="surface"> <v-card class="pr-0 rounded-12" color="surface">
<v-card-title> <v-card-title>
<span v-if="camera.type === 'PVUsbCameraInfo'">USB Camera:</span> <span v-if="camera.PVUsbCameraInfo">USB Camera:</span>
<span v-else-if="camera.type === 'PVCSICameraInfo'">CSI Camera:</span> <span v-else-if="camera.PVCSICameraInfo">CSI Camera:</span>
<span v-else-if="camera.type === 'PVFileCameraInfo'">File Camera:</span> <span v-else-if="camera.PVFileCameraInfo">File Camera:</span>
<span v-else>Unknown Camera:</span> <span v-else>Unknown Camera:</span>
&nbsp;<span>{{ camera.name }}</span> &nbsp;<span>{{ cameraInfoFor(camera)?.name ?? cameraInfoFor(camera)?.baseName }}</span>
</v-card-title> </v-card-title>
<v-card-subtitle>Status: Unassigned</v-card-subtitle> <v-card-subtitle>Status: Unassigned</v-card-subtitle>
<v-card-text class="pt-3"> <v-card-text class="pt-3">
<span style="word-break: break-all">{{ camera?.path }}</span> <span style="word-break: break-all">{{ cameraInfoFor(camera)?.path }}</span>
</v-card-text> </v-card-text>
<v-card-text class="pt-0"> <v-card-text class="pt-0">
<v-row> <v-row>
@@ -395,7 +436,7 @@ const getMatchedDevice = (info: PVCameraInfo | undefined): PVCameraInfo => {
<v-dialog v-model="viewingDetails" max-width="800"> <v-dialog v-model="viewingDetails" max-width="800">
<v-card v-if="viewingCamera[0] !== null" flat color="surface"> <v-card v-if="viewingCamera[0] !== null" flat color="surface">
<v-card-title class="d-flex justify-space-between"> <v-card-title class="d-flex justify-space-between">
<span>{{ viewingCamera[0].name }}</span> <span>{{ cameraInfoFor(viewingCamera[0])?.name ?? cameraInfoFor(viewingCamera[0])?.baseName }}</span>
<v-btn variant="text" @click="setCameraView(null, null)"> <v-btn variant="text" @click="setCameraView(null, null)">
<v-icon size="x-large">mdi-close</v-icon> <v-icon size="x-large">mdi-close</v-icon>
</v-btn> </v-btn>
@@ -405,7 +446,9 @@ const getMatchedDevice = (info: PVCameraInfo | undefined): PVCameraInfo => {
</v-card-text> </v-card-text>
<v-card-text <v-card-text
v-else-if=" v-else-if="
activeVisionModules.find((it) => it.matchedCameraInfo.uniquePath === viewingCamera[0]?.uniquePath)?.mismatch activeVisionModules.find(
(it) => cameraInfoFor(it.matchedCameraInfo).uniquePath === cameraInfoFor(viewingCamera[0]).uniquePath
)?.mismatch
" "
> >
<v-alert <v-alert

View File

@@ -77,18 +77,8 @@ const conflictingCameraShown = computed<boolean>(() => {
return useSettingsStore().general.conflictingCameras.length > 0; return useSettingsStore().general.conflictingCameras.length > 0;
}); });
const fpsLimitedCameras = computed<string>(() => { const fpsLimitWarningShown = computed<boolean>(() => {
return Object.values(useCameraSettingsStore().cameras) return Object.values(useCameraSettingsStore().cameras).some((c) => c.fpsLimit > 0);
.filter((c) => c.fpsLimit > 0)
.map((c) => c.nickname)
.join(", ");
});
const disabledCameras = computed<string>(() => {
return Object.values(useCameraSettingsStore().cameras)
.filter((c) => !c.isEnabled)
.map((c) => c.nickname)
.join(", ");
}); });
const showCameraSetupDialog = ref(useCameraSettingsStore().needsCameraConfiguration); const showCameraSetupDialog = ref(useCameraSettingsStore().needsCameraConfiguration);
@@ -121,7 +111,7 @@ const showCameraSetupDialog = ref(useCameraSettingsStore().needsCameraConfigurat
</span> </span>
</v-alert> </v-alert>
<v-alert <v-alert
v-if="fpsLimitedCameras" v-if="fpsLimitWarningShown"
class="mb-3" class="mb-3"
color="error" color="error"
density="compact" density="compact"
@@ -129,22 +119,10 @@ const showCameraSetupDialog = ref(useCameraSettingsStore().needsCameraConfigurat
:variant="theme.global.current.value.dark ? 'tonal' : 'elevated'" :variant="theme.global.current.value.dark ? 'tonal' : 'elevated'"
> >
<span <span
>{{ fpsLimitedCameras }} have an FPS limit set! This may cause performance issues. Check your logs for more >One or more cameras have an FPS limit set! This may cause performance issues. Check your logs for more
information. information.
</span> </span>
</v-alert> </v-alert>
<v-alert
v-if="disabledCameras"
class="mb-3"
color="error"
density="compact"
icon="mdi-alert-circle-outline"
:variant="theme.global.current.value.dark ? 'tonal' : 'elevated'"
>
<span
>{{ disabledCameras }} are disabled! This may cause performance issues. Check your logs for more information.
</span>
</v-alert>
<v-alert <v-alert
v-if="conflictingCameraShown" v-if="conflictingCameraShown"
class="mb-3" class="mb-3"

View File

@@ -1,6 +1,5 @@
<script setup lang="ts"> <script setup lang="ts">
const devMode = process.env.NODE_ENV === "development"; const devMode = process.env.NODE_ENV === "development";
const docsSrc = import.meta.env.MODE === "demo" ? "https://docs.photonvision.org" : "docs/index.html";
</script> </script>
<template> <template>
<div style="overflow: hidden; height: 100vh; width: 100%"> <div style="overflow: hidden; height: 100vh; width: 100%">
@@ -22,7 +21,7 @@ const docsSrc = import.meta.env.MODE === "demo" ? "https://docs.photonvision.org
</div> </div>
<div v-else style="width: 100%; height: 100%"> <div v-else style="width: 100%; height: 100%">
<!--suppress HtmlUnknownTarget --> <!--suppress HtmlUnknownTarget -->
<iframe :src="docsSrc" style="overflow: hidden; height: 100%; width: 100%; border: 0" /> <iframe src="docs/index.html" style="overflow: hidden; height: 100%; width: 100%; border: 0" />
</div> </div>
</div> </div>
</template> </template>

View File

@@ -0,0 +1,8 @@
{
"extends": "@vue/tsconfig/tsconfig.json",
"include": ["vite.config.*"],
"compilerOptions": {
"composite": true,
"types": ["node"]
}
}

View File

@@ -1,10 +1,9 @@
{ {
"extends": "@vue/tsconfig/tsconfig.dom.json", "extends": "@vue/tsconfig/tsconfig.dom.json",
"include": ["env.d.ts", "vite.config.ts", "playwright.config.ts", "src/**/*", "src/**/*.vue", "tests/**/*"], "include": ["env.d.ts", "src/**/*", "src/**/*.vue"],
"compilerOptions": { "compilerOptions": {
"lib": ["ES2022", "DOM", "DOM.Iterable"], "moduleResolution": "node",
"moduleResolution": "bundler", "noImplicitAny": false,
"noImplicitAny": true,
"strict": true, "strict": true,
"removeComments": true, "removeComments": true,
"sourceMap": true, "sourceMap": true,
@@ -15,4 +14,9 @@
"@/*": ["./src/*"] "@/*": ["./src/*"]
} }
}, },
"references": [
{
"path": "./tsconfig.config.json"
}
]
} }

View File

@@ -6,7 +6,7 @@ ext.licenseFile = file("$rootDir/LICENSE")
ext.externalLicensesFolder = file("$rootDir/ExternalLicenses") ext.externalLicensesFolder = file("$rootDir/ExternalLicenses")
apply from: "${rootDir}/shared/common.gradle" apply from: "${rootDir}/shared/common.gradle"
wpilibTools.deps.wpilibVersion = wpilibVersion wpilibTools.deps.wpilibVersion = wpi.versions.wpilibVersion.get()
def nativeConfigName = 'wpilibNatives' def nativeConfigName = 'wpilibNatives'
configurations { configurations {
@@ -23,12 +23,11 @@ dependencies {
wpilibNatives wpilibTools.deps.wpilib("wpimath") wpilibNatives wpilibTools.deps.wpilib("wpimath")
wpilibNatives wpilibTools.deps.wpilib("wpinet") wpilibNatives wpilibTools.deps.wpilib("wpinet")
wpilibNatives wpilibTools.deps.wpilib("wpiutil") wpilibNatives wpilibTools.deps.wpilib("wpiutil")
wpilibNatives wpilibTools.deps.wpilib("datalog")
wpilibNatives wpilibTools.deps.wpilib("ntcore") wpilibNatives wpilibTools.deps.wpilib("ntcore")
wpilibNatives wpilibTools.deps.wpilib("cscore") wpilibNatives wpilibTools.deps.wpilib("cscore")
wpilibNatives wpilibTools.deps.wpilib("apriltag") wpilibNatives wpilibTools.deps.wpilib("apriltag")
wpilibNatives wpilibTools.deps.wpilib("hal") wpilibNatives wpilibTools.deps.wpilib("hal")
wpilibNatives wpilibTools.deps.wpilibOpenCv(openCVversion) wpilibNatives wpilibTools.deps.wpilibOpenCv("frc" + openCVYear, wpi.versions.opencvVersion.get())
// These stay as implementation dependencies since they don't have native code that gets packaged // These stay as implementation dependencies since they don't have native code that gets packaged
implementation 'org.zeroturnaround:zt-zip:1.14' implementation 'org.zeroturnaround:zt-zip:1.14'
@@ -41,7 +40,7 @@ dependencies {
wpilibNatives("org.photonvision:rknn_jni-jni:$rknnVersion:$jniPlatform") { wpilibNatives("org.photonvision:rknn_jni-jni:$rknnVersion:$jniPlatform") {
transitive = false transitive = false
} }
wpilibNatives("org.photonvision:tflite_jni-jni:$tfliteVersion:$jniPlatform") { wpilibNatives("org.photonvision:rubik_jni-jni:$rubikVersion:$jniPlatform") {
transitive = false transitive = false
} }
wpilibNatives("org.photonvision:photon-libcamera-gl-driver-jni:$libcameraDriverVersion:$jniPlatform") { wpilibNatives("org.photonvision:photon-libcamera-gl-driver-jni:$libcameraDriverVersion:$jniPlatform") {
@@ -49,17 +48,11 @@ dependencies {
} }
} }
if (jniPlatform == "linuxx86-64") {
wpilibNatives("org.photonvision:tflite_jni-jni:$tfliteVersion:$jniPlatform") {
transitive = false
}
}
implementation("org.photonvision:rknn_jni-java:$rknnVersion") { implementation("org.photonvision:rknn_jni-java:$rknnVersion") {
transitive = false transitive = false
} }
implementation("org.photonvision:tflite_jni-java:$tfliteVersion") { implementation("org.photonvision:rubik_jni-java:$rubikVersion") {
transitive = false transitive = false
} }

View File

@@ -26,7 +26,7 @@ public class LoadJNI {
private static HashMap<JNITypes, Boolean> loadedMap = new HashMap<>(); private static HashMap<JNITypes, Boolean> loadedMap = new HashMap<>();
public enum JNITypes { public enum JNITypes {
RUBIK_DETECTOR("tflite_jni"), RUBIK_DETECTOR("tensorflowlite", "tensorflowlite_c", "external_delegate", "rubik_jni"),
RKNN_DETECTOR("rga", "rknnrt", "rknn_jni"), RKNN_DETECTOR("rga", "rknnrt", "rknn_jni"),
MRCAL("mrcal_jni"), MRCAL("mrcal_jni"),
LIBCAMERA("photonlibcamera"); LIBCAMERA("photonlibcamera");

View File

@@ -17,7 +17,10 @@
package org.photonvision.common.configuration; package org.photonvision.common.configuration;
import io.avaje.jsonb.Json; import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonProperty;
import edu.wpi.first.cscore.UsbCameraInfo;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.UUID; import java.util.UUID;
@@ -32,9 +35,7 @@ import org.photonvision.vision.camera.QuirkyCamera;
import org.photonvision.vision.pipeline.CVPipelineSettings; import org.photonvision.vision.pipeline.CVPipelineSettings;
import org.photonvision.vision.pipeline.DriverModePipelineSettings; import org.photonvision.vision.pipeline.DriverModePipelineSettings;
import org.photonvision.vision.processes.PipelineManager; import org.photonvision.vision.processes.PipelineManager;
import org.wpilib.vision.camera.UsbCameraInfo;
@Json
public class CameraConfiguration { public class CameraConfiguration {
private static final Logger logger = new Logger(CameraConfiguration.class, LogGroup.Camera); private static final Logger logger = new Logger(CameraConfiguration.class, LogGroup.Camera);
@@ -61,8 +62,11 @@ public class CameraConfiguration {
public int streamIndex = 0; // 0 index means ports [1181, 1182], 1 means [1183, 1184], etc... public int streamIndex = 0; // 0 index means ports [1181, 1182], 1 means [1183, 1184], etc...
public List<CVPipelineSettings> pipelineSettings = new ArrayList<>(); // Ignore the pipes, as we serialize them to their own column to hack around
// polymorphic lists
@JsonIgnore public List<CVPipelineSettings> pipelineSettings = new ArrayList<>();
@JsonIgnore
public DriverModePipelineSettings driveModeSettings = new DriverModePipelineSettings(); public DriverModePipelineSettings driveModeSettings = new DriverModePipelineSettings();
public CameraConfiguration(PVCameraInfo cameraInfo, String uniqueName, String nickname) { public CameraConfiguration(PVCameraInfo cameraInfo, String uniqueName, String nickname) {
@@ -74,22 +78,24 @@ public class CameraConfiguration {
logger.debug("Creating USB camera configuration for " + this.toShortString()); logger.debug("Creating USB camera configuration for " + this.toShortString());
} }
// JSON Constructor (can't be marked with @Json.Creator due to public fields that aren't part of // Shiny new constructor
// the parameters) @JsonCreator
public CameraConfiguration( public CameraConfiguration(
String uniqueName, @JsonProperty("uniqueName") String uniqueName,
PVCameraInfo matchedCameraInfo, @JsonProperty("matchedCameraInfo") PVCameraInfo matchedCameraInfo,
String nickname, @JsonProperty("nickname") String nickname,
boolean deactivated, @JsonProperty("deactivated") boolean deactivated,
QuirkyCamera cameraQuirks, @JsonProperty("cameraQuirks") QuirkyCamera cameraQuirks,
double FOV, @JsonProperty("FOV") double FOV,
int currentPipelineIndex) { @JsonProperty("calibrations") List<CameraCalibrationCoefficients> calibrations,
@JsonProperty("currentPipelineIndex") int currentPipelineIndex) {
this.uniqueName = uniqueName; this.uniqueName = uniqueName;
this.matchedCameraInfo = matchedCameraInfo; this.matchedCameraInfo = matchedCameraInfo;
this.nickname = nickname; this.nickname = nickname;
this.deactivated = deactivated; this.deactivated = deactivated;
this.cameraQuirks = cameraQuirks; this.cameraQuirks = cameraQuirks;
this.FOV = FOV; this.FOV = FOV;
this.calibrations = calibrations != null ? calibrations : new ArrayList<>();
this.currentPipelineIndex = currentPipelineIndex; this.currentPipelineIndex = currentPipelineIndex;
} }
@@ -114,14 +120,14 @@ public class CameraConfiguration {
PVCameraInfo matchedCameraInfo; PVCameraInfo matchedCameraInfo;
/** Legacy constructor for compat with 2024.3.1 */ /** Legacy constructor for compat with 2024.3.1 */
@Json.Creator @JsonCreator
public LegacyCameraConfigStruct( public LegacyCameraConfigStruct(
String baseName, @JsonProperty("baseName") String baseName,
String path, @JsonProperty("path") String path,
String[] otherPaths, @JsonProperty("otherPaths") String[] otherPaths,
CameraType cameraType, @JsonProperty("cameraType") CameraType cameraType,
int usbVID, @JsonProperty("usbVID") int usbVID,
int usbPID) { @JsonProperty("usbPID") int usbPID) {
if (cameraType == CameraType.UsbCamera) { if (cameraType == CameraType.UsbCamera) {
this.matchedCameraInfo = this.matchedCameraInfo =
PVCameraInfo.fromUsbCameraInfo( PVCameraInfo.fromUsbCameraInfo(
@@ -165,16 +171,16 @@ public class CameraConfiguration {
} }
/** /**
* Replace a calibration in our list with the same resolution with a new one, or add it if none * Replace a calibration in our list with the same unrotatedImageSize with a new one, or add it if
* exists yet. If we are replacing an existing calibration, the old one will be "released" and the * none exists yet. If we are replacing an existing calibration, the old one will be "released"
* underlying data matrices will become invalid. * and the underlying data matrices will become invalid.
* *
* @param calibration The calibration to add. * @param calibration The calibration to add.
*/ */
public void addCalibration(CameraCalibrationCoefficients calibration) { public void addCalibration(CameraCalibrationCoefficients calibration) {
logger.info("adding calibration " + calibration.resolution); logger.info("adding calibration " + calibration.unrotatedImageSize);
calibrations.stream() calibrations.stream()
.filter(it -> it.resolution.equals(calibration.resolution)) .filter(it -> it.unrotatedImageSize.equals(calibration.unrotatedImageSize))
.findAny() .findAny()
.ifPresent( .ifPresent(
(it) -> { (it) -> {
@@ -188,12 +194,12 @@ public class CameraConfiguration {
* Remove a calibration from our list. If found, the calibration will be "released". If not found, * Remove a calibration from our list. If found, the calibration will be "released". If not found,
* no-op. * no-op.
* *
* @param resolution The resolution to remove. * @param unrotatedImageSize The resolution to remove.
*/ */
public void removeCalibration(Size resolution) { public void removeCalibration(Size unrotatedImageSize) {
logger.info("deleting calibration " + resolution); logger.info("deleting calibration " + unrotatedImageSize);
calibrations.stream() calibrations.stream()
.filter(it -> it.resolution.equals(resolution)) .filter(it -> it.unrotatedImageSize.equals(unrotatedImageSize))
.findAny() .findAny()
.ifPresent( .ifPresent(
(it) -> { (it) -> {
@@ -209,6 +215,7 @@ public class CameraConfiguration {
* *
* <p>This represents our best guess at an immutable path to detect a camera at. * <p>This represents our best guess at an immutable path to detect a camera at.
*/ */
@JsonIgnore
public String getDevicePath() { public String getDevicePath() {
return matchedCameraInfo.uniquePath(); return matchedCameraInfo.uniquePath();
} }

View File

@@ -17,10 +17,7 @@
package org.photonvision.common.configuration; package org.photonvision.common.configuration;
import io.avaje.json.JsonException;
import io.avaje.jsonb.Jsonb;
import java.io.File; import java.io.File;
import java.io.FileWriter;
import java.io.IOException; import java.io.IOException;
import java.nio.file.Files; import java.nio.file.Files;
import java.nio.file.Path; import java.nio.file.Path;
@@ -37,6 +34,7 @@ import org.opencv.core.Size;
import org.photonvision.common.logging.LogGroup; import org.photonvision.common.logging.LogGroup;
import org.photonvision.common.logging.Logger; import org.photonvision.common.logging.Logger;
import org.photonvision.common.util.file.FileUtils; import org.photonvision.common.util.file.FileUtils;
import org.photonvision.common.util.file.JacksonUtils;
import org.photonvision.vision.processes.VisionSource; import org.photonvision.vision.processes.VisionSource;
import org.zeroturnaround.zip.ZipUtil; import org.zeroturnaround.zip.ZipUtil;
@@ -235,15 +233,14 @@ public class ConfigManager {
Path.of(getModelsDirectory().toString(), "photonvision-object-detection-models.json") Path.of(getModelsDirectory().toString(), "photonvision-object-detection-models.json")
.toFile(); .toFile();
try { try {
Jsonb.instance() JacksonUtils.serialize(
.type(NeuralNetworkModelsSettings.class) tempProperties.toPath(), this.getConfig().neuralNetworkPropertyManager());
.toJson(this.getConfig().getNeuralNetworkProperties(), new FileWriter(tempProperties));
ZipUtil.pack(getModelsDirectory(), out); ZipUtil.pack(getModelsDirectory(), out);
// Now delete the tempProperties // Now delete the tempProperties
if (tempProperties.exists()) { if (tempProperties.exists()) {
Files.delete(tempProperties.toPath()); Files.delete(tempProperties.toPath());
} }
} catch (IOException | IllegalStateException | JsonException e) { } catch (Exception e) {
e.printStackTrace(); e.printStackTrace();
} }
return out; return out;

View File

@@ -17,50 +17,40 @@
package org.photonvision.common.configuration; package org.photonvision.common.configuration;
import io.avaje.jsonb.Json; import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List;
import org.photonvision.common.hardware.statusLED.StatusLEDType;
@Json @JsonIgnoreProperties(ignoreUnknown = true)
public class HardwareConfig { public class HardwareConfig {
public String deviceName; public final String deviceName;
// LED control // LED control
public List<Integer> ledPins; public final ArrayList<Integer> ledPins;
public boolean ledsCanDim; public final boolean ledsCanDim;
public List<Integer> ledBrightnessRange; public final ArrayList<Integer> ledBrightnessRange;
public int ledPWMFrequency; public final int ledPWMFrequency;
public StatusLEDType statusLEDType; public final ArrayList<Integer> statusRGBPins;
public final boolean statusRGBActiveHigh;
// MIGRATION: 2026
@Json.Alias("statusRGBPins")
public List<Integer> statusLEDPins;
// MIGRATION: 2026
@Json.Alias("statusRGBActiveHigh")
public boolean statusLEDActiveHigh;
// Custom GPIO // Custom GPIO
public String getGPIOCommand; public final String getGPIOCommand;
public String setGPIOCommand; public final String setGPIOCommand;
public String setPWMCommand; public final String setPWMCommand;
public String setPWMFrequencyCommand; public final String setPWMFrequencyCommand;
public String releaseGPIOCommand; public final String releaseGPIOCommand;
// Device stuff // Device stuff
public String restartHardwareCommand; public final String restartHardwareCommand;
public double vendorFOV; // -1 for unmanaged public final double vendorFOV; // -1 for unmanaged
public HardwareConfig( public HardwareConfig(
String deviceName, String deviceName,
List<Integer> ledPins, ArrayList<Integer> ledPins,
boolean ledsCanDim, boolean ledsCanDim,
List<Integer> ledBrightnessRange, ArrayList<Integer> ledBrightnessRange,
int ledPwmFrequency, int ledPwmFrequency,
StatusLEDType statusLEDType, ArrayList<Integer> statusRGBPins,
List<Integer> statusLEDPins, boolean statusRGBActiveHigh,
boolean statusLEDActiveHigh,
String getGPIOCommand, String getGPIOCommand,
String setGPIOCommand, String setGPIOCommand,
String setPWMCommand, String setPWMCommand,
@@ -73,9 +63,8 @@ public class HardwareConfig {
this.ledsCanDim = ledsCanDim; this.ledsCanDim = ledsCanDim;
this.ledBrightnessRange = ledBrightnessRange; this.ledBrightnessRange = ledBrightnessRange;
this.ledPWMFrequency = ledPwmFrequency; this.ledPWMFrequency = ledPwmFrequency;
this.statusLEDType = statusLEDType; this.statusRGBPins = statusRGBPins;
this.statusLEDPins = statusLEDPins; this.statusRGBActiveHigh = statusRGBActiveHigh;
this.statusLEDActiveHigh = statusLEDActiveHigh;
this.getGPIOCommand = getGPIOCommand; this.getGPIOCommand = getGPIOCommand;
this.setGPIOCommand = setGPIOCommand; this.setGPIOCommand = setGPIOCommand;
this.setPWMCommand = setPWMCommand; this.setPWMCommand = setPWMCommand;
@@ -91,9 +80,8 @@ public class HardwareConfig {
ledsCanDim = false; ledsCanDim = false;
ledBrightnessRange = new ArrayList<>(); ledBrightnessRange = new ArrayList<>();
ledPWMFrequency = 0; ledPWMFrequency = 0;
statusLEDType = StatusLEDType.RGB; statusRGBPins = new ArrayList<>();
statusLEDPins = new ArrayList<>(); statusRGBActiveHigh = false;
statusLEDActiveHigh = false;
getGPIOCommand = ""; getGPIOCommand = "";
setGPIOCommand = ""; setGPIOCommand = "";
setPWMCommand = ""; setPWMCommand = "";
@@ -133,12 +121,10 @@ public class HardwareConfig {
+ ledBrightnessRange + ledBrightnessRange
+ ", ledPWMFrequency=" + ", ledPWMFrequency="
+ ledPWMFrequency + ledPWMFrequency
+ ", statusLEDType=" + ", statusRGBPins="
+ statusLEDType + statusRGBPins
+ ", statusLEDPins=" + ", statusRGBActiveHigh"
+ statusLEDPins + statusRGBActiveHigh
+ ", statusLEDActiveHigh"
+ statusLEDActiveHigh
+ ", getGPIOCommand=" + ", getGPIOCommand="
+ getGPIOCommand + getGPIOCommand
+ ", setGPIOCommand=" + ", setGPIOCommand="

View File

@@ -17,11 +17,10 @@
package org.photonvision.common.configuration; package org.photonvision.common.configuration;
import io.avaje.json.JsonException; import com.fasterxml.jackson.core.JsonProcessingException;
import io.avaje.jsonb.Jsonb; import edu.wpi.first.apriltag.AprilTagFieldLayout;
import edu.wpi.first.apriltag.AprilTagFields;
import java.io.File; import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException; import java.io.IOException;
import java.io.UncheckedIOException; import java.io.UncheckedIOException;
import java.nio.file.Files; import java.nio.file.Files;
@@ -37,9 +36,10 @@ import java.util.stream.Stream;
import org.photonvision.common.logging.LogGroup; import org.photonvision.common.logging.LogGroup;
import org.photonvision.common.logging.Logger; import org.photonvision.common.logging.Logger;
import org.photonvision.common.util.file.FileUtils; import org.photonvision.common.util.file.FileUtils;
import org.photonvision.common.util.file.JacksonUtils;
import org.photonvision.vision.pipeline.CVPipelineSettings;
import org.photonvision.vision.pipeline.DriverModePipelineSettings;
import org.photonvision.vision.processes.VisionSource; import org.photonvision.vision.processes.VisionSource;
import org.wpilib.vision.apriltag.AprilTagFieldLayout;
import org.wpilib.vision.apriltag.AprilTagFields;
import org.zeroturnaround.zip.ZipUtil; import org.zeroturnaround.zip.ZipUtil;
class LegacyConfigProvider extends ConfigProvider { class LegacyConfigProvider extends ConfigProvider {
@@ -126,13 +126,14 @@ class LegacyConfigProvider extends ConfigProvider {
AprilTagFieldLayout atfl = null; AprilTagFieldLayout atfl = null;
if (hardwareConfigFile.exists()) { if (hardwareConfigFile.exists()) {
try (var stream = new FileInputStream(hardwareConfigFile)) { try {
hardwareConfig = Jsonb.instance().type(HardwareConfig.class).fromJson(stream); hardwareConfig =
JacksonUtils.deserialize(hardwareConfigFile.toPath(), HardwareConfig.class);
if (hardwareConfig == null) { if (hardwareConfig == null) {
logger.error("Could not deserialize hardware config! Loading defaults"); logger.error("Could not deserialize hardware config! Loading defaults");
hardwareConfig = new HardwareConfig(); hardwareConfig = new HardwareConfig();
} }
} catch (IOException | IllegalStateException | JsonException e) { } catch (IOException e) {
logger.error("Could not deserialize hardware config! Loading defaults"); logger.error("Could not deserialize hardware config! Loading defaults");
hardwareConfig = new HardwareConfig(); hardwareConfig = new HardwareConfig();
} }
@@ -142,13 +143,14 @@ class LegacyConfigProvider extends ConfigProvider {
} }
if (hardwareSettingsFile.exists()) { if (hardwareSettingsFile.exists()) {
try (var stream = new FileInputStream(hardwareSettingsFile)) { try {
hardwareSettings = Jsonb.instance().type(HardwareSettings.class).fromJson(stream); hardwareSettings =
JacksonUtils.deserialize(hardwareSettingsFile.toPath(), HardwareSettings.class);
if (hardwareSettings == null) { if (hardwareSettings == null) {
logger.error("Could not deserialize hardware settings! Loading defaults"); logger.error("Could not deserialize hardware settings! Loading defaults");
hardwareSettings = new HardwareSettings(); hardwareSettings = new HardwareSettings();
} }
} catch (IOException | IllegalStateException | JsonException e) { } catch (IOException e) {
logger.error("Could not deserialize hardware settings! Loading defaults"); logger.error("Could not deserialize hardware settings! Loading defaults");
hardwareSettings = new HardwareSettings(); hardwareSettings = new HardwareSettings();
} }
@@ -158,13 +160,13 @@ class LegacyConfigProvider extends ConfigProvider {
} }
if (networkConfigFile.exists()) { if (networkConfigFile.exists()) {
try (var stream = new FileInputStream(networkConfigFile)) { try {
networkConfig = Jsonb.instance().type(NetworkConfig.class).fromJson(stream); networkConfig = JacksonUtils.deserialize(networkConfigFile.toPath(), NetworkConfig.class);
if (networkConfig == null) { if (networkConfig == null) {
logger.error("Could not deserialize network config! Loading defaults"); logger.error("Could not deserialize network config! Loading defaults");
networkConfig = new NetworkConfig(); networkConfig = new NetworkConfig();
} }
} catch (IOException | IllegalStateException | JsonException e) { } catch (IOException e) {
logger.error("Could not deserialize network config! Loading defaults"); logger.error("Could not deserialize network config! Loading defaults");
networkConfig = new NetworkConfig(); networkConfig = new NetworkConfig();
} }
@@ -182,12 +184,13 @@ class LegacyConfigProvider extends ConfigProvider {
} }
if (apriltagFieldLayoutFile.exists()) { if (apriltagFieldLayoutFile.exists()) {
try (var stream = new FileInputStream(apriltagFieldLayoutFile)) { try {
atfl = Jsonb.instance().type(AprilTagFieldLayout.class).fromJson(stream); atfl =
JacksonUtils.deserialize(apriltagFieldLayoutFile.toPath(), AprilTagFieldLayout.class);
if (atfl == null) { if (atfl == null) {
logger.error("Could not deserialize apriltag field layout! (still null)"); logger.error("Could not deserialize apriltag field layout! (still null)");
} }
} catch (IOException | IllegalStateException | JsonException e) { } catch (IOException e) {
logger.error("Could not deserialize apriltag field layout!", e); logger.error("Could not deserialize apriltag field layout!", e);
atfl = null; // not required, nice to be explicit atfl = null; // not required, nice to be explicit
} }
@@ -224,14 +227,14 @@ class LegacyConfigProvider extends ConfigProvider {
// Delete old configs // Delete old configs
FileUtils.deleteDirectory(camerasFolder.toPath()); FileUtils.deleteDirectory(camerasFolder.toPath());
try (var stream = new FileOutputStream(networkConfigFile)) { try {
Jsonb.instance().type(NetworkConfig.class).toJson(config.getNetworkConfig(), stream); JacksonUtils.serialize(networkConfigFile.toPath(), config.getNetworkConfig());
} catch (IOException | IllegalStateException | JsonException e) { } catch (IOException e) {
logger.error("Could not save network config!", e); logger.error("Could not save network config!", e);
} }
try (var stream = new FileOutputStream(hardwareSettingsFile)) { try {
Jsonb.instance().type(HardwareSettings.class).toJson(config.getHardwareSettings(), stream); JacksonUtils.serialize(hardwareSettingsFile.toPath(), config.getHardwareSettings());
} catch (IOException | IllegalStateException | JsonException e) { } catch (IOException e) {
logger.error("Could not save hardware config!", e); logger.error("Could not save hardware config!", e);
} }
@@ -246,11 +249,33 @@ class LegacyConfigProvider extends ConfigProvider {
subdir.toFile().mkdirs(); subdir.toFile().mkdirs();
} }
try (var stream = new FileOutputStream(Path.of(subdir.toString(), "config.json").toFile())) { try {
Jsonb.instance().type(CameraConfiguration.class).toJson(camConfig, stream); JacksonUtils.serialize(Path.of(subdir.toString(), "config.json"), camConfig);
} catch (IOException | IllegalStateException | JsonException e) { } catch (IOException e) {
logger.error("Could not save config.json for " + subdir, e); logger.error("Could not save config.json for " + subdir, e);
} }
try {
JacksonUtils.serialize(
Path.of(subdir.toString(), "drivermode.json"), camConfig.driveModeSettings);
} catch (IOException e) {
logger.error("Could not save drivermode.json for " + subdir, e);
}
for (var pipe : camConfig.pipelineSettings) {
var pipePath = Path.of(subdir.toString(), "pipelines", pipe.pipelineNickname + ".json");
if (!pipePath.getParent().toFile().exists()) {
// TODO: check for error
pipePath.getParent().toFile().mkdirs();
}
try {
JacksonUtils.serialize(pipePath, pipe);
} catch (IOException e) {
logger.error("Could not save " + pipe.pipelineNickname + ".json!", e);
}
}
} }
logger.info("Settings saved!"); logger.info("Settings saved!");
return false; // TODO, deal with this. Do I need to? return false; // TODO, deal with this. Do I need to?
@@ -264,9 +289,11 @@ class LegacyConfigProvider extends ConfigProvider {
for (var subdir : subdirectories) { for (var subdir : subdirectories) {
var cameraConfigPath = Path.of(subdir.toString(), "config.json"); var cameraConfigPath = Path.of(subdir.toString(), "config.json");
CameraConfiguration loadedConfig = null; CameraConfiguration loadedConfig = null;
try (var stream = new FileInputStream(cameraConfigPath.toFile())) { try {
loadedConfig = Jsonb.instance().type(CameraConfiguration.class).fromJson(stream); loadedConfig =
} catch (IllegalStateException | JsonException e) { JacksonUtils.deserialize(
cameraConfigPath.toAbsolutePath(), CameraConfiguration.class);
} catch (JsonProcessingException e) {
logger.error("Camera config deserialization failed!", e); logger.error("Camera config deserialization failed!", e);
e.printStackTrace(); e.printStackTrace();
} }
@@ -275,6 +302,63 @@ class LegacyConfigProvider extends ConfigProvider {
continue; // TODO how do we later try to load this camera if it gets reconnected? continue; // TODO how do we later try to load this camera if it gets reconnected?
} }
// At this point we have only loaded the base stuff
// We still need to deserialize pipelines, as well as
// driver mode settings
var driverModeFile = Path.of(subdir.toString(), "drivermode.json");
DriverModePipelineSettings driverMode;
try {
driverMode =
JacksonUtils.deserialize(
driverModeFile.toAbsolutePath(), DriverModePipelineSettings.class);
} catch (JsonProcessingException e) {
logger.error("Could not deserialize drivermode.json! Loading defaults");
logger.debug(Arrays.toString(e.getStackTrace()));
driverMode = new DriverModePipelineSettings();
}
if (driverMode == null) {
logger.warn(
"Could not load camera " + subdir + "'s drivermode.json! Loading" + " default");
driverMode = new DriverModePipelineSettings();
}
// Load pipelines by mapping the files within the pipelines subdir
// to their deserialized equivalents
var pipelineSubdirectory = Path.of(subdir.toString(), "pipelines");
List<CVPipelineSettings> settings = Collections.emptyList();
if (pipelineSubdirectory.toFile().exists()) {
try (Stream<Path> subdirectoryFiles = Files.list(pipelineSubdirectory)) {
settings =
subdirectoryFiles
.filter(p -> p.toFile().isFile())
.map(
p -> {
var relativizedFilePath =
configDirectoryFile
.toPath()
.toAbsolutePath()
.relativize(p)
.toString();
try {
return JacksonUtils.deserialize(p, CVPipelineSettings.class);
} catch (JsonProcessingException e) {
logger.error("Exception while deserializing " + relativizedFilePath, e);
} catch (IOException e) {
logger.warn(
"Could not load pipeline at "
+ relativizedFilePath
+ "! Skipping...");
}
return null;
})
.filter(Objects::nonNull)
.toList();
}
}
loadedConfig.driveModeSettings = driverMode;
loadedConfig.addPipelineSettings(settings);
loadedConfigurations.put(subdir.toFile().getName(), loadedConfig); loadedConfigurations.put(subdir.toFile().getName(), loadedConfig);
} }
} catch (IOException e) { } catch (IOException e) {

View File

@@ -17,17 +17,16 @@
package org.photonvision.common.configuration; package org.photonvision.common.configuration;
import io.avaje.jsonb.Json; import com.fasterxml.jackson.annotation.JsonAlias;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonProperty;
import org.photonvision.common.hardware.Platform; import org.photonvision.common.hardware.Platform;
import org.photonvision.common.networking.NetworkMode; import org.photonvision.common.networking.NetworkMode;
@Json
public class NetworkConfig { public class NetworkConfig {
// Can be an integer team number, or an IP address // Can be an integer team number, or an IP address
// MIGRATION: 2023
@Json.Alias("teamNumber")
public String ntServerAddress = "0"; public String ntServerAddress = "0";
public NetworkMode connectionType = NetworkMode.DHCP; public NetworkMode connectionType = NetworkMode.DHCP;
public String staticIp = ""; public String staticIp = "";
public String hostname = "photonvision"; public String hostname = "photonvision";
@@ -35,8 +34,8 @@ public class NetworkConfig {
public boolean shouldManage; public boolean shouldManage;
public boolean shouldPublishProto = false; public boolean shouldPublishProto = false;
public static final String NM_IFACE_STRING = "${interface}"; @JsonIgnore public static final String NM_IFACE_STRING = "${interface}";
public static final String NM_IP_STRING = "${ipaddr}"; @JsonIgnore public static final String NM_IP_STRING = "${ipaddr}";
public String networkManagerIface = ""; public String networkManagerIface = "";
// TODO: remove these strings if no longer needed // TODO: remove these strings if no longer needed
@@ -51,17 +50,19 @@ public class NetworkConfig {
setShouldManage(deviceCanManageNetwork()); setShouldManage(deviceCanManageNetwork());
} }
@JsonCreator
public NetworkConfig( public NetworkConfig(
String ntServerAddress, @JsonProperty("ntServerAddress") @JsonAlias({"ntServerAddress", "teamNumber"})
NetworkMode connectionType, String ntServerAddress,
String staticIp, @JsonProperty("connectionType") NetworkMode connectionType,
String hostname, @JsonProperty("staticIp") String staticIp,
boolean runNTServer, @JsonProperty("hostname") String hostname,
boolean shouldManage, @JsonProperty("runNTServer") boolean runNTServer,
boolean shouldPublishProto, @JsonProperty("shouldManage") boolean shouldManage,
String networkManagerIface, @JsonProperty("shouldPublishProto") boolean shouldPublishProto,
String setStaticCommand, @JsonProperty("networkManagerIface") String networkManagerIface,
String setDHCPcommand) { @JsonProperty("setStaticCommand") String setStaticCommand,
@JsonProperty("setDHCPcommand") String setDHCPcommand) {
this.ntServerAddress = ntServerAddress; this.ntServerAddress = ntServerAddress;
this.connectionType = connectionType; this.connectionType = connectionType;
this.staticIp = staticIp; this.staticIp = staticIp;
@@ -88,10 +89,12 @@ public class NetworkConfig {
config.setDHCPcommand); config.setDHCPcommand);
} }
@JsonIgnore
public String getPhysicalInterfaceName() { public String getPhysicalInterfaceName() {
return this.networkManagerIface; return this.networkManagerIface;
} }
@JsonIgnore
public String getEscapedInterfaceName() { public String getEscapedInterfaceName() {
return "\"" + networkManagerIface + "\""; return "\"" + networkManagerIface + "\"";
} }
@@ -100,6 +103,7 @@ public class NetworkConfig {
this.shouldManage = shouldManage && this.deviceCanManageNetwork(); this.shouldManage = shouldManage && this.deviceCanManageNetwork();
} }
@JsonIgnore
protected boolean deviceCanManageNetwork() { protected boolean deviceCanManageNetwork() {
return Platform.isLinux(); return Platform.isLinux();
} }

View File

@@ -280,10 +280,10 @@ public class NeuralNetworkModelManager {
* *
* <p>If this method returns `Optional.of(..)` then the model should be safe to load. * <p>If this method returns `Optional.of(..)` then the model should be safe to load.
* *
* @param modelPath the unique identifier of the model to retrieve * @param modelUID the unique identifier of the model to retrieve
* @return an Optional containing the model if found, or an empty Optional if not found * @return an Optional containing the model if found, or an empty Optional if not found
*/ */
public Optional<Model> getModel(Path modelPath) { public Optional<Model> getModel(String modelUID) {
if (models == null) { if (models == null) {
return Optional.empty(); return Optional.empty();
} }
@@ -292,7 +292,7 @@ public class NeuralNetworkModelManager {
for (Family backend : supportedBackends) { for (Family backend : supportedBackends) {
if (models.containsKey(backend)) { if (models.containsKey(backend)) {
Optional<Model> model = Optional<Model> model =
models.get(backend).stream().filter(m -> m.getPath().equals(modelPath)).findFirst(); models.get(backend).stream().filter(m -> m.getUID().equals(modelUID)).findFirst();
if (model.isPresent()) { if (model.isPresent()) {
return model; return model;
} }
@@ -318,7 +318,7 @@ public class NeuralNetworkModelManager {
} }
ModelProperties properties = ModelProperties properties =
ConfigManager.getInstance().getConfig().getNeuralNetworkProperties().getModel(path); ConfigManager.getInstance().getConfig().neuralNetworkPropertyManager().getModel(path);
if (properties == null) { if (properties == null) {
logger.warn( logger.warn(
@@ -332,7 +332,7 @@ public class NeuralNetworkModelManager {
// NeuralNetworkModelsSettings // NeuralNetworkModelsSettings
ConfigManager.getInstance() ConfigManager.getInstance()
.getConfig() .getConfig()
.getNeuralNetworkProperties() .neuralNetworkPropertyManager()
.addModelProperties(properties); .addModelProperties(properties);
} catch (IllegalArgumentException | IOException e) { } catch (IllegalArgumentException | IOException e) {
logger.error("Failed to translate legacy model filename to properties: " + path, e); logger.error("Failed to translate legacy model filename to properties: " + path, e);
@@ -406,8 +406,7 @@ public class NeuralNetworkModelManager {
// After loading all of the models, sort them by name to ensure a consistent // After loading all of the models, sort them by name to ensure a consistent
// ordering // ordering
models.forEach( models.forEach(
(backend, backendModels) -> (backend, backendModels) -> backendModels.sort((a, b) -> a.getUID().compareTo(b.getUID())));
backendModels.sort((a, b) -> a.getPath().compareTo(b.getPath())));
// Log // Log
StringBuilder sb = new StringBuilder(); StringBuilder sb = new StringBuilder();
@@ -415,7 +414,7 @@ public class NeuralNetworkModelManager {
models.forEach( models.forEach(
(backend, backendModels) -> { (backend, backendModels) -> {
sb.append(backend).append(" ["); sb.append(backend).append(" [");
backendModels.forEach(model -> sb.append(model.getPath()).append(", ")); backendModels.forEach(model -> sb.append(model.getUID()).append(", "));
sb.append("] "); sb.append("] ");
}); });
} }
@@ -487,7 +486,7 @@ public class NeuralNetworkModelManager {
.getConfig() .getConfig()
.setNeuralNetworkProperties( .setNeuralNetworkProperties(
supportedProperties.sum( supportedProperties.sum(
ConfigManager.getInstance().getConfig().getNeuralNetworkProperties())); ConfigManager.getInstance().getConfig().neuralNetworkPropertyManager()));
} }
public boolean clearModels() { public boolean clearModels() {
@@ -512,7 +511,7 @@ public class NeuralNetworkModelManager {
} }
// Delete model info // Delete model info
return ConfigManager.getInstance().getConfig().getNeuralNetworkProperties().clear(); return ConfigManager.getInstance().getConfig().neuralNetworkPropertyManager().clear();
} }
public File exportSingleModel(String modelPath) { public File exportSingleModel(String modelPath) {
@@ -526,7 +525,7 @@ public class NeuralNetworkModelManager {
ModelProperties properties = ModelProperties properties =
ConfigManager.getInstance() ConfigManager.getInstance()
.getConfig() .getConfig()
.getNeuralNetworkProperties() .neuralNetworkPropertyManager()
.getModel(Path.of(modelPath)); .getModel(Path.of(modelPath));
String fileName = ""; String fileName = "";

View File

@@ -17,10 +17,9 @@
package org.photonvision.common.configuration; package org.photonvision.common.configuration;
import io.avaje.jsonb.Json; import com.fasterxml.jackson.annotation.JsonCreator;
import io.avaje.jsonb.JsonType; import com.fasterxml.jackson.annotation.JsonIgnore;
import io.avaje.jsonb.Jsonb; import com.fasterxml.jackson.annotation.JsonProperty;
import io.avaje.jsonb.Types;
import java.io.IOException; import java.io.IOException;
import java.nio.file.Files; import java.nio.file.Files;
import java.nio.file.Path; import java.nio.file.Path;
@@ -28,29 +27,27 @@ import java.nio.file.Paths;
import java.util.Arrays; import java.util.Arrays;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map;
import java.util.regex.Matcher; import java.util.regex.Matcher;
import java.util.regex.Pattern; import java.util.regex.Pattern;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.photonvision.common.configuration.NeuralNetworkModelManager.Family; import org.photonvision.common.configuration.NeuralNetworkModelManager.Family;
import org.photonvision.common.configuration.NeuralNetworkModelManager.Version; import org.photonvision.common.configuration.NeuralNetworkModelManager.Version;
@Json
public class NeuralNetworkModelsSettings { public class NeuralNetworkModelsSettings {
/* /*
* The properties of the model. This is used to determine which model to load. * The properties of the model. This is used to determine which model to load.
* The only families currently supported are RKNN and Rubik (custom .tflite) * The only families currently supported are RKNN and Rubik (custom .tflite)
*/ */
@Json
public record ModelProperties( public record ModelProperties(
Path modelPath, @JsonProperty("modelPath") Path modelPath,
String nickname, @JsonProperty("nickname") String nickname,
List<String> labels, @JsonProperty("labels") List<String> labels,
int resolutionWidth, @JsonProperty("resolutionWidth") int resolutionWidth,
int resolutionHeight, @JsonProperty("resolutionHeight") int resolutionHeight,
Family family, @JsonProperty("family") Family family,
Version version) { @JsonProperty("version") Version version) {
@JsonCreator
public ModelProperties {}
ModelProperties(ModelProperties other) { ModelProperties(ModelProperties other) {
this( this(
other.modelPath, other.modelPath,
@@ -62,6 +59,13 @@ public class NeuralNetworkModelsSettings {
other.version); other.version);
} }
// In v2025.3.1, this was single string for the model path. but the first argument
// is now nickname
public ModelProperties(@JsonProperty("nickname") String filename)
throws IllegalArgumentException, IOException {
this(createFromFilename(filename));
}
// ============= Migration code from v2025.3.1 =========== // ============= Migration code from v2025.3.1 ===========
private static Pattern modelPattern = private static Pattern modelPattern =
@@ -156,58 +160,25 @@ public class NeuralNetworkModelsSettings {
// The path to the model is used as the key in the map because it is unique to // The path to the model is used as the key in the map because it is unique to
// the model, and should not change // the model, and should not change
@Json.Ignore @JsonProperty("modelPathToProperties")
private HashMap<Path, ModelProperties> modelPathToProperties = private HashMap<Path, ModelProperties> modelPathToProperties =
new HashMap<Path, ModelProperties>(); new HashMap<Path, ModelProperties>();
/** /**
* Constructor for the NeuralNetworkProperties class. * Constructor for the NeuralNetworkProperties class.
* *
* <p>This object holds a HashMap of {@link ModelProperties} objects * <p>This object holds a LinkedList of {@link ModelProperties} objects
*/ */
public NeuralNetworkModelsSettings() {} public NeuralNetworkModelsSettings() {}
/** /**
* Constructor for the NeuralNetworkProperties class. * Constructor for the NeuralNetworkProperties class.
* *
* <p>This object holds a HashMap of {@link ModelProperties} objects. * <p>This object holds a LinkedList of {@link ModelProperties} objects.
*
* @param modelPropertiesMap When the class is constructed, it will hold the provided map
*/
public NeuralNetworkModelsSettings(HashMap<Path, ModelProperties> modelPropertiesMap) {
modelPathToProperties = modelPropertiesMap;
}
/**
* Constructor for the NeuralNetworkProperties class.
*
* <p>This object holds a HashMap of {@link ModelProperties} objects.
* *
* @param modelPropertiesList When the class is constructed, it will hold the provided list * @param modelPropertiesList When the class is constructed, it will hold the provided list
*/ */
@Json.Creator public NeuralNetworkModelsSettings(HashMap<Path, ModelProperties> modelPropertiesList) {}
public NeuralNetworkModelsSettings(
ModelProperties[] models, @Json.Unmapped Map<String, Object> unmapped) {
JsonType<Map<String, ModelProperties>> modelPropsMapJsonb =
Jsonb.instance().type(Types.mapOf(ModelProperties.class));
Stream<ModelProperties> modelPropsStream;
if (models != null) {
modelPropsStream = Arrays.stream(models);
} else if (unmapped.containsKey("modelPathToProperties")) {
// MIGRATION: 2026
modelPropsStream =
modelPropsMapJsonb.fromObject(unmapped.get("modelPathToProperties")).values().stream();
} else {
modelPropsStream = Stream.empty();
}
this(
modelPropsStream.collect(
Collectors.toMap(
(model) -> model.modelPath(),
(model) -> model,
(prev, next) -> next,
HashMap::new)));
}
@Override @Override
public String toString() { public String toString() {
@@ -268,7 +239,7 @@ public class NeuralNetworkModelsSettings {
* *
* @return A list of all models * @return A list of all models
*/ */
@Json.Property("models") @JsonIgnore
public ModelProperties[] getModels() { public ModelProperties[] getModels() {
return modelPathToProperties.values().toArray(new ModelProperties[0]); return modelPathToProperties.values().toArray(new ModelProperties[0]);
} }

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