Compare commits

..

9 Commits
Dev ... v2026.7

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
485 changed files with 9301 additions and 7514 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -11,12 +11,13 @@ jobs:
steps:
- uses: actions/checkout@v6
- name: Install pnpm
uses: pnpm/action-setup@v5
uses: pnpm/action-setup@v4
with:
version: 10
- uses: actions/setup-node@v6
- name: Setup Node
uses: actions/setup-node@v6
with:
node-version: 24
node-version: 22
cache: pnpm
cache-dependency-path: website/pnpm-lock.yaml
- name: Install packages
@@ -36,27 +37,22 @@ jobs:
format-check:
name: Check Formatting
defaults:
run:
working-directory: website
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: pnpm/action-setup@v5
- name: Install pnpm
uses: pnpm/action-setup@v4
with:
version: 10
- uses: actions/setup-node@v6
- name: Setup Node
uses: actions/setup-node@v6
with:
node-version: 24
node-version: 22
cache: pnpm
cache-dependency-path: website/pnpm-lock.yaml
- name: Install Packages
run: pnpm i --frozen-lockfile
- run: |
set +e
pnpm run format-ci
exit_code=$?
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
working-directory: website
- name: Run Formatting Check
run: pnpm prettier -c .
working-directory: website

2
.gitignore vendored
View File

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

View File

@@ -2,7 +2,7 @@ cppHeaderFileInclude {
\.h$
}
generatedFileExclude {
modifiableFileExclude {
photon-lib/py/photonlibpy/generated/
photon-targeting/src/generated/
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!
* `-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
* macx86-64
* macx64
* macarm64
* linuxx86-64
* linuxx64
* linuxarm64
* linuxathena
- `-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
- `-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
@@ -67,7 +67,7 @@ PhotonVision was forked from [Chameleon Vision](https://github.com/Chameleon-Vis
* [EJML](https://github.com/lessthanoptimal/ejml)
* [Javalin](https://javalin.io/)
* [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)
* [OSHI](https://github.com/oshi/oshi)
* [QuickBuffers](https://github.com/HebiRobotics/QuickBuffers)

View File

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

View File

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

View File

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

View File

@@ -1,17 +1,19 @@
# NetworkTables API
## Usage
## About
:::{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
:::{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.
### Getting Target Information
@@ -62,3 +64,9 @@ These entries are global, meaning that they should be called on the main `photon
| Key | Type | Description |
| --------- | ----- | -------------------------------------------------------- |
| `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 ],
"ledsCanDim" : true,
"ledPWMFrequency" : 1000,
"statusLEDType": "GreenYellow",
"statusLEDPins": [ 5, 4 ],
"statusLEDActiveHigh": false,
"vendorFOV" : 75.76079874010732
}

View File

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

View File

@@ -25,7 +25,7 @@ Go to the [GitHub releases page](https://github.com/PhotonVision/photonvision/re
:::{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 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}

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
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

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".
:::
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}
.. tab-set-code::
@@ -46,7 +46,7 @@ This multi-target pose estimate can be accessed using PhotonLib. We suggest usin
{
auto multiTagResult = result.MultiTagResult();
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).
:::{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}

View File

@@ -8,11 +8,11 @@ This section contains the build instructions from the source code available at [
**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:**
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:**
@@ -47,30 +47,6 @@ In the photon-client directory:
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
In the photon-client directory:

View File

@@ -79,9 +79,9 @@ public class Robot extends TimedRobot {
camera.getAllUnreadResults();
}
var t1 = Timer.getMonotonicTimestamp();
var t1 = Timer.getFPGATimestamp();
light.set(true);
var t2 = Timer.getMonotonicTimestamp();
var t2 = Timer.getFPGATimestamp();
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.
## Website
### Formatting the website
To format the website, run `pnpm -C website run format`.
## Alias
The following [alias](https://www.computerworld.com/article/1373210/how-to-use-aliases-in-linux-shell-commands.html) can be added to your shell config, which will allow you to lint the entirety of the PhotonVision project by running `pvLint`. The alias will work on Linux, macOS, Git Bash on Windows, and WSL.
```sh
alias pvLint='wpiformat -v && ./gradlew spotlessApply && pnpm -C photon-client lint && pnpm -C photon-client format && doc8 docs && 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,
"ledBrightnessRange" : [ 0, 100 ],
"ledPWMFrequency" : 0,
"statusLEDType": "RGB",
"statusLEDPins" : [ ],
"statusLEDActiveHigh" : false,
"statusRGBPins" : [ ],
"statusRGBActiveHigh" : false,
}
```
There are currently two types of status LEDs supported:
* `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>`
:::{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.
:::
### GPIO Pinout
@@ -138,9 +134,8 @@ Here is a complete example `hardwareConfig.json`:
"ledsCanDim" : true,
"ledBrightnessRange" : [ 0, 100 ],
"ledPWMFrequency" : 0,
"statusLEDType": "RGB",
"statusLEDPins" : [ ],
"statusLEDActiveHigh" : false,
"statusRGBPins" : [ ],
"statusRGBActiveHigh" : false,
"getGPIOCommand" : "getGPIO {p}",
"setGPIOCommand" : "setGPIO {p} {s}",
"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
- 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.
- CPU: ARM Cortex-A53 (the CPU on Raspberry Pi 3) or better
- 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.
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
:::{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.
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
:::{warning}

View File

@@ -60,7 +60,7 @@ You can also get the pipeline latency from a pipeline result using the `getLaten
.. code-block:: c++
// Get the pipeline latency.
wpi::units::second_t latency = result.GetLatency();
units::second_t latency = result.GetLatency();
.. 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++
// Get a list of currently tracked targets.
std::span<photonlib::PhotonTrackedTarget> targets = result.GetTargets();
wpi::ArrayRef<photonlib::PhotonTrackedTarget> targets = result.GetTargets();
.. code-block:: python
@@ -166,8 +166,8 @@ You can get the {ref}`best target <docs/reflectiveAndShape/contour-filtering:Con
double pitch = target.GetPitch();
double area = target.GetArea();
double skew = target.GetSkew();
wpi::math::Transform2d pose = target.GetCameraToTarget();
wpi::util::SmallVector<std::pair<double, double>, 4> corners = target.GetCorners();
frc::Transform2d pose = target.GetCameraToTarget();
wpi::SmallVector<std::pair<double, double>, 4> corners = target.GetCorners();
.. code-block:: python
@@ -206,8 +206,8 @@ All of the data above (**except skew**) is available when using AprilTags.
// Get information from target.
int targetID = target.GetFiducialId();
double poseAmbiguity = target.GetPoseAmbiguity();
wpi::math::Transform3d bestCameraToTarget = target.getBestCameraToTarget();
wpi::math::Transform3d alternateCameraToTarget = target.getAlternateCameraToTarget();
frc::Transform3d bestCameraToTarget = target.getBestCameraToTarget();
frc::Transform3d alternateCameraToTarget = target.getAlternateCameraToTarget();
.. 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++
// Calculate robot's field relative pose
wpi::math::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);
frc::Pose2D robotPose = photonlib::EstimateFieldToRobot(
kCameraHeight, kTargetHeight, kCameraPitch, kTargetPitch, frc::Rotation2d(units::degree_t(-target.GetYaw())), frc::Rotation2d(units::degree_t(gyro.GetRotation2d)), targetPose, cameraToRobot);
.. code-block:: python
@@ -106,8 +106,8 @@ You can get a [translation](https://docs.wpilib.org/en/latest/docs/software/adva
.. code-block:: c++
// Calculate a translation from the camera to the target.
wpi::math::Translation2d translation = photonlib::PhotonUtils::EstimateCameraToTargetTranslation(
distance, wpi::math::Rotation2d(wpi::units::degree_t(-target.GetYaw())));
frc::Translation2d translation = photonlib::PhotonUtils::EstimateCameraToTargetTranslation(
distance, frc::Rotation2d(units::degree_t(-target.GetYaw())));
.. 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++
wpi::net::PortForwarder::GetInstance().Add(5800, "photonvision.local", 5800);
wpi::PortForwarder::GetInstance().Add(5800, "photonvision.local", 5800);
.. 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
logging
status-leds
camera-troubleshooting
networking-troubleshooting
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:
```
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

View File

@@ -1,6 +1,6 @@
distributionBase=GRADLE_USER_HOME
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
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME

View File

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

View File

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

View File

@@ -52,14 +52,14 @@ importers:
specifier: ^1.56.1
version: 1.56.1
'@types/node':
specifier: ^24.0.0
version: 24.12.2
specifier: ^22.15.14
version: 22.15.14
'@types/three':
specifier: ^0.178.0
version: 0.178.1
'@vitejs/plugin-vue':
specifier: ^6.0.6
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))
specifier: ^6.0.0
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':
specifier: ^10.2.0
version: 10.2.0(eslint@9.31.0)(prettier@3.6.2)
@@ -85,14 +85,11 @@ importers:
specifier: ^5.8.3
version: 5.8.3
vite:
specifier: ^8.0.10
version: 8.0.10(@types/node@24.12.2)(sass@1.89.2)
specifier: ^8.0.2
version: 8.0.2(@types/node@22.15.14)(sass@1.89.2)
vite-plugin-vuetify:
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)
vue-tsc:
specifier: ^3.2.5
version: 3.2.5(typescript@5.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)
packages:
@@ -120,14 +117,14 @@ packages:
'@dimforge/rapier3d-compat@0.12.0':
resolution: {integrity: sha512-uekIGetywIgopfD97oDL5PfeezkFpNhwlzlaEYNOA0N6ghdsOvh/HYjSMek5Q2O1PYvRSDFcqFVJl4r4ZBwOow==}
'@emnapi/core@1.10.0':
resolution: {integrity: sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==}
'@emnapi/core@1.9.1':
resolution: {integrity: sha512-mukuNALVsoix/w1BJwFzwXBN/dHeejQtuVzcDsfOEsdpCumXb/E9j8w11h5S54tT1xhifGfbbSm/ICrObRb3KA==}
'@emnapi/runtime@1.10.0':
resolution: {integrity: sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==}
'@emnapi/runtime@1.9.1':
resolution: {integrity: sha512-VYi5+ZVLhpgK4hQ0TAjiQiZ6ol0oe4mBx7mVv7IflsiEp0OWoVsp/+f9Vc1hOhE0TtkORVrI1GvzyreqpgWtkA==}
'@emnapi/wasi-threads@1.2.1':
resolution: {integrity: sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==}
'@emnapi/wasi-threads@1.2.0':
resolution: {integrity: sha512-N10dEJNSsUx41Z6pZsXU8FjPjpBEplgH24sfkmITrBED1/U2Esum9F3lfLrMjKHHjmi557zQn7kR9R+XWXu5Rg==}
'@eslint-community/eslint-utils@4.7.0':
resolution: {integrity: sha512-dyybb3AcajC7uha6CvhdVRJqaKyn7w2YKqKyAN37NKYgZT36w+iRb0Dymmc5qEJ549c/S31cMMSFd75bteCpCw==}
@@ -200,11 +197,8 @@ packages:
resolution: {integrity: sha512-JEW4DEtBzfe8HvUYecLU9e6+XJnKDlUAIve8FvPzF3Kzs6Xo/KuZkZJsDH0wJXl/qEZbeeE7edxDNY3kMs39hQ==}
engines: {node: '>= 18'}
'@napi-rs/wasm-runtime@1.1.4':
resolution: {integrity: sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow==}
peerDependencies:
'@emnapi/core': ^1.7.1
'@emnapi/runtime': ^1.7.1
'@napi-rs/wasm-runtime@1.1.1':
resolution: {integrity: sha512-p64ah1M1ld8xjWv3qbvFwHiFVWrq1yFvV4f7w+mzaqiR4IlSgkqhcRdHwsGgomwzBH51sRY4NEowLxnaBjcW/A==}
'@nodelib/fs.scandir@2.1.5':
resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==}
@@ -218,8 +212,8 @@ packages:
resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==}
engines: {node: '>= 8'}
'@oxc-project/types@0.127.0':
resolution: {integrity: sha512-aIYXQBo4lCbO4z0R3FHeucQHpF46l2LbMdxRvqvuRuW2OxdnSkcng5B8+K12spgLDj93rtN3+J2Vac/TIO+ciQ==}
'@oxc-project/types@0.122.0':
resolution: {integrity: sha512-oLAl5kBpV4w69UtFZ9xqcmTi+GENWOcPF7FCrczTiBbmC0ibXxCwyvZGbO39rCVEuLGAZM84DH0pUIyyv/YJzA==}
'@parcel/watcher-android-arm64@2.5.1':
resolution: {integrity: sha512-KF8+j9nNbUN8vzOFDpRMsaKBHZ/mcjEjMToVMJOhTozkDonQFFrRcfdLWn6yWKCmJKmdVxSgHiYvTCef4/qcBA==}
@@ -250,42 +244,36 @@ packages:
engines: {node: '>= 10.0.0'}
cpu: [arm]
os: [linux]
libc: [glibc]
'@parcel/watcher-linux-arm-musl@2.5.1':
resolution: {integrity: sha512-6E+m/Mm1t1yhB8X412stiKFG3XykmgdIOqhjWj+VL8oHkKABfu/gjFj8DvLrYVHSBNC+/u5PeNrujiSQ1zwd1Q==}
engines: {node: '>= 10.0.0'}
cpu: [arm]
os: [linux]
libc: [musl]
'@parcel/watcher-linux-arm64-glibc@2.5.1':
resolution: {integrity: sha512-LrGp+f02yU3BN9A+DGuY3v3bmnFUggAITBGriZHUREfNEzZh/GO06FF5u2kx8x+GBEUYfyTGamol4j3m9ANe8w==}
engines: {node: '>= 10.0.0'}
cpu: [arm64]
os: [linux]
libc: [glibc]
'@parcel/watcher-linux-arm64-musl@2.5.1':
resolution: {integrity: sha512-cFOjABi92pMYRXS7AcQv9/M1YuKRw8SZniCDw0ssQb/noPkRzA+HBDkwmyOJYp5wXcsTrhxO0zq1U11cK9jsFg==}
engines: {node: '>= 10.0.0'}
cpu: [arm64]
os: [linux]
libc: [musl]
'@parcel/watcher-linux-x64-glibc@2.5.1':
resolution: {integrity: sha512-GcESn8NZySmfwlTsIur+49yDqSny2IhPeZfXunQi48DMugKeZ7uy1FX83pO0X22sHntJ4Ub+9k34XQCX+oHt2A==}
engines: {node: '>= 10.0.0'}
cpu: [x64]
os: [linux]
libc: [glibc]
'@parcel/watcher-linux-x64-musl@2.5.1':
resolution: {integrity: sha512-n0E2EQbatQ3bXhcH2D1XIAANAcTZkQICBPVaxMeaCVBtOpBZpWJuf7LwyWPSBDITb7In8mqQgJ7gH8CILCURXg==}
engines: {node: '>= 10.0.0'}
cpu: [x64]
os: [linux]
libc: [musl]
'@parcel/watcher-win32-arm64@2.5.1':
resolution: {integrity: sha512-RFzklRvmc3PkjKjry3hLF9wD7ppR4AKcWNzH7kXR7GUe0Igb3Nz8fyPwtZCSquGrhU5HhUNDr/mKBqj7tqA2Vw==}
@@ -318,106 +306,100 @@ packages:
engines: {node: '>=18'}
hasBin: true
'@rolldown/binding-android-arm64@1.0.0-rc.17':
resolution: {integrity: sha512-s70pVGhw4zqGeFnXWvAzJDlvxhlRollagdCCKRgOsgUOH3N1l0LIxf83AtGzmb5SiVM4Hjl5HyarMRfdfj3DaQ==}
'@rolldown/binding-android-arm64@1.0.0-rc.11':
resolution: {integrity: sha512-SJ+/g+xNnOh6NqYxD0V3uVN4W3VfnrGsC9/hoglicgTNfABFG9JjISvkkU0dNY84MNHLWyOgxP9v9Y9pX4S7+A==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm64]
os: [android]
'@rolldown/binding-darwin-arm64@1.0.0-rc.17':
resolution: {integrity: sha512-4ksWc9n0mhlZpZ9PMZgTGjeOPRu8MB1Z3Tz0Mo02eWfWCHMW1zN82Qz/pL/rC+yQa+8ZnutMF0JjJe7PjwasYw==}
'@rolldown/binding-darwin-arm64@1.0.0-rc.11':
resolution: {integrity: sha512-7WQgR8SfOPwmDZGFkThUvsmd/nwAWv91oCO4I5LS7RKrssPZmOt7jONN0cW17ydGC1n/+puol1IpoieKqQidmg==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm64]
os: [darwin]
'@rolldown/binding-darwin-x64@1.0.0-rc.17':
resolution: {integrity: sha512-SUSDOI6WwUVNcWxd02QEBjLdY1VPHvlEkw6T/8nYG322iYWCTxRb1vzk4E+mWWYehTp7ERibq54LSJGjmouOsw==}
'@rolldown/binding-darwin-x64@1.0.0-rc.11':
resolution: {integrity: sha512-39Ks6UvIHq4rEogIfQBoBRusj0Q0nPVWIvqmwBLaT6aqQGIakHdESBVOPRRLacy4WwUPIx4ZKzfZ9PMW+IeyUQ==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [x64]
os: [darwin]
'@rolldown/binding-freebsd-x64@1.0.0-rc.17':
resolution: {integrity: sha512-hwnz3nw9dbJ05EDO/PvcjaaewqqDy7Y1rn1UO81l8iIK1GjenME75dl16ajbvSSMfv66WXSRCYKIqfgq2KCfxw==}
'@rolldown/binding-freebsd-x64@1.0.0-rc.11':
resolution: {integrity: sha512-jfsm0ZHfhiqrvWjJAmzsqiIFPz5e7mAoCOPBNTcNgkiid/LaFKiq92+0ojH+nmJmKYkre4t71BWXUZDNp7vsag==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [x64]
os: [freebsd]
'@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.17':
resolution: {integrity: sha512-IS+W7epTcwANmFSQFrS1SivEXHtl1JtuQA9wlxrZTcNi6mx+FDOYrakGevvvTwgj2JvWiK8B29/qD9BELZPyXQ==}
'@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.11':
resolution: {integrity: sha512-zjQaUtSyq1nVe3nxmlSCuR96T1LPlpvmJ0SZy0WJFEsV4kFbXcq2u68L4E6O0XeFj4aex9bEauqjW8UQBeAvfQ==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm]
os: [linux]
'@rolldown/binding-linux-arm64-gnu@1.0.0-rc.17':
resolution: {integrity: sha512-e6usGaHKW5BMNZOymS1UcEYGowQMWcgZ71Z17Sl/h2+ZziNJ1a9n3Zvcz6LdRyIW5572wBCTH/Z+bKuZouGk9Q==}
'@rolldown/binding-linux-arm64-gnu@1.0.0-rc.11':
resolution: {integrity: sha512-WMW1yE6IOnehTcFE9eipFkm3XN63zypWlrJQ2iF7NrQ9b2LDRjumFoOGJE8RJJTJCTBAdmLMnJ8uVitACUUo1Q==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm64]
os: [linux]
libc: [glibc]
'@rolldown/binding-linux-arm64-musl@1.0.0-rc.17':
resolution: {integrity: sha512-b/CgbwAJpmrRLp02RPfhbudf5tZnN9nsPWK82znefso832etkem8H7FSZwxrOI9djcdTP7U6YfNhbRnh7djErg==}
'@rolldown/binding-linux-arm64-musl@1.0.0-rc.11':
resolution: {integrity: sha512-jfndI9tsfm4APzjNt6QdBkYwre5lRPUgHeDHoI7ydKUuJvz3lZeCfMsI56BZj+7BYqiKsJm7cfd/6KYV7ubrBg==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm64]
os: [linux]
libc: [musl]
'@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.17':
resolution: {integrity: sha512-4EII1iNGRUN5WwGbF/kOh/EIkoDN9HsupgLQoXfY+D1oyJm7/F4t5PYU5n8SWZgG0FEwakyM8pGgwcBYruGTlA==}
'@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.11':
resolution: {integrity: sha512-ZlFgw46NOAGMgcdvdYwAGu2Q+SLFA9LzbJLW+iyMOJyhj5wk6P3KEE9Gct4xWwSzFoPI7JCdYmYMzVtlgQ+zfw==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [ppc64]
os: [linux]
libc: [glibc]
'@rolldown/binding-linux-s390x-gnu@1.0.0-rc.17':
resolution: {integrity: sha512-AH8oq3XqQo4IibpVXvPeLDI5pzkpYn0WiZAfT05kFzoJ6tQNzwRdDYQ45M8I/gslbodRZwW8uxLhbSBbkv96rA==}
'@rolldown/binding-linux-s390x-gnu@1.0.0-rc.11':
resolution: {integrity: sha512-hIOYmuT6ofM4K04XAZd3OzMySEO4K0/nc9+jmNcxNAxRi6c5UWpqfw3KMFV4MVFWL+jQsSh+bGw2VqmaPMTLyw==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [s390x]
os: [linux]
libc: [glibc]
'@rolldown/binding-linux-x64-gnu@1.0.0-rc.17':
resolution: {integrity: sha512-cLnjV3xfo7KslbU41Z7z8BH/E1y5mzUYzAqih1d1MDaIGZRCMqTijqLv76/P7fyHuvUcfGsIpqCdddbxLLK9rA==}
'@rolldown/binding-linux-x64-gnu@1.0.0-rc.11':
resolution: {integrity: sha512-qXBQQO9OvkjjQPLdUVr7Nr2t3QTZI7s4KZtfw7HzBgjbmAPSFwSv4rmET9lLSgq3rH/ndA3ngv3Qb8l2njoPNA==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [x64]
os: [linux]
libc: [glibc]
'@rolldown/binding-linux-x64-musl@1.0.0-rc.17':
resolution: {integrity: sha512-0phclDw1spsL7dUB37sIARuis2tAgomCJXAHZlpt8PXZ4Ba0dRP1e+66lsRqrfhISeN9bEGNjQs+T/Fbd7oYGw==}
'@rolldown/binding-linux-x64-musl@1.0.0-rc.11':
resolution: {integrity: sha512-/tpFfoSTzUkH9LPY+cYbqZBDyyX62w5fICq9qzsHLL8uTI6BHip3Q9Uzft0wylk/i8OOwKik8OxW+QAhDmzwmg==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [x64]
os: [linux]
libc: [musl]
'@rolldown/binding-openharmony-arm64@1.0.0-rc.17':
resolution: {integrity: sha512-0ag/hEgXOwgw4t8QyQvUCxvEg+V0KBcA6YuOx9g0r02MprutRF5dyljgm3EmR02O292UX7UeS6HzWHAl6KgyhA==}
'@rolldown/binding-openharmony-arm64@1.0.0-rc.11':
resolution: {integrity: sha512-mcp3Rio2w72IvdZG0oQ4bM2c2oumtwHfUfKncUM6zGgz0KgPz4YmDPQfnXEiY5t3+KD/i8HG2rOB/LxdmieK2g==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm64]
os: [openharmony]
'@rolldown/binding-wasm32-wasi@1.0.0-rc.17':
resolution: {integrity: sha512-LEXei6vo0E5wTGwpkJ4KoT3OZJRnglwldt5ziLzOlc6qqb55z4tWNq2A+PFqCJuvWWdP53CVhG1Z9NtToDPJrA==}
engines: {node: ^20.19.0 || >=22.12.0}
'@rolldown/binding-wasm32-wasi@1.0.0-rc.11':
resolution: {integrity: sha512-LXk5Hii1Ph9asuGRjBuz8TUxdc1lWzB7nyfdoRgI0WGPZKmCxvlKk8KfYysqtr4MfGElu/f/pEQRh8fcEgkrWw==}
engines: {node: '>=14.0.0'}
cpu: [wasm32]
'@rolldown/binding-win32-arm64-msvc@1.0.0-rc.17':
resolution: {integrity: sha512-gUmyzBl3SPMa6hrqFUth9sVfcLBlYsbMzBx5PlexMroZStgzGqlZ26pYG89rBb45Mnia+oil6YAIFeEWGWhoZA==}
'@rolldown/binding-win32-arm64-msvc@1.0.0-rc.11':
resolution: {integrity: sha512-dDwf5otnx0XgRY1yqxOC4ITizcdzS/8cQ3goOWv3jFAo4F+xQYni+hnMuO6+LssHHdJW7+OCVL3CoU4ycnh35Q==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm64]
os: [win32]
'@rolldown/binding-win32-x64-msvc@1.0.0-rc.17':
resolution: {integrity: sha512-3hkiolcUAvPB9FLb3UZdfjVVNWherN1f/skkGWJP/fgSQhYUZpSIRr0/I8ZK9TkF3F7kxvJAk0+IcKvPHk9qQg==}
'@rolldown/binding-win32-x64-msvc@1.0.0-rc.11':
resolution: {integrity: sha512-LN4/skhSggybX71ews7dAj6r2geaMJfm3kMbK2KhFMg9B10AZXnKoLCVVgzhMHL0S+aKtr4p8QbAW8k+w95bAA==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [x64]
os: [win32]
'@rolldown/pluginutils@1.0.0-rc.13':
resolution: {integrity: sha512-3ngTAv6F/Py35BsYbeeLeecvhMKdsKm4AoOETVhAA+Qc8nrA2I0kF7oa93mE9qnIurngOSpMnQ0x2nQY2FPviA==}
'@rolldown/pluginutils@1.0.0-beta.19':
resolution: {integrity: sha512-3FL3mnMbPu0muGOCaKAhhFEYmqv9eTfPSJRJmANrCwtgK8VuxpsZDGK+m0LYAGoyO8+0j5uRe4PeyPDK1yA/hA==}
'@rolldown/pluginutils@1.0.0-rc.17':
resolution: {integrity: sha512-n8iosDOt6Ig1UhJ2AYqoIhHWh/isz0xpicHTzpKBeotdVsTEcxsSA/i3EVM7gQAj0rU27OLAxCjzlj15IWY7bg==}
'@rolldown/pluginutils@1.0.0-rc.11':
resolution: {integrity: sha512-xQO9vbwBecJRv9EUcQ/y0dzSTJgA7Q6UVN7xp6B81+tBGSLVAK03yJ9NkJaUA7JFD91kbjxRSC/mDnmvXzbHoQ==}
'@tweenjs/tween.js@23.1.3':
resolution: {integrity: sha512-vJmvvwFxYuGnF2axRtPYocag6Clbb5YS7kLL+SO/TeVFzHqDIWrNKYtcsPMibjDx9O+bu+psAy9NKfWklassUA==}
@@ -431,8 +413,8 @@ packages:
'@types/json-schema@7.0.15':
resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==}
'@types/node@24.12.2':
resolution: {integrity: sha512-A1sre26ke7HDIuY/M23nd9gfB+nrmhtYyMINbjI1zHJxYteKR6qSMX56FsmjMcDb3SMcjJg5BiRRgOCC/yBD0g==}
'@types/node@22.15.14':
resolution: {integrity: sha512-BL1eyu/XWsFGTtDWOYULQEs4KR0qdtYfCxYAUYRoB7JP7h9ETYLgQTww6kH8Sj2C0pFGgrpM0XKv6/kbIzYJ1g==}
'@types/raf@3.4.3':
resolution: {integrity: sha512-c4YAvMedbPZ5tEyxzQdMoOhhJ4RD3rngZIdwC2/qDN3d7JpEhB6fiBRKVY1lg5B7Wk+uPBjn5f39j1/2MY1oOw==}
@@ -496,22 +478,13 @@ packages:
resolution: {integrity: sha512-1rYQTCLFFzOI5Nl0c8LUpJT8HxpwVRn9E4CkMsYfuN6ctmQqExjSTzzSk0Tz2apmXy7WU6/6fyaZVVA/thPN+w==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
'@vitejs/plugin-vue@6.0.6':
resolution: {integrity: sha512-u9HHgfrq3AjXlysn0eINFnWQOJQLO9WN6VprZ8FXl7A2bYisv3Hui9Ij+7QZ41F/WYWarHjwBbXtD7dKg3uxbg==}
'@vitejs/plugin-vue@6.0.0':
resolution: {integrity: sha512-iAliE72WsdhjzTOp2DtvKThq1VBC4REhwRcaA+zPAAph6I+OQhUXv+Xu2KS7ElxYtb7Zc/3R30Hwv1DxEo7NXQ==}
engines: {node: ^20.19.0 || >=22.12.0}
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
'@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':
resolution: {integrity: sha512-oOdAkwqUfW1WqpwSYJce06wvt6HljgY3fGeM9NcVA1HaYOij3mZG9Rkysn0OHuyUAGMbEbARIpsG+LPVlBJ5/Q==}
@@ -553,9 +526,6 @@ packages:
typescript:
optional: true
'@vue/language-core@3.2.5':
resolution: {integrity: sha512-d3OIxN/+KRedeM5wQ6H6NIpwS3P5gC9nmyaHgBk+rO6dIsjY+tOh4UlPpiZbAh3YtLdCGEX4M16RmsBqPmJV+g==}
'@vue/reactivity@3.5.13':
resolution: {integrity: sha512-NaCwtw8o48B9I6L1zl2p41OHo/2Z4wqYGGIK1Khu5T7yxrn+ATOixn/Udn2m+6kZKB/J7cuT9DbWWhRxqixACg==}
@@ -611,9 +581,6 @@ packages:
ajv@6.12.6:
resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==}
alien-signals@3.1.2:
resolution: {integrity: sha512-d9dYqZTS90WLiU0I5c6DHj/HcKkF8ZyGN3G5x8wSbslulz70KOxaqCT0hQCo9KOyhVqzqGojvNdJXoTumZOtcw==}
ansi-styles@4.3.0:
resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==}
engines: {node: '>=8'}
@@ -1084,28 +1051,24 @@ packages:
engines: {node: '>= 12.0.0'}
cpu: [arm64]
os: [linux]
libc: [glibc]
lightningcss-linux-arm64-musl@1.32.0:
resolution: {integrity: sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==}
engines: {node: '>= 12.0.0'}
cpu: [arm64]
os: [linux]
libc: [musl]
lightningcss-linux-x64-gnu@1.32.0:
resolution: {integrity: sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==}
engines: {node: '>= 12.0.0'}
cpu: [x64]
os: [linux]
libc: [glibc]
lightningcss-linux-x64-musl@1.32.0:
resolution: {integrity: sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==}
engines: {node: '>= 12.0.0'}
cpu: [x64]
os: [linux]
libc: [musl]
lightningcss-win32-arm64-msvc@1.32.0:
resolution: {integrity: sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==}
@@ -1172,9 +1135,6 @@ packages:
ms@2.1.3:
resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
muggle-string@0.4.1:
resolution: {integrity: sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ==}
nanoid@3.3.11:
resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==}
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
@@ -1205,9 +1165,6 @@ packages:
resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==}
engines: {node: '>=6'}
path-browserify@1.0.1:
resolution: {integrity: sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==}
path-exists@4.0.0:
resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==}
engines: {node: '>=8'}
@@ -1256,14 +1213,14 @@ packages:
resolution: {integrity: sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==}
engines: {node: '>=4'}
postcss@8.5.13:
resolution: {integrity: sha512-qif0+jGGZoLWdHey3UFHHWP0H7Gbmsk8T5VEqyYFbWqPr1XqvLGBbk/sl8V5exGmcYJklJOhOQq1pV9IcsiFag==}
engines: {node: ^10 || ^12 || >=14}
postcss@8.5.3:
resolution: {integrity: sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A==}
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:
resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==}
engines: {node: '>= 0.8.0'}
@@ -1312,8 +1269,8 @@ packages:
resolution: {integrity: sha512-9aZLIrhRaD97sgVhtJOW6ckOEh6/GnvQtdVNfdZ6s67+3/XwLS9lBcQYzEEhYVeUowN7pRzMLsyGhK2i/xvWbw==}
engines: {node: '>= 0.8.15'}
rolldown@1.0.0-rc.17:
resolution: {integrity: sha512-ZrT53oAKrtA4+YtBWPQbtPOxIbVDbxT0orcYERKd63VJTF13zPcgXTvD4843L8pcsI7M6MErt8QtON6lrB9tyA==}
rolldown@1.0.0-rc.11:
resolution: {integrity: sha512-NRjoKMusSjfRbSYiH3VSumlkgFe7kYAa3pzVOsVYVFY3zb5d7nS+a3KGQ7hJKXuYWbzJKPVQ9Wxq2UvyK+ENpw==}
engines: {node: ^20.19.0 || >=22.12.0}
hasBin: true
@@ -1376,8 +1333,8 @@ packages:
three@0.178.0:
resolution: {integrity: sha512-ybFIB0+x8mz0wnZgSGy2MO/WCO6xZhQSZnmfytSPyNpM0sBafGRVhdaj+erYh5U+RhQOAg/eXqw5uVDiM2BjhQ==}
tinyglobby@0.2.16:
resolution: {integrity: sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==}
tinyglobby@0.2.15:
resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==}
engines: {node: '>=12.0.0'}
to-regex-range@5.0.1:
@@ -1412,8 +1369,8 @@ packages:
engines: {node: '>=14.17'}
hasBin: true
undici-types@7.16.0:
resolution: {integrity: sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==}
undici-types@6.21.0:
resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==}
upath@2.0.1:
resolution: {integrity: sha512-1uEe95xksV1O0CYKXo8vQvN1JEbtJp7lb7C5U9HMsIp6IVwntkH/oNUzyVNQSd4S1sYk2FpSSW44FqMc8qee5w==}
@@ -1436,14 +1393,14 @@ packages:
vue: ^3.0.0
vuetify: ^3.0.0
vite@8.0.10:
resolution: {integrity: sha512-rZuUu9j6J5uotLDs+cAA4O5H4K1SfPliUlQwqa6YEwSrWDZzP4rhm00oJR5snMewjxF5V/K3D4kctsUTsIU9Mw==}
vite@8.0.2:
resolution: {integrity: sha512-1gFhNi+bHhRE/qKZOJXACm6tX4bA3Isy9KuKF15AgSRuRazNBOJfdDemPBU16/mpMxApDPrWvZ08DcLPEoRnuA==}
engines: {node: ^20.19.0 || >=22.12.0}
hasBin: true
peerDependencies:
'@types/node': ^20.19.0 || >=22.12.0
'@vitejs/devtools': ^0.1.0
esbuild: ^0.27.0 || ^0.28.0
esbuild: ^0.27.0
jiti: '>=1.21.0'
less: ^4.0.0
sass: ^1.70.0
@@ -1479,9 +1436,6 @@ packages:
yaml:
optional: true
vscode-uri@3.1.0:
resolution: {integrity: sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ==}
vue-eslint-parser@10.1.3:
resolution: {integrity: sha512-dbCBnd2e02dYWsXoqX5yKUZlOt+ExIpq7hmHKPb5ZqKcjf++Eo0hMseFTZMLKThrUk61m+Uv6A2YSBve6ZvuDQ==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
@@ -1493,12 +1447,6 @@ packages:
peerDependencies:
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:
resolution: {integrity: sha512-G4KxITUOy9D4ro15zOp40D6ogmMefzjIyMsBKqN3xGbV1P6dlKYMx+BBXCKm3Nr/6iipcUKM272Sh2AJRyWMyQ==}
peerDependencies:
@@ -1567,18 +1515,18 @@ snapshots:
'@dimforge/rapier3d-compat@0.12.0': {}
'@emnapi/core@1.10.0':
'@emnapi/core@1.9.1':
dependencies:
'@emnapi/wasi-threads': 1.2.1
'@emnapi/wasi-threads': 1.2.0
tslib: 2.8.1
optional: true
'@emnapi/runtime@1.10.0':
'@emnapi/runtime@1.9.1':
dependencies:
tslib: 2.8.1
optional: true
'@emnapi/wasi-threads@1.2.1':
'@emnapi/wasi-threads@1.2.0':
dependencies:
tslib: 2.8.1
optional: true
@@ -1648,10 +1596,10 @@ snapshots:
'@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:
'@emnapi/core': 1.10.0
'@emnapi/runtime': 1.10.0
'@emnapi/core': 1.9.1
'@emnapi/runtime': 1.9.1
'@tybys/wasm-util': 0.10.1
optional: true
@@ -1667,7 +1615,7 @@ snapshots:
'@nodelib/fs.scandir': 2.1.5
fastq: 1.19.1
'@oxc-project/types@0.127.0': {}
'@oxc-project/types@0.122.0': {}
'@parcel/watcher-android-arm64@2.5.1':
optional: true
@@ -1736,58 +1684,56 @@ snapshots:
dependencies:
playwright: 1.56.1
'@rolldown/binding-android-arm64@1.0.0-rc.17':
'@rolldown/binding-android-arm64@1.0.0-rc.11':
optional: true
'@rolldown/binding-darwin-arm64@1.0.0-rc.17':
'@rolldown/binding-darwin-arm64@1.0.0-rc.11':
optional: true
'@rolldown/binding-darwin-x64@1.0.0-rc.17':
'@rolldown/binding-darwin-x64@1.0.0-rc.11':
optional: true
'@rolldown/binding-freebsd-x64@1.0.0-rc.17':
'@rolldown/binding-freebsd-x64@1.0.0-rc.11':
optional: true
'@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.17':
'@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.11':
optional: true
'@rolldown/binding-linux-arm64-gnu@1.0.0-rc.17':
'@rolldown/binding-linux-arm64-gnu@1.0.0-rc.11':
optional: true
'@rolldown/binding-linux-arm64-musl@1.0.0-rc.17':
'@rolldown/binding-linux-arm64-musl@1.0.0-rc.11':
optional: true
'@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.17':
'@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.11':
optional: true
'@rolldown/binding-linux-s390x-gnu@1.0.0-rc.17':
'@rolldown/binding-linux-s390x-gnu@1.0.0-rc.11':
optional: true
'@rolldown/binding-linux-x64-gnu@1.0.0-rc.17':
'@rolldown/binding-linux-x64-gnu@1.0.0-rc.11':
optional: true
'@rolldown/binding-linux-x64-musl@1.0.0-rc.17':
'@rolldown/binding-linux-x64-musl@1.0.0-rc.11':
optional: true
'@rolldown/binding-openharmony-arm64@1.0.0-rc.17':
'@rolldown/binding-openharmony-arm64@1.0.0-rc.11':
optional: true
'@rolldown/binding-wasm32-wasi@1.0.0-rc.17':
'@rolldown/binding-wasm32-wasi@1.0.0-rc.11':
dependencies:
'@emnapi/core': 1.10.0
'@emnapi/runtime': 1.10.0
'@napi-rs/wasm-runtime': 1.1.4(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)
'@napi-rs/wasm-runtime': 1.1.1
optional: true
'@rolldown/binding-win32-arm64-msvc@1.0.0-rc.17':
'@rolldown/binding-win32-arm64-msvc@1.0.0-rc.11':
optional: true
'@rolldown/binding-win32-x64-msvc@1.0.0-rc.17':
'@rolldown/binding-win32-x64-msvc@1.0.0-rc.11':
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': {}
@@ -1800,9 +1746,9 @@ snapshots:
'@types/json-schema@7.0.15': {}
'@types/node@24.12.2':
'@types/node@22.15.14':
dependencies:
undici-types: 7.16.0
undici-types: 6.21.0
'@types/raf@3.4.3':
optional: true
@@ -1901,24 +1847,12 @@ snapshots:
'@typescript-eslint/types': 8.32.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:
'@rolldown/pluginutils': 1.0.0-rc.13
vite: 8.0.10(@types/node@24.12.2)(sass@1.89.2)
'@rolldown/pluginutils': 1.0.0-beta.19
vite: 8.0.2(@types/node@22.15.14)(sass@1.89.2)
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':
dependencies:
'@babel/parser': 7.27.2
@@ -1991,16 +1925,6 @@ snapshots:
transitivePeerDependencies:
- 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':
dependencies:
'@vue/shared': 3.5.13
@@ -2057,8 +1981,6 @@ snapshots:
json-schema-traverse: 0.4.1
uri-js: 4.4.1
alien-signals@3.1.2: {}
ansi-styles@4.3.0:
dependencies:
color-convert: 2.0.1
@@ -2579,8 +2501,6 @@ snapshots:
ms@2.1.3: {}
muggle-string@0.4.1: {}
nanoid@3.3.11: {}
natural-compare@1.4.0: {}
@@ -2613,8 +2533,6 @@ snapshots:
dependencies:
callsites: 3.1.0
path-browserify@1.0.1: {}
path-exists@4.0.0: {}
path-key@3.1.1: {}
@@ -2650,13 +2568,13 @@ snapshots:
cssesc: 3.0.0
util-deprecate: 1.0.2
postcss@8.5.13:
postcss@8.5.3:
dependencies:
nanoid: 3.3.11
picocolors: 1.1.1
source-map-js: 1.2.1
postcss@8.5.3:
postcss@8.5.8:
dependencies:
nanoid: 3.3.11
picocolors: 1.1.1
@@ -2695,26 +2613,26 @@ snapshots:
rgbcolor@1.0.1:
optional: true
rolldown@1.0.0-rc.17:
rolldown@1.0.0-rc.11:
dependencies:
'@oxc-project/types': 0.127.0
'@rolldown/pluginutils': 1.0.0-rc.17
'@oxc-project/types': 0.122.0
'@rolldown/pluginutils': 1.0.0-rc.11
optionalDependencies:
'@rolldown/binding-android-arm64': 1.0.0-rc.17
'@rolldown/binding-darwin-arm64': 1.0.0-rc.17
'@rolldown/binding-darwin-x64': 1.0.0-rc.17
'@rolldown/binding-freebsd-x64': 1.0.0-rc.17
'@rolldown/binding-linux-arm-gnueabihf': 1.0.0-rc.17
'@rolldown/binding-linux-arm64-gnu': 1.0.0-rc.17
'@rolldown/binding-linux-arm64-musl': 1.0.0-rc.17
'@rolldown/binding-linux-ppc64-gnu': 1.0.0-rc.17
'@rolldown/binding-linux-s390x-gnu': 1.0.0-rc.17
'@rolldown/binding-linux-x64-gnu': 1.0.0-rc.17
'@rolldown/binding-linux-x64-musl': 1.0.0-rc.17
'@rolldown/binding-openharmony-arm64': 1.0.0-rc.17
'@rolldown/binding-wasm32-wasi': 1.0.0-rc.17
'@rolldown/binding-win32-arm64-msvc': 1.0.0-rc.17
'@rolldown/binding-win32-x64-msvc': 1.0.0-rc.17
'@rolldown/binding-android-arm64': 1.0.0-rc.11
'@rolldown/binding-darwin-arm64': 1.0.0-rc.11
'@rolldown/binding-darwin-x64': 1.0.0-rc.11
'@rolldown/binding-freebsd-x64': 1.0.0-rc.11
'@rolldown/binding-linux-arm-gnueabihf': 1.0.0-rc.11
'@rolldown/binding-linux-arm64-gnu': 1.0.0-rc.11
'@rolldown/binding-linux-arm64-musl': 1.0.0-rc.11
'@rolldown/binding-linux-ppc64-gnu': 1.0.0-rc.11
'@rolldown/binding-linux-s390x-gnu': 1.0.0-rc.11
'@rolldown/binding-linux-x64-gnu': 1.0.0-rc.11
'@rolldown/binding-linux-x64-musl': 1.0.0-rc.11
'@rolldown/binding-openharmony-arm64': 1.0.0-rc.11
'@rolldown/binding-wasm32-wasi': 1.0.0-rc.11
'@rolldown/binding-win32-arm64-msvc': 1.0.0-rc.11
'@rolldown/binding-win32-x64-msvc': 1.0.0-rc.11
run-parallel@1.2.0:
dependencies:
@@ -2768,7 +2686,7 @@ snapshots:
three@0.178.0: {}
tinyglobby@0.2.16:
tinyglobby@0.2.15:
dependencies:
fdir: 6.5.0(picomatch@4.0.4)
picomatch: 4.0.4
@@ -2801,7 +2719,7 @@ snapshots:
typescript@5.8.3: {}
undici-types@7.16.0: {}
undici-types@6.21.0: {}
upath@2.0.1: {}
@@ -2816,31 +2734,29 @@ snapshots:
base64-arraybuffer: 1.0.2
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:
'@vuetify/loader-shared': 2.1.0(vue@3.5.13(typescript@5.8.3))(vuetify@3.8.3)
debug: 4.4.0
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)
vuetify: 3.8.3(typescript@5.8.3)(vite-plugin-vuetify@2.1.1)(vue@3.5.13(typescript@5.8.3))
transitivePeerDependencies:
- 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:
lightningcss: 1.32.0
picomatch: 4.0.4
postcss: 8.5.13
rolldown: 1.0.0-rc.17
tinyglobby: 0.2.16
postcss: 8.5.8
rolldown: 1.0.0-rc.11
tinyglobby: 0.2.15
optionalDependencies:
'@types/node': 24.12.2
'@types/node': 22.15.14
fsevents: 2.3.3
sass: 1.89.2
vscode-uri@3.1.0: {}
vue-eslint-parser@10.1.3(eslint@9.31.0):
dependencies:
debug: 4.4.0
@@ -2859,12 +2775,6 @@ snapshots:
'@vue/devtools-api': 6.6.4
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)):
dependencies:
vue: 3.5.13(typescript@5.8.3)
@@ -2884,7 +2794,7 @@ snapshots:
vue: 3.5.13(typescript@5.8.3)
optionalDependencies:
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:
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";
const is_demo = import.meta.env.MODE === "demo";
const backendHost = inject<string>("backendHost");
if (!is_demo) {
const websocket = new AutoReconnectingWebsocket(
`ws://${backendHost}/websocket_data`,
`ws://${inject("backendHost")}/websocket_data`,
() => {
useStateStore().$patch({ backendConnected: true });
},
@@ -76,6 +75,36 @@ onBeforeMount(() => {
<photon-log-view />
<photon-error-snackbar />
</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>
<style lang="scss">
@@ -118,4 +147,33 @@ onBeforeMount(() => {
div.v-layout {
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>

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

View File

@@ -1,13 +1,5 @@
<script setup lang="ts">
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 {
AmbientLight,
AxesHelper,
@@ -24,7 +16,7 @@ const {
SphereGeometry,
WebGLRenderer
} = 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 axios from "axios";
import { useCameraSettingsStore } from "@/stores/settings/CameraSettingsStore";
@@ -39,12 +31,12 @@ const props = defineProps<{
title: string;
}>();
let scene: SceneType | undefined;
let camera: PerspectiveCameraType | undefined;
let renderer: WebGLRendererType | undefined;
let controls: TrackballControlsType | undefined;
let scene: Scene | undefined;
let camera: PerspectiveCamera | undefined;
let renderer: WebGLRenderer | undefined;
let controls: TrackballControls | undefined;
const createChessboard = (obs: BoardObservation, cal: CameraCalibrationResult): GroupType => {
const createChessboard = (obs: BoardObservation, cal: CameraCalibrationResult): Group => {
const group = new Group();
if (obs.locationInImageSpace.length === 0) return group;
@@ -202,6 +194,9 @@ const resetCamThirdPerson = () => {
let animationFrameId: number | null = null;
onMounted(async () => {
// Grab data first off
fetchCalibrationData();
scene = new Scene();
camera = new PerspectiveCamera(75, 800 / 800, 0.1, 1000);
@@ -261,10 +256,6 @@ onMounted(async () => {
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 = () => {
if (!scene || !camera || !renderer || !controls) {
return;
@@ -327,7 +318,7 @@ if (import.meta.hot) {
}
watchEffect(() => {
void drawCalibration(calibrationData.value);
drawCalibration(calibrationData.value);
});
watch(
@@ -337,9 +328,9 @@ watch(
props.resolution.height,
useCameraSettingsStore().getCalibrationCoeffs(props.resolution)
],
async () => {
() => {
console.log("Camera or resolution changed, refetching calibration");
await fetchCalibrationData();
fetchCalibrationData();
}
);
</script>

View File

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

View File

@@ -1,5 +1,5 @@
<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 { useStateStore } from "@/stores/StateStore";
import LogEntry from "@/components/app/photon-log-entry.vue";
@@ -10,10 +10,10 @@ const backendHost = inject<string>("backendHost");
const searchQuery = ref("");
const timeInput = ref<string>();
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 exportLogFile = useTemplateRef("exportLogFile");
const selectedLogLevels = ref<Record<number, boolean>>({
const exportLogFile = ref();
const selectedLogLevels = ref({
[LogLevel.ERROR]: true,
[LogLevel.WARN]: true,
[LogLevel.INFO]: true,
@@ -48,7 +48,7 @@ watch(logs, () => {
);
autoScroll.value = bottomOffset < 50;
if (autoScroll.value) logList.value?.scrollToBottom();
if (autoScroll.value) logList.value.scrollToBottom();
});
const getLogLevelFromIndex = (index: number): string => {
@@ -56,7 +56,7 @@ const getLogLevelFromIndex = (index: number): string => {
};
const handleLogExport = () => {
exportLogFile.value?.click();
exportLogFile.value.click();
};
const handleLogClear = () => {

View File

@@ -20,7 +20,6 @@ const PromptRegular = import("@/assets/fonts/PromptRegular");
const jspdf = import("jspdf");
const theme = useTheme();
const MM_PER_INCH = 25.4;
const settingsValid = ref(true);
@@ -39,11 +38,6 @@ const getUniqueVideoFormatsByResolution = (): VideoFormat[] => {
if (!skip) {
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) {
// Mean overall reprojection error
// Calculated as average of each observation's mean error
@@ -66,10 +60,7 @@ const getUniqueVideoFormatsByResolution = (): VideoFormat[] => {
) *
(180 / Math.PI);
}
if (resArea >= minPixelCount) {
uniqueResolutions.push(format);
}
uniqueResolutions.push(format);
}
});
uniqueResolutions.sort(
@@ -90,26 +81,18 @@ 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.
// This avoids trying to index into an array that may be empty during page reload.
watchEffect(() => {
const currentIndex = useCameraSettingsStore().currentVideoFormat.index ?? 0;
useStateStore().calibrationData.videoFormatIndex = currentIndex;
const names = useCameraSettingsStore().currentCameraSettings.validVideoFormats.map((f) =>
getResolutionString(f.resolution)
);
const currentFormatIndex = useCameraSettingsStore().currentVideoFormat.index ?? 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;
uniqueVideoResolutionString.value = names[currentIndex] ?? names[0] ?? "";
});
const dimensionUnit = ref<"in" | "mm">("in");
const squareSizeIn = ref(1);
const markerSizeIn = ref(0.75);
const patternWidth = ref(8);
@@ -119,28 +102,6 @@ const useOldPattern = ref(false);
const tagFamily = ref<CalibrationTagFamilies>(CalibrationTagFamilies.Dict_4X4_1000);
const requestedVideoFormatIndex = ref(0);
const convertInchesToDisplay = (valueInInches: number) =>
dimensionUnit.value === "mm" ? valueInInches * MM_PER_INCH : valueInInches;
const convertDisplayToInches = (displayValue: number) =>
dimensionUnit.value === "mm" ? displayValue / MM_PER_INCH : displayValue;
const squareSize = computed({
get: () => convertInchesToDisplay(squareSizeIn.value),
set(value) {
squareSizeIn.value = convertDisplayToInches(value);
}
});
const markerSize = computed({
get: () => convertInchesToDisplay(markerSizeIn.value),
set(value) {
markerSizeIn.value = convertDisplayToInches(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
const tooManyPoints = computed(
() => useStateStore().calibrationData.imageCount * patternWidth.value * patternHeight.value > 700000
@@ -215,7 +176,7 @@ const downloadCalibBoard = async () => {
};
const isCalibrating = computed(
() => useCameraSettingsStore().currentCameraSettings.currentPipelineIndex === WebsocketPipelineType.Calib3d.valueOf()
() => useCameraSettingsStore().currentCameraSettings.currentPipelineIndex === WebsocketPipelineType.Calib3d
);
const startCalibration = () => {
@@ -242,7 +203,7 @@ const endCalibration = () => {
calibSuccess.value = undefined;
calibEndpointFail.value = false;
if (!hasEnoughImages.value) {
if (!useStateStore().calibrationData.hasEnoughImages) {
calibCanceled.value = true;
}
@@ -270,10 +231,6 @@ const endCalibration = () => {
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 selectedVideoFormat = ref<VideoFormat | undefined>(undefined);
const setSelectedVideoFormat = (format: VideoFormat) => {
@@ -338,23 +295,23 @@ const setSelectedVideoFormat = (format: VideoFormat) => {
>
<v-form v-model="settingsValid">
<pv-select
v-model="uniqueVideoResolutionIndex"
v-model="uniqueVideoResolutionString"
label="Resolution"
:select-cols="8"
:disabled="isCalibrating"
tooltip="Resolution to calibrate at (you will have to calibrate every resolution you use 3D mode on)"
:items="getUniqueVideoResolutionStrings()"
@update:model-value="(value) => (useStateStore().calibrationData.videoFormatIndex = value)"
@update:model-value="
useStateStore().calibrationData.videoFormatIndex =
getUniqueVideoResolutionStrings().find((v) => v.value === $event)?.value || 0
"
/>
<pv-select
v-model="boardType"
label="Board Type"
tooltip="Calibration board pattern to use"
:select-cols="8"
:items="[
{ value: CalibrationBoardTypes.Charuco, name: 'ChArUco' },
{ value: CalibrationBoardTypes.Chessboard, name: 'Chessboard' }
]"
:items="['Chessboard', 'ChArUco']"
:disabled="isCalibrating"
/>
<v-alert
@@ -384,43 +341,25 @@ const setSelectedVideoFormat = (format: VideoFormat) => {
label="Tag Family"
tooltip="Dictionary of ArUco markers on the ChArUco board"
:select-cols="8"
:items="[
{ 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' }
]"
:items="['Dict_4X4_1000', 'Dict_5X5_1000', 'Dict_6X6_1000', 'Dict_7X7_1000']"
:disabled="isCalibrating"
/>
<pv-number-input
v-model="squareSize"
:label="`Pattern Spacing (${dimensionUnit})`"
:tooltip="`Spacing between pattern features in ${dimensionUnit === 'mm' ? 'millimeters' : 'inches'}`"
v-model="squareSizeIn"
label="Pattern Spacing (in)"
tooltip="Spacing between pattern features in inches"
:disabled="isCalibrating"
:rules="[(v) => v > 0 || 'Size must be positive']"
:label-cols="4"
:step="dimensionStep"
/>
<pv-number-input
v-if="boardType === CalibrationBoardTypes.Charuco"
v-model="markerSize"
:label="`Marker Size (${dimensionUnit})`"
:tooltip="`Size of the tag markers in ${dimensionUnit === 'mm' ? 'millimeters' : 'inches'}; must be smaller than pattern spacing`"
v-model="markerSizeIn"
label="Marker Size (in)"
tooltip="Size of the tag markers in inches must be smaller than pattern spacing"
:disabled="isCalibrating"
:rules="[(v) => v > 0 || 'Size must be positive']"
:label-cols="4"
:step="dimensionStep"
/>
<pv-number-input
v-model="patternWidth"
@@ -544,22 +483,11 @@ const setSelectedVideoFormat = (format: VideoFormat) => {
<v-chip
:variant="theme.global.current.value.dark ? 'tonal' : 'elevated'"
label
:color="hasEnoughImages ? 'buttonPassive' : 'light-grey'"
:color="useStateStore().calibrationData.hasEnoughImages ? 'buttonPassive' : 'light-grey'"
>
Snapshots: {{ useStateStore().calibrationData.imageCount }} of at least
{{ minCount }}
{{ useStateStore().calibrationData.minimumImageCount }}
</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>
<v-btn
@@ -604,14 +532,16 @@ const setSelectedVideoFormat = (format: VideoFormat) => {
size="small"
block
:variant="theme.global.current.value.dark ? 'outlined' : 'elevated'"
:color="hasEnoughImages ? 'buttonActive' : 'error'"
:color="useStateStore().calibrationData.hasEnoughImages ? 'buttonActive' : 'error'"
:disabled="!isCalibrating || !settingsValid"
@click="endCalibration"
>
<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>
<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-col>
</div>

View File

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

View File

@@ -53,7 +53,7 @@ const fetchSnapshots = () => {
.get("/utils/getImageSnapshots")
.then((response) => {
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);
return {
@@ -99,7 +99,7 @@ const expanded = ref([]);
<v-card-text class="pt-0">
<v-btn
color="buttonPassive"
:variant="theme.global.current.value.dark ? 'outlined' : 'elevated'"
:variant="theme.global.current.value.dark ? 'outlined' : 'tonal'"
@click="fetchSnapshots"
>
<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 { useTheme } from "vuetify";
import { axiosPost } from "@/lib/PhotonUtils";
import { WebsocketPipelineType } from "@/types/WebsocketDataTypes";
const theme = useTheme();
@@ -21,7 +20,7 @@ const focusMode = computed<boolean>({
get: () => useCameraSettingsStore().isFocusMode,
set: (v) =>
useCameraSettingsStore().changeCurrentPipelineIndex(
v ? WebsocketPipelineType.FocusCamera : useCameraSettingsStore().currentCameraSettings.lastPipelineIndex || 0,
v ? -3 : useCameraSettingsStore().currentCameraSettings.lastPipelineIndex || 0,
true
)
});
@@ -66,8 +65,8 @@ const settingsHaveChanged = (): boolean => {
const a = tempSettingsStruct.value;
const b = useCameraSettingsStore().currentCameraSettings;
for (const quirk of Object.values(ValidQuirks)) {
if (a.quirksToChange[quirk] !== b.cameraQuirks.quirks[quirk]) return true;
for (const q in ValidQuirks) {
if (a.quirksToChange[q] !== b.cameraQuirks.quirks[q]) return true;
}
return a.fov !== b.fov.value;
@@ -121,12 +120,12 @@ watchEffect(() => {
});
const showDeleteCamera = ref(false);
const deleteThisCamera = async () => {
await axiosPost("/utils/nukeOneCamera", "delete this camera", {
const deleteThisCamera = () => {
axiosPost("/utils/nukeOneCamera", "delete this camera", {
cameraUniqueName: useStateStore().currentCameraUniqueName
});
};
const wrappedCameras = computed<SelectItem<string>[]>(() =>
const wrappedCameras = computed<SelectItem[]>(() =>
Object.keys(useCameraSettingsStore().cameras).map((cameraUniqueName) => ({
name: useCameraSettingsStore().cameras[cameraUniqueName].nickname,
value: cameraUniqueName
@@ -160,7 +159,7 @@ const wrappedCameras = computed<SelectItem<string>[]>(() =>
v-model="arducamSelectWrapper"
label="Arducam Model"
:items="[
{ name: 'None', value: 0 },
{ name: 'None', value: 0, disabled: true },
{ name: 'OV9281', value: 1 },
{ name: 'OV2311', value: 2 },
{ name: 'OV9782', value: 3 }

View File

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

View File

@@ -1,51 +1,69 @@
<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>
<template>
<div>
<v-table density="compact" :style="{ backgroundColor: 'var(--v-primary-base)' }">
<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>{{ camera.dev }}</td>
<td>{{ cameraInfoFor(camera).dev }}</td>
</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>{{ camera.name }}</td>
<td>{{ cameraInfoFor(camera).name }}</td>
</tr>
<tr>
<td>Type:</td>
<td v-if="camera.type === '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.type === 'PVFileCameraInfo'" class="mb-3">File Camera</td>
<td v-if="camera.PVUsbCameraInfo" class="mb-3">USB Camera</td>
<td v-else-if="camera.PVCSICameraInfo" class="mb-3">CSI Camera</td>
<td v-else-if="camera.PVFileCameraInfo" class="mb-3">File Camera</td>
<td v-else>Unidentified Camera Type</td>
</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>{{ camera.baseName }}</td>
<td>{{ cameraInfoFor(camera).baseName }}</td>
</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>{{ camera.vendorId }}</td>
<td>{{ cameraInfoFor(camera).vendorId }}</td>
</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>{{ camera.productId }}</td>
<td>{{ cameraInfoFor(camera).productId }}</td>
</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 style="word-break: break-all">{{ camera.path }}</td>
<td style="word-break: break-all">{{ cameraInfoFor(camera).path }}</td>
</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 style="word-break: break-all">{{ camera.uniquePath }}</td>
<td style="word-break: break-all">{{ cameraInfoFor(camera).uniquePath }}</td>
</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>{{ camera.otherPaths }}</td>
<td>{{ cameraInfoFor(camera).otherPaths }}</td>
</tr>
</tbody>
</v-table>

View File

@@ -1,5 +1,5 @@
<script setup lang="ts">
import type { PVCameraInfo } from "@/types/SettingTypes";
import { PVCameraInfo } from "@/types/SettingTypes";
function isEqual<T>(a: T, b: T): boolean {
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>
<template>
@@ -28,70 +50,79 @@ const { saved, current } = defineProps<{ saved: PVCameraInfo; current: PVCameraI
<th>Current</th>
</tr>
<tr
v-if="'dev' in saved && 'dev' in current && saved.dev !== null"
:class="saved.dev !== current.dev ? 'mismatch' : ''"
v-if="cameraInfoFor(saved).dev !== undefined && cameraInfoFor(saved).dev !== null"
:class="cameraInfoFor(saved).dev !== cameraInfoFor(current).dev ? 'mismatch' : ''"
>
<td>Device Number:</td>
<td>{{ saved.dev }}</td>
<td>{{ 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>
<td>{{ cameraInfoFor(saved).dev }}</td>
<td>{{ cameraInfoFor(current).dev }}</td>
</tr>
<tr
v-if="'baseName' in saved && 'baseName' in current && saved.baseName !== null"
:class="saved.baseName !== current.baseName ? 'mismatch' : ''"
v-if="cameraInfoFor(saved).name !== undefined && cameraInfoFor(saved).name !== null"
: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>{{ saved.baseName }}</td>
<td>{{ current.baseName }}</td>
<td>{{ cameraInfoFor(saved).baseName }}</td>
<td>{{ cameraInfoFor(current).baseName }}</td>
</tr>
<tr>
<td>Type:</td>
<td v-if="saved.type === '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.type === 'PVFileCameraInfo'" class="mb-3">File Camera</td>
<td v-if="saved.PVUsbCameraInfo" class="mb-3">USB Camera</td>
<td v-else-if="saved.PVCSICameraInfo" class="mb-3">CSI Camera</td>
<td v-else-if="saved.PVFileCameraInfo" class="mb-3">File Camera</td>
<td v-else>Unidentified Camera Type</td>
<td v-if="current.type === '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.type === 'PVFileCameraInfo'" class="mb-3">File Camera</td>
<td v-if="current.PVUsbCameraInfo" class="mb-3">USB Camera</td>
<td v-else-if="current.PVCSICameraInfo" class="mb-3">CSI Camera</td>
<td v-else-if="current.PVFileCameraInfo" class="mb-3">File Camera</td>
<td v-else>Unidentified Camera Type</td>
</tr>
<tr
v-if="'vendorId' in saved && 'vendorId' in current && saved.vendorId !== null"
:class="saved.vendorId !== current.vendorId ? 'mismatch' : ''"
v-if="cameraInfoFor(saved).vendorId !== undefined && cameraInfoFor(saved).vendorId !== null"
:class="cameraInfoFor(saved).vendorId !== cameraInfoFor(current).vendorId ? 'mismatch' : ''"
>
<td>Vendor ID:</td>
<td>{{ saved.vendorId }}</td>
<td>{{ current.vendorId }}</td>
<td>{{ cameraInfoFor(saved).vendorId }}</td>
<td>{{ cameraInfoFor(current).vendorId }}</td>
</tr>
<tr
v-if="'productId' in saved && 'productId' in current && saved.productId !== null"
:class="saved.productId !== current.productId ? 'mismatch' : ''"
v-if="cameraInfoFor(saved).productId !== undefined && cameraInfoFor(saved).productId !== null"
:class="cameraInfoFor(saved).productId !== cameraInfoFor(current).productId ? 'mismatch' : ''"
>
<td>Product ID:</td>
<td>{{ saved.productId }}</td>
<td>{{ 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>
<td>{{ cameraInfoFor(saved).productId }}</td>
<td>{{ cameraInfoFor(current).productId }}</td>
</tr>
<tr
v-if="'otherPaths' in saved && 'otherPaths' in current && saved.otherPaths !== null"
:class="isEqual(saved.otherPaths, current.otherPaths) ? '' : 'mismatch'"
v-if="cameraInfoFor(saved).path !== undefined && cameraInfoFor(saved).path !== null"
: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>{{ saved.otherPaths }}</td>
<td>{{ current.otherPaths }}</td>
<td>{{ cameraInfoFor(saved).otherPaths }}</td>
<td>{{ cameraInfoFor(current).otherPaths }}</td>
</tr>
</tbody>
</v-table>

View File

@@ -25,7 +25,7 @@ const emit = defineEmits<{
(e: "onEscape"): void;
}>();
const handleKeydown = ({ key }: KeyboardEvent) => {
const handleKeydown = ({ key }) => {
switch (key) {
case "Enter":
// 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 TooltippedLabel from "@/components/common/pv-tooltipped-label.vue";
export interface SelectItem<TValue extends string | number> {
export interface SelectItem {
name: string | number;
value: TValue;
value: string | number;
disabled?: boolean;
}
type SelectItems = SelectItem<T>[] | ReadonlyArray<T>;
const value = defineModel<T>({ required: true });
const value = defineModel<string | number | undefined>({ required: true });
const props = withDefaults(
defineProps<{
@@ -17,7 +15,7 @@ const props = withDefaults(
tooltip?: string;
selectCols?: number;
disabled?: boolean;
items: SelectItems;
items: string[] | number[] | SelectItem[];
}>(),
{
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
const items = computed<SelectItem<T>[]>(() => {
const items = computed<SelectItem[]>(() => {
// Trivial case for empty list; we have no data
if (!props.items.length) {
return [];
}
if (areSelectItems(props.items)) {
return props.items;
// Check if the prop exists on the object to infer object type
if ((props.items[0] as SelectItem).name) {
return props.items as SelectItem[];
}
return props.items.map((item) => ({ name: item, value: item }));
return props.items.map((v, i) => ({ name: v, value: i }));
});
</script>
@@ -53,7 +49,7 @@ const items = computed<SelectItem<T>[]>(() => {
:items="items"
item-title="name"
item-value="value"
item-props
item-props.disabled="disabled"
:disabled="disabled"
hide-details="auto"
variant="underlined"

View File

@@ -18,11 +18,11 @@ const props = withDefaults(
const emit = defineEmits<{ (e: "update:modelValue", value: number): void }>();
// Debounce function
function debounce(func: (...args: number[]) => void, wait: number) {
function debounce(func: (...args: any[]) => void, wait: number) {
let timeout: ReturnType<typeof setTimeout>;
return function (...args: number[]) {
return function (...args: any[]) {
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 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
const nameChangeRegex = /^[A-Za-z0-9_ \-)(]*[A-Za-z0-9][A-Za-z0-9_ \-)(.]*$/;
@@ -65,17 +87,17 @@ const cancelCameraNameEdit = () => {
};
// Pipeline Name Edit
const pipelineNamesWrapper = computed(() => {
const pipelineNamesWrapper = computed<SelectItem[]>(() => {
const pipelineNames = useCameraSettingsStore().pipelineNames.map((name, index) => ({ name: name, value: index }));
if (useCameraSettingsStore().isDriverMode) {
pipelineNames.push({ name: "Driver Mode", value: WebsocketPipelineType.DriverMode.valueOf() });
pipelineNames.push({ name: "Driver Mode", value: WebsocketPipelineType.DriverMode });
}
if (useCameraSettingsStore().isFocusMode) {
pipelineNames.push({ name: "Focus Mode", value: WebsocketPipelineType.FocusCamera.valueOf() });
pipelineNames.push({ name: "Focus Mode", value: WebsocketPipelineType.FocusCamera });
}
if (useCameraSettingsStore().isCalibrationMode) {
pipelineNames.push({ name: "3D Calibration Mode", value: WebsocketPipelineType.Calib3d.valueOf() });
pipelineNames.push({ name: "3D Calibration Mode", value: WebsocketPipelineType.Calib3d });
}
return pipelineNames;
@@ -218,7 +240,7 @@ useCameraSettingsStore().$subscribe((mutation, state) => {
break;
}
});
const wrappedCameras = computed<SelectItem<string>[]>(() =>
const wrappedCameras = computed<SelectItem[]>(() =>
Object.keys(useCameraSettingsStore().cameras).map((cameraUniqueName) => ({
name: useCameraSettingsStore().cameras[cameraUniqueName].nickname,
value: cameraUniqueName
@@ -235,7 +257,7 @@ const wrappedCameras = computed<SelectItem<string>[]>(() =>
v-model="useStateStore().currentCameraUniqueName"
label="Camera"
:items="wrappedCameras"
@update:modelValue="pipelineType = useCameraSettingsStore().currentWebsocketPipelineType"
@update:modelValue="changeCurrentCameraUniqueName"
/>
<pv-input
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 PnPTab from "@/components/dashboard/tabs/PnPTab.vue";
import Map3DTab from "@/components/dashboard/tabs/Map3DTab.vue";
import { PipelineType } from "@/types/PipelineTypes";
import { WebsocketPipelineType } from "@/types/WebsocketDataTypes";
import { useDisplay, useTheme } from "vuetify";
import { useDisplay } from "vuetify/lib/composables/display";
import { useTheme } from "vuetify";
const theme = useTheme();
@@ -106,17 +106,6 @@ const tabGroups = computed<ConfigOption[][]>(() => {
.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 = () => {
// Force the current tab to the input tab on driver mode change
if (useCameraSettingsStore().isDriverMode) {
@@ -140,7 +129,7 @@ const onBeforeTabUpdate = () => {
<v-col
v-for="(tabGroupData, tabGroupIndex) in tabGroups"
:key="tabGroupIndex"
:cols="tabGroupIndex === 1 && shouldUseWideSecondTabGroup ? 7 : ''"
:cols="tabGroupIndex === 1 && useCameraSettingsStore().currentPipelineSettings.doMultiTarget ? 7 : ''"
:class="tabGroupIndex !== tabGroups.length - 1 && 'pr-3'"
@vue:before-update="onBeforeTabUpdate"
>

View File

@@ -1,17 +1,18 @@
<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 PvSlider from "@/components/common/pv-slider.vue";
import PvSwitch from "@/components/common/pv-switch.vue";
import { computed } from "vue";
import { useStateStore } from "@/stores/StateStore";
import type { ActivePipelineSettings } from "@/types/PipelineTypes";
import { useCameraSettingsStore } from "@/stores/settings/CameraSettingsStore";
import { useDisplay } from "vuetify";
// TODO fix pipeline typing in order to fix this, the store settings call should be able to infer that only valid pipeline type settings are exposed based on pre-checks for the entire config section
// Defer reference to store access method
const currentPipelineSettings = computed<AprilTagPipelineSettings>(
() => useCameraSettingsStore().currentPipelineSettings as AprilTagPipelineSettings
const currentPipelineSettings = computed<ActivePipelineSettings>(
() => useCameraSettingsStore().currentPipelineSettings
);
const { mdAndDown } = useDisplay();
const interactiveCols = computed(() =>
@@ -24,10 +25,7 @@ const interactiveCols = computed(() =>
<pv-select
v-model="currentPipelineSettings.tagFamily"
label="Target family"
:items="[
{ value: AprilTagFamily.Family36h11, name: 'AprilTag 36h11 (6.5in)' },
{ value: AprilTagFamily.Family16h5, name: 'AprilTag 16h5 (6in)' }
]"
:items="['AprilTag 36h11 (6.5in)', 'AprilTag 16h5 (6in)']"
:select-cols="interactiveCols"
@update:modelValue="(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ tagFamily: value }, false)"
/>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,5 +1,5 @@
<script setup lang="ts">
import { ref, computed, inject, useTemplateRef } from "vue";
import { ref, computed, inject } from "vue";
import { useStateStore } from "@/stores/StateStore";
import { useSettingsStore } from "@/stores/settings/GeneralSettingsStore";
import { type ObjectDetectionModelProperties } from "@/types/SettingTypes";
@@ -46,7 +46,7 @@ const handleImport = async () => {
if (
await axiosPost("/objectdetection/import", "import an object detection model", formData, {
headers: { "Content-Type": "multipart/form-data" },
onUploadProgress: ({ progress }: { progress?: number }) => {
onUploadProgress: ({ progress }) => {
const uploadPercentage = (progress || 0) * 100.0;
if (uploadPercentage < 99.5) {
useStateStore().showSnackbarMessage({
@@ -74,20 +74,20 @@ const handleImport = async () => {
importVersion.value = null;
};
const deleteModel = async (model: ObjectDetectionModelProperties) => {
await axiosPost("/objectdetection/delete", "delete an object detection model", {
const deleteModel = (model: ObjectDetectionModelProperties) => {
axiosPost("/objectdetection/delete", "delete an object detection model", {
modelPath: model.modelPath
});
};
const renameModel = async (model: ObjectDetectionModelProperties, newName: string) => {
const renameModel = (model: ObjectDetectionModelProperties, newName: string) => {
useStateStore().showSnackbarMessage({
message: "Renaming Object Detection Model...",
color: "secondary",
timeout: -1
});
await axiosPost("/objectdetection/rename", "rename an object detection model", {
axiosPost("/objectdetection/rename", "rename an object detection model", {
modelPath: model.modelPath,
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.
const supportedModels = computed(() => {
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
return supportedBackends.some((backend: string) => backend.toLowerCase() === model.family.toLowerCase());
};
@@ -106,19 +106,19 @@ const supportedModels = computed(() => {
return availableModels.filter(isSupported);
});
const exportModels = useTemplateRef("exportModels");
const exportModels = ref();
const openExportPrompt = () => {
exportModels.value?.click();
exportModels.value.click();
};
const exportIndividualModel = useTemplateRef("exportIndividualModel");
const exportIndividualModel = ref();
const openExportIndividualModelPrompt = () => {
exportIndividualModel.value?.click();
exportIndividualModel.value.click();
};
const showNukeDialog = ref(false);
const nukeModels = async () => {
await axiosPost("/objectdetection/nuke", "clear and reset object detection models");
const nukeModels = () => {
axiosPost("/objectdetection/nuke", "clear and reset object detection models");
};
const showBulkImportDialog = ref(false);
@@ -132,7 +132,7 @@ const handleBulkImport = async () => {
if (
await axiosPost("/objectdetection/bulkimport", "import object detection models", formData, {
headers: { "Content-Type": "multipart/form-data" },
onUploadProgress: ({ progress }: { progress?: number }) => {
onUploadProgress: ({ progress }) => {
const uploadPercentage = (progress || 0) * 100.0;
if (uploadPercentage < 99.5) {
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.
*
* @param data data to send
* @param encodeData whether or not to encode the data using msgpack (defaults to true)
* @see isConnected
*
*/
send(data: unknown) {
send(data, encodeData = true) {
// Only send data if the websocket is open
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 type { Resolution } from "@/types/SettingTypes";
import axios, { type AxiosRequestConfig } from "axios";
import axios from "axios";
export const resolutionsAreEqual = (a: Resolution, b: Resolution) => {
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}`;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export const parseJsonFile = async <T extends Record<string, any>>(file: File): Promise<T> => {
return new Promise((resolve, reject) => {
const fileReader = new FileReader();
fileReader.onload = (event) => {
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);
};
fileReader.onerror = () => reject(new Error("Error reading file"));
fileReader.onerror = (error) => reject(error);
fileReader.readAsText(file);
});
};
@@ -74,13 +73,7 @@ export const parseJsonFile = async <T extends Record<string, any>>(file: File):
* @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.
*/
export const axiosPost = async (
url: string,
description: string,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
data?: any,
config?: AxiosRequestConfig
): Promise<boolean> => {
export const axiosPost = async (url: string, description: string, data?: any, config?: any): Promise<boolean> => {
try {
await axios.post(url, data, config);
useStateStore().showSnackbarMessage({
@@ -88,7 +81,6 @@ export const axiosPost = async (
color: "success"
});
return true;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} catch (error: any) {
if (error.response) {
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 axios from "axios";
import setup from "@/lib/quarky.js";
type PhotonClientRuntimeMode = "production" | "development" | "local-network-development";
const runtimeMode: PhotonClientRuntimeMode = process.env.NODE_ENV as PhotonClientRuntimeMode;
let backendHost: string;
let backendHostname: string;
switch (runtimeMode) {
switch (runtimeMode as PhotonClientRuntimeMode) {
case "development":
backendHost = `${location.hostname}:5800`;
backendHostname = location.hostname;
@@ -45,3 +47,4 @@ app.use(pinia);
app.use(vuetify);
app.use(router);
app.mount("#app");
setup();

View File

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

View File

@@ -31,8 +31,7 @@ interface StateStore {
currentCameraUniqueName: string;
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<string, PipelineResult>;
backendResults: Record<number, PipelineResult>;
multitagResultBuffer: Record<string, MultitagResult[]>;
colorPickingMode: boolean;
@@ -40,6 +39,8 @@ interface StateStore {
calibrationData: {
imageCount: number;
videoFormatIndex: number;
minimumImageCount: number;
hasEnoughImages: boolean;
};
snackbarData: {
@@ -87,7 +88,9 @@ export const useStateStore = defineStore("state", {
calibrationData: {
imageCount: 0,
videoFormatIndex: 0
videoFormatIndex: 0,
minimumImageCount: 12,
hasEnoughImages: false
},
snackbarData: {
@@ -158,7 +161,9 @@ export const useStateStore = defineStore("state", {
updateCalibrationStateValuesFromWebsocket(data: WebsocketCalibrationData) {
this.calibrationData = {
imageCount: data.count,
videoFormatIndex: data.videoModeIndex
videoFormatIndex: data.videoModeIndex,
minimumImageCount: data.minCount,
hasEnoughImages: data.hasEnough
};
},
updateDiscoveredCameras(data: VsmState) {

View File

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

View File

@@ -10,7 +10,6 @@ import { NetworkConnectionType } from "@/types/SettingTypes";
import { useStateStore } from "@/stores/StateStore";
import axios from "axios";
import type { WebsocketSettingsUpdate } from "@/types/WebsocketDataTypes";
import type { AprilTagFieldLayout } from "@/types/PhotonTrackingTypes";
import { ref } from "vue";
interface GeneralSettingsStore {
@@ -18,7 +17,7 @@ interface GeneralSettingsStore {
network: NetworkSettings;
lighting: LightingSettings;
metrics: MetricData;
currentFieldLayout: AprilTagFieldLayout;
currentFieldLayout;
}
interface MetricsEntry {
@@ -185,7 +184,7 @@ export const useSettingsStore = defineStore("settings", {
const payload = {
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()`)
*/
export enum PipelineType {
Calibration3d = 1,
DriverMode = 2,
Reflective = 3,
ColoredShape = 4,
@@ -36,62 +35,18 @@ export enum TargetModel {
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 {
offsetRobotOffsetMode: RobotOffsetPointMode;
streamingFrameDivisor: number;
offsetDualPointBArea: number;
contourGroupingMode: ContourGroupingMode;
contourGroupingMode: number;
hsvValue: WebsocketNumberPair | [number, number];
cameraGain: number;
cameraBlueGain: number;
cameraRedGain: number;
cornerDetectionSideCount: number;
contourRatio: WebsocketNumberPair | [number, number];
contourTargetOffsetPointEdge: ContourTargetOffsetPointEdge;
contourTargetOffsetPointEdge: number;
pipelineNickname: string;
inputImageRotationMode: number;
contourArea: WebsocketNumberPair | [number, number];
@@ -101,7 +56,7 @@ export interface PipelineSettings {
inputShouldShow: boolean;
cameraAutoExposure: boolean;
contourSpecklePercentage: number;
contourTargetOrientation: ContourTargetOrientation;
contourTargetOrientation: number;
targetModel: TargetModel;
cornerDetectionUseConvexHulls: boolean;
outputShouldShow: boolean;
@@ -112,7 +67,7 @@ export interface PipelineSettings {
ledMode: boolean;
hueInverted: boolean;
outputMaximumTargets: number;
contourSortMode: ContourSortMode;
contourSortMode: number;
cameraExposureRaw: number;
cameraMinExposureRaw: number;
cameraMaxExposureRaw: number;
@@ -125,13 +80,11 @@ export interface PipelineSettings {
cornerDetectionAccuracyPercentage: number;
hsvSaturation: WebsocketNumberPair | [number, number];
pipelineType: PipelineType;
contourIntersection: ContourIntersection;
contourIntersection: number;
cameraAutoWhiteBalance: boolean;
cameraWhiteBalanceTemp: number;
crosshair: boolean;
blockForFrames: boolean;
}
export type ConfigurablePipelineSettings = Partial<
@@ -160,13 +113,13 @@ export const DefaultPipelineSettings: Omit<
offsetRobotOffsetMode: RobotOffsetPointMode.None,
streamingFrameDivisor: 0,
offsetDualPointBArea: 0,
contourGroupingMode: ContourGroupingMode.Single,
contourGroupingMode: 0,
hsvValue: { first: 50, second: 255 },
cameraBlueGain: 20,
cameraRedGain: 11,
cornerDetectionSideCount: 4,
contourRatio: { first: 0, second: 20 },
contourTargetOffsetPointEdge: ContourTargetOffsetPointEdge.Center,
contourTargetOffsetPointEdge: 0,
pipelineNickname: "Placeholder Pipeline",
inputImageRotationMode: 0,
contourArea: { first: 0, second: 100 },
@@ -176,7 +129,7 @@ export const DefaultPipelineSettings: Omit<
inputShouldShow: false,
cameraAutoExposure: false,
contourSpecklePercentage: 5,
contourTargetOrientation: ContourTargetOrientation.Landscape,
contourTargetOrientation: 1,
cornerDetectionUseConvexHulls: true,
outputShouldShow: true,
outputShouldDraw: true,
@@ -185,7 +138,7 @@ export const DefaultPipelineSettings: Omit<
hsvHue: { first: 50, second: 180 },
hueInverted: false,
outputMaximumTargets: 20,
contourSortMode: ContourSortMode.Largest,
contourSortMode: 0,
offsetSinglePoint: { x: 0, y: 0 },
cameraBrightness: 50,
offsetDualPointAArea: 0,
@@ -194,12 +147,11 @@ export const DefaultPipelineSettings: Omit<
cornerDetectionStrategy: 0,
cornerDetectionAccuracyPercentage: 10,
hsvSaturation: { first: 50, second: 255 },
contourIntersection: ContourIntersection.Up,
contourIntersection: 1,
cameraAutoWhiteBalance: false,
cameraWhiteBalanceTemp: 4000,
cameraMinExposureRaw: 1,
cameraMaxExposureRaw: 2,
crosshair: true,
blockForFrames: true
};
@@ -232,7 +184,7 @@ export interface ColoredShapePipelineSettings extends PipelineSettings {
contourRadius: WebsocketNumberPair | [number, number];
circleDetectThreshold: number;
accuracyPercentage: number;
contourShape: ContourShape;
contourShape: number;
contourPerimeter: WebsocketNumberPair | [number, number];
minDist: number;
maxCannyThresh: number;
@@ -257,7 +209,7 @@ export const DefaultColoredShapePipelineSettings: ColoredShapePipelineSettings =
contourRadius: { first: 0, second: 100 },
circleDetectThreshold: 5,
accuracyPercentage: 10,
contourShape: ContourShape.Triangle,
contourShape: 2,
contourPerimeter: { first: 0, second: 1.7976931348623157e308 },
minDist: 20,
maxCannyThresh: 90
@@ -372,14 +324,13 @@ export const DefaultObjectDetectionPipelineSettings: ObjectDetectionPipelineSett
};
export interface Calibration3dPipelineSettings extends PipelineSettings {
pipelineType: PipelineType.Calibration3d;
drawAllSnapshots: boolean;
}
export type ConfigurableCalibration3dPipelineSettings = Partial<Omit<Calibration3dPipelineSettings, "pipelineType">> &
ConfigurablePipelineSettings;
export const DefaultCalibration3dPipelineSettings: Calibration3dPipelineSettings = {
...DefaultPipelineSettings,
pipelineType: PipelineType.Calibration3d,
pipelineType: PipelineType.ObjectDetection,
cameraGain: 20,
targetModel: TargetModel.InfiniteRechargeHighGoalOuter,
ledMode: true,

View File

@@ -75,29 +75,46 @@ export type ConfigurableNetworkSettings = Omit<
"canManage" | "networkInterfaceNames" | "networkingDisabled"
>;
interface PVCameraInfoBase {
type: "PVUsbCameraInfo" | "PVCSICameraInfo" | "PVFileCameraInfo";
export interface PVCameraInfoBase {
/*
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;
name: string;
// In Java, PVCameraInfo provides a uniquePath property so we can have one Source of Truth here
uniquePath: string;
}
export interface PVUsbCameraInfo extends PVCameraInfoBase {
type: "PVUsbCameraInfo";
dev: number;
otherPaths: string[];
vendorId: number;
productId: number;
// This camera info will only ever hold one of its members - the others should be undefined.
export class PVCameraInfo {
PVUsbCameraInfo: PVUsbCameraInfo | undefined;
PVCSICameraInfo: PVCSICameraInfo | undefined;
PVFileCameraInfo: PVFileCameraInfo | undefined;
}
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 {
disabledConfigs: WebsocketCameraSettingsUpdate[];
@@ -259,7 +276,6 @@ export interface UiCameraConfiguration {
maxWhiteBalanceTemp: number;
fpsLimit: number;
isEnabled: boolean;
matchedCameraInfo: PVCameraInfo;
isConnected: boolean;
@@ -422,13 +438,15 @@ export const PlaceholderCameraSettings: UiCameraConfiguration = reactive({
minWhiteBalanceTemp: 2000,
maxWhiteBalanceTemp: 10000,
matchedCameraInfo: {
type: "PVFileCameraInfo",
name: "Foobar",
path: "/dev/foobar",
uniquePath: "/dev/foobar2"
PVFileCameraInfo: {
name: "Foobar",
path: "/dev/foobar",
uniquePath: "/dev/foobar2"
},
PVCSICameraInfo: undefined,
PVUsbCameraInfo: undefined
},
fpsLimit: -1,
isEnabled: true,
isConnected: true,
hasConnected: true,
mismatch: false

View File

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

View File

@@ -2,7 +2,13 @@
import { useCameraSettingsStore } from "@/stores/settings/CameraSettingsStore";
import { computed, inject, ref } from "vue";
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 PhotonCameraStream from "@/components/app/photon-camera-stream.vue";
import PvDeleteModal from "@/components/common/pv-delete-modal.vue";
@@ -12,22 +18,20 @@ import { useTheme } from "vuetify";
const theme = useTheme();
const backendHostname = inject<string>("backendHostname");
const formatUrl = (port: number) => `http://${backendHostname}:${port}/stream.mjpg`;
const formatUrl = (port) => `http://${inject("backendHostname")}:${port}/stream.mjpg`;
const activatingModule = ref(false);
const activateModule = async (moduleUniqueName: string) => {
const activateModule = (moduleUniqueName: string) => {
if (activatingModule.value) return;
activatingModule.value = true;
await axiosPost("/utils/activateMatchedCamera", "activate a matched camera", {
axiosPost("/utils/activateMatchedCamera", "activate a matched camera", {
cameraUniqueName: moduleUniqueName
});
activatingModule.value = false;
}).finally(() => (activatingModule.value = false));
};
const assigningCamera = ref(false);
const assignCamera = async (cameraInfo: PVCameraInfo) => {
const assignCamera = (cameraInfo: PVCameraInfo) => {
if (assigningCamera.value) return;
assigningCamera.value = true;
@@ -35,39 +39,48 @@ const assignCamera = async (cameraInfo: PVCameraInfo) => {
cameraInfo: cameraInfo
};
await axiosPost("/utils/assignUnmatchedCamera", "assign an unmatched camera", payload);
assigningCamera.value = false;
axiosPost("/utils/assignUnmatchedCamera", "assign an unmatched camera", payload).finally(
() => (assigningCamera.value = false)
);
};
const deactivatingModule = ref(false);
const deactivateModule = async (cameraUniqueName: string) => {
const deactivateModule = (cameraUniqueName: string) => {
if (deactivatingModule.value) return;
deactivatingModule.value = true;
await axiosPost("/utils/unassignCamera", "unassign a camera", { cameraUniqueName: cameraUniqueName });
deactivatingModule.value = false;
axiosPost("/utils/unassignCamera", "unassign a camera", { cameraUniqueName: cameraUniqueName }).finally(
() => (deactivatingModule.value = false)
);
};
const confirmDeleteDialog = ref({ show: false, nickname: "", cameraUniqueName: "" });
const deletingCamera = ref<string | null>(null);
const deleteThisCamera = async (cameraUniqueName: string) => {
const deleteThisCamera = (cameraUniqueName: string) => {
if (deletingCamera.value) return;
deletingCamera.value = cameraUniqueName;
await axiosPost("/utils/nukeOneCamera", "delete a camera", { cameraUniqueName: cameraUniqueName });
deletingCamera.value = null;
axiosPost("/utils/nukeOneCamera", "delete a camera", { cameraUniqueName: cameraUniqueName }).finally(() => {
deletingCamera.value = null;
});
};
const cameraConnected = (uniquePath: string | undefined): boolean => {
if (!uniquePath) return false;
return useStateStore().vsmState.allConnectedCameras.find((it) => it.uniquePath === uniquePath) !== undefined;
const cameraConnected = (uniquePath: string): boolean => {
return (
useStateStore().vsmState.allConnectedCameras.find((it) => cameraInfoFor(it).uniquePath === uniquePath) !== undefined
);
};
const unmatchedCameras = computed(() => {
const activeVmPaths = Object.values(useCameraSettingsStore().cameras).map((it) => it.matchedCameraInfo.uniquePath);
const disabledVmPaths = useStateStore().vsmState.disabledConfigs.map((it) => it.matchedCameraInfo.uniquePath);
const activeVmPaths = Object.values(useCameraSettingsStore().cameras).map(
(it) => cameraInfoFor(it.matchedCameraInfo).uniquePath
);
const disabledVmPaths = useStateStore().vsmState.disabledConfigs.map(
(it) => cameraInfoFor(it.matchedCameraInfo).uniquePath
);
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
.sort(
(first, second) =>
(cameraConnected(second.matchedCameraInfo.uniquePath) ? 1 : 0) -
(cameraConnected(first.matchedCameraInfo.uniquePath) ? 1 : 0)
(cameraConnected(cameraInfoFor(second.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];
};
/**
* Get the connection-type-specific camera info from the given PVCameraInfo object.
*/
const cameraInfoFor = (camera: PVCameraInfo | null): PVUsbCameraInfo | PVCSICameraInfo | PVFileCameraInfo | any => {
if (!camera) return null;
if (camera.PVUsbCameraInfo) {
return camera.PVUsbCameraInfo;
}
if (camera.PVCSICameraInfo) {
return camera.PVCSICameraInfo;
}
if (camera.PVFileCameraInfo) {
return camera.PVFileCameraInfo;
}
return {};
};
/**
* Find the PVCameraInfo currently occupying the same uniquePath as the the given module
*/
const getMatchedDevice = (info: PVCameraInfo | undefined): PVCameraInfo => {
if (!info) {
return {
type: "PVFileCameraInfo",
path: "",
name: "",
uniquePath: ""
PVFileCameraInfo: undefined,
PVCSICameraInfo: undefined,
PVUsbCameraInfo: undefined
};
}
return (
useStateStore().vsmState.allConnectedCameras.find((it) => it.uniquePath === info.uniquePath) || {
type: "PVFileCameraInfo",
path: "",
name: "",
uniquePath: ""
useStateStore().vsmState.allConnectedCameras.find(
(it) => cameraInfoFor(it).uniquePath === cameraInfoFor(info).uniquePath
) || {
PVFileCameraInfo: undefined,
PVCSICameraInfo: undefined,
PVUsbCameraInfo: undefined
}
);
};
@@ -128,11 +158,12 @@ const getMatchedDevice = (info: PVCameraInfo | undefined): PVCameraInfo => {
class="pr-0"
>
<v-card color="surface" class="rounded-12">
<v-card-title>{{ module.matchedCameraInfo.name }}</v-card-title>
<v-card-subtitle v-if="!cameraConnected(module.matchedCameraInfo.uniquePath)"
<v-card-title>{{ cameraInfoFor(module.matchedCameraInfo).name }}</v-card-title>
<v-card-subtitle v-if="!cameraConnected(cameraInfoFor(module.matchedCameraInfo).uniquePath)"
>Status: <span class="inactive-status">Disconnected</span></v-card-subtitle
>
<v-card-subtitle v-else-if="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
>
<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>
<tr
v-if="
cameraConnected(module.matchedCameraInfo.uniquePath) &&
cameraConnected(cameraInfoFor(module.matchedCameraInfo).uniquePath) &&
useStateStore().backendResults[module.uniqueName]
"
>
@@ -183,7 +214,7 @@ const getMatchedDevice = (info: PVCameraInfo | undefined): PVCameraInfo => {
</tbody>
</v-table>
<div
v-if="cameraConnected(module.matchedCameraInfo.uniquePath)"
v-if="cameraConnected(cameraInfoFor(module.matchedCameraInfo).uniquePath)"
:id="`stream-container-${index}`"
class="d-flex flex-column justify-center align-center mt-3"
style="height: 250px"
@@ -202,7 +233,12 @@ const getMatchedDevice = (info: PVCameraInfo | undefined): PVCameraInfo => {
color="buttonPassive"
style="width: 100%"
: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>
</v-btn>
@@ -279,7 +315,7 @@ const getMatchedDevice = (info: PVCameraInfo | undefined): PVCameraInfo => {
</tr>
<tr>
<td>Connected</td>
<td>{{ cameraConnected(module.matchedCameraInfo.uniquePath) }}</td>
<td>{{ cameraConnected(cameraInfoFor(module.matchedCameraInfo).uniquePath) }}</td>
</tr>
</tbody>
</v-table>
@@ -291,7 +327,12 @@ const getMatchedDevice = (info: PVCameraInfo | undefined): PVCameraInfo => {
color="buttonPassive"
style="width: 100%"
: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>
</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-card class="pr-0 rounded-12" color="surface">
<v-card-title>
<span v-if="camera.type === 'PVUsbCameraInfo'">USB Camera:</span>
<span v-else-if="camera.type === 'PVCSICameraInfo'">CSI Camera:</span>
<span v-else-if="camera.type === 'PVFileCameraInfo'">File Camera:</span>
<span v-if="camera.PVUsbCameraInfo">USB Camera:</span>
<span v-else-if="camera.PVCSICameraInfo">CSI Camera:</span>
<span v-else-if="camera.PVFileCameraInfo">File Camera:</span>
<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-subtitle>Status: Unassigned</v-card-subtitle>
<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 class="pt-0">
<v-row>
@@ -395,7 +436,7 @@ const getMatchedDevice = (info: PVCameraInfo | undefined): PVCameraInfo => {
<v-dialog v-model="viewingDetails" max-width="800">
<v-card v-if="viewingCamera[0] !== null" flat color="surface">
<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-icon size="x-large">mdi-close</v-icon>
</v-btn>
@@ -405,7 +446,9 @@ const getMatchedDevice = (info: PVCameraInfo | undefined): PVCameraInfo => {
</v-card-text>
<v-card-text
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

View File

@@ -77,18 +77,8 @@ const conflictingCameraShown = computed<boolean>(() => {
return useSettingsStore().general.conflictingCameras.length > 0;
});
const fpsLimitedCameras = computed<string>(() => {
return Object.values(useCameraSettingsStore().cameras)
.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 fpsLimitWarningShown = computed<boolean>(() => {
return Object.values(useCameraSettingsStore().cameras).some((c) => c.fpsLimit > 0);
});
const showCameraSetupDialog = ref(useCameraSettingsStore().needsCameraConfiguration);
@@ -121,7 +111,7 @@ const showCameraSetupDialog = ref(useCameraSettingsStore().needsCameraConfigurat
</span>
</v-alert>
<v-alert
v-if="fpsLimitedCameras"
v-if="fpsLimitWarningShown"
class="mb-3"
color="error"
density="compact"
@@ -129,22 +119,10 @@ const showCameraSetupDialog = ref(useCameraSettingsStore().needsCameraConfigurat
:variant="theme.global.current.value.dark ? 'tonal' : 'elevated'"
>
<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.
</span>
</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-if="conflictingCameraShown"
class="mb-3"

View File

@@ -1,6 +1,5 @@
<script setup lang="ts">
const devMode = process.env.NODE_ENV === "development";
const docsSrc = import.meta.env.MODE === "demo" ? "https://docs.photonvision.org" : "docs/index.html";
</script>
<template>
<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 v-else style="width: 100%; height: 100%">
<!--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>
</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",
"include": ["env.d.ts", "vite.config.ts", "playwright.config.ts", "src/**/*", "src/**/*.vue", "tests/**/*"],
"include": ["env.d.ts", "src/**/*", "src/**/*.vue"],
"compilerOptions": {
"lib": ["ES2022", "DOM", "DOM.Iterable"],
"moduleResolution": "bundler",
"noImplicitAny": true,
"moduleResolution": "node",
"noImplicitAny": false,
"strict": true,
"removeComments": true,
"sourceMap": true,
@@ -15,4 +14,9 @@
"@/*": ["./src/*"]
}
},
"references": [
{
"path": "./tsconfig.config.json"
}
]
}

View File

@@ -6,7 +6,7 @@ ext.licenseFile = file("$rootDir/LICENSE")
ext.externalLicensesFolder = file("$rootDir/ExternalLicenses")
apply from: "${rootDir}/shared/common.gradle"
wpilibTools.deps.wpilibVersion = wpilibVersion
wpilibTools.deps.wpilibVersion = wpi.versions.wpilibVersion.get()
def nativeConfigName = 'wpilibNatives'
configurations {
@@ -23,12 +23,11 @@ dependencies {
wpilibNatives wpilibTools.deps.wpilib("wpimath")
wpilibNatives wpilibTools.deps.wpilib("wpinet")
wpilibNatives wpilibTools.deps.wpilib("wpiutil")
wpilibNatives wpilibTools.deps.wpilib("datalog")
wpilibNatives wpilibTools.deps.wpilib("ntcore")
wpilibNatives wpilibTools.deps.wpilib("cscore")
wpilibNatives wpilibTools.deps.wpilib("apriltag")
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
implementation 'org.zeroturnaround:zt-zip:1.14'
@@ -41,7 +40,7 @@ dependencies {
wpilibNatives("org.photonvision:rknn_jni-jni:$rknnVersion:$jniPlatform") {
transitive = false
}
wpilibNatives("org.photonvision:tflite_jni-jni:$tfliteVersion:$jniPlatform") {
wpilibNatives("org.photonvision:rubik_jni-jni:$rubikVersion:$jniPlatform") {
transitive = false
}
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") {
transitive = false
}
implementation("org.photonvision:tflite_jni-java:$tfliteVersion") {
implementation("org.photonvision:rubik_jni-java:$rubikVersion") {
transitive = false
}

View File

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

View File

@@ -17,7 +17,10 @@
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.List;
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.DriverModePipelineSettings;
import org.photonvision.vision.processes.PipelineManager;
import org.wpilib.vision.camera.UsbCameraInfo;
@Json
public class CameraConfiguration {
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 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 CameraConfiguration(PVCameraInfo cameraInfo, String uniqueName, String nickname) {
@@ -74,22 +78,24 @@ public class CameraConfiguration {
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
// the parameters)
// Shiny new constructor
@JsonCreator
public CameraConfiguration(
String uniqueName,
PVCameraInfo matchedCameraInfo,
String nickname,
boolean deactivated,
QuirkyCamera cameraQuirks,
double FOV,
int currentPipelineIndex) {
@JsonProperty("uniqueName") String uniqueName,
@JsonProperty("matchedCameraInfo") PVCameraInfo matchedCameraInfo,
@JsonProperty("nickname") String nickname,
@JsonProperty("deactivated") boolean deactivated,
@JsonProperty("cameraQuirks") QuirkyCamera cameraQuirks,
@JsonProperty("FOV") double FOV,
@JsonProperty("calibrations") List<CameraCalibrationCoefficients> calibrations,
@JsonProperty("currentPipelineIndex") int currentPipelineIndex) {
this.uniqueName = uniqueName;
this.matchedCameraInfo = matchedCameraInfo;
this.nickname = nickname;
this.deactivated = deactivated;
this.cameraQuirks = cameraQuirks;
this.FOV = FOV;
this.calibrations = calibrations != null ? calibrations : new ArrayList<>();
this.currentPipelineIndex = currentPipelineIndex;
}
@@ -114,14 +120,14 @@ public class CameraConfiguration {
PVCameraInfo matchedCameraInfo;
/** Legacy constructor for compat with 2024.3.1 */
@Json.Creator
@JsonCreator
public LegacyCameraConfigStruct(
String baseName,
String path,
String[] otherPaths,
CameraType cameraType,
int usbVID,
int usbPID) {
@JsonProperty("baseName") String baseName,
@JsonProperty("path") String path,
@JsonProperty("otherPaths") String[] otherPaths,
@JsonProperty("cameraType") CameraType cameraType,
@JsonProperty("usbVID") int usbVID,
@JsonProperty("usbPID") int usbPID) {
if (cameraType == CameraType.UsbCamera) {
this.matchedCameraInfo =
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
* exists yet. If we are replacing an existing calibration, the old one will be "released" and the
* underlying data matrices will become invalid.
* Replace a calibration in our list with the same unrotatedImageSize with a new one, or add it if
* none exists yet. If we are replacing an existing calibration, the old one will be "released"
* and the underlying data matrices will become invalid.
*
* @param calibration The calibration to add.
*/
public void addCalibration(CameraCalibrationCoefficients calibration) {
logger.info("adding calibration " + calibration.resolution);
logger.info("adding calibration " + calibration.unrotatedImageSize);
calibrations.stream()
.filter(it -> it.resolution.equals(calibration.resolution))
.filter(it -> it.unrotatedImageSize.equals(calibration.unrotatedImageSize))
.findAny()
.ifPresent(
(it) -> {
@@ -188,12 +194,12 @@ public class CameraConfiguration {
* Remove a calibration from our list. If found, the calibration will be "released". If not found,
* no-op.
*
* @param resolution The resolution to remove.
* @param unrotatedImageSize The resolution to remove.
*/
public void removeCalibration(Size resolution) {
logger.info("deleting calibration " + resolution);
public void removeCalibration(Size unrotatedImageSize) {
logger.info("deleting calibration " + unrotatedImageSize);
calibrations.stream()
.filter(it -> it.resolution.equals(resolution))
.filter(it -> it.unrotatedImageSize.equals(unrotatedImageSize))
.findAny()
.ifPresent(
(it) -> {
@@ -209,6 +215,7 @@ public class CameraConfiguration {
*
* <p>This represents our best guess at an immutable path to detect a camera at.
*/
@JsonIgnore
public String getDevicePath() {
return matchedCameraInfo.uniquePath();
}

View File

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

View File

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

View File

@@ -17,11 +17,10 @@
package org.photonvision.common.configuration;
import io.avaje.json.JsonException;
import io.avaje.jsonb.Jsonb;
import com.fasterxml.jackson.core.JsonProcessingException;
import edu.wpi.first.apriltag.AprilTagFieldLayout;
import edu.wpi.first.apriltag.AprilTagFields;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.UncheckedIOException;
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.Logger;
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.wpilib.vision.apriltag.AprilTagFieldLayout;
import org.wpilib.vision.apriltag.AprilTagFields;
import org.zeroturnaround.zip.ZipUtil;
class LegacyConfigProvider extends ConfigProvider {
@@ -126,13 +126,14 @@ class LegacyConfigProvider extends ConfigProvider {
AprilTagFieldLayout atfl = null;
if (hardwareConfigFile.exists()) {
try (var stream = new FileInputStream(hardwareConfigFile)) {
hardwareConfig = Jsonb.instance().type(HardwareConfig.class).fromJson(stream);
try {
hardwareConfig =
JacksonUtils.deserialize(hardwareConfigFile.toPath(), HardwareConfig.class);
if (hardwareConfig == null) {
logger.error("Could not deserialize hardware config! Loading defaults");
hardwareConfig = new HardwareConfig();
}
} catch (IOException | IllegalStateException | JsonException e) {
} catch (IOException e) {
logger.error("Could not deserialize hardware config! Loading defaults");
hardwareConfig = new HardwareConfig();
}
@@ -142,13 +143,14 @@ class LegacyConfigProvider extends ConfigProvider {
}
if (hardwareSettingsFile.exists()) {
try (var stream = new FileInputStream(hardwareSettingsFile)) {
hardwareSettings = Jsonb.instance().type(HardwareSettings.class).fromJson(stream);
try {
hardwareSettings =
JacksonUtils.deserialize(hardwareSettingsFile.toPath(), HardwareSettings.class);
if (hardwareSettings == null) {
logger.error("Could not deserialize hardware settings! Loading defaults");
hardwareSettings = new HardwareSettings();
}
} catch (IOException | IllegalStateException | JsonException e) {
} catch (IOException e) {
logger.error("Could not deserialize hardware settings! Loading defaults");
hardwareSettings = new HardwareSettings();
}
@@ -158,13 +160,13 @@ class LegacyConfigProvider extends ConfigProvider {
}
if (networkConfigFile.exists()) {
try (var stream = new FileInputStream(networkConfigFile)) {
networkConfig = Jsonb.instance().type(NetworkConfig.class).fromJson(stream);
try {
networkConfig = JacksonUtils.deserialize(networkConfigFile.toPath(), NetworkConfig.class);
if (networkConfig == null) {
logger.error("Could not deserialize network config! Loading defaults");
networkConfig = new NetworkConfig();
}
} catch (IOException | IllegalStateException | JsonException e) {
} catch (IOException e) {
logger.error("Could not deserialize network config! Loading defaults");
networkConfig = new NetworkConfig();
}
@@ -182,12 +184,13 @@ class LegacyConfigProvider extends ConfigProvider {
}
if (apriltagFieldLayoutFile.exists()) {
try (var stream = new FileInputStream(apriltagFieldLayoutFile)) {
atfl = Jsonb.instance().type(AprilTagFieldLayout.class).fromJson(stream);
try {
atfl =
JacksonUtils.deserialize(apriltagFieldLayoutFile.toPath(), AprilTagFieldLayout.class);
if (atfl == 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);
atfl = null; // not required, nice to be explicit
}
@@ -224,14 +227,14 @@ class LegacyConfigProvider extends ConfigProvider {
// Delete old configs
FileUtils.deleteDirectory(camerasFolder.toPath());
try (var stream = new FileOutputStream(networkConfigFile)) {
Jsonb.instance().type(NetworkConfig.class).toJson(config.getNetworkConfig(), stream);
} catch (IOException | IllegalStateException | JsonException e) {
try {
JacksonUtils.serialize(networkConfigFile.toPath(), config.getNetworkConfig());
} catch (IOException e) {
logger.error("Could not save network config!", e);
}
try (var stream = new FileOutputStream(hardwareSettingsFile)) {
Jsonb.instance().type(HardwareSettings.class).toJson(config.getHardwareSettings(), stream);
} catch (IOException | IllegalStateException | JsonException e) {
try {
JacksonUtils.serialize(hardwareSettingsFile.toPath(), config.getHardwareSettings());
} catch (IOException e) {
logger.error("Could not save hardware config!", e);
}
@@ -246,11 +249,33 @@ class LegacyConfigProvider extends ConfigProvider {
subdir.toFile().mkdirs();
}
try (var stream = new FileOutputStream(Path.of(subdir.toString(), "config.json").toFile())) {
Jsonb.instance().type(CameraConfiguration.class).toJson(camConfig, stream);
} catch (IOException | IllegalStateException | JsonException e) {
try {
JacksonUtils.serialize(Path.of(subdir.toString(), "config.json"), camConfig);
} catch (IOException 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!");
return false; // TODO, deal with this. Do I need to?
@@ -264,9 +289,11 @@ class LegacyConfigProvider extends ConfigProvider {
for (var subdir : subdirectories) {
var cameraConfigPath = Path.of(subdir.toString(), "config.json");
CameraConfiguration loadedConfig = null;
try (var stream = new FileInputStream(cameraConfigPath.toFile())) {
loadedConfig = Jsonb.instance().type(CameraConfiguration.class).fromJson(stream);
} catch (IllegalStateException | JsonException e) {
try {
loadedConfig =
JacksonUtils.deserialize(
cameraConfigPath.toAbsolutePath(), CameraConfiguration.class);
} catch (JsonProcessingException e) {
logger.error("Camera config deserialization failed!", e);
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?
}
// 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);
}
} catch (IOException e) {

View File

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

View File

@@ -318,7 +318,7 @@ public class NeuralNetworkModelManager {
}
ModelProperties properties =
ConfigManager.getInstance().getConfig().getNeuralNetworkProperties().getModel(path);
ConfigManager.getInstance().getConfig().neuralNetworkPropertyManager().getModel(path);
if (properties == null) {
logger.warn(
@@ -332,7 +332,7 @@ public class NeuralNetworkModelManager {
// NeuralNetworkModelsSettings
ConfigManager.getInstance()
.getConfig()
.getNeuralNetworkProperties()
.neuralNetworkPropertyManager()
.addModelProperties(properties);
} catch (IllegalArgumentException | IOException e) {
logger.error("Failed to translate legacy model filename to properties: " + path, e);
@@ -486,7 +486,7 @@ public class NeuralNetworkModelManager {
.getConfig()
.setNeuralNetworkProperties(
supportedProperties.sum(
ConfigManager.getInstance().getConfig().getNeuralNetworkProperties()));
ConfigManager.getInstance().getConfig().neuralNetworkPropertyManager()));
}
public boolean clearModels() {
@@ -511,7 +511,7 @@ public class NeuralNetworkModelManager {
}
// Delete model info
return ConfigManager.getInstance().getConfig().getNeuralNetworkProperties().clear();
return ConfigManager.getInstance().getConfig().neuralNetworkPropertyManager().clear();
}
public File exportSingleModel(String modelPath) {
@@ -525,7 +525,7 @@ public class NeuralNetworkModelManager {
ModelProperties properties =
ConfigManager.getInstance()
.getConfig()
.getNeuralNetworkProperties()
.neuralNetworkPropertyManager()
.getModel(Path.of(modelPath));
String fileName = "";

View File

@@ -17,10 +17,9 @@
package org.photonvision.common.configuration;
import io.avaje.jsonb.Json;
import io.avaje.jsonb.JsonType;
import io.avaje.jsonb.Jsonb;
import io.avaje.jsonb.Types;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonProperty;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
@@ -28,29 +27,27 @@ import java.nio.file.Paths;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
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.Version;
@Json
public class NeuralNetworkModelsSettings {
/*
* 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)
*/
@Json
public record ModelProperties(
Path modelPath,
String nickname,
List<String> labels,
int resolutionWidth,
int resolutionHeight,
Family family,
Version version) {
@JsonProperty("modelPath") Path modelPath,
@JsonProperty("nickname") String nickname,
@JsonProperty("labels") List<String> labels,
@JsonProperty("resolutionWidth") int resolutionWidth,
@JsonProperty("resolutionHeight") int resolutionHeight,
@JsonProperty("family") Family family,
@JsonProperty("version") Version version) {
@JsonCreator
public ModelProperties {}
ModelProperties(ModelProperties other) {
this(
other.modelPath,
@@ -62,6 +59,13 @@ public class NeuralNetworkModelsSettings {
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 ===========
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 model, and should not change
@Json.Ignore
@JsonProperty("modelPathToProperties")
private HashMap<Path, ModelProperties> modelPathToProperties =
new HashMap<Path, ModelProperties>();
/**
* 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() {}
/**
* Constructor for the NeuralNetworkProperties class.
*
* <p>This object holds a HashMap 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.
* <p>This object holds a LinkedList of {@link ModelProperties} objects.
*
* @param modelPropertiesList When the class is constructed, it will hold the provided list
*/
@Json.Creator
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)));
}
public NeuralNetworkModelsSettings(HashMap<Path, ModelProperties> modelPropertiesList) {}
@Override
public String toString() {
@@ -268,7 +239,7 @@ public class NeuralNetworkModelsSettings {
*
* @return A list of all models
*/
@Json.Property("models")
@JsonIgnore
public ModelProperties[] getModels() {
return modelPathToProperties.values().toArray(new ModelProperties[0]);
}

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