Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ac8cccaa2f | ||
|
|
98fee3bd1f | ||
|
|
d7bac45e76 | ||
|
|
9883008ed3 | ||
|
|
01b8b8ccb3 | ||
|
|
fd3d9f6ccc | ||
|
|
d7f0e17dda | ||
|
|
966071ae2d | ||
|
|
502ae644a4 |
369
.github/workflows/build.yml
vendored
@@ -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')
|
||||
|
||||
5
.github/workflows/dependency-submission.yml
vendored
@@ -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
|
||||
|
||||
13
.github/workflows/lint-format.yml
vendored
@@ -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
|
||||
|
||||
54
.github/workflows/photon-api-docs.yml
vendored
@@ -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 }}
|
||||
|
||||
43
.github/workflows/python.yml
vendored
@@ -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
|
||||
|
||||
30
.github/workflows/website.yml
vendored
@@ -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
@@ -161,5 +161,3 @@ photon-client/playwright/.auth/
|
||||
shell.nix
|
||||
flake.nix
|
||||
flake.lock
|
||||
|
||||
/workspace
|
||||
|
||||
@@ -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/
|
||||
|
||||
10
README.md
@@ -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)
|
||||
|
||||
47
build.gradle
@@ -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 = {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"]
|
||||
|
||||
@@ -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.
|
||||
:::
|
||||
|
||||
@@ -3,8 +3,5 @@
|
||||
"ledPins" : [ 13, 18 ],
|
||||
"ledsCanDim" : true,
|
||||
"ledPWMFrequency" : 1000,
|
||||
"statusLEDType": "GreenYellow",
|
||||
"statusLEDPins": [ 5, 4 ],
|
||||
"statusLEDActiveHigh": false,
|
||||
"vendorFOV" : 75.76079874010732
|
||||
}
|
||||
|
||||
@@ -2,8 +2,5 @@
|
||||
"deviceName" : "Limelight 2",
|
||||
"ledPins" : [ 17, 18 ],
|
||||
"ledsCanDim" : false,
|
||||
"statusLEDType": "GreenYellow",
|
||||
"statusLEDPins": [ 5, 4 ],
|
||||
"statusLEDActiveHigh": false,
|
||||
"vendorFOV" : 75.76079874010732
|
||||
}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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.
|
||||
:::
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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++) {
|
||||
|
||||
@@ -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'
|
||||
```
|
||||
|
||||
@@ -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}",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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 |
@@ -5,7 +5,6 @@
|
||||
|
||||
common-errors
|
||||
logging
|
||||
status-leds
|
||||
camera-troubleshooting
|
||||
networking-troubleshooting
|
||||
unix-commands
|
||||
|
||||
@@ -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 }}
|
||||
@@ -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
|
||||
|
||||
|
||||
2
gradle/wrapper/gradle-wrapper.properties
vendored
@@ -1,6 +1,6 @@
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=permwrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-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
|
||||
|
||||
2
photon-client/env.d.ts
vendored
@@ -1,3 +1 @@
|
||||
/// <reference types="vite/client" />
|
||||
|
||||
declare module "vue3-virtual-scroll-list";
|
||||
|
||||
@@ -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 }]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
352
photon-client/pnpm-lock.yaml
generated
@@ -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:
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
allowBuilds:
|
||||
'@parcel/watcher': true
|
||||
core-js: true
|
||||
@@ -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>
|
||||
|
||||
BIN
photon-client/src/assets/images/blink.gif
Normal file
|
After Width: | Height: | Size: 17 KiB |
BIN
photon-client/src/assets/images/grow.gif
Normal file
|
After Width: | Height: | Size: 101 KiB |
BIN
photon-client/src/assets/images/idle.gif
Normal file
|
After Width: | Height: | Size: 2.6 KiB |
BIN
photon-client/src/assets/images/point.gif
Normal file
|
After Width: | Height: | Size: 82 KiB |
BIN
photon-client/src/assets/images/shrink.gif
Normal file
|
After Width: | Height: | Size: 102 KiB |
BIN
photon-client/src/assets/images/speak.gif
Normal file
|
After Width: | Height: | Size: 113 KiB |
BIN
photon-client/src/assets/images/wave.gif
Normal file
|
After Width: | Height: | Size: 99 KiB |
@@ -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>
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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 = () => {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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
|
||||
)
|
||||
});
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
>
|
||||
|
||||
@@ -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)"
|
||||
/>
|
||||
|
||||
@@ -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)"
|
||||
/>
|
||||
|
||||
@@ -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)
|
||||
"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 ||
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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({
|
||||
|
||||
452
photon-client/src/lib/quarky.js
Normal 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!",
|
||||
"Don’t forget to disable auto-exposure! Or enable it. I'm not sure. ",
|
||||
"Have you glued your lenses to keep them in focus?",
|
||||
"Don’t 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? It’s 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();
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
<span>{{ camera.name }}</span>
|
||||
<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
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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>
|
||||
|
||||
8
photon-client/tsconfig.config.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"extends": "@vue/tsconfig/tsconfig.json",
|
||||
"include": ["vite.config.*"],
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"types": ["node"]
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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="
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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 = "";
|
||||
|
||||
@@ -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]);
|
||||
}
|
||||
|
||||