mirror of
https://github.com/PhotonVision/photonvision
synced 2026-06-19 00:41:41 +00:00
Compare commits
74 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c04c8d76ed | ||
|
|
7b30c9306e | ||
|
|
ae22742f05 | ||
|
|
860d6807a8 | ||
|
|
e9006a2803 | ||
|
|
d3de87f72b | ||
|
|
be27bb9916 | ||
|
|
0525e762b4 | ||
|
|
4db3d7be57 | ||
|
|
02d0f2b047 | ||
|
|
7e9a67ec6a | ||
|
|
b78b0dad97 | ||
|
|
c146d559d0 | ||
|
|
645ff41ce9 | ||
|
|
6d579edcd5 | ||
|
|
6ef0f91600 | ||
|
|
a6288afcb4 | ||
|
|
996ca3649e | ||
|
|
0e834f0851 | ||
|
|
c221aed0c2 | ||
|
|
363b8274d0 | ||
|
|
2372e110f9 | ||
|
|
adfa39b6f4 | ||
|
|
94acd9d631 | ||
|
|
d587cd19bb | ||
|
|
3c017ab961 | ||
|
|
4c9b36aa5c | ||
|
|
de5fe76123 | ||
|
|
b8e9e4f5c7 | ||
|
|
dbf71029b7 | ||
|
|
c195d7ccc0 | ||
|
|
f640c28ac1 | ||
|
|
b2ee79164b | ||
|
|
d8dddf379f | ||
|
|
b99e85d0ba | ||
|
|
439f5fcd91 | ||
|
|
dbbe006fd3 | ||
|
|
803bdb222c | ||
|
|
d1a02a542c | ||
|
|
e970446c4c | ||
|
|
68fc1e7129 | ||
|
|
4412df1516 | ||
|
|
7764ccc533 | ||
|
|
872b587bae | ||
|
|
934eed21d2 | ||
|
|
c34c854583 | ||
|
|
83b4522bf3 | ||
|
|
d83d53650a | ||
|
|
0734d1b806 | ||
|
|
3ace6122b0 | ||
|
|
a5f9a0b673 | ||
|
|
ed1b31cb7f | ||
|
|
68bdb38a3d | ||
|
|
a5be3d062c | ||
|
|
3379a1a132 | ||
|
|
5f59e9ab22 | ||
|
|
515a1a3d78 | ||
|
|
03ffcb1215 | ||
|
|
131098bfdd | ||
|
|
b5277e5f4c | ||
|
|
52f1f7726f | ||
|
|
e19534da47 | ||
|
|
488646e755 | ||
|
|
a8a0024321 | ||
|
|
032deba775 | ||
|
|
7b240a027a | ||
|
|
d4bfe643d5 | ||
|
|
12446a6c44 | ||
|
|
846528ce9c | ||
|
|
7383a9040d | ||
|
|
ab09e7fa14 | ||
|
|
cd3d9b06bb | ||
|
|
fd4628d419 | ||
|
|
5fdfa3132f |
338
.github/workflows/build.yml
vendored
338
.github/workflows/build.yml
vendored
@@ -10,7 +10,7 @@ concurrency:
|
||||
cancel-in-progress: true
|
||||
|
||||
env:
|
||||
IMAGE_VERSION: v2026.1.3
|
||||
IMAGE_VERSION: v2027.0.0
|
||||
|
||||
jobs:
|
||||
|
||||
@@ -21,102 +21,116 @@ 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
|
||||
- name: Install Java 17
|
||||
uses: actions/setup-java@v5
|
||||
with:
|
||||
java-version: 17
|
||||
distribution: temurin
|
||||
- name: Install RoboRIO Toolchain
|
||||
run: ./gradlew installRoboRioToolchain
|
||||
- name: Delete duplicate toolchains
|
||||
run: |
|
||||
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%/}"
|
||||
# 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
|
||||
# # 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"
|
||||
typecheck-client:
|
||||
name: "Typecheck Client"
|
||||
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
|
||||
- name: Install Java 17
|
||||
uses: actions/setup-java@v5
|
||||
with:
|
||||
java-version: 17
|
||||
distribution: temurin
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
uses: pnpm/action-setup@v5
|
||||
with:
|
||||
version: 10
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: 22
|
||||
node-version: 24
|
||||
cache: pnpm
|
||||
cache-dependency-path: photon-client/pnpm-lock.yaml
|
||||
- name: Typecheck Client
|
||||
working-directory: photon-client
|
||||
run: |
|
||||
pnpm install --frozen-lockfile
|
||||
pnpm type-check
|
||||
playwright-tests:
|
||||
name: "Playwright E2E tests"
|
||||
runs-on: ubuntu-24.04
|
||||
needs: [validation]
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v6
|
||||
- uses: actions/setup-java@v5
|
||||
with:
|
||||
java-version: 25
|
||||
distribution: temurin
|
||||
- uses: pnpm/action-setup@v5
|
||||
with:
|
||||
version: 10
|
||||
- uses: actions/setup-node@v6
|
||||
with:
|
||||
cache: pnpm
|
||||
cache-dependency-path: photon-client/pnpm-lock.yaml
|
||||
node-version: 24
|
||||
- name: Setup tests
|
||||
working-directory: photon-client
|
||||
run: |
|
||||
@@ -127,10 +141,10 @@ jobs:
|
||||
- name: Run Playwright tests
|
||||
working-directory: photon-client
|
||||
run: pnpm test
|
||||
- uses: actions/upload-artifact@v6
|
||||
- uses: actions/upload-artifact@v7
|
||||
if: ${{ !cancelled() }}
|
||||
with:
|
||||
name: playwright-report
|
||||
name: "Playwright Report"
|
||||
path: photon-client/playwright-report/
|
||||
retention-days: 30
|
||||
build-gradle:
|
||||
@@ -138,26 +152,24 @@ 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
|
||||
- name: Install Java 17
|
||||
uses: actions/setup-java@v5
|
||||
- uses: actions/setup-java@v5
|
||||
with:
|
||||
java-version: 17
|
||||
java-version: 25
|
||||
distribution: temurin
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
- uses: pnpm/action-setup@v5
|
||||
with:
|
||||
version: 10
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v6
|
||||
- uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: 22
|
||||
cache: pnpm
|
||||
cache-dependency-path: photon-client/pnpm-lock.yaml
|
||||
node-version: 24
|
||||
- name: Gradle Build
|
||||
run: ./gradlew photon-targeting:build photon-core:build photon-server:build -x check
|
||||
- name: Gradle Tests and Coverage
|
||||
@@ -184,7 +196,7 @@ jobs:
|
||||
working-directory: docs
|
||||
run: |
|
||||
make html
|
||||
- uses: actions/upload-artifact@v6
|
||||
- uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: built-docs
|
||||
path: docs/build/html
|
||||
@@ -198,10 +210,9 @@ jobs:
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Install Java 17
|
||||
uses: actions/setup-java@v5
|
||||
- uses: actions/setup-java@v5
|
||||
with:
|
||||
java-version: 17
|
||||
java-version: 25
|
||||
distribution: temurin
|
||||
|
||||
# grab all tags
|
||||
@@ -214,9 +225,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@v6
|
||||
- uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: photonlib-vendor-json
|
||||
archive: false
|
||||
path: photon-lib/build/generated/vendordeps/photonlib-*.json
|
||||
|
||||
build-photonlib-host:
|
||||
@@ -226,12 +237,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 }}
|
||||
@@ -240,10 +251,9 @@ jobs:
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Install Java 17
|
||||
uses: actions/setup-java@v5
|
||||
- uses: actions/setup-java@v5
|
||||
with:
|
||||
java-version: 17
|
||||
java-version: 25
|
||||
distribution: temurin
|
||||
- run: git fetch --tags --force
|
||||
- run: ./gradlew photon-targeting:build photon-lib:build
|
||||
@@ -252,10 +262,10 @@ jobs:
|
||||
name: Publish
|
||||
env:
|
||||
ARTIFACTORY_API_KEY: ${{ secrets.ARTIFACTORY_API_KEY }}
|
||||
if: github.event_name == 'push' && github.repository_owner == 'photonvision'
|
||||
if: github.event_name == 'push' && github.repository_owner == 'photonvision' && (github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/v'))
|
||||
# Copy artifacts to build/outputs/maven
|
||||
- run: ./gradlew photon-lib:publish photon-targeting:publish -PcopyOfflineArtifacts
|
||||
- uses: actions/upload-artifact@v6
|
||||
- uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: maven-${{ matrix.artifact-name }}
|
||||
path: build/outputs
|
||||
@@ -265,13 +275,13 @@ jobs:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- container: wpilib/roborio-cross-ubuntu:2025-24.04
|
||||
artifact-name: Athena
|
||||
build-options: "-Ponlylinuxathena"
|
||||
- container: wpilib/raspbian-cross-ubuntu:bullseye-22.04
|
||||
- container: wpilib/systemcore-cross-ubuntu:2027-24.04
|
||||
artifact-name: SystemCore
|
||||
build-options: "-Ponlylinuxsystemcore"
|
||||
- container: wpilib/raspbian-cross-ubuntu:2027-bookworm-24.04
|
||||
artifact-name: Raspbian
|
||||
build-options: "-Ponlylinuxarm32"
|
||||
- container: wpilib/aarch64-cross-ubuntu:bullseye-22.04
|
||||
- container: wpilib/aarch64-cross-ubuntu:2027-bookworm-24.04
|
||||
artifact-name: Aarch64
|
||||
build-options: "-Ponlylinuxarm64"
|
||||
|
||||
@@ -293,10 +303,10 @@ jobs:
|
||||
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'
|
||||
if: github.event_name == 'push' && github.repository_owner == 'photonvision' && (github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/v'))
|
||||
# Copy artifacts to build/outputs/maven
|
||||
- run: ./gradlew photon-lib:publish photon-targeting:publish -PcopyOfflineArtifacts ${{ matrix.build-options }}
|
||||
- uses: actions/upload-artifact@v6
|
||||
- uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: maven-${{ matrix.artifact-name }}
|
||||
path: build/outputs
|
||||
@@ -311,7 +321,7 @@ jobs:
|
||||
fetch-depth: 0
|
||||
- run: git fetch --tags --force
|
||||
# download all maven-* artifacts to outputs/
|
||||
- uses: actions/download-artifact@v7
|
||||
- uses: actions/download-artifact@v8
|
||||
with:
|
||||
merge-multiple: true
|
||||
path: output
|
||||
@@ -321,7 +331,7 @@ jobs:
|
||||
name: ZIP stuff up
|
||||
working-directory: output
|
||||
- run: ls output
|
||||
- uses: actions/upload-artifact@v6
|
||||
- uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: photonlib-offline
|
||||
path: output/*.zip
|
||||
@@ -335,7 +345,7 @@ jobs:
|
||||
include:
|
||||
- os: ubuntu-24.04
|
||||
artifact-name: Linux
|
||||
arch-override: linuxx64
|
||||
arch-override: linuxx86-64
|
||||
- os: ubuntu-24.04
|
||||
artifact-name: LinuxArm64
|
||||
arch-override: linuxarm64
|
||||
@@ -347,25 +357,22 @@ jobs:
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Install Java 17
|
||||
uses: actions/setup-java@v5
|
||||
- uses: actions/setup-java@v5
|
||||
with:
|
||||
java-version: 17
|
||||
java-version: 25
|
||||
distribution: temurin
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
- uses: pnpm/action-setup@v5
|
||||
with:
|
||||
version: 10
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v6
|
||||
- uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: 22
|
||||
node-version: 24
|
||||
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@v7
|
||||
- uses: actions/download-artifact@v8
|
||||
with:
|
||||
name: built-docs
|
||||
path: photon-server/src/main/resources/web/docs
|
||||
@@ -373,11 +380,11 @@ jobs:
|
||||
if: ${{ (matrix.arch-override != 'none') }}
|
||||
- run: ./gradlew photon-server:shadowJar
|
||||
if: ${{ (matrix.arch-override == 'none') }}
|
||||
- uses: actions/upload-artifact@v6
|
||||
- uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: jar-${{ matrix.artifact-name }}
|
||||
archive: false
|
||||
path: photon-server/build/libs
|
||||
- uses: actions/upload-artifact@v6
|
||||
- uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: photon-targeting_jar-${{ matrix.artifact-name }}
|
||||
path: photon-targeting/build/libs
|
||||
@@ -394,7 +401,7 @@ jobs:
|
||||
arch-override: macarm64
|
||||
- os: macos-latest
|
||||
artifact-name: macOS
|
||||
arch-override: macx64
|
||||
arch-override: macx86-64
|
||||
|
||||
runs-on: ${{ matrix.os }}
|
||||
name: "Build fat JAR - ${{ matrix.artifact-name }}"
|
||||
@@ -409,8 +416,8 @@ jobs:
|
||||
matrix:
|
||||
include:
|
||||
- os: windows-latest
|
||||
artifact-name: Win64
|
||||
arch-override: winx64
|
||||
artifact-name: Win
|
||||
arch-override: winx86-64
|
||||
|
||||
runs-on: ${{ matrix.os }}
|
||||
name: "Build fat JAR - ${{ matrix.artifact-name }}"
|
||||
@@ -425,24 +432,26 @@ jobs:
|
||||
matrix:
|
||||
include:
|
||||
- os: ubuntu-24.04
|
||||
artifact-name: jar-Linux
|
||||
artifact-name: photonvision-*-linuxx86-64.jar
|
||||
extraOpts: -Djdk.lang.Process.launchMechanism=vfork
|
||||
- os: windows-latest
|
||||
artifact-name: jar-Win64
|
||||
artifact-name: photonvision-*-winx86-64.jar
|
||||
- os: macos-latest
|
||||
artifact-name: jar-macOS
|
||||
artifact-name: photonvision-*-macarm64.jar
|
||||
- os: ubuntu-24.04-arm
|
||||
artifact-name: photonvision-*-linuxarm64.jar
|
||||
|
||||
|
||||
runs-on: ${{ matrix.os }}
|
||||
|
||||
steps:
|
||||
- name: Install Java 17
|
||||
uses: actions/setup-java@v5
|
||||
- uses: actions/setup-java@v5
|
||||
with:
|
||||
java-version: 17
|
||||
java-version: 25
|
||||
distribution: temurin
|
||||
- uses: actions/download-artifact@v7
|
||||
- uses: actions/download-artifact@v8
|
||||
with:
|
||||
name: ${{ matrix.artifact-name }}
|
||||
pattern: ${{ matrix.artifact-name }}
|
||||
# The jar is run twice to exercise different code paths.
|
||||
- run: |
|
||||
echo "=== First run ==="
|
||||
@@ -479,85 +488,66 @@ 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
|
||||
@@ -573,9 +563,9 @@ jobs:
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- uses: actions/download-artifact@v7
|
||||
- uses: actions/download-artifact@v8
|
||||
with:
|
||||
name: jar-${{ matrix.artifact-name }}
|
||||
pattern: photonvision-*-linuxarm64.jar
|
||||
- uses: photonvision/photon-image-runner@HEAD
|
||||
name: Generate image
|
||||
id: generate_image
|
||||
@@ -611,10 +601,9 @@ jobs:
|
||||
# Point smoketest to the old image
|
||||
echo "smoketest_image_loc=${{ steps.generate_image.outputs.image }}" >> $GITHUB_ENV
|
||||
|
||||
- uses: actions/upload-artifact@v6
|
||||
name: Upload image
|
||||
- uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: image-${{ matrix.image_suffix }}
|
||||
archive: false
|
||||
path: photonvision*.xz
|
||||
|
||||
# This is done after uploading the image to avoid contaminating the image with logs, caches, etc.
|
||||
@@ -643,25 +632,24 @@ jobs:
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
# Download all fat JARs
|
||||
- uses: actions/download-artifact@v7
|
||||
- uses: actions/download-artifact@v8
|
||||
with:
|
||||
merge-multiple: true
|
||||
pattern: jar-*
|
||||
pattern: photonvision-*.jar
|
||||
# Download offline photonlib
|
||||
- uses: actions/download-artifact@v7
|
||||
- uses: actions/download-artifact@v8
|
||||
with:
|
||||
merge-multiple: true
|
||||
pattern: photonlib-offline
|
||||
# Download vendor json
|
||||
- uses: actions/download-artifact@v7
|
||||
- uses: actions/download-artifact@v8
|
||||
with:
|
||||
merge-multiple: true
|
||||
pattern: photonlib-vendor-json
|
||||
pattern: photonlib-*.json
|
||||
# Download all images
|
||||
- uses: actions/download-artifact@v7
|
||||
- uses: actions/download-artifact@v8
|
||||
with:
|
||||
merge-multiple: true
|
||||
pattern: image-*
|
||||
pattern: photonvision-*.xz
|
||||
|
||||
- run: find
|
||||
# Push to dev release
|
||||
|
||||
5
.github/workflows/dependency-submission.yml
vendored
5
.github/workflows/dependency-submission.yml
vendored
@@ -13,10 +13,9 @@ jobs:
|
||||
steps:
|
||||
- name: Checkout sources
|
||||
uses: actions/checkout@v6
|
||||
- name: Setup Java
|
||||
uses: actions/setup-java@v5
|
||||
- uses: actions/setup-java@v5
|
||||
with:
|
||||
distribution: 'temurin'
|
||||
java-version: 17
|
||||
java-version: 25
|
||||
- name: Generate and submit dependency graph
|
||||
uses: gradle/actions/dependency-submission@v5
|
||||
|
||||
13
.github/workflows/lint-format.yml
vendored
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@v6
|
||||
- uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: wpiformat fixes
|
||||
archive: false
|
||||
path: wpiformat-fixes.patch
|
||||
if: ${{ failure() }}
|
||||
javaformat:
|
||||
@@ -61,7 +61,7 @@ jobs:
|
||||
fetch-depth: 0
|
||||
- uses: actions/setup-java@v5
|
||||
with:
|
||||
java-version: 17
|
||||
java-version: 25
|
||||
distribution: temurin
|
||||
- run: |
|
||||
set +e
|
||||
@@ -81,13 +81,12 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
uses: pnpm/action-setup@v5
|
||||
with:
|
||||
version: 10
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v6
|
||||
- uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: 22
|
||||
node-version: 24
|
||||
cache: pnpm
|
||||
cache-dependency-path: photon-client/pnpm-lock.yaml
|
||||
- name: Install Dependencies
|
||||
|
||||
21
.github/workflows/photon-api-docs.yml
vendored
21
.github/workflows/photon-api-docs.yml
vendored
@@ -30,21 +30,19 @@ jobs:
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
- uses: pnpm/action-setup@v5
|
||||
with:
|
||||
version: 10
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v6
|
||||
- uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: 22
|
||||
node-version: 24
|
||||
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@v6
|
||||
- uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: built-demo
|
||||
path: photon-client/dist/
|
||||
@@ -60,16 +58,15 @@ jobs:
|
||||
fetch-depth: 0
|
||||
- name: Fetch tags
|
||||
run: git fetch --tags --force
|
||||
- name: Install Java 17
|
||||
uses: actions/setup-java@v5
|
||||
- uses: actions/setup-java@v5
|
||||
with:
|
||||
java-version: 17
|
||||
java-version: 25
|
||||
distribution: temurin
|
||||
- name: Build javadocs/doxygen
|
||||
run: |
|
||||
chmod +x gradlew
|
||||
./gradlew photon-docs:generateJavaDocs photon-docs:doxygen
|
||||
- uses: actions/upload-artifact@v6
|
||||
- uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: docs-java-cpp
|
||||
path: photon-docs/build/docs
|
||||
@@ -80,7 +77,7 @@ jobs:
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
# Download docs artifact
|
||||
- uses: actions/download-artifact@v7
|
||||
- uses: actions/download-artifact@v8
|
||||
with:
|
||||
pattern: docs-*
|
||||
- run: find .
|
||||
@@ -106,7 +103,7 @@ jobs:
|
||||
needs: [build_demo]
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- uses: actions/download-artifact@v7
|
||||
- uses: actions/download-artifact@v8
|
||||
with:
|
||||
name: built-demo
|
||||
- run: find .
|
||||
|
||||
11
.github/workflows/python.yml
vendored
11
.github/workflows/python.yml
vendored
@@ -36,7 +36,7 @@ jobs:
|
||||
run: python setup.py sdist bdist_wheel
|
||||
|
||||
- name: Upload artifacts
|
||||
uses: actions/upload-artifact@v6
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: dist
|
||||
path: ./photon-lib/py/dist/
|
||||
@@ -62,7 +62,7 @@ jobs:
|
||||
pip install pytest mypy
|
||||
|
||||
- name: Download artifacts
|
||||
uses: actions/download-artifact@v6
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
name: dist
|
||||
path: dist/
|
||||
@@ -100,9 +100,10 @@ jobs:
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
python -m pip install mypy
|
||||
|
||||
- name: Download artifacts
|
||||
uses: actions/download-artifact@v6
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
name: dist
|
||||
path: ./photon-lib/py/dist/
|
||||
@@ -120,7 +121,7 @@ jobs:
|
||||
for folder in */;
|
||||
do
|
||||
echo $folder
|
||||
./run.sh $folder
|
||||
./test.sh $folder
|
||||
done
|
||||
|
||||
deploy:
|
||||
@@ -131,7 +132,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Download artifacts
|
||||
uses: actions/download-artifact@v6
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
name: dist
|
||||
path: dist/
|
||||
|
||||
30
.github/workflows/website.yml
vendored
30
.github/workflows/website.yml
vendored
@@ -11,13 +11,12 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
uses: pnpm/action-setup@v5
|
||||
with:
|
||||
version: 10
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v6
|
||||
- uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: 22
|
||||
node-version: 24
|
||||
cache: pnpm
|
||||
cache-dependency-path: website/pnpm-lock.yaml
|
||||
- name: Install packages
|
||||
@@ -37,22 +36,27 @@ jobs:
|
||||
|
||||
format-check:
|
||||
name: Check Formatting
|
||||
defaults:
|
||||
run:
|
||||
working-directory: website
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
- uses: pnpm/action-setup@v5
|
||||
with:
|
||||
version: 10
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v6
|
||||
- uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: 22
|
||||
node-version: 24
|
||||
cache: pnpm
|
||||
cache-dependency-path: website/pnpm-lock.yaml
|
||||
- name: Install Packages
|
||||
run: pnpm i --frozen-lockfile
|
||||
working-directory: website
|
||||
- name: Run Formatting Check
|
||||
run: pnpm prettier -c .
|
||||
working-directory: website
|
||||
- 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
|
||||
|
||||
7
.gitignore
vendored
7
.gitignore
vendored
@@ -156,3 +156,10 @@ photon-client/playwright-report/
|
||||
photon-client/blob-report/
|
||||
photon-client/playwright/.cache/
|
||||
photon-client/playwright/.auth/
|
||||
|
||||
# Nix files
|
||||
shell.nix
|
||||
flake.nix
|
||||
flake.lock
|
||||
|
||||
/workspace
|
||||
|
||||
@@ -2,7 +2,7 @@ cppHeaderFileInclude {
|
||||
\.h$
|
||||
}
|
||||
|
||||
modifiableFileExclude {
|
||||
generatedFileExclude {
|
||||
photon-lib/py/photonlibpy/generated/
|
||||
photon-targeting/src/generated/
|
||||
photon-targeting/src/main/native/cpp/photon/constrained_solvepnp/generate/
|
||||
|
||||
10
README.md
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:
|
||||
* winx64
|
||||
* winx86-64
|
||||
* winarm64
|
||||
* macx64
|
||||
* macx86-64
|
||||
* macarm64
|
||||
* linuxx64
|
||||
* linuxx86-64
|
||||
* 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 installRoboRioToolchain`
|
||||
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`
|
||||
|
||||
## 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)
|
||||
* [FasterXML](https://github.com/FasterXML) - Specifically [jackson](https://github.com/FasterXML/jackson)
|
||||
* [Avaje](https://avaje.io) - Specifically [jsonb](https://avaje.io/jsonb/)
|
||||
* [MessagePack for Java](https://github.com/msgpack/msgpack-java)
|
||||
* [OSHI](https://github.com/oshi/oshi)
|
||||
* [QuickBuffers](https://github.com/HebiRobotics/QuickBuffers)
|
||||
|
||||
47
build.gradle
47
build.gradle
@@ -1,16 +1,17 @@
|
||||
import edu.wpi.first.toolchain.*
|
||||
import org.wpilib.toolchain.*
|
||||
|
||||
plugins {
|
||||
id "cpp"
|
||||
id "com.diffplug.spotless" version "8.1.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.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 "org.ysb33r.doxygen" version "2.0.0" apply false
|
||||
id 'com.gradleup.shadow' version '8.3.4' apply false
|
||||
id "com.github.node-gradle.node" version "7.0.1" apply false
|
||||
id 'com.gradleup.shadow' version '9.0.0' apply false
|
||||
id "com.github.node-gradle.node" version "7.1.0" apply false
|
||||
}
|
||||
|
||||
allprojects {
|
||||
@@ -21,6 +22,7 @@ allprojects {
|
||||
maven { url = "https://maven.photonvision.org/releases" }
|
||||
maven { url = "https://maven.photonvision.org/snapshots" }
|
||||
}
|
||||
wpilibRepositories.use2027Repos()
|
||||
wpilibRepositories.addAllReleaseRepositories(it)
|
||||
wpilibRepositories.addAllDevelopmentRepositories(it)
|
||||
}
|
||||
@@ -28,20 +30,21 @@ allprojects {
|
||||
ext.localMavenURL = file("$project.buildDir/outputs/maven")
|
||||
ext.allOutputsFolder = file("$project.buildDir/outputs")
|
||||
|
||||
// Configure the version number.
|
||||
apply from: "versioningHelper.gradle"
|
||||
|
||||
ext {
|
||||
wpilibVersion = "2026.2.1"
|
||||
wpimathVersion = wpilibVersion
|
||||
openCVYear = "2025"
|
||||
openCVversion = "4.10.0-3"
|
||||
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";
|
||||
|
||||
javalinVersion = "6.7.0"
|
||||
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";
|
||||
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";
|
||||
|
||||
pubVersion = versionString
|
||||
isDev = pubVersion.startsWith("dev")
|
||||
@@ -62,7 +65,7 @@ spotless {
|
||||
java {
|
||||
target fileTree('.') {
|
||||
include '**/*.java'
|
||||
exclude '**/build/**', '**/build-*/**', '**/src/generated/**'
|
||||
exclude '**/build/**', '**/build-*/**', '**/src/generated/**', "**/bin/generated-sources/**"
|
||||
}
|
||||
toggleOffOn()
|
||||
googleJavaFormat()
|
||||
@@ -94,7 +97,7 @@ spotless {
|
||||
}
|
||||
|
||||
wrapper {
|
||||
gradleVersion = '8.14.3'
|
||||
gradleVersion = '9.4.0'
|
||||
}
|
||||
|
||||
ext.getCurrentArch = {
|
||||
|
||||
@@ -36,7 +36,6 @@ 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"]
|
||||
myst_enable_extensions = ["colon_fence", "substitution", "attrs_inline"]
|
||||
|
||||
@@ -1,19 +1,17 @@
|
||||
# NetworkTables API
|
||||
|
||||
## About
|
||||
## Usage
|
||||
|
||||
:::{warning}
|
||||
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!**
|
||||
PhotonVision's NetworkTables API is not designed for robot consumption, only internal at-a-glance debugging.**
|
||||
:::
|
||||
|
||||
**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
|
||||
@@ -64,9 +62,3 @@ 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.
|
||||
:::
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
{
|
||||
"deviceName" : "Limelight 2+",
|
||||
"supportURL" : "https://limelightvision.io",
|
||||
"ledPins" : [ 13, 18 ],
|
||||
"ledsCanDim" : true,
|
||||
"ledPWMFrequency" : 1000,
|
||||
"statusLEDType": "GreenYellow",
|
||||
"statusLEDPins": [ 5, 4 ],
|
||||
"statusLEDActiveHigh": false,
|
||||
"vendorFOV" : 75.76079874010732
|
||||
}
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
{
|
||||
"deviceName" : "Limelight 2",
|
||||
"supportURL" : "https://limelightvision.io",
|
||||
"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 Linuxx64.jar file.
|
||||
If your coprocessor has an 64 bit x86 based CPU architecture (Mini PC, laptop, etc.), download the Linuxx86-64.jar file.
|
||||
:::
|
||||
|
||||
:::{warning}
|
||||
|
||||
@@ -1,53 +1,9 @@
|
||||
# Mac OS Installation
|
||||
# MacOS Installation
|
||||
|
||||
:::{warning}
|
||||
Due to current [cscore](https://github.com/wpilibsuite/allwpilib/tree/main/cscore) restrictions, the PhotonVision server backend may have issues running macOS.
|
||||
:::
|
||||
## Builds
|
||||
|
||||
:::{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).
|
||||
:::
|
||||
Builds for macOS are currently generated in CI for testing compatibility. This allows us to ensure that macOS remains a viable development platform.
|
||||
|
||||
VERY Limited macOS support is available.
|
||||
## Use as a coprocessor
|
||||
|
||||
## 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.
|
||||
:::
|
||||
This is not a recommended path. MacOS is not officially supported, and your mileage may vary.
|
||||
|
||||
@@ -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 winx64.jar file.
|
||||
Go to the [GitHub releases page](https://github.com/PhotonVision/photonvision/releases) and download the winx86-64.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>` 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).
|
||||
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).
|
||||
|
||||
```{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()) {
|
||||
frc::Transform3d fieldToCamera = multiTagResult->estimatedPose.best;
|
||||
wpi::math::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}
|
||||
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.
|
||||
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.
|
||||
:::
|
||||
|
||||
:::{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) 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).
|
||||
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).
|
||||
|
||||
**Node JS:**
|
||||
|
||||
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/).
|
||||
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/).
|
||||
|
||||
**pnpm:**
|
||||
|
||||
@@ -47,6 +47,30 @@ 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.getFPGATimestamp();
|
||||
var t1 = Timer.getMonotonicTimestamp();
|
||||
light.set(true);
|
||||
var t2 = Timer.getFPGATimestamp();
|
||||
var t2 = Timer.getMonotonicTimestamp();
|
||||
|
||||
|
||||
for (int i = 0; i < 100; i++) {
|
||||
|
||||
68
docs/source/docs/contributing/guidelines.md
Normal file
68
docs/source/docs/contributing/guidelines.md
Normal file
@@ -0,0 +1,68 @@
|
||||
# Welcome!
|
||||
|
||||
First and foremost, welcome to PhotonVision Development! We're pumped that you're interested to jump in, and help out!
|
||||
|
||||
Like most things in FIRST, PhotonVision is reliant on the hard work of dedicated volunteers. It doesn't exist without you. You're joining in with a strong tradition of open-source software development.
|
||||
|
||||
This page talks a bit about how we develop PhotonVision, as a community. It applies to all repos and community aspects for PhotonVision.
|
||||
|
||||
## Getting Started
|
||||
|
||||
The very first thing we'd recommend - get your computer set up to be able to build and run PhotonVision. [Docs for that are here](building-photon.md).
|
||||
|
||||
The two best ways to figure out what to do first:
|
||||
|
||||
1. Take a look at [the main PhotonVision Repo's Issues](https://github.com/PhotonVision/photonvision/issues) - especially those marked `bug` or `good first issue`!
|
||||
2. Connect on [the Discord Server](https://discord.gg/wYxTwym) - Introduce yourself, and talk about what you're interested in!
|
||||
|
||||
From there - assign yourself to an issue if you intend to work it. Create a Fork in github, and make and test your changes. Once passing builds, meeting your expectations, and passing CI, open a pull request back to the main repo.
|
||||
|
||||
## Submitting A PR
|
||||
|
||||
Pull Requests are the mechanism used to ensure we only merge high-quality, reviewed, concrete, and organized changes to the codebase.
|
||||
|
||||
Things that peer reviewers will look for include:
|
||||
|
||||
* All CI checks are passing on the server.
|
||||
* Documentation - does the PR match the issue? Is the description detailed?
|
||||
* Cohesiveness - does the PR express a singular, related set of changes?
|
||||
* Architecture - is the code consistent with other code around it? Is it maintainable for the long term?
|
||||
* Testing - have unit tests been added as appropriate? Has the change been tested on real hardware?
|
||||
|
||||
Work as you can to clean up any changes requested. Once all changes are addressed, the contain should get merged, and included in the next release! Horary!
|
||||
|
||||
## General Developer Interaction
|
||||
|
||||
The main guiding principle: remember that we're all volunteers. While promptness is always appreciated (and occasionally required as release deadlines approach), it's important to remember each individual is only contributing when their personal schedule allows. Expect delays, prefer asynchronous communication, and be polite with reminders.
|
||||
|
||||
While most of the community members are either FIRST robotics mentors or students, the PhotonVision development team is primarily focused on delivering high quality software. Mentorship can and does occur, but is not the primary goal. Members are expected to do their learning fairly independently.
|
||||
|
||||
Seek to build trust in the quality of your work. Think carefully on your opinions before asking others to think about them too.
|
||||
|
||||
Bias toward action, and productionizable code. Limit the number of active PR's to help keep focus.
|
||||
|
||||
Finally, be sure to embody the ethos of Gracious Professionalism in all your actions, on all platforms in the project. See more in our [code of conduct](https://github.com/PhotonVision/.github/blob/738dfcb792fdbfc2e8408c0135e389179fc483c0/codeofcoduct.md)
|
||||
|
||||
### AI Usage
|
||||
|
||||
Coding assistants driven by Large Language Models ("AI") are extremely powerful tools, and have been used on more than one occasion to accelerate development.
|
||||
|
||||
PhotonVision still maintains a fundamental philosophy that the human submitting the pull request is responsible for the code and its behavior, regardless of the tools used to create it.
|
||||
|
||||
These tools can also generate a large volume of code changes, very rapidly. The above guidelines on PR quality still apply - large, undirected, or overly-scoped PR's are likely to be ignored, regardless of tooling used to generate them.
|
||||
|
||||
### Violations
|
||||
|
||||
We're thankful that we've rarely experienced major issues in our community. In all cases, the project leads shall have the final decision making authority when dealing with violations of these guidelines.
|
||||
|
||||
## Yearly Development Cycle
|
||||
|
||||
PhotonVision's Development Cycle follows the same general flow as the FRC Build Season. Larger experiments get done over the summer, fall focuses on testing and "production-readiness", build season focuses on keeping all teams running smoothly.
|
||||
|
||||
The actual priorities also shift as developers have more or time to commit, or express interest in specific direction.
|
||||
|
||||
PR's are absolutely always welcome. Just note depending on the scope and size, they may not be reviewed or merged immediately.
|
||||
|
||||
## Project Governance
|
||||
|
||||
This project is jointly lead by Matt and Banks, who may be contacted on Discord. They serve as a "Benevolent leader for life" role, coordinating and approving architecture as needed, and curating the team of developers who have Pull Request review and merge responsibilities.
|
||||
@@ -1,6 +1,7 @@
|
||||
# Contributing to PhotonVision Projects
|
||||
|
||||
```{toctree}
|
||||
guidelines
|
||||
building-photon
|
||||
building-docs
|
||||
linting
|
||||
|
||||
@@ -34,10 +34,16 @@ 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'
|
||||
alias pvLint='wpiformat -v && ./gradlew spotlessApply && pnpm -C photon-client lint && pnpm -C photon-client format && doc8 docs && pnpm -C website format'
|
||||
```
|
||||
|
||||
@@ -19,14 +19,18 @@ When running on Linux, PhotonVision can use [diozero](https://www.diozero.com) t
|
||||
"ledsCanDim" : true,
|
||||
"ledBrightnessRange" : [ 0, 100 ],
|
||||
"ledPWMFrequency" : 0,
|
||||
"statusRGBPins" : [ ],
|
||||
"statusRGBActiveHigh" : false,
|
||||
"statusLEDType": "RGB",
|
||||
"statusLEDPins" : [ ],
|
||||
"statusLEDActiveHigh" : false,
|
||||
}
|
||||
```
|
||||
|
||||
:::{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.
|
||||
:::
|
||||
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>`
|
||||
|
||||
### GPIO Pinout
|
||||
|
||||
@@ -103,9 +107,9 @@ If your hardware contains a camera with a known field of vision, it can be enter
|
||||
}
|
||||
```
|
||||
|
||||
## Cosmetic & Branding
|
||||
## Device Name Branding
|
||||
|
||||
To help differentiate your hardware from other solutions, some customization is allowed.
|
||||
To help differentiate your hardware from other solutions, a device name may be set.
|
||||
|
||||
```{eval-rst}
|
||||
.. tab-set-code::
|
||||
@@ -113,8 +117,6 @@ To help differentiate your hardware from other solutions, some customization is
|
||||
|
||||
{
|
||||
"deviceName" : "Super Cool Custom Hardware",
|
||||
"deviceLogoPath" : "",
|
||||
"supportURL" : "https://cat-bounce.com/",
|
||||
}
|
||||
```
|
||||
|
||||
@@ -132,14 +134,13 @@ Here is a complete example `hardwareConfig.json`:
|
||||
|
||||
{
|
||||
"deviceName" : "Blinky McBlinkface",
|
||||
"deviceLogoPath" : "",
|
||||
"supportURL" : "https://www.youtube.com/watch?v=b-CvLWbfZhU",
|
||||
"ledPins" : [2, 13],
|
||||
"ledsCanDim" : true,
|
||||
"ledBrightnessRange" : [ 0, 100 ],
|
||||
"ledPWMFrequency" : 0,
|
||||
"statusRGBPins" : [ ],
|
||||
"statusRGBActiveHigh" : false,
|
||||
"statusLEDType": "RGB",
|
||||
"statusLEDPins" : [ ],
|
||||
"statusLEDActiveHigh" : 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 22.04 LTS or Windows 10/11
|
||||
- Ubuntu 24.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,6 +8,8 @@ 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,6 +8,8 @@ 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.
|
||||
units::second_t latency = result.GetLatency();
|
||||
wpi::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.
|
||||
wpi::ArrayRef<photonlib::PhotonTrackedTarget> targets = result.GetTargets();
|
||||
std::span<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();
|
||||
frc::Transform2d pose = target.GetCameraToTarget();
|
||||
wpi::SmallVector<std::pair<double, double>, 4> corners = target.GetCorners();
|
||||
wpi::math::Transform2d pose = target.GetCameraToTarget();
|
||||
wpi::util::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();
|
||||
frc::Transform3d bestCameraToTarget = target.getBestCameraToTarget();
|
||||
frc::Transform3d alternateCameraToTarget = target.getAlternateCameraToTarget();
|
||||
wpi::math::Transform3d bestCameraToTarget = target.getBestCameraToTarget();
|
||||
wpi::math::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
|
||||
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);
|
||||
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);
|
||||
|
||||
.. 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.
|
||||
frc::Translation2d translation = photonlib::PhotonUtils::EstimateCameraToTargetTranslation(
|
||||
distance, frc::Rotation2d(units::degree_t(-target.GetYaw())));
|
||||
wpi::math::Translation2d translation = photonlib::PhotonUtils::EstimateCameraToTargetTranslation(
|
||||
distance, wpi::math::Rotation2d(wpi::units::degree_t(-target.GetYaw())));
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
|
||||
@@ -52,10 +52,10 @@ Only use a static IP when connected to the **robot radio**, and never when testi
|
||||
3. Open the settings tab on the left pane.
|
||||
4. Under the Networking section, set your team number.
|
||||
5. Change your IP to Static.
|
||||
6. Set your coprocessor's IP address to “10.TE.AM.11”. More information on IP format can be found [here](https://docs.wpilib.org/en/stable/docs/networking/networking-introduction/ip-configurations.html#on-the-field-static-configuration).
|
||||
6. Set your coprocessor's IP address to “10.TE.AM.xx”. "xx" should be a unique number not currently used by another device on the robot in the `.6-.19` range. More information on IP format can be found [here](https://docs.wpilib.org/en/stable/docs/networking/networking-introduction/ip-configurations.html#on-the-field-static-configuration).
|
||||
7. Click the “Save” button.
|
||||
|
||||
Power-cycle your robot and then you will now be access the PhotonVision dashboard at `10.TE.AM.11:5800`.
|
||||
Power-cycle your robot and then you will now be access the PhotonVision dashboard at `10.TE.AM.xx:5800`.
|
||||
|
||||
```{image} images/static.png
|
||||
:alt: Correctly set static IP
|
||||
@@ -81,7 +81,7 @@ If you would like to access your Ethernet-connected vision device from a compute
|
||||
|
||||
.. code-block:: c++
|
||||
|
||||
wpi::PortForwarder::GetInstance().Add(5800, "photonvision.local", 5800);
|
||||
wpi::net::PortForwarder::GetInstance().Add(5800, "photonvision.local", 5800);
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
|
||||
@@ -95,6 +95,27 @@ These `TargetModel` are paired with a target pose to create a `VisionTargetSim`.
|
||||
The pose of a `VisionTargetSim` object can be updated to simulate moving targets. Note, however, that this will break latency simulation for that target.
|
||||
:::
|
||||
|
||||
To use simulated object detection, you must provide an objDetClassId (zero-indexed class ID) and confidence value. When you set objDetConf to -1, the simulation computes confidence based on the area of the target in the camera's field of view. To simulate a object detection model with one class (fuel, index 0) and specify confidence, you'd write:
|
||||
|
||||
```{eval-rst}
|
||||
.. tab-set-code::
|
||||
|
||||
.. code-block:: java
|
||||
|
||||
// arbitrary position on field
|
||||
final var targetPose = new Pose3d(new Translation3d(2, 0, 0), new Rotation3d());
|
||||
// Class id, zero-indexed
|
||||
final int classId = 0;
|
||||
// Confidence, between 0 and 1.
|
||||
final float conf = 0.67f;
|
||||
// 6 inch diameter ball
|
||||
final TargetModel ballModel = new TargetModel(Units.inchesToMeters(6));
|
||||
final var ballTargetSim = new VisionTargetSim(targetPose, ballModel, classId, conf);
|
||||
|
||||
// Add this vision target to the vision system simulation to make it visible
|
||||
visionSim.addVisionTargets(visionTarget);
|
||||
```
|
||||
|
||||
For convenience, an `AprilTagFieldLayout` can also be added to automatically create a target for each of its AprilTags.
|
||||
|
||||
```{eval-rst}
|
||||
@@ -196,6 +217,42 @@ If the camera is mounted on a mobile mechanism (like a turret) this transform ca
|
||||
visionSim.adjustCamera(cameraSim, robotToCamera);
|
||||
```
|
||||
|
||||
## Low-Resource Vision Simulation with Photonvision
|
||||
|
||||
By default, PhotonCameraSim renders two simulated camera streams using OpenCV:
|
||||
|
||||
- Raw stream - The unprocessed camera view
|
||||
- Processed stream - The camera view with vision processing overlays
|
||||
|
||||
These streams are nice if you want to actually view the simulated images, but they can be computationally expensive. This may cause lag and reduced simulation performance on lower-powered computers.
|
||||
Lightweight Configuration
|
||||
|
||||
The following configuration disables both streams while still allowing tag detection and pose simulation to work. It's not perfect, but it's much better performance-wise than the default configuration.
|
||||
|
||||
.. code-block:: java
|
||||
|
||||
// lightweight config version
|
||||
// var cameraProperties = new SimCameraProperties();
|
||||
// cameraSim = new PhotonCameraSim(camera, cameraProperties, aprilTagLayout);
|
||||
// cameraSim.enableRawStream(false); // disables raw image stream
|
||||
// cameraSim.enableProcessedStream(false); // disables processed image stream
|
||||
|
||||
**Use Case**
|
||||
|
||||
This configuration is ideal for Chromebooks or low-spec machines where rendering the simulated camera images causes lag, but vision data is still desired for testing.
|
||||
|
||||
**What Still Works**
|
||||
|
||||
- AprilTag detection
|
||||
- Pose estimation
|
||||
- NetworkTables data publishing
|
||||
- Robot positioning and targeting
|
||||
|
||||
**What's Disabled**
|
||||
|
||||
- Visual camera stream rendering
|
||||
- Real-time visual debugging of camera output
|
||||
|
||||
## Updating The Simulation World
|
||||
|
||||
To update the `VisionSystemSim`, we simply have to pass in the simulated robot pose periodically (in `simulationPeriodic()`).
|
||||
|
||||
10
docs/source/docs/troubleshooting/images/led.svg
Normal file
10
docs/source/docs/troubleshooting/images/led.svg
Normal file
@@ -0,0 +1,10 @@
|
||||
<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>
|
||||
|
After Width: | Height: | Size: 927 B |
@@ -5,6 +5,7 @@
|
||||
|
||||
common-errors
|
||||
logging
|
||||
status-leds
|
||||
camera-troubleshooting
|
||||
networking-troubleshooting
|
||||
unix-commands
|
||||
|
||||
101
docs/source/docs/troubleshooting/status-leds.md
Normal file
101
docs/source/docs/troubleshooting/status-leds.md
Normal file
@@ -0,0 +1,101 @@
|
||||
---
|
||||
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 pi@hostname
|
||||
ssh photon@hostname
|
||||
```
|
||||
|
||||
For PhotonVision, the username will be `pi` and the password will be `raspberry`.
|
||||
For PhotonVision images, the username will be `photon` and the password will be `vision`.
|
||||
|
||||
### ip
|
||||
|
||||
|
||||
2
gradle/wrapper/gradle-wrapper.properties
vendored
2
gradle/wrapper/gradle-wrapper.properties
vendored
@@ -1,6 +1,6 @@
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=permwrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.3-bin.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-9.4.0-bin.zip
|
||||
networkTimeout=10000
|
||||
validateDistributionUrl=true
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
|
||||
2
photon-client/env.d.ts
vendored
2
photon-client/env.d.ts
vendored
@@ -1 +1,3 @@
|
||||
/// <reference types="vite/client" />
|
||||
|
||||
declare module "vue3-virtual-scroll-list";
|
||||
|
||||
@@ -4,8 +4,8 @@ import { defineConfigWithVueTs, vueTsConfigs } from "@vue/eslint-config-typescri
|
||||
import skipFormattingConfig from "@vue/eslint-config-prettier/skip-formatting";
|
||||
|
||||
export default defineConfigWithVueTs(
|
||||
pluginVue.configs["flat/recommended"],
|
||||
vueTsConfigs.recommended,
|
||||
pluginVue.configs["flat/recommended-error"],
|
||||
vueTsConfigs.recommendedTypeChecked,
|
||||
skipFormattingConfig,
|
||||
{
|
||||
ignores: ["**/dist/**", "playwright-report"]
|
||||
@@ -26,12 +26,29 @@ export default defineConfigWithVueTs(
|
||||
|
||||
semi: ["error", "always"],
|
||||
"eol-last": "error",
|
||||
eqeqeq: "error",
|
||||
"no-useless-concat": "error",
|
||||
"object-curly-spacing": ["error", "always"],
|
||||
"quote-props": ["error", "as-needed"],
|
||||
"no-case-declarations": "off",
|
||||
"vue/eqeqeq": "error",
|
||||
"vue/no-useless-concat": "error",
|
||||
"vue/no-constant-condition": "error",
|
||||
"vue/no-empty-pattern": "error",
|
||||
"vue/no-undef-directives": "error",
|
||||
"vue/no-undef-properties": "error",
|
||||
"vue/no-unused-properties": "error",
|
||||
"vue/no-unused-refs": "error",
|
||||
"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-explicit-any": "off",
|
||||
"@typescript-eslint/no-empty-object-type": "error",
|
||||
"@typescript-eslint/no-explicit-any": "error",
|
||||
"vue/valid-v-slot": ["error", { allowModifiers: true }]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,9 @@
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"engines": {
|
||||
"node": "24.x"
|
||||
},
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"preview": "vite preview --port 4173",
|
||||
@@ -14,8 +17,8 @@
|
||||
"format-ci": "prettier --check src/",
|
||||
"test": "playwright test",
|
||||
"test-ui": "playwright test --ui",
|
||||
"test-setup": "playwright install --with-deps"
|
||||
|
||||
"test-setup": "playwright install --with-deps",
|
||||
"type-check": "vue-tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"@fontsource/prompt": "^5.2.6",
|
||||
@@ -34,18 +37,19 @@
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.31.0",
|
||||
"@playwright/test": "^1.56.1",
|
||||
"@types/node": "^22.15.14",
|
||||
"@types/node": "^24.0.0",
|
||||
"@types/three": "^0.178.0",
|
||||
"@vitejs/plugin-vue": "^6.0.0",
|
||||
"@vitejs/plugin-vue": "^6.0.6",
|
||||
"vue-tsc": "^3.2.5",
|
||||
"@vue/eslint-config-prettier": "^10.2.0",
|
||||
"@vue/eslint-config-typescript": "^14.5.0",
|
||||
"@vue/tsconfig": "^0.7.0",
|
||||
"eslint": "^9.31.0",
|
||||
"eslint-plugin-vue": "^10.3.0",
|
||||
"eslint-plugin-vue": "^10.7.0",
|
||||
"prettier": "^3.6.2",
|
||||
"sass": "^1.89.2",
|
||||
"typescript": "^5.8.3",
|
||||
"vite": "^7.0.5",
|
||||
"vite": "^8.0.10",
|
||||
"vite-plugin-vuetify": "^2.1.1"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -74,7 +74,7 @@ export default defineConfig({
|
||||
|
||||
/* Run your local dev server before starting the tests */
|
||||
webServer: {
|
||||
command: process.platform == "win32" ? "" : "./" + "gradlew run",
|
||||
command: process.platform === "win32" ? "" : "./gradlew run",
|
||||
url: "http://localhost:5800",
|
||||
timeout: 300 * 1000,
|
||||
reuseExistingServer: !process.env.CI,
|
||||
|
||||
863
photon-client/pnpm-lock.yaml
generated
863
photon-client/pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
3
photon-client/pnpm-workspace.yaml
Normal file
3
photon-client/pnpm-workspace.yaml
Normal file
@@ -0,0 +1,3 @@
|
||||
allowBuilds:
|
||||
'@parcel/watcher': true
|
||||
core-js: true
|
||||
@@ -11,9 +11,10 @@ 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://${inject("backendHost")}/websocket_data`,
|
||||
`ws://${backendHost}/websocket_data`,
|
||||
() => {
|
||||
useStateStore().$patch({ backendConnected: true });
|
||||
},
|
||||
|
||||
@@ -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";
|
||||
import type { TrackballControls } from "three/examples/jsm/controls/TrackballControls.js";
|
||||
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");
|
||||
const { TrackballControls } = await import("three/examples/jsm/controls/TrackballControls.js");
|
||||
|
||||
import { useCameraSettingsStore } from "@/stores/settings/CameraSettingsStore";
|
||||
import { createPerspectiveCamera } from "@/lib/ThreeUtils";
|
||||
@@ -50,8 +50,8 @@ const drawTargets = async (targets: PhotonTarget[]) => {
|
||||
return;
|
||||
}
|
||||
|
||||
if (theme.global.name.value === "LightTheme") scene.background = new Color(0xa9a9a9);
|
||||
else scene.background = new Color(0x000000);
|
||||
if (theme.global.current.value.dark) scene.background = new Color(0x000000);
|
||||
else scene.background = new Color(0xa9a9a9);
|
||||
|
||||
scene.remove(...previousTargets);
|
||||
previousTargets = [];
|
||||
@@ -158,8 +158,8 @@ onMounted(async () => {
|
||||
if (!canvas) return;
|
||||
renderer = new WebGLRenderer({ canvas: canvas });
|
||||
|
||||
if (theme.global.name.value === "LightTheme") scene.background = new Color(0xa9a9a9);
|
||||
else scene.background = new Color(0x000000);
|
||||
if (theme.global.current.value.dark) scene.background = new Color(0x000000);
|
||||
else scene.background = new Color(0xa9a9a9);
|
||||
|
||||
onWindowResize();
|
||||
window.addEventListener("resize", onWindowResize);
|
||||
@@ -213,14 +213,14 @@ onMounted(async () => {
|
||||
renderer.render(scene, camera);
|
||||
};
|
||||
|
||||
drawTargets(props.targets);
|
||||
await drawTargets(props.targets);
|
||||
animate();
|
||||
});
|
||||
onBeforeUnmount(() => {
|
||||
window.removeEventListener("resize", onWindowResize);
|
||||
});
|
||||
watchEffect(() => {
|
||||
drawTargets(props.targets);
|
||||
void drawTargets(props.targets);
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -234,7 +234,7 @@ watchEffect(() => {
|
||||
<v-btn
|
||||
style="width: 100%"
|
||||
color="buttonActive"
|
||||
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
|
||||
:variant="theme.global.current.value.dark ? 'outlined' : 'elevated'"
|
||||
@click="resetCamFirstPerson"
|
||||
>
|
||||
First Person
|
||||
@@ -244,7 +244,7 @@ watchEffect(() => {
|
||||
<v-btn
|
||||
style="width: 100%"
|
||||
color="buttonActive"
|
||||
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
|
||||
:variant="theme.global.current.value.dark ? 'outlined' : 'elevated'"
|
||||
@click="resetCamThirdPerson"
|
||||
>
|
||||
Third Person
|
||||
|
||||
@@ -1,5 +1,13 @@
|
||||
<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,
|
||||
@@ -16,7 +24,7 @@ const {
|
||||
SphereGeometry,
|
||||
WebGLRenderer
|
||||
} = await import("three");
|
||||
const { TrackballControls } = await import("three/examples/jsm/controls/TrackballControls");
|
||||
const { TrackballControls } = await import("three/examples/jsm/controls/TrackballControls.js");
|
||||
import type { BoardObservation, CameraCalibrationResult } from "@/types/SettingTypes";
|
||||
import axios from "axios";
|
||||
import { useCameraSettingsStore } from "@/stores/settings/CameraSettingsStore";
|
||||
@@ -31,12 +39,12 @@ const props = defineProps<{
|
||||
title: string;
|
||||
}>();
|
||||
|
||||
let scene: Scene | undefined;
|
||||
let camera: PerspectiveCamera | undefined;
|
||||
let renderer: WebGLRenderer | undefined;
|
||||
let controls: TrackballControls | undefined;
|
||||
let scene: SceneType | undefined;
|
||||
let camera: PerspectiveCameraType | undefined;
|
||||
let renderer: WebGLRendererType | undefined;
|
||||
let controls: TrackballControlsType | undefined;
|
||||
|
||||
const createChessboard = (obs: BoardObservation, cal: CameraCalibrationResult): Group => {
|
||||
const createChessboard = (obs: BoardObservation, cal: CameraCalibrationResult): GroupType => {
|
||||
const group = new Group();
|
||||
|
||||
if (obs.locationInImageSpace.length === 0) return group;
|
||||
@@ -194,9 +202,6 @@ 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);
|
||||
|
||||
@@ -208,8 +213,8 @@ onMounted(async () => {
|
||||
const ambientLight = new AmbientLight(0xffffff, 0.6);
|
||||
scene.add(ambientLight);
|
||||
|
||||
if (theme.global.name.value === "LightTheme") scene.background = new Color(0xa9a9a9);
|
||||
else scene.background = new Color(0x000000);
|
||||
if (theme.global.current.value.dark) scene.background = new Color(0x000000);
|
||||
else scene.background = new Color(0xa9a9a9);
|
||||
|
||||
// Initialize a stable aspect ratio so subsequent resize events derive
|
||||
// height from width, avoiding layout feedback during continuous resizing
|
||||
@@ -256,6 +261,10 @@ 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;
|
||||
@@ -318,7 +327,7 @@ if (import.meta.hot) {
|
||||
}
|
||||
|
||||
watchEffect(() => {
|
||||
drawCalibration(calibrationData.value);
|
||||
void drawCalibration(calibrationData.value);
|
||||
});
|
||||
|
||||
watch(
|
||||
@@ -328,9 +337,9 @@ watch(
|
||||
props.resolution.height,
|
||||
useCameraSettingsStore().getCalibrationCoeffs(props.resolution)
|
||||
],
|
||||
() => {
|
||||
async () => {
|
||||
console.log("Camera or resolution changed, refetching calibration");
|
||||
fetchCalibrationData();
|
||||
await fetchCalibrationData();
|
||||
}
|
||||
);
|
||||
</script>
|
||||
@@ -347,7 +356,7 @@ watch(
|
||||
<v-btn
|
||||
style="width: 100%"
|
||||
color="buttonActive"
|
||||
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
|
||||
:variant="theme.global.current.value.dark ? 'outlined' : 'elevated'"
|
||||
@click="resetCamFirstPerson"
|
||||
>
|
||||
First Person
|
||||
@@ -357,7 +366,7 @@ watch(
|
||||
<v-btn
|
||||
style="width: 100%"
|
||||
color="buttonActive"
|
||||
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
|
||||
:variant="theme.global.current.value.dark ? 'outlined' : 'elevated'"
|
||||
@click="resetCamThirdPerson"
|
||||
>
|
||||
Third Person
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, inject, ref, onBeforeUnmount } from "vue";
|
||||
import { computed, inject, onBeforeUnmount, useTemplateRef } from "vue";
|
||||
import { useStateStore } from "@/stores/StateStore";
|
||||
import { useCameraSettingsStore } from "@/stores/settings/CameraSettingsStore";
|
||||
import type { StyleValue } from "vue";
|
||||
@@ -13,6 +13,7 @@ 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"];
|
||||
@@ -21,7 +22,7 @@ const streamSrc = computed<string>(() => {
|
||||
return emptyStreamSrc;
|
||||
}
|
||||
|
||||
return `http://${inject("backendHostname")}:${port}/stream.mjpg`;
|
||||
return `http://${backendHostname}:${port}/stream.mjpg`;
|
||||
});
|
||||
const streamDesc = computed<string>(() => `${props.streamType} Stream View`);
|
||||
const streamStyle = computed<StyleValue>(() => {
|
||||
@@ -50,7 +51,7 @@ const containerStyle = computed<StyleValue>(() => {
|
||||
});
|
||||
|
||||
const overlayStyle = computed<StyleValue>(() => {
|
||||
if (useStateStore().colorPickingMode || streamSrc.value == emptyStreamSrc) {
|
||||
if (useStateStore().colorPickingMode || streamSrc.value === emptyStreamSrc) {
|
||||
return { display: "none" };
|
||||
} else {
|
||||
return {};
|
||||
@@ -67,26 +68,26 @@ const handleCaptureClick = () => {
|
||||
const handlePopoutClick = () => {
|
||||
window.open(streamSrc.value);
|
||||
};
|
||||
const handleFullscreenRequest = () => {
|
||||
const handleFullscreenRequest = async () => {
|
||||
const stream = document.getElementById(props.id);
|
||||
if (!stream) return;
|
||||
stream.requestFullscreen();
|
||||
await stream.requestFullscreen();
|
||||
};
|
||||
|
||||
const mjpgStream: any = ref(null);
|
||||
const mjpgStream = useTemplateRef("mjpgStream");
|
||||
|
||||
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>
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ import { useStateStore } from "@/stores/StateStore";
|
||||
{{ useStateStore().snackbarData.message }}
|
||||
</p>
|
||||
<v-progress-linear
|
||||
v-if="useStateStore().snackbarData.progressBar != -1"
|
||||
v-if="useStateStore().snackbarData.progressBar !== -1"
|
||||
v-model="useStateStore().snackbarData.progressBar"
|
||||
height="15"
|
||||
:color="useStateStore().snackbarData.progressBarColor"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, inject, ref, watch } from "vue";
|
||||
import { computed, inject, ref, useTemplateRef, 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 = ref();
|
||||
const logList = useTemplateRef<InstanceType<typeof VirtualList>>("logList"); // this needs to be typed in the definition since vue has trouble inferring it
|
||||
const logKeeps = ref(40);
|
||||
const exportLogFile = ref();
|
||||
const selectedLogLevels = ref({
|
||||
const exportLogFile = useTemplateRef("exportLogFile");
|
||||
const selectedLogLevels = ref<Record<number, boolean>>({
|
||||
[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 = () => {
|
||||
|
||||
@@ -38,7 +38,7 @@ const renderCompact = computed<boolean>(() => compact.value || !mdAndUp.value);
|
||||
<v-list-item link to="/settings" prepend-icon="mdi-cog">
|
||||
<v-list-item-title>Settings</v-list-item-title>
|
||||
</v-list-item>
|
||||
<v-list-item ref="camerasTabOpener" link to="/cameras" prepend-icon="mdi-camera">
|
||||
<v-list-item link to="/cameras" prepend-icon="mdi-camera">
|
||||
<v-list-item-title>Camera</v-list-item-title>
|
||||
</v-list-item>
|
||||
<v-list-item
|
||||
@@ -73,7 +73,7 @@ const renderCompact = computed<boolean>(() => compact.value || !mdAndUp.value);
|
||||
</v-list-item>
|
||||
<v-list-item
|
||||
link
|
||||
:prepend-icon="theme.global.name.value === 'LightTheme' ? 'mdi-white-balance-sunny' : 'mdi-weather-night'"
|
||||
:prepend-icon="theme.global.current.value.dark ? 'mdi-weather-night' : 'mdi-white-balance-sunny'"
|
||||
@click="() => toggleTheme(theme)"
|
||||
>
|
||||
<v-list-item-title>Theme</v-list-item-title>
|
||||
|
||||
@@ -20,6 +20,7 @@ const PromptRegular = import("@/assets/fonts/PromptRegular");
|
||||
const jspdf = import("jspdf");
|
||||
|
||||
const theme = useTheme();
|
||||
const MM_PER_INCH = 25.4;
|
||||
|
||||
const settingsValid = ref(true);
|
||||
|
||||
@@ -28,7 +29,7 @@ const getUniqueVideoFormatsByResolution = (): VideoFormat[] => {
|
||||
if (useCameraSettingsStore().currentCameraSettings.validVideoFormats.length === 0) return uniqueResolutions;
|
||||
useCameraSettingsStore().currentCameraSettings.validVideoFormats.forEach((format) => {
|
||||
const index = uniqueResolutions.findIndex((v) => resolutionsAreEqual(v.resolution, format.resolution));
|
||||
const contains = index != -1;
|
||||
const contains = index !== -1;
|
||||
let skip = false;
|
||||
if (contains && format.fps > uniqueResolutions[index].fps) {
|
||||
uniqueResolutions.splice(index, 1);
|
||||
@@ -38,6 +39,11 @@ 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
|
||||
@@ -60,7 +66,10 @@ const getUniqueVideoFormatsByResolution = (): VideoFormat[] => {
|
||||
) *
|
||||
(180 / Math.PI);
|
||||
}
|
||||
uniqueResolutions.push(format);
|
||||
|
||||
if (resArea >= minPixelCount) {
|
||||
uniqueResolutions.push(format);
|
||||
}
|
||||
}
|
||||
});
|
||||
uniqueResolutions.sort(
|
||||
@@ -81,18 +90,26 @@ const calibrationDivisors = computed(() =>
|
||||
})
|
||||
);
|
||||
|
||||
const uniqueVideoResolutionString = ref("");
|
||||
const uniqueVideoResolutionIndex = ref(getUniqueVideoResolutionStrings()?.[0]?.value);
|
||||
|
||||
// Use a watchEffect so the value is populated/reacts when the stores become available or update.
|
||||
// This avoids trying to index into an array that may be empty during page reload.
|
||||
watchEffect(() => {
|
||||
const currentIndex = useCameraSettingsStore().currentVideoFormat.index ?? 0;
|
||||
useStateStore().calibrationData.videoFormatIndex = currentIndex;
|
||||
const names = useCameraSettingsStore().currentCameraSettings.validVideoFormats.map((f) =>
|
||||
getResolutionString(f.resolution)
|
||||
);
|
||||
uniqueVideoResolutionString.value = names[currentIndex] ?? names[0] ?? "";
|
||||
const 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;
|
||||
});
|
||||
const dimensionUnit = ref<"in" | "mm">("in");
|
||||
const squareSizeIn = ref(1);
|
||||
const markerSizeIn = ref(0.75);
|
||||
const patternWidth = ref(8);
|
||||
@@ -102,6 +119,28 @@ 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
|
||||
@@ -132,7 +171,7 @@ const downloadCalibBoard = async () => {
|
||||
const yPos = chessboardStartY + squareY * squareSizeIn.value;
|
||||
|
||||
// Only draw the odd squares to create the chessboard pattern
|
||||
if (squareY % 2 != squareX % 2) {
|
||||
if (squareY % 2 !== squareX % 2) {
|
||||
doc.rect(xPos, yPos, squareSizeIn.value, squareSizeIn.value, "F");
|
||||
}
|
||||
}
|
||||
@@ -176,7 +215,7 @@ const downloadCalibBoard = async () => {
|
||||
};
|
||||
|
||||
const isCalibrating = computed(
|
||||
() => useCameraSettingsStore().currentCameraSettings.currentPipelineIndex === WebsocketPipelineType.Calib3d
|
||||
() => useCameraSettingsStore().currentCameraSettings.currentPipelineIndex === WebsocketPipelineType.Calib3d.valueOf()
|
||||
);
|
||||
|
||||
const startCalibration = () => {
|
||||
@@ -203,7 +242,7 @@ const endCalibration = () => {
|
||||
calibSuccess.value = undefined;
|
||||
calibEndpointFail.value = false;
|
||||
|
||||
if (!useStateStore().calibrationData.hasEnoughImages) {
|
||||
if (!hasEnoughImages.value) {
|
||||
calibCanceled.value = true;
|
||||
}
|
||||
|
||||
@@ -231,6 +270,10 @@ 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) => {
|
||||
@@ -293,25 +336,25 @@ const setSelectedVideoFormat = (format: VideoFormat) => {
|
||||
<v-card-subtitle v-if="!isCalibrating" class="pl-0 pb-3 pt-4 opacity-100"
|
||||
>Configure New Calibration</v-card-subtitle
|
||||
>
|
||||
<v-form ref="form" v-model="settingsValid">
|
||||
<v-form v-model="settingsValid">
|
||||
<pv-select
|
||||
v-model="uniqueVideoResolutionString"
|
||||
v-model="uniqueVideoResolutionIndex"
|
||||
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="
|
||||
useStateStore().calibrationData.videoFormatIndex =
|
||||
getUniqueVideoResolutionStrings().find((v) => v.value === $event)?.value || 0
|
||||
"
|
||||
@update:model-value="(value) => (useStateStore().calibrationData.videoFormatIndex = value)"
|
||||
/>
|
||||
<pv-select
|
||||
v-model="boardType"
|
||||
label="Board Type"
|
||||
tooltip="Calibration board pattern to use"
|
||||
:select-cols="8"
|
||||
:items="['Chessboard', 'ChArUco']"
|
||||
:items="[
|
||||
{ value: CalibrationBoardTypes.Charuco, name: 'ChArUco' },
|
||||
{ value: CalibrationBoardTypes.Chessboard, name: 'Chessboard' }
|
||||
]"
|
||||
:disabled="isCalibrating"
|
||||
/>
|
||||
<v-alert
|
||||
@@ -341,25 +384,43 @@ const setSelectedVideoFormat = (format: VideoFormat) => {
|
||||
label="Tag Family"
|
||||
tooltip="Dictionary of ArUco markers on the ChArUco board"
|
||||
:select-cols="8"
|
||||
:items="['Dict_4X4_1000', 'Dict_5X5_1000', 'Dict_6X6_1000', 'Dict_7X7_1000']"
|
||||
: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' }
|
||||
]"
|
||||
:disabled="isCalibrating"
|
||||
/>
|
||||
<pv-number-input
|
||||
v-model="squareSizeIn"
|
||||
label="Pattern Spacing (in)"
|
||||
tooltip="Spacing between pattern features in inches"
|
||||
v-model="squareSize"
|
||||
:label="`Pattern Spacing (${dimensionUnit})`"
|
||||
:tooltip="`Spacing between pattern features in ${dimensionUnit === 'mm' ? 'millimeters' : '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="markerSizeIn"
|
||||
label="Marker Size (in)"
|
||||
tooltip="Size of the tag markers in inches must be smaller than pattern spacing"
|
||||
v-model="markerSize"
|
||||
:label="`Marker Size (${dimensionUnit})`"
|
||||
:tooltip="`Size of the tag markers in ${dimensionUnit === 'mm' ? 'millimeters' : '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"
|
||||
@@ -470,7 +531,7 @@ const setSelectedVideoFormat = (format: VideoFormat) => {
|
||||
closable
|
||||
density="compact"
|
||||
class="mb-5"
|
||||
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'tonal'"
|
||||
:variant="theme.global.current.value.dark ? 'tonal' : 'elevated'"
|
||||
:color="useSettingsStore().general.mrCalWorking ? 'buttonPassive' : 'error'"
|
||||
:icon="useSettingsStore().general.mrCalWorking ? 'mdi-check' : 'mdi-close'"
|
||||
:text="
|
||||
@@ -481,20 +542,31 @@ const setSelectedVideoFormat = (format: VideoFormat) => {
|
||||
/>
|
||||
<div v-if="isCalibrating" class="d-flex justify-center align-center pb-5">
|
||||
<v-chip
|
||||
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'tonal'"
|
||||
:variant="theme.global.current.value.dark ? 'tonal' : 'elevated'"
|
||||
label
|
||||
:color="useStateStore().calibrationData.hasEnoughImages ? 'buttonPassive' : 'light-grey'"
|
||||
:color="hasEnoughImages ? 'buttonPassive' : 'light-grey'"
|
||||
>
|
||||
Snapshots: {{ useStateStore().calibrationData.imageCount }} of at least
|
||||
{{ useStateStore().calibrationData.minimumImageCount }}
|
||||
{{ minCount }}
|
||||
</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
|
||||
color="buttonPassive"
|
||||
size="small"
|
||||
block
|
||||
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
|
||||
:variant="theme.global.current.value.dark ? 'outlined' : 'elevated'"
|
||||
:disabled="!settingsValid"
|
||||
@click="downloadCalibBoard"
|
||||
>
|
||||
@@ -509,7 +581,7 @@ const setSelectedVideoFormat = (format: VideoFormat) => {
|
||||
density="compact"
|
||||
text="Too many corners. Finish calibration now!"
|
||||
icon="mdi-alert-circle-outline"
|
||||
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'tonal'"
|
||||
:variant="theme.global.current.value.dark ? 'tonal' : 'elevated'"
|
||||
/>
|
||||
<div class="d-flex pt-5">
|
||||
<v-col cols="6" class="pa-0 pr-2">
|
||||
@@ -517,7 +589,7 @@ const setSelectedVideoFormat = (format: VideoFormat) => {
|
||||
size="small"
|
||||
block
|
||||
color="buttonActive"
|
||||
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
|
||||
:variant="theme.global.current.value.dark ? 'outlined' : 'elevated'"
|
||||
:disabled="!settingsValid || tooManyPoints"
|
||||
@click="isCalibrating ? useCameraSettingsStore().takeCalibrationSnapshot() : startCalibration()"
|
||||
>
|
||||
@@ -531,17 +603,15 @@ const setSelectedVideoFormat = (format: VideoFormat) => {
|
||||
<v-btn
|
||||
size="small"
|
||||
block
|
||||
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
|
||||
:color="useStateStore().calibrationData.hasEnoughImages ? 'buttonActive' : 'error'"
|
||||
:variant="theme.global.current.value.dark ? 'outlined' : 'elevated'"
|
||||
:color="hasEnoughImages ? 'buttonActive' : 'error'"
|
||||
:disabled="!isCalibrating || !settingsValid"
|
||||
@click="endCalibration"
|
||||
>
|
||||
<v-icon start class="calib-btn-icon" size="large">
|
||||
{{ useStateStore().calibrationData.hasEnoughImages ? "mdi-flag-checkered" : "mdi-flag-off-outline" }}
|
||||
{{ hasEnoughImages ? "mdi-flag-checkered" : "mdi-flag-off-outline" }}
|
||||
</v-icon>
|
||||
<span class="calib-btn-label">{{
|
||||
useStateStore().calibrationData.hasEnoughImages ? "Finish Calibration" : "Cancel Calibration"
|
||||
}}</span>
|
||||
<span class="calib-btn-label">{{ 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 } from "vue";
|
||||
import { computed, inject, ref, useTemplateRef } from "vue";
|
||||
import { axiosPost, getResolutionString, parseJsonFile } from "@/lib/PhotonUtils";
|
||||
import { useTheme } from "vuetify";
|
||||
import PvDeleteModal from "@/components/common/pv-delete-modal.vue";
|
||||
@@ -13,35 +13,35 @@ const props = defineProps<{
|
||||
videoFormat: VideoFormat;
|
||||
}>();
|
||||
|
||||
const confirmRemoveDialog = ref({ show: false, vf: props.videoFormat as VideoFormat });
|
||||
const confirmRemoveDialog = ref({ show: false, vf: props.videoFormat });
|
||||
|
||||
const removeCalibration = (vf: VideoFormat) => {
|
||||
axiosPost("/calibration/remove", "delete a camera calibration", {
|
||||
const removeCalibration = async (vf: VideoFormat) => {
|
||||
await axiosPost("/calibration/remove", "delete a camera calibration", {
|
||||
cameraUniqueName: useCameraSettingsStore().currentCameraSettings.uniqueName,
|
||||
width: vf.resolution.width,
|
||||
height: vf.resolution.height
|
||||
});
|
||||
};
|
||||
|
||||
const exportCalibration = ref();
|
||||
const exportCalibration = useTemplateRef("exportCalibration");
|
||||
const openExportCalibrationPrompt = () => {
|
||||
exportCalibration.value.click();
|
||||
exportCalibration.value?.click();
|
||||
};
|
||||
|
||||
const importCalibrationFromPhotonJson = ref();
|
||||
const importCalibrationFromPhotonJson = useTemplateRef("importCalibrationFromPhotonJson");
|
||||
const openUploadPhotonCalibJsonPrompt = () => {
|
||||
importCalibrationFromPhotonJson.value.click();
|
||||
importCalibrationFromPhotonJson.value?.click();
|
||||
};
|
||||
const importCalibration = async () => {
|
||||
const files = importCalibrationFromPhotonJson.value.files;
|
||||
if (files.length === 0) return;
|
||||
const files = importCalibrationFromPhotonJson.value?.files;
|
||||
if (!files?.length) return;
|
||||
const uploadedJson = files[0];
|
||||
|
||||
const data = await parseJsonFile<CameraCalibrationResult>(uploadedJson);
|
||||
|
||||
if (
|
||||
data.resolution.height != props.videoFormat.resolution.height ||
|
||||
data.resolution.width != props.videoFormat.resolution.width
|
||||
data.resolution.height !== props.videoFormat.resolution.height ||
|
||||
data.resolution.width !== props.videoFormat.resolution.width
|
||||
) {
|
||||
useStateStore().showSnackbarMessage({
|
||||
color: "error",
|
||||
@@ -121,7 +121,7 @@ const viewingImg = ref(0);
|
||||
<v-btn
|
||||
color="buttonPassive"
|
||||
style="width: 100%"
|
||||
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
|
||||
:variant="theme.global.current.value.dark ? 'outlined' : 'elevated'"
|
||||
@click="openUploadPhotonCalibJsonPrompt"
|
||||
>
|
||||
<v-icon start size="large"> mdi-import</v-icon>
|
||||
@@ -140,7 +140,7 @@ const viewingImg = ref(0);
|
||||
color="buttonPassive"
|
||||
:disabled="!currentCalibrationCoeffs"
|
||||
style="width: 100%"
|
||||
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
|
||||
:variant="theme.global.current.value.dark ? 'outlined' : 'elevated'"
|
||||
@click="openExportCalibrationPrompt"
|
||||
>
|
||||
<v-icon start size="large">mdi-export</v-icon>
|
||||
@@ -318,7 +318,7 @@ const viewingImg = ref(0);
|
||||
color="primary"
|
||||
text="The selected video format has not been calibrated."
|
||||
icon="mdi-alert-circle-outline"
|
||||
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'tonal'"
|
||||
:variant="theme.global.current.value.dark ? 'tonal' : 'elevated'"
|
||||
/>
|
||||
</div>
|
||||
<Suspense v-else-if="tab === 'details'">
|
||||
@@ -341,7 +341,7 @@ const viewingImg = ref(0);
|
||||
<pv-delete-modal
|
||||
v-model="confirmRemoveDialog.show"
|
||||
:width="500"
|
||||
:title="'Delete Calibration'"
|
||||
title="Delete Calibration"
|
||||
:description="`Are you sure you want to delete the calibration for '${confirmRemoveDialog.vf.resolution.width}x${confirmRemoveDialog.vf.resolution.height}'? This action cannot be undone.`"
|
||||
:on-confirm="() => removeCalibration(confirmRemoveDialog.vf)"
|
||||
/>
|
||||
|
||||
@@ -53,7 +53,7 @@ const fetchSnapshots = () => {
|
||||
.get("/utils/getImageSnapshots")
|
||||
.then((response) => {
|
||||
imgData.value = response.data.map(
|
||||
(snapshotData: { snapshotName: string; cameraUniqueName: string; snapshotData: string }, index) => {
|
||||
(snapshotData: { snapshotName: string; cameraUniqueName: string; snapshotData: string }, index: number) => {
|
||||
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.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
|
||||
:variant="theme.global.current.value.dark ? 'outlined' : 'elevated'"
|
||||
@click="fetchSnapshots"
|
||||
>
|
||||
<v-icon start class="open-icon" size="large"> mdi-folder </v-icon>
|
||||
@@ -115,7 +115,7 @@ const expanded = ref([]);
|
||||
density="compact"
|
||||
text="There are currently no saved snapshots."
|
||||
icon="mdi-information-outline"
|
||||
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'tonal'"
|
||||
:variant="theme.global.current.value.dark ? 'tonal' : 'elevated'"
|
||||
/>
|
||||
</v-card-text>
|
||||
<v-card-text v-else class="pt-0">
|
||||
@@ -125,7 +125,7 @@ const expanded = ref([]);
|
||||
density="compact"
|
||||
text="Snapshot timestamps depend on when the coprocessor was last connected to the internet."
|
||||
icon="mdi-information-outline"
|
||||
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'tonal'"
|
||||
:variant="theme.global.current.value.dark ? 'tonal' : 'elevated'"
|
||||
/>
|
||||
<v-data-table
|
||||
v-model:expanded="expanded"
|
||||
|
||||
@@ -9,6 +9,7 @@ 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();
|
||||
|
||||
@@ -20,7 +21,7 @@ const focusMode = computed<boolean>({
|
||||
get: () => useCameraSettingsStore().isFocusMode,
|
||||
set: (v) =>
|
||||
useCameraSettingsStore().changeCurrentPipelineIndex(
|
||||
v ? -3 : useCameraSettingsStore().currentCameraSettings.lastPipelineIndex || 0,
|
||||
v ? WebsocketPipelineType.FocusCamera : useCameraSettingsStore().currentCameraSettings.lastPipelineIndex || 0,
|
||||
true
|
||||
)
|
||||
});
|
||||
@@ -65,11 +66,11 @@ const settingsHaveChanged = (): boolean => {
|
||||
const a = tempSettingsStruct.value;
|
||||
const b = useCameraSettingsStore().currentCameraSettings;
|
||||
|
||||
for (const q in ValidQuirks) {
|
||||
if (a.quirksToChange[q] != b.cameraQuirks.quirks[q]) return true;
|
||||
for (const quirk of Object.values(ValidQuirks)) {
|
||||
if (a.quirksToChange[quirk] !== b.cameraQuirks.quirks[quirk]) return true;
|
||||
}
|
||||
|
||||
return a.fov != b.fov.value;
|
||||
return a.fov !== b.fov.value;
|
||||
};
|
||||
|
||||
const resetTempSettingsStruct = () => {
|
||||
@@ -120,12 +121,12 @@ watchEffect(() => {
|
||||
});
|
||||
|
||||
const showDeleteCamera = ref(false);
|
||||
const deleteThisCamera = () => {
|
||||
axiosPost("/utils/nukeOneCamera", "delete this camera", {
|
||||
const deleteThisCamera = async () => {
|
||||
await axiosPost("/utils/nukeOneCamera", "delete this camera", {
|
||||
cameraUniqueName: useStateStore().currentCameraUniqueName
|
||||
});
|
||||
};
|
||||
const wrappedCameras = computed<SelectItem[]>(() =>
|
||||
const wrappedCameras = computed<SelectItem<string>[]>(() =>
|
||||
Object.keys(useCameraSettingsStore().cameras).map((cameraUniqueName) => ({
|
||||
name: useCameraSettingsStore().cameras[cameraUniqueName].nickname,
|
||||
value: cameraUniqueName
|
||||
@@ -159,7 +160,7 @@ const wrappedCameras = computed<SelectItem[]>(() =>
|
||||
v-model="arducamSelectWrapper"
|
||||
label="Arducam Model"
|
||||
:items="[
|
||||
{ name: 'None', value: 0, disabled: true },
|
||||
{ name: 'None', value: 0 },
|
||||
{ name: 'OV9281', value: 1 },
|
||||
{ name: 'OV2311', value: 2 },
|
||||
{ name: 'OV9782', value: 3 }
|
||||
@@ -179,7 +180,7 @@ const wrappedCameras = computed<SelectItem[]>(() =>
|
||||
size="small"
|
||||
color="buttonActive"
|
||||
:disabled="!settingsHaveChanged()"
|
||||
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
|
||||
:variant="theme.global.current.value.dark ? 'outlined' : 'elevated'"
|
||||
@click="saveCameraSettings"
|
||||
>
|
||||
<v-icon start size="large"> mdi-content-save </v-icon>
|
||||
@@ -191,7 +192,7 @@ const wrappedCameras = computed<SelectItem[]>(() =>
|
||||
block
|
||||
size="small"
|
||||
color="error"
|
||||
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
|
||||
:variant="theme.global.current.value.dark ? 'outlined' : 'elevated'"
|
||||
@click="() => (showDeleteCamera = true)"
|
||||
>
|
||||
<v-icon start size="large"> mdi-trash-can-outline </v-icon>
|
||||
|
||||
@@ -6,6 +6,7 @@ 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();
|
||||
|
||||
@@ -15,7 +16,7 @@ const driverMode = computed<boolean>({
|
||||
get: () => useCameraSettingsStore().isDriverMode,
|
||||
set: (v) =>
|
||||
useCameraSettingsStore().changeCurrentPipelineIndex(
|
||||
v ? -1 : useCameraSettingsStore().currentCameraSettings.lastPipelineIndex || 0,
|
||||
v ? WebsocketPipelineType.DriverMode : useCameraSettingsStore().currentCameraSettings.lastPipelineIndex || 0,
|
||||
true
|
||||
)
|
||||
});
|
||||
@@ -103,7 +104,7 @@ const fpsTooLow = computed<boolean>(() => {
|
||||
<v-btn
|
||||
color="buttonPassive"
|
||||
class="fill"
|
||||
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
|
||||
:variant="theme.global.current.value.dark ? 'outlined' : 'elevated'"
|
||||
:disabled="
|
||||
useCameraSettingsStore().isDriverMode ||
|
||||
useCameraSettingsStore().isCalibrationMode ||
|
||||
@@ -116,7 +117,7 @@ const fpsTooLow = computed<boolean>(() => {
|
||||
<v-btn
|
||||
color="buttonPassive"
|
||||
class="fill"
|
||||
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
|
||||
:variant="theme.global.current.value.dark ? 'outlined' : 'elevated'"
|
||||
:disabled="
|
||||
useCameraSettingsStore().isDriverMode ||
|
||||
useCameraSettingsStore().isCalibrationMode ||
|
||||
|
||||
@@ -1,69 +1,51 @@
|
||||
<script setup lang="ts">
|
||||
import { PVCameraInfo } from "@/types/SettingTypes";
|
||||
import type { PVCameraInfo } from "@/types/SettingTypes";
|
||||
|
||||
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 {};
|
||||
};
|
||||
const { camera } = defineProps<{ camera: PVCameraInfo }>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<v-table density="compact" :style="{ backgroundColor: 'var(--v-primary-base)' }">
|
||||
<tbody>
|
||||
<tr v-if="cameraInfoFor(camera).dev !== undefined && cameraInfoFor(camera).dev !== null">
|
||||
<tr v-if="'dev' in camera && camera.dev !== null">
|
||||
<td>Device Number:</td>
|
||||
<td>{{ cameraInfoFor(camera).dev }}</td>
|
||||
<td>{{ camera.dev }}</td>
|
||||
</tr>
|
||||
<tr v-if="cameraInfoFor(camera).name !== undefined && cameraInfoFor(camera).name !== null">
|
||||
<tr v-if="'name' in camera && camera.name !== null">
|
||||
<td>Name:</td>
|
||||
<td>{{ cameraInfoFor(camera).name }}</td>
|
||||
<td>{{ camera.name }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Type:</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-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-else>Unidentified Camera Type</td>
|
||||
</tr>
|
||||
<tr v-if="cameraInfoFor(camera).baseName !== undefined && cameraInfoFor(camera).baseName !== null">
|
||||
<tr v-if="'baseName' in camera && camera.baseName !== null">
|
||||
<td>Base Name:</td>
|
||||
<td>{{ cameraInfoFor(camera).baseName }}</td>
|
||||
<td>{{ camera.baseName }}</td>
|
||||
</tr>
|
||||
<tr v-if="cameraInfoFor(camera).vendorId !== undefined && cameraInfoFor(camera).vendorId !== null">
|
||||
<tr v-if="'vendorId' in camera && camera.vendorId !== null">
|
||||
<td>Vendor ID:</td>
|
||||
<td>{{ cameraInfoFor(camera).vendorId }}</td>
|
||||
<td>{{ camera.vendorId }}</td>
|
||||
</tr>
|
||||
<tr v-if="cameraInfoFor(camera).productId !== undefined && cameraInfoFor(camera).productId !== null">
|
||||
<tr v-if="'productId' in camera && camera.productId !== null">
|
||||
<td>Product ID:</td>
|
||||
<td>{{ cameraInfoFor(camera).productId }}</td>
|
||||
<td>{{ camera.productId }}</td>
|
||||
</tr>
|
||||
<tr v-if="cameraInfoFor(camera).path !== undefined && cameraInfoFor(camera).path !== null">
|
||||
<tr v-if="'path' in camera && camera.path !== null">
|
||||
<td>Path:</td>
|
||||
<td style="word-break: break-all">{{ cameraInfoFor(camera).path }}</td>
|
||||
<td style="word-break: break-all">{{ camera.path }}</td>
|
||||
</tr>
|
||||
<tr v-if="cameraInfoFor(camera).uniquePath !== undefined && cameraInfoFor(camera).uniquePath !== null">
|
||||
<tr v-if="'uniquePath' in camera && camera.uniquePath !== null">
|
||||
<td>Unique Path:</td>
|
||||
<td style="word-break: break-all">{{ cameraInfoFor(camera).uniquePath }}</td>
|
||||
<td style="word-break: break-all">{{ camera.uniquePath }}</td>
|
||||
</tr>
|
||||
<tr v-if="cameraInfoFor(camera).otherPaths !== undefined && cameraInfoFor(camera).otherPaths !== null">
|
||||
<tr v-if="'otherPaths' in camera && camera.otherPaths !== null">
|
||||
<td>Other Paths:</td>
|
||||
<td>{{ cameraInfoFor(camera).otherPaths }}</td>
|
||||
<td>{{ camera.otherPaths }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</v-table>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { PVCameraInfo } from "@/types/SettingTypes";
|
||||
import type { PVCameraInfo } from "@/types/SettingTypes";
|
||||
|
||||
function isEqual<T>(a: T, b: T): boolean {
|
||||
if (a === b) {
|
||||
@@ -15,29 +15,7 @@ function isEqual<T>(a: T, b: T): boolean {
|
||||
);
|
||||
}
|
||||
|
||||
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 {};
|
||||
};
|
||||
const { saved, current } = defineProps<{ saved: PVCameraInfo; current: PVCameraInfo }>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -50,79 +28,70 @@ const cameraInfoFor = (camera: PVCameraInfo): any => {
|
||||
<th>Current</th>
|
||||
</tr>
|
||||
<tr
|
||||
v-if="cameraInfoFor(saved).dev !== undefined && cameraInfoFor(saved).dev !== null"
|
||||
:class="cameraInfoFor(saved).dev !== cameraInfoFor(current).dev ? 'mismatch' : ''"
|
||||
v-if="'dev' in saved && 'dev' in current && saved.dev !== null"
|
||||
:class="saved.dev !== current.dev ? 'mismatch' : ''"
|
||||
>
|
||||
<td>Device Number:</td>
|
||||
<td>{{ cameraInfoFor(saved).dev }}</td>
|
||||
<td>{{ cameraInfoFor(current).dev }}</td>
|
||||
<td>{{ saved.dev }}</td>
|
||||
<td>{{ current.dev }}</td>
|
||||
</tr>
|
||||
<tr
|
||||
v-if="cameraInfoFor(saved).name !== undefined && cameraInfoFor(saved).name !== null"
|
||||
:class="cameraInfoFor(saved).name !== cameraInfoFor(current).name ? 'mismatch' : ''"
|
||||
>
|
||||
<tr v-if="saved.name !== null" :class="saved.name !== current.name ? 'mismatch' : ''">
|
||||
<td>Name:</td>
|
||||
<td>{{ cameraInfoFor(saved).name }}</td>
|
||||
<td>{{ cameraInfoFor(current).name }}</td>
|
||||
<td>{{ saved.name }}</td>
|
||||
<td>{{ current.name }}</td>
|
||||
</tr>
|
||||
<tr
|
||||
v-if="cameraInfoFor(saved).baseName !== undefined && cameraInfoFor(saved).baseName !== null"
|
||||
:class="cameraInfoFor(saved).baseName !== cameraInfoFor(current).baseName ? 'mismatch' : ''"
|
||||
v-if="'baseName' in saved && 'baseName' in current && saved.baseName !== null"
|
||||
:class="saved.baseName !== current.baseName ? 'mismatch' : ''"
|
||||
>
|
||||
<td>Base Name:</td>
|
||||
<td>{{ cameraInfoFor(saved).baseName }}</td>
|
||||
<td>{{ cameraInfoFor(current).baseName }}</td>
|
||||
<td>{{ saved.baseName }}</td>
|
||||
<td>{{ current.baseName }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Type:</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-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-else>Unidentified Camera Type</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-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-else>Unidentified Camera Type</td>
|
||||
</tr>
|
||||
<tr
|
||||
v-if="cameraInfoFor(saved).vendorId !== undefined && cameraInfoFor(saved).vendorId !== null"
|
||||
:class="cameraInfoFor(saved).vendorId !== cameraInfoFor(current).vendorId ? 'mismatch' : ''"
|
||||
v-if="'vendorId' in saved && 'vendorId' in current && saved.vendorId !== null"
|
||||
:class="saved.vendorId !== current.vendorId ? 'mismatch' : ''"
|
||||
>
|
||||
<td>Vendor ID:</td>
|
||||
<td>{{ cameraInfoFor(saved).vendorId }}</td>
|
||||
<td>{{ cameraInfoFor(current).vendorId }}</td>
|
||||
<td>{{ saved.vendorId }}</td>
|
||||
<td>{{ current.vendorId }}</td>
|
||||
</tr>
|
||||
<tr
|
||||
v-if="cameraInfoFor(saved).productId !== undefined && cameraInfoFor(saved).productId !== null"
|
||||
:class="cameraInfoFor(saved).productId !== cameraInfoFor(current).productId ? 'mismatch' : ''"
|
||||
v-if="'productId' in saved && 'productId' in current && saved.productId !== null"
|
||||
:class="saved.productId !== current.productId ? 'mismatch' : ''"
|
||||
>
|
||||
<td>Product ID:</td>
|
||||
<td>{{ cameraInfoFor(saved).productId }}</td>
|
||||
<td>{{ cameraInfoFor(current).productId }}</td>
|
||||
<td>{{ saved.productId }}</td>
|
||||
<td>{{ current.productId }}</td>
|
||||
</tr>
|
||||
<tr
|
||||
v-if="cameraInfoFor(saved).path !== undefined && cameraInfoFor(saved).path !== null"
|
||||
:class="cameraInfoFor(saved).path !== cameraInfoFor(current).path ? 'mismatch' : ''"
|
||||
>
|
||||
<tr v-if="saved.path !== null" :class="saved.path !== 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>
|
||||
<td style="word-break: break-all">{{ saved.path }}</td>
|
||||
<td style="word-break: break-all">{{ current.path }}</td>
|
||||
</tr>
|
||||
<tr
|
||||
v-if="cameraInfoFor(saved).uniquePath !== undefined && cameraInfoFor(saved).uniquePath !== null"
|
||||
:class="cameraInfoFor(saved).uniquePath !== cameraInfoFor(current).uniquePath ? 'mismatch' : ''"
|
||||
>
|
||||
<tr v-if="saved.uniquePath !== null" :class="saved.uniquePath !== 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>
|
||||
<td style="word-break: break-all">{{ saved.uniquePath }}</td>
|
||||
<td style="word-break: break-all">{{ current.uniquePath }}</td>
|
||||
</tr>
|
||||
<tr
|
||||
v-if="cameraInfoFor(saved).otherPaths !== undefined && cameraInfoFor(saved).otherPaths !== null"
|
||||
:class="isEqual(cameraInfoFor(saved).otherPaths, cameraInfoFor(current).otherPaths) ? '' : 'mismatch'"
|
||||
v-if="'otherPaths' in saved && 'otherPaths' in current && saved.otherPaths !== null"
|
||||
:class="isEqual(saved.otherPaths, current.otherPaths) ? '' : 'mismatch'"
|
||||
>
|
||||
<td>Other Paths:</td>
|
||||
<td>{{ cameraInfoFor(saved).otherPaths }}</td>
|
||||
<td>{{ cameraInfoFor(current).otherPaths }}</td>
|
||||
<td>{{ saved.otherPaths }}</td>
|
||||
<td>{{ current.otherPaths }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</v-table>
|
||||
|
||||
@@ -49,7 +49,7 @@ const confirmationText = ref("");
|
||||
color="buttonActive"
|
||||
style="float: right"
|
||||
width="100%"
|
||||
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
|
||||
:variant="theme.global.current.value.dark ? 'outlined' : 'elevated'"
|
||||
@click="onBackup"
|
||||
>
|
||||
<v-icon start class="open-icon" size="large"> mdi-export </v-icon>
|
||||
@@ -65,7 +65,7 @@ const confirmationText = ref("");
|
||||
? confirmationText.toLowerCase() !== expectedConfirmationText.toLowerCase()
|
||||
: false
|
||||
"
|
||||
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
|
||||
:variant="theme.global.current.value.dark ? 'outlined' : 'elevated'"
|
||||
@click="
|
||||
onConfirm();
|
||||
confirmationText = '';
|
||||
|
||||
@@ -25,7 +25,7 @@ const emit = defineEmits<{
|
||||
(e: "onEscape"): void;
|
||||
}>();
|
||||
|
||||
const handleKeydown = ({ key }) => {
|
||||
const handleKeydown = ({ key }: KeyboardEvent) => {
|
||||
switch (key) {
|
||||
case "Enter":
|
||||
// Explicitly check that all rule props return true
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
<script setup lang="ts">
|
||||
<script setup lang="ts" generic="T extends string | number">
|
||||
import { computed } from "vue";
|
||||
import TooltippedLabel from "@/components/common/pv-tooltipped-label.vue";
|
||||
|
||||
export interface SelectItem {
|
||||
export interface SelectItem<TValue extends string | number> {
|
||||
name: string | number;
|
||||
value: string | number;
|
||||
value: TValue;
|
||||
disabled?: boolean;
|
||||
}
|
||||
const value = defineModel<string | number | undefined>({ required: true });
|
||||
|
||||
type SelectItems = SelectItem<T>[] | ReadonlyArray<T>;
|
||||
const value = defineModel<T>({ required: true });
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
@@ -15,7 +17,7 @@ const props = withDefaults(
|
||||
tooltip?: string;
|
||||
selectCols?: number;
|
||||
disabled?: boolean;
|
||||
items: string[] | number[] | SelectItem[];
|
||||
items: SelectItems;
|
||||
}>(),
|
||||
{
|
||||
selectCols: 9,
|
||||
@@ -23,18 +25,20 @@ const props = withDefaults(
|
||||
}
|
||||
);
|
||||
|
||||
const areSelectItems = (items: SelectItems): items is SelectItem<T>[] => typeof items[0] === "object";
|
||||
|
||||
// Computed in case items changes
|
||||
const items = computed<SelectItem[]>(() => {
|
||||
const items = computed<SelectItem<T>[]>(() => {
|
||||
// Trivial case for empty list; we have no data
|
||||
if (!props.items.length) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// Check if the prop exists on the object to infer object type
|
||||
if ((props.items[0] as SelectItem).name) {
|
||||
return props.items as SelectItem[];
|
||||
if (areSelectItems(props.items)) {
|
||||
return props.items;
|
||||
}
|
||||
return props.items.map((v, i) => ({ name: v, value: i }));
|
||||
|
||||
return props.items.map((item) => ({ name: item, value: item }));
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -49,7 +53,7 @@ const items = computed<SelectItem[]>(() => {
|
||||
:items="items"
|
||||
item-title="name"
|
||||
item-value="value"
|
||||
item-props.disabled="disabled"
|
||||
item-props
|
||||
: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: any[]) => void, wait: number) {
|
||||
function debounce(func: (...args: number[]) => void, wait: number) {
|
||||
let timeout: ReturnType<typeof setTimeout>;
|
||||
return function (...args: any[]) {
|
||||
return function (...args: number[]) {
|
||||
clearTimeout(timeout);
|
||||
timeout = setTimeout(() => func.apply(this, args), wait);
|
||||
timeout = setTimeout(() => func(...args), wait);
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -13,28 +13,6 @@ 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_ \-)(.]*$/;
|
||||
|
||||
@@ -87,17 +65,17 @@ const cancelCameraNameEdit = () => {
|
||||
};
|
||||
|
||||
// Pipeline Name Edit
|
||||
const pipelineNamesWrapper = computed<SelectItem[]>(() => {
|
||||
const pipelineNamesWrapper = computed(() => {
|
||||
const pipelineNames = useCameraSettingsStore().pipelineNames.map((name, index) => ({ name: name, value: index }));
|
||||
|
||||
if (useCameraSettingsStore().isDriverMode) {
|
||||
pipelineNames.push({ name: "Driver Mode", value: WebsocketPipelineType.DriverMode });
|
||||
pipelineNames.push({ name: "Driver Mode", value: WebsocketPipelineType.DriverMode.valueOf() });
|
||||
}
|
||||
if (useCameraSettingsStore().isFocusMode) {
|
||||
pipelineNames.push({ name: "Focus Mode", value: WebsocketPipelineType.FocusCamera });
|
||||
pipelineNames.push({ name: "Focus Mode", value: WebsocketPipelineType.FocusCamera.valueOf() });
|
||||
}
|
||||
if (useCameraSettingsStore().isCalibrationMode) {
|
||||
pipelineNames.push({ name: "3D Calibration Mode", value: WebsocketPipelineType.Calib3d });
|
||||
pipelineNames.push({ name: "3D Calibration Mode", value: WebsocketPipelineType.Calib3d.valueOf() });
|
||||
}
|
||||
|
||||
return pipelineNames;
|
||||
@@ -240,7 +218,7 @@ useCameraSettingsStore().$subscribe((mutation, state) => {
|
||||
break;
|
||||
}
|
||||
});
|
||||
const wrappedCameras = computed<SelectItem[]>(() =>
|
||||
const wrappedCameras = computed<SelectItem<string>[]>(() =>
|
||||
Object.keys(useCameraSettingsStore().cameras).map((cameraUniqueName) => ({
|
||||
name: useCameraSettingsStore().cameras[cameraUniqueName].nickname,
|
||||
value: cameraUniqueName
|
||||
@@ -257,7 +235,7 @@ const wrappedCameras = computed<SelectItem[]>(() =>
|
||||
v-model="useStateStore().currentCameraUniqueName"
|
||||
label="Camera"
|
||||
:items="wrappedCameras"
|
||||
@update:modelValue="changeCurrentCameraUniqueName"
|
||||
@update:modelValue="pipelineType = useCameraSettingsStore().currentWebsocketPipelineType"
|
||||
/>
|
||||
<pv-input
|
||||
v-else
|
||||
@@ -407,14 +385,14 @@ const wrappedCameras = computed<SelectItem[]>(() =>
|
||||
<v-card-actions class="pr-5 pt-10px pb-5">
|
||||
<v-btn
|
||||
color="buttonPassive"
|
||||
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
|
||||
:variant="theme.global.current.value.dark ? 'outlined' : 'elevated'"
|
||||
@click="cancelPipelineCreation"
|
||||
>
|
||||
Cancel
|
||||
</v-btn>
|
||||
<v-btn
|
||||
color="buttonActive"
|
||||
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
|
||||
:variant="theme.global.current.value.dark ? 'outlined' : 'elevated'"
|
||||
:disabled="checkPipelineName(newPipelineName) !== true"
|
||||
@click="createNewPipeline"
|
||||
>
|
||||
@@ -441,7 +419,7 @@ const wrappedCameras = computed<SelectItem[]>(() =>
|
||||
<v-card-actions class="pa-5 pt-0">
|
||||
<v-btn
|
||||
color="buttonPassive"
|
||||
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
|
||||
:variant="theme.global.current.value.dark ? 'outlined' : 'elevated'"
|
||||
class="text-black"
|
||||
@click="cancelChangePipelineType"
|
||||
>
|
||||
@@ -449,7 +427,7 @@ const wrappedCameras = computed<SelectItem[]>(() =>
|
||||
</v-btn>
|
||||
<v-btn
|
||||
color="buttonActive"
|
||||
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
|
||||
:variant="theme.global.current.value.dark ? 'outlined' : 'elevated'"
|
||||
@click="confirmChangePipelineType"
|
||||
>
|
||||
Confirm
|
||||
|
||||
@@ -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 } from "vuetify/lib/composables/display";
|
||||
import { useTheme } from "vuetify";
|
||||
import { useDisplay, useTheme } from "vuetify";
|
||||
|
||||
const theme = useTheme();
|
||||
|
||||
@@ -106,6 +106,17 @@ 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) {
|
||||
@@ -122,14 +133,14 @@ const onBeforeTabUpdate = () => {
|
||||
density="compact"
|
||||
text="Camera is not connected. Please check your connection and try again."
|
||||
icon="mdi-alert-circle-outline"
|
||||
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'tonal'"
|
||||
:variant="theme.global.current.value.dark ? 'tonal' : 'elevated'"
|
||||
/>
|
||||
</template>
|
||||
<template v-else>
|
||||
<v-col
|
||||
v-for="(tabGroupData, tabGroupIndex) in tabGroups"
|
||||
:key="tabGroupIndex"
|
||||
:cols="tabGroupIndex == 1 && useCameraSettingsStore().currentPipelineSettings.doMultiTarget ? 7 : ''"
|
||||
:cols="tabGroupIndex === 1 && shouldUseWideSecondTabGroup ? 7 : ''"
|
||||
:class="tabGroupIndex !== tabGroups.length - 1 && 'pr-3'"
|
||||
@vue:before-update="onBeforeTabUpdate"
|
||||
>
|
||||
|
||||
@@ -35,7 +35,7 @@ const processingMode = computed<number>({
|
||||
<v-btn
|
||||
color="buttonPassive"
|
||||
:disabled="!useCameraSettingsStore().hasConnected"
|
||||
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
|
||||
:variant="theme.global.current.value.dark ? 'outlined' : 'elevated'"
|
||||
class="w-50"
|
||||
>
|
||||
<template #prepend>
|
||||
@@ -48,10 +48,10 @@ const processingMode = computed<number>({
|
||||
:disabled="
|
||||
!useCameraSettingsStore().hasConnected ||
|
||||
!useCameraSettingsStore().isCurrentVideoFormatCalibrated ||
|
||||
useCameraSettingsStore().currentPipelineSettings.pipelineType == PipelineType.ObjectDetection ||
|
||||
useCameraSettingsStore().currentPipelineSettings.pipelineType == PipelineType.ColoredShape
|
||||
useCameraSettingsStore().currentPipelineSettings.pipelineType === PipelineType.ObjectDetection ||
|
||||
useCameraSettingsStore().currentPipelineSettings.pipelineType === PipelineType.ColoredShape
|
||||
"
|
||||
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
|
||||
:variant="theme.global.current.value.dark ? 'outlined' : 'elevated'"
|
||||
class="w-50"
|
||||
>
|
||||
<template #prepend>
|
||||
@@ -69,7 +69,7 @@ const processingMode = computed<number>({
|
||||
<v-btn
|
||||
color="buttonPassive"
|
||||
class="fill w-50"
|
||||
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
|
||||
:variant="theme.global.current.value.dark ? 'outlined' : 'elevated'"
|
||||
>
|
||||
<v-icon start class="mode-btn-icon" size="large">mdi-import</v-icon>
|
||||
<span class="mode-btn-label">Raw</span>
|
||||
@@ -77,7 +77,7 @@ const processingMode = computed<number>({
|
||||
<v-btn
|
||||
color="buttonPassive"
|
||||
class="fill w-50"
|
||||
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
|
||||
:variant="theme.global.current.value.dark ? 'outlined' : 'elevated'"
|
||||
>
|
||||
<v-icon start class="mode-btn-icon" size="large">mdi-export</v-icon>
|
||||
<span class="mode-btn-label">Processed</span>
|
||||
|
||||
@@ -1,18 +1,17 @@
|
||||
<script setup lang="ts">
|
||||
import { PipelineType } from "@/types/PipelineTypes";
|
||||
import { PipelineType, type AprilTagPipelineSettings, AprilTagFamily } 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<ActivePipelineSettings>(
|
||||
() => useCameraSettingsStore().currentPipelineSettings
|
||||
const currentPipelineSettings = computed<AprilTagPipelineSettings>(
|
||||
() => useCameraSettingsStore().currentPipelineSettings as AprilTagPipelineSettings
|
||||
);
|
||||
const { mdAndDown } = useDisplay();
|
||||
const interactiveCols = computed(() =>
|
||||
@@ -25,7 +24,10 @@ const interactiveCols = computed(() =>
|
||||
<pv-select
|
||||
v-model="currentPipelineSettings.tagFamily"
|
||||
label="Target family"
|
||||
:items="['AprilTag 36h11 (6.5in)', 'AprilTag 16h5 (6in)']"
|
||||
:items="[
|
||||
{ value: AprilTagFamily.Family36h11, name: 'AprilTag 36h11 (6.5in)' },
|
||||
{ value: AprilTagFamily.Family16h5, name: '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 ActivePipelineSettings } from "@/types/PipelineTypes";
|
||||
import { PipelineType, type ArucoPipelineSettings, AprilTagFamily } 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<ActivePipelineSettings>(
|
||||
() => useCameraSettingsStore().currentPipelineSettings
|
||||
const currentPipelineSettings = computed<ArucoPipelineSettings>(
|
||||
() => useCameraSettingsStore().currentPipelineSettings as ArucoPipelineSettings
|
||||
);
|
||||
const { mdAndDown } = useDisplay();
|
||||
const interactiveCols = computed(() =>
|
||||
@@ -25,7 +25,10 @@ const interactiveCols = computed(() =>
|
||||
<pv-select
|
||||
v-model="currentPipelineSettings.tagFamily"
|
||||
label="Target family"
|
||||
:items="['AprilTag Family 36h11', 'AprilTag Family 16h5']"
|
||||
:items="[
|
||||
{ value: AprilTagFamily.Family36h11, name: 'AprilTag 36h11 (6.5in)' },
|
||||
{ value: AprilTagFamily.Family16h5, name: 'AprilTag 16h5 (6in)' }
|
||||
]"
|
||||
:select-cols="interactiveCols"
|
||||
@update:modelValue="(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ tagFamily: value }, false)"
|
||||
/>
|
||||
|
||||
@@ -1,6 +1,14 @@
|
||||
<script setup lang="ts">
|
||||
import { useCameraSettingsStore } from "@/stores/settings/CameraSettingsStore";
|
||||
import { type ActivePipelineSettings, PipelineType } from "@/types/PipelineTypes";
|
||||
import {
|
||||
type ActivePipelineSettings,
|
||||
PipelineType,
|
||||
ContourSortMode,
|
||||
ContourTargetOrientation,
|
||||
ContourGroupingMode,
|
||||
ContourIntersection,
|
||||
ContourShape
|
||||
} 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";
|
||||
@@ -61,7 +69,10 @@ 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="['Portrait', 'Landscape']"
|
||||
:items="[
|
||||
{ value: ContourTargetOrientation.Portrait, name: 'Portrait' },
|
||||
{ value: ContourTargetOrientation.Landscape, name: 'Landscape' }
|
||||
]"
|
||||
:select-cols="interactiveCols"
|
||||
@update:modelValue="
|
||||
(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ contourTargetOrientation: value }, false)
|
||||
@@ -72,7 +83,15 @@ 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="['Largest', 'Smallest', 'Highest', 'Lowest', 'Rightmost', 'Leftmost', 'Centermost']"
|
||||
: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' }
|
||||
]"
|
||||
@update:modelValue="
|
||||
(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ contourSortMode: value }, false)
|
||||
"
|
||||
@@ -166,7 +185,11 @@ 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="['Single', 'Dual', 'Two or More']"
|
||||
:items="[
|
||||
{ value: ContourGroupingMode.Single, name: 'Single' },
|
||||
{ value: ContourGroupingMode.Dual, name: 'Dual' },
|
||||
{ value: ContourGroupingMode.TwoOrMore, name: 'Two or More' }
|
||||
]"
|
||||
@update:modelValue="
|
||||
(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ contourGroupingMode: value }, false)
|
||||
"
|
||||
@@ -176,7 +199,13 @@ 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="['None', 'Up', 'Down', 'Left', 'Right']"
|
||||
: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' }
|
||||
]"
|
||||
:disabled="useCameraSettingsStore().currentPipelineSettings.contourGroupingMode === 0"
|
||||
@update:modelValue="
|
||||
(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ contourIntersection: value }, false)
|
||||
@@ -189,7 +218,12 @@ const interactiveCols = computed(() =>
|
||||
label="Target Shape"
|
||||
tooltip="The shape of targets to look for"
|
||||
:select-cols="interactiveCols"
|
||||
:items="['Circle', 'Polygon', 'Triangle', 'Quadrilateral']"
|
||||
:items="[
|
||||
{ value: ContourShape.Circle, name: 'Circle' },
|
||||
{ value: ContourShape.Polygon, name: 'Polygon' },
|
||||
{ value: ContourShape.Triangle, name: 'Triangle' },
|
||||
{ value: ContourShape.Quadrilateral, name: 'Quadrilateral' }
|
||||
]"
|
||||
@update:modelValue="
|
||||
(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ contourShape: value }, false)
|
||||
"
|
||||
|
||||
@@ -30,11 +30,11 @@ const getFilteredStreamDivisors = (): number[] => {
|
||||
};
|
||||
const getNumberOfSkippedDivisors = () => streamDivisors.length - getFilteredStreamDivisors().length;
|
||||
|
||||
const cameraResolutions = computed(() =>
|
||||
useCameraSettingsStore().currentCameraSettings.validVideoFormats.map(
|
||||
(f) => `${getResolutionString(f.resolution)} at ${f.fps} FPS, ${f.pixelFormat}`
|
||||
)
|
||||
);
|
||||
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 handleResolutionChange = (value: number) => {
|
||||
useCameraSettingsStore().changeCurrentPipelineSetting({ cameraVideoModeIndex: value }, false);
|
||||
|
||||
@@ -49,20 +49,23 @@ const handleResolutionChange = (value: number) => {
|
||||
const streamResolutions = computed(() => {
|
||||
const streamDivisors = getFilteredStreamDivisors();
|
||||
const currentResolution = useCameraSettingsStore().currentVideoFormat.resolution;
|
||||
return streamDivisors.map(
|
||||
(x) =>
|
||||
`${getResolutionString({
|
||||
width: Math.floor(currentResolution.width / x),
|
||||
height: Math.floor(currentResolution.height / x)
|
||||
})}`
|
||||
);
|
||||
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()
|
||||
});
|
||||
}
|
||||
});
|
||||
const handleStreamResolutionChange = (value: number) => {
|
||||
useCameraSettingsStore().changeCurrentPipelineSetting(
|
||||
{ streamingFrameDivisor: value + getNumberOfSkippedDivisors() },
|
||||
false
|
||||
);
|
||||
};
|
||||
const { mdAndDown } = useDisplay();
|
||||
|
||||
const interactiveCols = computed(() =>
|
||||
@@ -160,7 +163,7 @@ const interactiveCols = computed(() =>
|
||||
/>
|
||||
<pv-switch
|
||||
v-model="useCameraSettingsStore().currentPipelineSettings.blockForFrames"
|
||||
:disabled="!useCameraSettingsStore().currentCameraSettings.matchedCameraInfo.PVUsbCameraInfo"
|
||||
:disabled="useCameraSettingsStore().currentCameraSettings.matchedCameraInfo.type !== '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."
|
||||
@@ -182,17 +185,16 @@ 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="useCameraSettingsStore().currentPipelineSettings.streamingFrameDivisor"
|
||||
v-model="currentStreamResolutionIndex"
|
||||
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,8 +1,13 @@
|
||||
<script setup lang="ts">
|
||||
import { useCameraSettingsStore } from "@/stores/settings/CameraSettingsStore";
|
||||
import { type ObjectDetectionPipelineSettings, PipelineType } from "@/types/PipelineTypes";
|
||||
import {
|
||||
type ObjectDetectionPipelineSettings,
|
||||
PipelineType,
|
||||
ContourSortMode,
|
||||
ContourTargetOrientation
|
||||
} from "@/types/PipelineTypes";
|
||||
import PvSlider from "@/components/common/pv-slider.vue";
|
||||
import PvSelect from "@/components/common/pv-select.vue";
|
||||
import PvSelect, { type SelectItem } from "@/components/common/pv-select.vue";
|
||||
import PvRangeSlider from "@/components/common/pv-range-slider.vue";
|
||||
import { computed } from "vue";
|
||||
import { useStateStore } from "@/stores/StateStore";
|
||||
@@ -44,19 +49,19 @@ const supportedModels = computed<ObjectDetectionModelProperties[]>(() => {
|
||||
return availableModels.filter(isSupported);
|
||||
});
|
||||
|
||||
const selectedModel = computed({
|
||||
get: () => {
|
||||
const currentModel = currentPipelineSettings.value.model;
|
||||
if (!currentModel) return undefined;
|
||||
const modelWrapper = computed<SelectItem<string>[]>(() =>
|
||||
supportedModels.value.map((model) => ({
|
||||
name: model.nickname,
|
||||
value: model.modelPath
|
||||
}))
|
||||
);
|
||||
|
||||
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);
|
||||
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);
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -69,7 +74,7 @@ const selectedModel = computed({
|
||||
label="Model"
|
||||
tooltip="The model used to detect objects in the camera feed"
|
||||
:select-cols="interactiveCols"
|
||||
:items="supportedModels.map((model) => model.nickname)"
|
||||
:items="modelWrapper"
|
||||
/>
|
||||
|
||||
<pv-slider
|
||||
@@ -123,14 +128,13 @@ const selectedModel = computed({
|
||||
v-model="useCameraSettingsStore().currentPipelineSettings.contourTargetOrientation"
|
||||
label="Target Orientation"
|
||||
tooltip="Used to determine how to calculate target landmarks, as well as aspect ratio"
|
||||
:items="['Portrait', 'Landscape']"
|
||||
:items="[
|
||||
{ value: ContourTargetOrientation.Portrait, name: 'Portrait' },
|
||||
{ value: ContourTargetOrientation.Landscape, name: 'Landscape' }
|
||||
]"
|
||||
:select-cols="interactiveCols"
|
||||
@update:modelValue="
|
||||
(value) =>
|
||||
useCameraSettingsStore().changeCurrentPipelineSetting(
|
||||
{ contourTargetOrientation: typeof value === 'string' ? Number(value) : value },
|
||||
false
|
||||
)
|
||||
(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ contourTargetOrientation: value }, false)
|
||||
"
|
||||
/>
|
||||
<pv-select
|
||||
@@ -138,13 +142,17 @@ const selectedModel = computed({
|
||||
label="Target Sort"
|
||||
tooltip="Chooses the sorting mode used to determine the 'best' targets to provide to user code"
|
||||
:select-cols="interactiveCols"
|
||||
:items="['Largest', 'Smallest', 'Highest', 'Lowest', 'Rightmost', 'Leftmost', 'Centermost']"
|
||||
: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' }
|
||||
]"
|
||||
@update:modelValue="
|
||||
(value) =>
|
||||
useCameraSettingsStore().changeCurrentPipelineSetting(
|
||||
{ contourSortMode: typeof value === 'string' ? Number(value) : value },
|
||||
false
|
||||
)
|
||||
(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ contourSortMode: value }, false)
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -1,7 +1,13 @@
|
||||
<script setup lang="ts">
|
||||
import PvSelect from "@/components/common/pv-select.vue";
|
||||
import { useCameraSettingsStore } from "@/stores/settings/CameraSettingsStore";
|
||||
import { type ActivePipelineSettings, PipelineType, RobotOffsetPointMode } from "@/types/PipelineTypes";
|
||||
import {
|
||||
type ActivePipelineSettings,
|
||||
PipelineType,
|
||||
RobotOffsetPointMode,
|
||||
ContourTargetOrientation,
|
||||
ContourTargetOffsetPointEdge
|
||||
} from "@/types/PipelineTypes";
|
||||
import PvSwitch from "@/components/common/pv-switch.vue";
|
||||
import PvSlider from "@/components/common/pv-slider.vue";
|
||||
import { computed } from "vue";
|
||||
@@ -108,7 +114,13 @@ 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="['Center', 'Top', 'Bottom', 'Left', 'Right']"
|
||||
: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' }
|
||||
]"
|
||||
:select-cols="interactiveCols"
|
||||
@update:modelValue="
|
||||
(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ contourTargetOffsetPointEdge: value }, false)
|
||||
@@ -119,7 +131,10 @@ 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="['Portrait', 'Landscape']"
|
||||
:items="[
|
||||
{ value: ContourTargetOrientation.Portrait, name: 'Portrait' },
|
||||
{ value: ContourTargetOrientation.Landscape, name: 'Landscape' }
|
||||
]"
|
||||
:select-cols="interactiveCols"
|
||||
@update:modelValue="
|
||||
(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ contourTargetOrientation: value }, false)
|
||||
@@ -129,7 +144,11 @@ 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="['None', 'Single Point', 'Dual Point']"
|
||||
:items="[
|
||||
{ value: RobotOffsetPointMode.None, name: 'None' },
|
||||
{ value: RobotOffsetPointMode.Single, name: 'Single Point' },
|
||||
{ value: RobotOffsetPointMode.Dual, name: 'Dual Point' }
|
||||
]"
|
||||
:select-cols="interactiveCols"
|
||||
@update:modelValue="
|
||||
(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ offsetRobotOffsetMode: value }, false)
|
||||
@@ -168,7 +187,7 @@ const interactiveCols = computed(() =>
|
||||
block
|
||||
color="primary"
|
||||
class="text-black"
|
||||
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
|
||||
:variant="theme.global.current.value.dark ? 'outlined' : 'elevated'"
|
||||
@click="useCameraSettingsStore().takeRobotOffsetPoint(RobotOffsetType.Single)"
|
||||
>
|
||||
Take Point
|
||||
@@ -179,7 +198,7 @@ const interactiveCols = computed(() =>
|
||||
size="small"
|
||||
block
|
||||
color="error"
|
||||
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
|
||||
:variant="theme.global.current.value.dark ? 'outlined' : 'elevated'"
|
||||
@click="useCameraSettingsStore().takeRobotOffsetPoint(RobotOffsetType.Clear)"
|
||||
>
|
||||
Clear All Points
|
||||
@@ -196,7 +215,7 @@ const interactiveCols = computed(() =>
|
||||
block
|
||||
color="primary"
|
||||
class="text-black"
|
||||
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
|
||||
:variant="theme.global.current.value.dark ? 'outlined' : 'elevated'"
|
||||
@click="useCameraSettingsStore().takeRobotOffsetPoint(RobotOffsetType.DualFirst)"
|
||||
>
|
||||
Take First Point
|
||||
@@ -208,7 +227,7 @@ const interactiveCols = computed(() =>
|
||||
block
|
||||
color="primary"
|
||||
class="text-black"
|
||||
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
|
||||
:variant="theme.global.current.value.dark ? 'outlined' : 'elevated'"
|
||||
@click="useCameraSettingsStore().takeRobotOffsetPoint(RobotOffsetType.DualSecond)"
|
||||
>
|
||||
Take Second Point
|
||||
@@ -219,7 +238,7 @@ const interactiveCols = computed(() =>
|
||||
size="small"
|
||||
block
|
||||
color="error"
|
||||
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
|
||||
:variant="theme.global.current.value.dark ? 'outlined' : 'elevated'"
|
||||
@click="useCameraSettingsStore().takeRobotOffsetPoint(RobotOffsetType.Clear)"
|
||||
>
|
||||
Clear All Points
|
||||
|
||||
@@ -207,7 +207,7 @@ const resetCurrentBuffer = () => {
|
||||
color="buttonActive"
|
||||
class="mb-4 mt-1"
|
||||
style="width: min-content"
|
||||
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
|
||||
:variant="theme.global.current.value.dark ? 'outlined' : 'elevated'"
|
||||
@click="resetCurrentBuffer"
|
||||
>Reset Samples</v-btn
|
||||
>
|
||||
|
||||
@@ -191,7 +191,7 @@ const interactiveCols = computed(() =>
|
||||
block
|
||||
color="primary"
|
||||
class="text-black"
|
||||
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
|
||||
:variant="theme.global.current.value.dark ? 'outlined' : 'elevated'"
|
||||
@click="enableColorPicking(useCameraSettingsStore().currentPipelineSettings.hueInverted ? 2 : 3)"
|
||||
>
|
||||
<v-icon start size="large"> mdi-minus </v-icon>
|
||||
@@ -204,7 +204,7 @@ const interactiveCols = computed(() =>
|
||||
class="text-black"
|
||||
size="small"
|
||||
block
|
||||
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
|
||||
:variant="theme.global.current.value.dark ? 'outlined' : 'elevated'"
|
||||
@click="enableColorPicking(1)"
|
||||
>
|
||||
<v-icon start size="large"> mdi-plus-minus </v-icon>
|
||||
@@ -217,7 +217,7 @@ const interactiveCols = computed(() =>
|
||||
block
|
||||
color="primary"
|
||||
class="text-black"
|
||||
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
|
||||
:variant="theme.global.current.value.dark ? 'outlined' : 'elevated'"
|
||||
@click="enableColorPicking(useCameraSettingsStore().currentPipelineSettings.hueInverted ? 3 : 2)"
|
||||
>
|
||||
<v-icon start size="large"> mdi-plus </v-icon>
|
||||
@@ -232,7 +232,7 @@ const interactiveCols = computed(() =>
|
||||
color="primary"
|
||||
class="text-black"
|
||||
size="small"
|
||||
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
|
||||
:variant="theme.global.current.value.dark ? 'outlined' : 'elevated'"
|
||||
@click="disableColorPicking"
|
||||
>
|
||||
Cancel
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
@ -0,0 +1,565 @@
|
||||
<script setup lang="ts">
|
||||
import { inject, computed, ref, watch } from "vue";
|
||||
import { inject, computed, ref, watch, useTemplateRef } from "vue";
|
||||
import { useStateStore } from "@/stores/StateStore";
|
||||
import { useSettingsStore } from "@/stores/settings/GeneralSettingsStore";
|
||||
import PvSelect from "@/components/common/pv-select.vue";
|
||||
@@ -15,20 +14,20 @@ const theme = useTheme();
|
||||
|
||||
const restartProgram = async () => {
|
||||
if (await axiosPost("/utils/restartProgram", "restart PhotonVision")) {
|
||||
forceReloadPage();
|
||||
await forceReloadPage();
|
||||
}
|
||||
};
|
||||
const restartDevice = async () => {
|
||||
if (await axiosPost("/utils/restartDevice", "restart the device")) {
|
||||
forceReloadPage();
|
||||
await forceReloadPage();
|
||||
}
|
||||
};
|
||||
|
||||
const address = inject<string>("backendHost");
|
||||
|
||||
const offlineUpdate = ref();
|
||||
const offlineUpdate = useTemplateRef("offlineUpdate");
|
||||
const openOfflineUpdatePrompt = () => {
|
||||
offlineUpdate.value.click();
|
||||
offlineUpdate.value?.click();
|
||||
};
|
||||
|
||||
const offlineUpdateRegex = new RegExp("photonvision-((?:dev-)?v[\\w.-]+)-((?:linux|win|mac)\\w+)\\.jar");
|
||||
@@ -37,8 +36,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 === 0) return;
|
||||
const files = offlineUpdate.value?.files;
|
||||
if (!files?.length) return;
|
||||
|
||||
const match = files[0].name.match(offlineUpdateRegex);
|
||||
if (!match) {
|
||||
@@ -68,7 +67,7 @@ const handleOfflineUpdateRequest = async () => {
|
||||
});
|
||||
return;
|
||||
} else if (versionMatch && !dev) {
|
||||
handleOfflineUpdate(files[0]);
|
||||
await handleOfflineUpdate(files[0]);
|
||||
} else if (!versionMatch && !dev) {
|
||||
offlineUpdateDialog.value = {
|
||||
show: true,
|
||||
@@ -99,7 +98,7 @@ const handleOfflineUpdate = async (file: File) => {
|
||||
if (
|
||||
await axiosPost("/utils/offlineUpdate", "upload new software", formData, {
|
||||
headers: { "Content-Type": "multipart/form-data" },
|
||||
onUploadProgress: ({ progress }) => {
|
||||
onUploadProgress: ({ progress }: { progress?: number }) => {
|
||||
const uploadPercentage = (progress || 0) * 100.0;
|
||||
if (uploadPercentage < 99.5) {
|
||||
useStateStore().showSnackbarMessage({
|
||||
@@ -118,18 +117,18 @@ const handleOfflineUpdate = async (file: File) => {
|
||||
color: "secondary",
|
||||
timeout: -1
|
||||
});
|
||||
forceReloadPage();
|
||||
await forceReloadPage();
|
||||
}
|
||||
};
|
||||
|
||||
const exportLogFile = ref();
|
||||
const exportLogFile = useTemplateRef("exportLogFile");
|
||||
const openExportLogsPrompt = () => {
|
||||
exportLogFile.value.click();
|
||||
exportLogFile.value?.click();
|
||||
};
|
||||
|
||||
const exportSettings = ref();
|
||||
const exportSettings = useTemplateRef("exportSettings");
|
||||
const openExportSettingsPrompt = () => {
|
||||
exportSettings.value.click();
|
||||
exportSettings.value?.click();
|
||||
};
|
||||
|
||||
enum ImportType {
|
||||
@@ -141,10 +140,10 @@ enum ImportType {
|
||||
}
|
||||
|
||||
const showImportDialog = ref(false);
|
||||
const importType = ref<ImportType | undefined>(undefined);
|
||||
const importType = ref<ImportType>(ImportType.AllSettings);
|
||||
const importFile = ref<File | null>(null);
|
||||
|
||||
const handleSettingsImport = () => {
|
||||
const handleSettingsImport = async () => {
|
||||
if (importType.value === undefined || importFile.value === null) return;
|
||||
const formData = new FormData();
|
||||
formData.append("data", importFile.value);
|
||||
@@ -167,18 +166,18 @@ const handleSettingsImport = () => {
|
||||
settingsEndpoint = "";
|
||||
break;
|
||||
}
|
||||
axiosPost(`/settings${settingsEndpoint}`, "import settings", formData, {
|
||||
await axiosPost(`/settings${settingsEndpoint}`, "import settings", formData, {
|
||||
headers: { "Content-Type": "multipart/form-data" }
|
||||
});
|
||||
showImportDialog.value = false;
|
||||
importType.value = undefined;
|
||||
importType.value = ImportType.AllSettings;
|
||||
importFile.value = null;
|
||||
};
|
||||
|
||||
const showFactoryReset = ref(false);
|
||||
const nukePhotonConfigDirectory = async () => {
|
||||
if (await axiosPost("/utils/nukeConfigDirectory", "delete the config directory")) {
|
||||
forceReloadPage();
|
||||
await forceReloadPage();
|
||||
}
|
||||
};
|
||||
|
||||
@@ -312,7 +311,7 @@ watch(metricsHistorySnapshot, () => {
|
||||
<v-col>
|
||||
<v-btn
|
||||
color="buttonPassive"
|
||||
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
|
||||
:variant="theme.global.current.value.dark ? 'outlined' : 'elevated'"
|
||||
@click="useStateStore().showLogModal = true"
|
||||
>
|
||||
<v-icon start class="open-icon" size="large"> mdi-eye </v-icon>
|
||||
@@ -322,7 +321,7 @@ watch(metricsHistorySnapshot, () => {
|
||||
<v-col>
|
||||
<v-btn
|
||||
color="buttonPassive"
|
||||
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
|
||||
:variant="theme.global.current.value.dark ? 'outlined' : 'elevated'"
|
||||
@click="openExportLogsPrompt"
|
||||
>
|
||||
<v-icon start class="open-icon" size="large"> mdi-download </v-icon>
|
||||
@@ -345,7 +344,7 @@ watch(metricsHistorySnapshot, () => {
|
||||
<v-col>
|
||||
<v-btn
|
||||
color="buttonPassive"
|
||||
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
|
||||
:variant="theme.global.current.value.dark ? 'outlined' : 'elevated'"
|
||||
@click="() => (showImportDialog = true)"
|
||||
>
|
||||
<v-icon start class="open-icon" size="large"> mdi-import </v-icon>
|
||||
@@ -355,7 +354,7 @@ watch(metricsHistorySnapshot, () => {
|
||||
<v-col>
|
||||
<v-btn
|
||||
color="buttonPassive"
|
||||
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
|
||||
:variant="theme.global.current.value.dark ? 'outlined' : 'elevated'"
|
||||
@click="openExportSettingsPrompt"
|
||||
>
|
||||
<v-icon start class="open-icon" size="large"> mdi-export </v-icon>
|
||||
@@ -369,7 +368,7 @@ watch(metricsHistorySnapshot, () => {
|
||||
<v-col cols="12" sm="6"
|
||||
><v-btn
|
||||
color="buttonActive"
|
||||
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
|
||||
:variant="theme.global.current.value.dark ? 'outlined' : 'elevated'"
|
||||
@click="restartProgram"
|
||||
>
|
||||
<v-icon start class="open-icon" size="large"> mdi-restart </v-icon>
|
||||
@@ -379,7 +378,7 @@ watch(metricsHistorySnapshot, () => {
|
||||
<v-col cols="12" sm="6">
|
||||
<v-btn
|
||||
color="buttonPassive"
|
||||
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
|
||||
:variant="theme.global.current.value.dark ? 'outlined' : 'elevated'"
|
||||
@click="openOfflineUpdatePrompt"
|
||||
>
|
||||
<v-icon start class="open-icon" size="large"> mdi-upload </v-icon>
|
||||
@@ -400,7 +399,7 @@ watch(metricsHistorySnapshot, () => {
|
||||
<v-col cols="12" sm="6">
|
||||
<v-btn
|
||||
color="buttonActive"
|
||||
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
|
||||
:variant="theme.global.current.value.dark ? 'outlined' : 'elevated'"
|
||||
@click="restartDevice"
|
||||
>
|
||||
<v-icon start class="open-icon" size="large"> mdi-restart-alert </v-icon>
|
||||
@@ -410,7 +409,7 @@ watch(metricsHistorySnapshot, () => {
|
||||
<v-col cols="12" sm="6">
|
||||
<v-btn
|
||||
color="error"
|
||||
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
|
||||
:variant="theme.global.current.value.dark ? 'outlined' : 'elevated'"
|
||||
@click="() => (showFactoryReset = true)"
|
||||
>
|
||||
<v-icon start class="open-icon" size="large"> mdi-trash-can-outline </v-icon>
|
||||
@@ -453,7 +452,7 @@ watch(metricsHistorySnapshot, () => {
|
||||
<v-card-text class="pt-0 flex-0-0 pb-2">
|
||||
<div class="d-flex justify-space-between pb-3 pt-3">
|
||||
<span>CPU Temperature</span>
|
||||
<span>{{ cpuTempData.at(-1)?.value == -1 ? "--- " : Math.round(cpuTempData.at(-1)?.value ?? 0) }}°C</span>
|
||||
<span>{{ cpuTempData.at(-1)?.value === -1 ? "--- " : Math.round(cpuTempData.at(-1)?.value ?? 0) }}°C</span>
|
||||
</div>
|
||||
<Suspense>
|
||||
<!-- Allows us to import echarts when it's actually needed -->
|
||||
@@ -470,7 +469,10 @@ watch(metricsHistorySnapshot, () => {
|
||||
tooltip="Measured rate for this coprocessor ONLY. This FMS limit is for ALL robot communication. If you are experiencing bandwidth issues while under this limit, check other sources."
|
||||
/>
|
||||
<span
|
||||
>{{ networkUsageData.at(-1)?.value == -1 ? "---" : networkUsageData.at(-1)?.value.toFixed(3) }} Mb/s</span
|
||||
>{{
|
||||
networkUsageData.at(-1)?.value === -1 ? "---" : networkUsageData.at(-1)?.value.toFixed(3)
|
||||
}}
|
||||
Mb/s</span
|
||||
>
|
||||
</div>
|
||||
<Suspense>
|
||||
@@ -500,7 +502,7 @@ watch(metricsHistorySnapshot, () => {
|
||||
width="600"
|
||||
@update:modelValue="
|
||||
() => {
|
||||
importType = undefined;
|
||||
importType = ImportType.AllSettings;
|
||||
importFile = null;
|
||||
}
|
||||
"
|
||||
@@ -514,7 +516,13 @@ watch(metricsHistorySnapshot, () => {
|
||||
v-model="importType"
|
||||
label="Type"
|
||||
tooltip="Select the type of settings file you are trying to upload"
|
||||
:items="['All Settings', 'Hardware Config', 'Hardware Settings', 'Network Config', 'Apriltag Layout']"
|
||||
: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' }
|
||||
]"
|
||||
:select-cols="10"
|
||||
style="width: 100%"
|
||||
/>
|
||||
@@ -529,7 +537,7 @@ watch(metricsHistorySnapshot, () => {
|
||||
<v-btn
|
||||
color="primary"
|
||||
:disabled="importFile === null"
|
||||
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
|
||||
:variant="theme.global.current.value.dark ? 'outlined' : 'elevated'"
|
||||
@click="handleSettingsImport"
|
||||
>
|
||||
<v-icon start class="open-icon"> mdi-import </v-icon>
|
||||
@@ -552,10 +560,12 @@ watch(metricsHistorySnapshot, () => {
|
||||
<v-btn
|
||||
color="buttonActive"
|
||||
width="100%"
|
||||
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
|
||||
:variant="theme.global.current.value.dark ? 'outlined' : 'elevated'"
|
||||
@click="
|
||||
offlineUpdateDialog.show = false;
|
||||
handleOfflineUpdate(offlineUpdate.value.files[0]);
|
||||
if (offlineUpdate?.files?.length) {
|
||||
handleOfflineUpdate(offlineUpdate.files[0]);
|
||||
}
|
||||
"
|
||||
>
|
||||
<v-icon start class="open-icon" size="large"> mdi-upload </v-icon>
|
||||
|
||||
@@ -106,6 +106,7 @@ 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) {
|
||||
@@ -150,14 +151,11 @@ const saveGeneralSettings = async () => {
|
||||
}
|
||||
};
|
||||
|
||||
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])
|
||||
const currentNetworkInterface = computed<string>({
|
||||
get: () => useSettingsStore().network.networkManagerIface || "",
|
||||
set: (v) => {
|
||||
tempSettingsStruct.value.networkManagerIface = v;
|
||||
}
|
||||
});
|
||||
|
||||
watchEffect(() => {
|
||||
@@ -185,7 +183,7 @@ watchEffect(() => {
|
||||
</v-card-title>
|
||||
<div class="pa-5 pt-0">
|
||||
<v-card-title class="pl-0 pt-0 pb-10px">Networking</v-card-title>
|
||||
<v-form ref="form" v-model="settingsValid">
|
||||
<v-form v-model="settingsValid">
|
||||
<pv-input
|
||||
v-model="tempSettingsStruct.ntServerAddress"
|
||||
label="Team Number/NetworkTables Server Address"
|
||||
@@ -205,7 +203,7 @@ watchEffect(() => {
|
||||
density="compact"
|
||||
text="The NetworkTables Server Address is not set or is invalid. NetworkTables is unable to connect."
|
||||
icon="mdi-alert-circle-outline"
|
||||
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'tonal'"
|
||||
:variant="theme.global.current.value.dark ? 'tonal' : 'elevated'"
|
||||
/>
|
||||
<pv-radio
|
||||
v-show="!useSettingsStore().network.networkingDisabled"
|
||||
@@ -256,7 +254,7 @@ watchEffect(() => {
|
||||
/>
|
||||
<pv-select
|
||||
v-show="!useSettingsStore().network.networkingDisabled"
|
||||
v-model="currentNetworkInterfaceIndex"
|
||||
v-model="currentNetworkInterface"
|
||||
label="NetworkManager interface"
|
||||
:disabled="
|
||||
!tempSettingsStruct.shouldManage ||
|
||||
@@ -279,7 +277,7 @@ watchEffect(() => {
|
||||
density="compact"
|
||||
text="Cannot detect any wired connections! Send program logs to the developers for help."
|
||||
icon="mdi-alert-circle-outline"
|
||||
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'tonal'"
|
||||
:variant="theme.global.current.value.dark ? 'tonal' : 'elevated'"
|
||||
/>
|
||||
<pv-switch
|
||||
v-model="tempSettingsStruct.runNTServer"
|
||||
@@ -293,7 +291,7 @@ watchEffect(() => {
|
||||
density="compact"
|
||||
text="This mode is intended for debugging and should be off for proper usage. PhotonLib will NOT work!"
|
||||
icon="mdi-information-outline"
|
||||
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'tonal'"
|
||||
:variant="theme.global.current.value.dark ? 'tonal' : 'elevated'"
|
||||
/>
|
||||
<v-card-title class="pl-0 pt-3 pb-10px">Miscellaneous</v-card-title>
|
||||
<pv-switch
|
||||
@@ -308,13 +306,13 @@ watchEffect(() => {
|
||||
density="compact"
|
||||
text="This mode is intended for debugging and may reduce performance; it should be off for field use."
|
||||
icon="mdi-information-outline"
|
||||
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'tonal'"
|
||||
:variant="theme.global.current.value.dark ? 'tonal' : 'elevated'"
|
||||
/>
|
||||
</v-form>
|
||||
<v-btn
|
||||
color="primary"
|
||||
class="mt-3"
|
||||
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
|
||||
:variant="theme.global.current.value.dark ? 'outlined' : 'elevated'"
|
||||
style="color: black; width: 100%"
|
||||
:disabled="!settingsValid || !settingsHaveChanged()"
|
||||
@click="saveGeneralSettings"
|
||||
@@ -377,7 +375,7 @@ watchEffect(() => {
|
||||
</v-card-text>
|
||||
<v-card-actions class="pa-5 pt-0">
|
||||
<v-btn
|
||||
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
|
||||
:variant="theme.global.current.value.dark ? 'outlined' : 'elevated'"
|
||||
color="buttonPassive"
|
||||
class="text-black"
|
||||
@click="showThemeConfig = false"
|
||||
@@ -385,7 +383,7 @@ watchEffect(() => {
|
||||
Close
|
||||
</v-btn>
|
||||
<v-btn
|
||||
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
|
||||
:variant="theme.global.current.value.dark ? 'outlined' : 'elevated'"
|
||||
color="buttonActive"
|
||||
class="text-black"
|
||||
@click="
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { onMounted, ref, onBeforeUnmount, watch } from "vue";
|
||||
import { onMounted, onBeforeUnmount, watch, useTemplateRef } 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-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 }
|
||||
"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 }
|
||||
};
|
||||
const DEFAULT_COLOR = "blue";
|
||||
|
||||
@@ -26,9 +26,13 @@ const typeLabels = {
|
||||
};
|
||||
|
||||
const theme = useTheme();
|
||||
const chartRef = ref(null);
|
||||
const chartRef = useTemplateRef("chartRef");
|
||||
let chart: echarts.ECharts | null = null;
|
||||
|
||||
interface TooltipSeriesParam {
|
||||
value: [number, number];
|
||||
}
|
||||
|
||||
const getOptions = (data: ChartData[] = []) => {
|
||||
const now = Date.now();
|
||||
return {
|
||||
@@ -37,7 +41,7 @@ const getOptions = (data: ChartData[] = []) => {
|
||||
},
|
||||
tooltip: {
|
||||
trigger: "axis",
|
||||
formatter: (params: any) => {
|
||||
formatter: (params: TooltipSeriesParam[]) => {
|
||||
const p = params[0];
|
||||
const append = typeLabels[props.type];
|
||||
const fmsLimitLabel = "FMS Limit - 7.000 Mb/s";
|
||||
@@ -53,9 +57,9 @@ const getOptions = (data: ChartData[] = []) => {
|
||||
|
||||
return `${tooltip}</div>`;
|
||||
},
|
||||
backgroundColor: theme.themes.value[theme.global.name.value].colors.background,
|
||||
backgroundColor: theme.global.current.value.colors.background,
|
||||
textStyle: {
|
||||
color: theme.themes.value[theme.global.name.value].colors.onBackground
|
||||
color: theme.global.current.value.colors.onBackground
|
||||
},
|
||||
axisPointer: {
|
||||
animation: false
|
||||
@@ -80,12 +84,12 @@ const getOptions = (data: ChartData[] = []) => {
|
||||
min: now - 55 * 1000,
|
||||
axisLine: {
|
||||
lineStyle: {
|
||||
color: theme.global.name.value === "LightTheme" ? "#aaa" : "#777"
|
||||
color: theme.global.current.value.dark ? "#777" : "#aaa"
|
||||
}
|
||||
},
|
||||
axisLabel: {
|
||||
align: "left",
|
||||
color: theme.global.name.value === "LightTheme" ? "#fff" : "#ddd",
|
||||
color: theme.global.current.value.dark ? "#ddd" : "#fff",
|
||||
formatter: (value: number) => {
|
||||
const date = new Date(value);
|
||||
return date.toLocaleTimeString([], {
|
||||
@@ -102,12 +106,12 @@ const getOptions = (data: ChartData[] = []) => {
|
||||
position: "right",
|
||||
min:
|
||||
props.min ??
|
||||
function (value) {
|
||||
function (value: { min: number; max: number }) {
|
||||
return Math.max(0, (value.min - 10) | 0);
|
||||
},
|
||||
max:
|
||||
props.max ??
|
||||
function (value) {
|
||||
function (value: { min: number; max: number }) {
|
||||
return (value.max + 10) | 0;
|
||||
},
|
||||
splitNumber: 2,
|
||||
@@ -118,7 +122,7 @@ const getOptions = (data: ChartData[] = []) => {
|
||||
}
|
||||
},
|
||||
axisLabel: {
|
||||
color: theme.global.name.value === "LightTheme" ? "#fff" : "#ddd"
|
||||
color: theme.global.current.value.dark ? "#ddd" : "#fff"
|
||||
}
|
||||
},
|
||||
series: getSeries(data),
|
||||
@@ -127,7 +131,7 @@ const getOptions = (data: ChartData[] = []) => {
|
||||
};
|
||||
|
||||
const getSeries = (data: ChartData[] = []) => {
|
||||
const color = colors[`${props.color ?? DEFAULT_COLOR}-${theme.global.name.value}`];
|
||||
const color = colors[`${props.color ?? DEFAULT_COLOR}-${theme.global.current.value.dark ? "dark" : "light"}`];
|
||||
return [
|
||||
{
|
||||
type: "line",
|
||||
@@ -149,10 +153,9 @@ const getSeries = (data: ChartData[] = []) => {
|
||||
: null,
|
||||
lineStyle: {
|
||||
width: 1.5,
|
||||
color:
|
||||
theme.global.name.value === "LightTheme"
|
||||
? theme.themes.value[theme.global.name.value].colors.primary
|
||||
: `rgb(${color.r}, ${color.g}, ${color.b})`
|
||||
color: theme.global.current.value.dark
|
||||
? `rgb(${color.r}, ${color.g}, ${color.b})`
|
||||
: theme.global.current.value.colors.primary
|
||||
},
|
||||
areaStyle: {
|
||||
color: {
|
||||
@@ -164,17 +167,15 @@ const getSeries = (data: ChartData[] = []) => {
|
||||
colorStops: [
|
||||
{
|
||||
offset: 0,
|
||||
color:
|
||||
theme.global.name.value === "LightTheme"
|
||||
? `${theme.themes.value[theme.global.name.value].colors.primary}40`
|
||||
: `rgba(${color.r}, ${color.g}, ${color.b}, 0.15)`
|
||||
color: theme.global.current.value.dark
|
||||
? `rgba(${color.r}, ${color.g}, ${color.b}, 0.15)`
|
||||
: `${theme.global.current.value.colors.primary}40`
|
||||
},
|
||||
{
|
||||
offset: 1,
|
||||
color:
|
||||
theme.global.name.value === "LightTheme"
|
||||
? `${theme.themes.value[theme.global.name.value].colors.primary}40`
|
||||
: `rgba(${color.r}, ${color.g}, ${color.b}, 0.15)`
|
||||
color: theme.global.current.value.dark
|
||||
? `rgba(${color.r}, ${color.g}, ${color.b}, 0.15)`
|
||||
: `${theme.global.current.value.colors.primary}40`
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -191,10 +192,10 @@ interface ChartData {
|
||||
// Type options: "percentage", "temperature", "mb"
|
||||
const props = defineProps<{
|
||||
data: ChartData[];
|
||||
type: string;
|
||||
type: keyof typeof typeLabels;
|
||||
min?: number;
|
||||
max?: number;
|
||||
color?: string;
|
||||
color?: "red" | "green" | "blue" | "purple";
|
||||
}>();
|
||||
|
||||
onMounted(async () => {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, inject } from "vue";
|
||||
import { ref, computed, inject, useTemplateRef } 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 }) => {
|
||||
onUploadProgress: ({ progress }: { progress?: number }) => {
|
||||
const uploadPercentage = (progress || 0) * 100.0;
|
||||
if (uploadPercentage < 99.5) {
|
||||
useStateStore().showSnackbarMessage({
|
||||
@@ -74,20 +74,20 @@ const handleImport = async () => {
|
||||
importVersion.value = null;
|
||||
};
|
||||
|
||||
const deleteModel = (model: ObjectDetectionModelProperties) => {
|
||||
axiosPost("/objectdetection/delete", "delete an object detection model", {
|
||||
const deleteModel = async (model: ObjectDetectionModelProperties) => {
|
||||
await axiosPost("/objectdetection/delete", "delete an object detection model", {
|
||||
modelPath: model.modelPath
|
||||
});
|
||||
};
|
||||
|
||||
const renameModel = (model: ObjectDetectionModelProperties, newName: string) => {
|
||||
const renameModel = async (model: ObjectDetectionModelProperties, newName: string) => {
|
||||
useStateStore().showSnackbarMessage({
|
||||
message: "Renaming Object Detection Model...",
|
||||
color: "secondary",
|
||||
timeout: -1
|
||||
});
|
||||
|
||||
axiosPost("/objectdetection/rename", "rename an object detection model", {
|
||||
await axiosPost("/objectdetection/rename", "rename an object detection model", {
|
||||
modelPath: model.modelPath,
|
||||
newName: newName
|
||||
});
|
||||
@@ -97,7 +97,7 @@ const renameModel = (model: ObjectDetectionModelProperties, newName: string) =>
|
||||
// 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: any) => {
|
||||
const isSupported = (model: ObjectDetectionModelProperties) => {
|
||||
// Check if model's family is in the list of supported backends
|
||||
return supportedBackends.some((backend: string) => backend.toLowerCase() === model.family.toLowerCase());
|
||||
};
|
||||
@@ -106,19 +106,19 @@ const supportedModels = computed(() => {
|
||||
return availableModels.filter(isSupported);
|
||||
});
|
||||
|
||||
const exportModels = ref();
|
||||
const exportModels = useTemplateRef("exportModels");
|
||||
const openExportPrompt = () => {
|
||||
exportModels.value.click();
|
||||
exportModels.value?.click();
|
||||
};
|
||||
|
||||
const exportIndividualModel = ref();
|
||||
const exportIndividualModel = useTemplateRef("exportIndividualModel");
|
||||
const openExportIndividualModelPrompt = () => {
|
||||
exportIndividualModel.value.click();
|
||||
exportIndividualModel.value?.click();
|
||||
};
|
||||
|
||||
const showNukeDialog = ref(false);
|
||||
const nukeModels = () => {
|
||||
axiosPost("/objectdetection/nuke", "clear and reset object detection models");
|
||||
const nukeModels = async () => {
|
||||
await 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 }) => {
|
||||
onUploadProgress: ({ progress }: { progress?: number }) => {
|
||||
const uploadPercentage = (progress || 0) * 100.0;
|
||||
if (uploadPercentage < 99.5) {
|
||||
useStateStore().showSnackbarMessage({
|
||||
@@ -166,7 +166,7 @@ const handleBulkImport = async () => {
|
||||
<v-btn
|
||||
color="buttonActive"
|
||||
class="justify-center"
|
||||
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
|
||||
:variant="theme.global.current.value.dark ? 'outlined' : 'elevated'"
|
||||
@click="() => (showImportDialog = true)"
|
||||
>
|
||||
<v-icon start class="open-icon"> mdi-import </v-icon>
|
||||
@@ -245,7 +245,7 @@ const handleBulkImport = async () => {
|
||||
importHeight === null ||
|
||||
importVersion === null
|
||||
"
|
||||
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
|
||||
:variant="theme.global.current.value.dark ? 'outlined' : 'elevated'"
|
||||
@click="handleImport()"
|
||||
>
|
||||
<v-icon start class="open-icon" size="large"> mdi-import </v-icon>
|
||||
@@ -260,7 +260,7 @@ const handleBulkImport = async () => {
|
||||
<v-btn
|
||||
color="buttonActive"
|
||||
class="justify-center"
|
||||
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
|
||||
:variant="theme.global.current.value.dark ? 'outlined' : 'elevated'"
|
||||
@click="() => (showBulkImportDialog = true)"
|
||||
>
|
||||
<v-icon start class="open-icon"> mdi-import </v-icon>
|
||||
@@ -278,7 +278,7 @@ const handleBulkImport = async () => {
|
||||
color="buttonActive"
|
||||
width="100%"
|
||||
:disabled="importFile === null"
|
||||
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
|
||||
:variant="theme.global.current.value.dark ? 'outlined' : 'elevated'"
|
||||
@click="handleBulkImport()"
|
||||
>
|
||||
<v-icon start class="open-icon" size="large"> mdi-import </v-icon>
|
||||
@@ -292,7 +292,7 @@ const handleBulkImport = async () => {
|
||||
<v-col cols="12" sm="6">
|
||||
<v-btn
|
||||
color="buttonPassive"
|
||||
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
|
||||
:variant="theme.global.current.value.dark ? 'outlined' : 'elevated'"
|
||||
@click="openExportPrompt"
|
||||
>
|
||||
<v-icon start class="open-icon"> mdi-export </v-icon>
|
||||
@@ -309,7 +309,7 @@ const handleBulkImport = async () => {
|
||||
<v-col cols="12" sm="6">
|
||||
<v-btn
|
||||
color="error"
|
||||
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
|
||||
:variant="theme.global.current.value.dark ? 'outlined' : 'elevated'"
|
||||
@click="() => (showNukeDialog = true)"
|
||||
>
|
||||
<v-icon left class="open-icon"> mdi-trash </v-icon>
|
||||
@@ -339,7 +339,7 @@ const handleBulkImport = async () => {
|
||||
small
|
||||
color="error"
|
||||
title="Delete Model"
|
||||
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
|
||||
:variant="theme.global.current.value.dark ? 'outlined' : 'elevated'"
|
||||
@click="() => (confirmDeleteDialog = { show: true, model })"
|
||||
>
|
||||
<v-icon size="large">mdi-trash-can-outline</v-icon>
|
||||
@@ -351,7 +351,7 @@ const handleBulkImport = async () => {
|
||||
small
|
||||
color="buttonActive"
|
||||
title="Rename Model"
|
||||
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
|
||||
:variant="theme.global.current.value.dark ? 'outlined' : 'elevated'"
|
||||
@click="() => (showRenameDialog = { show: true, model, newName: '' })"
|
||||
>
|
||||
<v-icon size="large">mdi-pencil</v-icon>
|
||||
@@ -362,7 +362,7 @@ const handleBulkImport = async () => {
|
||||
icon
|
||||
small
|
||||
color="buttonPassive"
|
||||
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
|
||||
:variant="theme.global.current.value.dark ? 'outlined' : 'elevated'"
|
||||
@click="() => (showInfo = { show: true, model })"
|
||||
>
|
||||
<v-icon size="large">mdi-information</v-icon>
|
||||
@@ -391,13 +391,13 @@ const handleBulkImport = async () => {
|
||||
</div>
|
||||
<v-card-actions class="pt-5 pb-0 pr-0" style="justify-content: flex-end">
|
||||
<v-btn
|
||||
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
|
||||
:variant="theme.global.current.value.dark ? 'outlined' : 'elevated'"
|
||||
color="error"
|
||||
@click="showRenameDialog.show = false"
|
||||
>Cancel</v-btn
|
||||
>
|
||||
<v-btn
|
||||
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
|
||||
:variant="theme.global.current.value.dark ? 'outlined' : 'elevated'"
|
||||
color="buttonActive"
|
||||
@click="renameModel(showRenameDialog.model, showRenameDialog.newName)"
|
||||
>Rename</v-btn
|
||||
@@ -413,7 +413,7 @@ const handleBulkImport = async () => {
|
||||
<v-btn
|
||||
color="buttonPassive"
|
||||
width="100%"
|
||||
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
|
||||
:variant="theme.global.current.value.dark ? 'outlined' : 'elevated'"
|
||||
@click="openExportIndividualModelPrompt"
|
||||
>
|
||||
<v-icon left class="open-icon" size="large"> mdi-export </v-icon>
|
||||
@@ -446,8 +446,8 @@ const handleBulkImport = async () => {
|
||||
:on-backup="openExportPrompt"
|
||||
:on-confirm="nukeModels"
|
||||
title="Delete and Reset All Object Detection Models"
|
||||
:description="'This will delete ALL object detection models and re-extract the default object detection models. This action cannot be undone.'"
|
||||
:expected-confirmation-text="'Delete Models'"
|
||||
description="This will delete ALL object detection models and re-extract the default object detection models. This action cannot be undone."
|
||||
expected-confirmation-text="Delete Models"
|
||||
delete-text="Delete all models"
|
||||
/>
|
||||
</v-card>
|
||||
|
||||
@@ -40,18 +40,13 @@ 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, encodeData = true) {
|
||||
send(data: unknown) {
|
||||
// Only send data if the websocket is open
|
||||
if (this.isConnected()) {
|
||||
if (encodeData) {
|
||||
this.websocket?.send(encode(data));
|
||||
} else {
|
||||
this.websocket?.send(data);
|
||||
}
|
||||
this.websocket?.send(encode(data));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useStateStore } from "@/stores/StateStore";
|
||||
import type { Resolution } from "@/types/SettingTypes";
|
||||
import axios from "axios";
|
||||
import axios, { type AxiosRequestConfig } from "axios";
|
||||
|
||||
export const resolutionsAreEqual = (a: Resolution, b: Resolution) => {
|
||||
return a.height === b.height && a.width === b.width;
|
||||
@@ -51,15 +51,16 @@ 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();
|
||||
if (target === null) reject(new Error("FileReader event target is null"));
|
||||
else resolve(JSON.parse(target.result as string) as T);
|
||||
};
|
||||
fileReader.onerror = (error) => reject(error);
|
||||
fileReader.onerror = () => reject(new Error("Error reading file"));
|
||||
fileReader.readAsText(file);
|
||||
});
|
||||
};
|
||||
@@ -73,7 +74,13 @@ 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, data?: any, config?: any): Promise<boolean> => {
|
||||
export const axiosPost = async (
|
||||
url: string,
|
||||
description: string,
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
data?: any,
|
||||
config?: AxiosRequestConfig
|
||||
): Promise<boolean> => {
|
||||
try {
|
||||
await axios.post(url, data, config);
|
||||
useStateStore().showSnackbarMessage({
|
||||
@@ -81,6 +88,7 @@ export const axiosPost = async (url: string, description: string, data?: any, co
|
||||
color: "success"
|
||||
});
|
||||
return true;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
} catch (error: any) {
|
||||
if (error.response) {
|
||||
useStateStore().showSnackbarMessage({
|
||||
|
||||
@@ -2,7 +2,7 @@ import { type ThemeInstance } from "vuetify";
|
||||
import { LightTheme, DarkTheme } from "@/plugins/vuetify";
|
||||
|
||||
export const resetTheme = (theme: ThemeInstance) => {
|
||||
const themeType = theme.global.name.value === "LightTheme" ? "light" : "dark";
|
||||
const themeType = theme.global.current.value.dark ? "dark" : "light";
|
||||
localStorage.removeItem(`${themeType}-background`);
|
||||
localStorage.removeItem(`${themeType}-primary`);
|
||||
localStorage.removeItem(`${themeType}-secondary`);
|
||||
@@ -12,13 +12,13 @@ export const resetTheme = (theme: ThemeInstance) => {
|
||||
};
|
||||
|
||||
export const getThemeColor = (theme: ThemeInstance, color: string): string => {
|
||||
const themeType = theme.global.name.value === "LightTheme" ? "light" : "dark";
|
||||
const defaultTheme = theme.global.name.value === "LightTheme" ? LightTheme : DarkTheme;
|
||||
const themeType = theme.global.current.value.dark ? "dark" : "light";
|
||||
const defaultTheme = theme.global.current.value.dark ? DarkTheme : LightTheme;
|
||||
return localStorage.getItem(`${themeType}-${color}`) ?? defaultTheme.colors![color]!;
|
||||
};
|
||||
|
||||
export const setThemeColor = (theme: ThemeInstance, color: string, value: string | null) => {
|
||||
const themeType = theme.global.name.value === "LightTheme" ? "light" : "dark";
|
||||
const themeType = theme.global.current.value.dark ? "dark" : "light";
|
||||
if (value) localStorage.setItem(`${themeType}-${color}`, value);
|
||||
else localStorage.removeItem(`${themeType}-${color}`);
|
||||
|
||||
@@ -38,7 +38,7 @@ export const restoreThemeConfig = (theme: ThemeInstance) => {
|
||||
if (storedTheme) theme.global.name.value = storedTheme;
|
||||
|
||||
// Restore custom theme colors
|
||||
const themeType = theme.global.name.value === "LightTheme" ? "light" : "dark";
|
||||
const themeType = theme.global.current.value.dark ? "dark" : "light";
|
||||
const defaultTheme = theme.global.name.value === "LightTheme" ? LightTheme : DarkTheme;
|
||||
|
||||
const customBackground = localStorage.getItem(`${themeType}-background`);
|
||||
@@ -47,7 +47,7 @@ export const restoreThemeConfig = (theme: ThemeInstance) => {
|
||||
const customSurface = localStorage.getItem(`${themeType}-surface`);
|
||||
|
||||
theme.themes.value[theme.global.name.value].colors.background = customBackground ?? defaultTheme.colors!.background!;
|
||||
theme.themes.value[theme.global.name.value].colors.sidebar = theme.themes.value[theme.global.name.value].dark
|
||||
theme.themes.value[theme.global.name.value].colors.sidebar = theme.global.current.value.dark
|
||||
? (customBackground ?? defaultTheme.colors!.sidebar!)
|
||||
: (customSurface ?? defaultTheme.colors!.sidebar!);
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ const runtimeMode: PhotonClientRuntimeMode = process.env.NODE_ENV as PhotonClien
|
||||
|
||||
let backendHost: string;
|
||||
let backendHostname: string;
|
||||
switch (runtimeMode as PhotonClientRuntimeMode) {
|
||||
switch (runtimeMode) {
|
||||
case "development":
|
||||
backendHost = `${location.hostname}:5800`;
|
||||
backendHostname = location.hostname;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import "vuetify/styles";
|
||||
import("@mdi/font/css/materialdesignicons.css");
|
||||
import type { ThemeDefinition } from "vuetify/lib/composables/theme";
|
||||
void import("@mdi/font/css/materialdesignicons.css");
|
||||
import type { ThemeDefinition } from "vuetify";
|
||||
import { createVuetify } from "vuetify";
|
||||
|
||||
const CommonColors = {
|
||||
|
||||
@@ -31,7 +31,8 @@ interface StateStore {
|
||||
currentCameraUniqueName: string;
|
||||
networkUsageHistory: NetworkUsageEntry[];
|
||||
|
||||
backendResults: Record<number, PipelineResult>;
|
||||
// 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>;
|
||||
multitagResultBuffer: Record<string, MultitagResult[]>;
|
||||
|
||||
colorPickingMode: boolean;
|
||||
@@ -39,8 +40,6 @@ interface StateStore {
|
||||
calibrationData: {
|
||||
imageCount: number;
|
||||
videoFormatIndex: number;
|
||||
minimumImageCount: number;
|
||||
hasEnoughImages: boolean;
|
||||
};
|
||||
|
||||
snackbarData: {
|
||||
@@ -88,9 +87,7 @@ export const useStateStore = defineStore("state", {
|
||||
|
||||
calibrationData: {
|
||||
imageCount: 0,
|
||||
videoFormatIndex: 0,
|
||||
minimumImageCount: 12,
|
||||
hasEnoughImages: false
|
||||
videoFormatIndex: 0
|
||||
},
|
||||
|
||||
snackbarData: {
|
||||
@@ -161,9 +158,7 @@ export const useStateStore = defineStore("state", {
|
||||
updateCalibrationStateValuesFromWebsocket(data: WebsocketCalibrationData) {
|
||||
this.calibrationData = {
|
||||
imageCount: data.count,
|
||||
videoFormatIndex: data.videoModeIndex,
|
||||
minimumImageCount: data.minCount,
|
||||
hasEnoughImages: data.hasEnough
|
||||
videoFormatIndex: data.videoModeIndex
|
||||
};
|
||||
},
|
||||
updateDiscoveredCameras(data: VsmState) {
|
||||
|
||||
@@ -64,17 +64,14 @@ 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;
|
||||
return this.currentCameraSettings.currentPipelineIndex === WebsocketPipelineType.DriverMode.valueOf();
|
||||
},
|
||||
isCalibrationMode(): boolean {
|
||||
return this.currentCameraSettings.currentPipelineIndex == WebsocketPipelineType.Calib3d;
|
||||
return this.currentCameraSettings.currentPipelineIndex === WebsocketPipelineType.Calib3d.valueOf();
|
||||
},
|
||||
isFocusMode(): boolean {
|
||||
return this.currentCameraSettings.currentPipelineIndex == WebsocketPipelineType.FocusCamera;
|
||||
return this.currentCameraSettings.currentPipelineIndex === WebsocketPipelineType.FocusCamera.valueOf();
|
||||
},
|
||||
isCSICamera(): boolean {
|
||||
return this.currentCameraSettings.isCSICamera;
|
||||
@@ -94,6 +91,9 @@ export const useCameraSettingsStore = defineStore("cameraSettings", {
|
||||
fpsLimit(): number {
|
||||
return this.currentCameraSettings.fpsLimit;
|
||||
},
|
||||
isEnabled(): boolean {
|
||||
return this.currentCameraSettings.isEnabled;
|
||||
},
|
||||
isConnected(): boolean {
|
||||
return this.currentCameraSettings.isConnected;
|
||||
},
|
||||
@@ -116,23 +116,20 @@ export const useCameraSettingsStore = defineStore("cameraSettings", {
|
||||
inputPort: d.inputStreamPort,
|
||||
outputPort: d.outputStreamPort
|
||||
},
|
||||
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
|
||||
})),
|
||||
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
|
||||
})),
|
||||
completeCalibrations: d.calibrations,
|
||||
isCSICamera: d.isCSICamera,
|
||||
minExposureRaw: d.minExposureRaw,
|
||||
@@ -145,6 +142,7 @@ 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
|
||||
@@ -196,7 +194,7 @@ export const useCameraSettingsStore = defineStore("cameraSettings", {
|
||||
addNewPipeline: [newPipelineName, pipelineType],
|
||||
cameraUniqueName: cameraUniqueName
|
||||
};
|
||||
useStateStore().websocket?.send(payload, true);
|
||||
useStateStore().websocket?.send(payload);
|
||||
},
|
||||
/**
|
||||
* Modify the settings of the currently selected pipeline of the provided camera.
|
||||
@@ -220,15 +218,13 @@ export const useCameraSettingsStore = defineStore("cameraSettings", {
|
||||
if (updateStore) {
|
||||
this.changePipelineSettingsInStore(settings, cameraUniqueName);
|
||||
}
|
||||
useStateStore().websocket?.send(payload, true);
|
||||
useStateStore().websocket?.send(payload);
|
||||
},
|
||||
changePipelineSettingsInStore(
|
||||
settings: Partial<ActivePipelineSettings>,
|
||||
cameraUniqueName: string = useStateStore().currentCameraUniqueName
|
||||
) {
|
||||
Object.entries(settings).forEach(([k, v]) => {
|
||||
this.cameras[cameraUniqueName].pipelineSettings[k] = v;
|
||||
});
|
||||
Object.assign(this.cameras[cameraUniqueName].pipelineSettings, settings);
|
||||
},
|
||||
/**
|
||||
* Change the nickname of the currently selected pipeline of the provided camera.
|
||||
@@ -249,7 +245,7 @@ export const useCameraSettingsStore = defineStore("cameraSettings", {
|
||||
if (updateStore) {
|
||||
this.cameras[cameraUniqueName].pipelineSettings.pipelineNickname = newName;
|
||||
}
|
||||
useStateStore().websocket?.send(payload, true);
|
||||
useStateStore().websocket?.send(payload);
|
||||
},
|
||||
/**
|
||||
* 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.
|
||||
@@ -265,7 +261,7 @@ export const useCameraSettingsStore = defineStore("cameraSettings", {
|
||||
pipelineType: type,
|
||||
cameraUniqueName: cameraUniqueName
|
||||
};
|
||||
useStateStore().websocket?.send(payload, true);
|
||||
useStateStore().websocket?.send(payload);
|
||||
},
|
||||
/**
|
||||
* Change the index of the pipeline of the currently selected camera.
|
||||
@@ -285,21 +281,22 @@ export const useCameraSettingsStore = defineStore("cameraSettings", {
|
||||
};
|
||||
if (updateStore) {
|
||||
if (
|
||||
this.cameras[cameraUniqueName].currentPipelineIndex !== -1 &&
|
||||
this.cameras[cameraUniqueName].currentPipelineIndex !== -2
|
||||
this.cameras[cameraUniqueName].currentPipelineIndex !== WebsocketPipelineType.DriverMode.valueOf() &&
|
||||
this.cameras[cameraUniqueName].currentPipelineIndex !== WebsocketPipelineType.Calib3d.valueOf() &&
|
||||
this.cameras[cameraUniqueName].currentPipelineIndex !== WebsocketPipelineType.FocusCamera.valueOf()
|
||||
) {
|
||||
this.cameras[cameraUniqueName].lastPipelineIndex = this.cameras[cameraUniqueName].currentPipelineIndex;
|
||||
}
|
||||
this.cameras[cameraUniqueName].currentPipelineIndex = index;
|
||||
}
|
||||
useStateStore().websocket?.send(payload, true);
|
||||
useStateStore().websocket?.send(payload);
|
||||
},
|
||||
setDriverMode(isDriverMode: boolean, cameraUniqueName: string = useStateStore().currentCameraUniqueName) {
|
||||
const payload = {
|
||||
driverMode: isDriverMode,
|
||||
cameraUniqueName: cameraUniqueName
|
||||
};
|
||||
useStateStore().websocket?.send(payload, true);
|
||||
useStateStore().websocket?.send(payload);
|
||||
},
|
||||
/**
|
||||
* Change the currently selected pipeline of the provided camera.
|
||||
@@ -311,7 +308,7 @@ export const useCameraSettingsStore = defineStore("cameraSettings", {
|
||||
deleteCurrentPipeline: {},
|
||||
cameraUniqueName: cameraUniqueName
|
||||
};
|
||||
useStateStore().websocket?.send(payload, true);
|
||||
useStateStore().websocket?.send(payload);
|
||||
},
|
||||
/**
|
||||
* Duplicate the pipeline at the provided index.
|
||||
@@ -324,7 +321,7 @@ export const useCameraSettingsStore = defineStore("cameraSettings", {
|
||||
duplicatePipeline: pipelineIndex,
|
||||
cameraUniqueName: cameraUniqueName
|
||||
};
|
||||
useStateStore().websocket?.send(payload, true);
|
||||
useStateStore().websocket?.send(payload);
|
||||
},
|
||||
/**
|
||||
* Change the currently set camera
|
||||
@@ -339,7 +336,7 @@ export const useCameraSettingsStore = defineStore("cameraSettings", {
|
||||
if (updateStore) {
|
||||
useStateStore().currentCameraUniqueName = cameraUniqueName;
|
||||
}
|
||||
useStateStore().websocket?.send(payload, true);
|
||||
useStateStore().websocket?.send(payload);
|
||||
},
|
||||
/**
|
||||
* Change the nickname of the provided camera.
|
||||
@@ -385,14 +382,12 @@ 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, true);
|
||||
useStateStore().websocket?.send(payload);
|
||||
},
|
||||
/**
|
||||
* End the 3D calibration process for the provided camera.
|
||||
@@ -424,7 +419,7 @@ export const useCameraSettingsStore = defineStore("cameraSettings", {
|
||||
takeCalibrationSnapshot: true,
|
||||
cameraUniqueName: cameraUniqueName
|
||||
};
|
||||
useStateStore().websocket?.send(payload, true);
|
||||
useStateStore().websocket?.send(payload);
|
||||
},
|
||||
/**
|
||||
* Save a snapshot of the input frame of the camera.
|
||||
@@ -436,7 +431,7 @@ export const useCameraSettingsStore = defineStore("cameraSettings", {
|
||||
saveInputSnapshot: true,
|
||||
cameraUniqueName: cameraUniqueName
|
||||
};
|
||||
useStateStore().websocket?.send(payload, true);
|
||||
useStateStore().websocket?.send(payload);
|
||||
},
|
||||
/**
|
||||
* Save a snapshot of the output frame of the camera.
|
||||
@@ -448,7 +443,7 @@ export const useCameraSettingsStore = defineStore("cameraSettings", {
|
||||
saveOutputSnapshot: true,
|
||||
cameraUniqueName: cameraUniqueName
|
||||
};
|
||||
useStateStore().websocket?.send(payload, true);
|
||||
useStateStore().websocket?.send(payload);
|
||||
},
|
||||
/**
|
||||
* Set the robot offset mode type.
|
||||
@@ -461,7 +456,7 @@ export const useCameraSettingsStore = defineStore("cameraSettings", {
|
||||
robotOffsetPoint: type,
|
||||
cameraUniqueName: cameraUniqueName
|
||||
};
|
||||
useStateStore().websocket?.send(payload, true);
|
||||
useStateStore().websocket?.send(payload);
|
||||
},
|
||||
getCalibrationCoeffs(
|
||||
resolution: Resolution,
|
||||
|
||||
@@ -10,6 +10,7 @@ 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 {
|
||||
@@ -17,7 +18,7 @@ interface GeneralSettingsStore {
|
||||
network: NetworkSettings;
|
||||
lighting: LightingSettings;
|
||||
metrics: MetricData;
|
||||
currentFieldLayout;
|
||||
currentFieldLayout: AprilTagFieldLayout;
|
||||
}
|
||||
|
||||
interface MetricsEntry {
|
||||
@@ -184,7 +185,7 @@ export const useSettingsStore = defineStore("settings", {
|
||||
const payload = {
|
||||
enabledLEDPercentage: brightness
|
||||
};
|
||||
useStateStore().websocket?.send(payload, true);
|
||||
useStateStore().websocket?.send(payload);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -5,6 +5,7 @@ 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,
|
||||
@@ -35,18 +36,62 @@ 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: number;
|
||||
contourGroupingMode: ContourGroupingMode;
|
||||
hsvValue: WebsocketNumberPair | [number, number];
|
||||
cameraGain: number;
|
||||
cameraBlueGain: number;
|
||||
cameraRedGain: number;
|
||||
cornerDetectionSideCount: number;
|
||||
contourRatio: WebsocketNumberPair | [number, number];
|
||||
contourTargetOffsetPointEdge: number;
|
||||
contourTargetOffsetPointEdge: ContourTargetOffsetPointEdge;
|
||||
pipelineNickname: string;
|
||||
inputImageRotationMode: number;
|
||||
contourArea: WebsocketNumberPair | [number, number];
|
||||
@@ -56,7 +101,7 @@ export interface PipelineSettings {
|
||||
inputShouldShow: boolean;
|
||||
cameraAutoExposure: boolean;
|
||||
contourSpecklePercentage: number;
|
||||
contourTargetOrientation: number;
|
||||
contourTargetOrientation: ContourTargetOrientation;
|
||||
targetModel: TargetModel;
|
||||
cornerDetectionUseConvexHulls: boolean;
|
||||
outputShouldShow: boolean;
|
||||
@@ -67,7 +112,7 @@ export interface PipelineSettings {
|
||||
ledMode: boolean;
|
||||
hueInverted: boolean;
|
||||
outputMaximumTargets: number;
|
||||
contourSortMode: number;
|
||||
contourSortMode: ContourSortMode;
|
||||
cameraExposureRaw: number;
|
||||
cameraMinExposureRaw: number;
|
||||
cameraMaxExposureRaw: number;
|
||||
@@ -80,11 +125,13 @@ export interface PipelineSettings {
|
||||
cornerDetectionAccuracyPercentage: number;
|
||||
hsvSaturation: WebsocketNumberPair | [number, number];
|
||||
pipelineType: PipelineType;
|
||||
contourIntersection: number;
|
||||
contourIntersection: ContourIntersection;
|
||||
|
||||
cameraAutoWhiteBalance: boolean;
|
||||
cameraWhiteBalanceTemp: number;
|
||||
|
||||
crosshair: boolean;
|
||||
|
||||
blockForFrames: boolean;
|
||||
}
|
||||
export type ConfigurablePipelineSettings = Partial<
|
||||
@@ -113,13 +160,13 @@ export const DefaultPipelineSettings: Omit<
|
||||
offsetRobotOffsetMode: RobotOffsetPointMode.None,
|
||||
streamingFrameDivisor: 0,
|
||||
offsetDualPointBArea: 0,
|
||||
contourGroupingMode: 0,
|
||||
contourGroupingMode: ContourGroupingMode.Single,
|
||||
hsvValue: { first: 50, second: 255 },
|
||||
cameraBlueGain: 20,
|
||||
cameraRedGain: 11,
|
||||
cornerDetectionSideCount: 4,
|
||||
contourRatio: { first: 0, second: 20 },
|
||||
contourTargetOffsetPointEdge: 0,
|
||||
contourTargetOffsetPointEdge: ContourTargetOffsetPointEdge.Center,
|
||||
pipelineNickname: "Placeholder Pipeline",
|
||||
inputImageRotationMode: 0,
|
||||
contourArea: { first: 0, second: 100 },
|
||||
@@ -129,7 +176,7 @@ export const DefaultPipelineSettings: Omit<
|
||||
inputShouldShow: false,
|
||||
cameraAutoExposure: false,
|
||||
contourSpecklePercentage: 5,
|
||||
contourTargetOrientation: 1,
|
||||
contourTargetOrientation: ContourTargetOrientation.Landscape,
|
||||
cornerDetectionUseConvexHulls: true,
|
||||
outputShouldShow: true,
|
||||
outputShouldDraw: true,
|
||||
@@ -138,7 +185,7 @@ export const DefaultPipelineSettings: Omit<
|
||||
hsvHue: { first: 50, second: 180 },
|
||||
hueInverted: false,
|
||||
outputMaximumTargets: 20,
|
||||
contourSortMode: 0,
|
||||
contourSortMode: ContourSortMode.Largest,
|
||||
offsetSinglePoint: { x: 0, y: 0 },
|
||||
cameraBrightness: 50,
|
||||
offsetDualPointAArea: 0,
|
||||
@@ -147,11 +194,12 @@ export const DefaultPipelineSettings: Omit<
|
||||
cornerDetectionStrategy: 0,
|
||||
cornerDetectionAccuracyPercentage: 10,
|
||||
hsvSaturation: { first: 50, second: 255 },
|
||||
contourIntersection: 1,
|
||||
contourIntersection: ContourIntersection.Up,
|
||||
cameraAutoWhiteBalance: false,
|
||||
cameraWhiteBalanceTemp: 4000,
|
||||
cameraMinExposureRaw: 1,
|
||||
cameraMaxExposureRaw: 2,
|
||||
crosshair: true,
|
||||
blockForFrames: true
|
||||
};
|
||||
|
||||
@@ -184,7 +232,7 @@ export interface ColoredShapePipelineSettings extends PipelineSettings {
|
||||
contourRadius: WebsocketNumberPair | [number, number];
|
||||
circleDetectThreshold: number;
|
||||
accuracyPercentage: number;
|
||||
contourShape: number;
|
||||
contourShape: ContourShape;
|
||||
contourPerimeter: WebsocketNumberPair | [number, number];
|
||||
minDist: number;
|
||||
maxCannyThresh: number;
|
||||
@@ -209,7 +257,7 @@ export const DefaultColoredShapePipelineSettings: ColoredShapePipelineSettings =
|
||||
contourRadius: { first: 0, second: 100 },
|
||||
circleDetectThreshold: 5,
|
||||
accuracyPercentage: 10,
|
||||
contourShape: 2,
|
||||
contourShape: ContourShape.Triangle,
|
||||
contourPerimeter: { first: 0, second: 1.7976931348623157e308 },
|
||||
minDist: 20,
|
||||
maxCannyThresh: 90
|
||||
@@ -324,13 +372,14 @@ 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.ObjectDetection,
|
||||
pipelineType: PipelineType.Calibration3d,
|
||||
cameraGain: 20,
|
||||
targetModel: TargetModel.InfiniteRechargeHighGoalOuter,
|
||||
ledMode: true,
|
||||
|
||||
@@ -75,46 +75,29 @@ export type ConfigurableNetworkSettings = Omit<
|
||||
"canManage" | "networkInterfaceNames" | "networkingDisabled"
|
||||
>;
|
||||
|
||||
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";
|
||||
interface PVCameraInfoBase {
|
||||
type: "PVUsbCameraInfo" | "PVCSICameraInfo" | "PVFileCameraInfo";
|
||||
path: string;
|
||||
name: string;
|
||||
uniquePath: string;
|
||||
}
|
||||
|
||||
export interface PVUsbCameraInfo {
|
||||
export interface PVUsbCameraInfo extends PVCameraInfoBase {
|
||||
type: "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 {
|
||||
export interface PVCSICameraInfo extends PVCameraInfoBase {
|
||||
type: "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 PVFileCameraInfo extends PVCameraInfoBase {
|
||||
type: "PVFileCameraInfo";
|
||||
}
|
||||
|
||||
// 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 type PVCameraInfo = PVUsbCameraInfo | PVCSICameraInfo | PVFileCameraInfo;
|
||||
|
||||
export interface VsmState {
|
||||
disabledConfigs: WebsocketCameraSettingsUpdate[];
|
||||
@@ -276,6 +259,7 @@ export interface UiCameraConfiguration {
|
||||
maxWhiteBalanceTemp: number;
|
||||
|
||||
fpsLimit: number;
|
||||
isEnabled: boolean;
|
||||
|
||||
matchedCameraInfo: PVCameraInfo;
|
||||
isConnected: boolean;
|
||||
@@ -438,15 +422,13 @@ export const PlaceholderCameraSettings: UiCameraConfiguration = reactive({
|
||||
minWhiteBalanceTemp: 2000,
|
||||
maxWhiteBalanceTemp: 10000,
|
||||
matchedCameraInfo: {
|
||||
PVFileCameraInfo: {
|
||||
name: "Foobar",
|
||||
path: "/dev/foobar",
|
||||
uniquePath: "/dev/foobar2"
|
||||
},
|
||||
PVCSICameraInfo: undefined,
|
||||
PVUsbCameraInfo: undefined
|
||||
type: "PVFileCameraInfo",
|
||||
name: "Foobar",
|
||||
path: "/dev/foobar",
|
||||
uniquePath: "/dev/foobar2"
|
||||
},
|
||||
fpsLimit: -1,
|
||||
isEnabled: true,
|
||||
isConnected: true,
|
||||
hasConnected: true,
|
||||
mismatch: false
|
||||
|
||||
@@ -30,21 +30,18 @@ export interface WebsocketNumberPair {
|
||||
second: 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;
|
||||
}
|
||||
>;
|
||||
export type WebsocketVideoFormat = {
|
||||
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 {
|
||||
@@ -68,6 +65,7 @@ export interface WebsocketCameraSettingsUpdate {
|
||||
maxWhiteBalanceTemp: number;
|
||||
matchedCameraInfo: PVCameraInfo;
|
||||
fpsLimit: number;
|
||||
isEnabled: boolean;
|
||||
isConnected: boolean;
|
||||
hasConnected: boolean;
|
||||
mismatch: boolean;
|
||||
|
||||
@@ -2,13 +2,7 @@
|
||||
import { useCameraSettingsStore } from "@/stores/settings/CameraSettingsStore";
|
||||
import { computed, inject, ref } from "vue";
|
||||
import { useStateStore } from "@/stores/StateStore";
|
||||
import {
|
||||
PlaceholderCameraSettings,
|
||||
PVCameraInfo,
|
||||
type PVCSICameraInfo,
|
||||
type PVFileCameraInfo,
|
||||
type PVUsbCameraInfo
|
||||
} from "@/types/SettingTypes";
|
||||
import { PlaceholderCameraSettings, type PVCameraInfo } 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";
|
||||
@@ -18,20 +12,22 @@ import { useTheme } from "vuetify";
|
||||
|
||||
const theme = useTheme();
|
||||
|
||||
const formatUrl = (port) => `http://${inject("backendHostname")}:${port}/stream.mjpg`;
|
||||
const backendHostname = inject<string>("backendHostname");
|
||||
const formatUrl = (port: number) => `http://${backendHostname}:${port}/stream.mjpg`;
|
||||
|
||||
const activatingModule = ref(false);
|
||||
const activateModule = (moduleUniqueName: string) => {
|
||||
const activateModule = async (moduleUniqueName: string) => {
|
||||
if (activatingModule.value) return;
|
||||
activatingModule.value = true;
|
||||
|
||||
axiosPost("/utils/activateMatchedCamera", "activate a matched camera", {
|
||||
await axiosPost("/utils/activateMatchedCamera", "activate a matched camera", {
|
||||
cameraUniqueName: moduleUniqueName
|
||||
}).finally(() => (activatingModule.value = false));
|
||||
});
|
||||
activatingModule.value = false;
|
||||
};
|
||||
|
||||
const assigningCamera = ref(false);
|
||||
const assignCamera = (cameraInfo: PVCameraInfo) => {
|
||||
const assignCamera = async (cameraInfo: PVCameraInfo) => {
|
||||
if (assigningCamera.value) return;
|
||||
assigningCamera.value = true;
|
||||
|
||||
@@ -39,48 +35,39 @@ const assignCamera = (cameraInfo: PVCameraInfo) => {
|
||||
cameraInfo: cameraInfo
|
||||
};
|
||||
|
||||
axiosPost("/utils/assignUnmatchedCamera", "assign an unmatched camera", payload).finally(
|
||||
() => (assigningCamera.value = false)
|
||||
);
|
||||
await axiosPost("/utils/assignUnmatchedCamera", "assign an unmatched camera", payload);
|
||||
assigningCamera.value = false;
|
||||
};
|
||||
|
||||
const deactivatingModule = ref(false);
|
||||
const deactivateModule = (cameraUniqueName: string) => {
|
||||
const deactivateModule = async (cameraUniqueName: string) => {
|
||||
if (deactivatingModule.value) return;
|
||||
deactivatingModule.value = true;
|
||||
axiosPost("/utils/unassignCamera", "unassign a camera", { cameraUniqueName: cameraUniqueName }).finally(
|
||||
() => (deactivatingModule.value = false)
|
||||
);
|
||||
await axiosPost("/utils/unassignCamera", "unassign a camera", { cameraUniqueName: cameraUniqueName });
|
||||
deactivatingModule.value = false;
|
||||
};
|
||||
|
||||
const confirmDeleteDialog = ref({ show: false, nickname: "", cameraUniqueName: "" });
|
||||
const deletingCamera = ref<string | null>(null);
|
||||
|
||||
const deleteThisCamera = (cameraUniqueName: string) => {
|
||||
const deleteThisCamera = async (cameraUniqueName: string) => {
|
||||
if (deletingCamera.value) return;
|
||||
deletingCamera.value = cameraUniqueName;
|
||||
axiosPost("/utils/nukeOneCamera", "delete a camera", { cameraUniqueName: cameraUniqueName }).finally(() => {
|
||||
deletingCamera.value = null;
|
||||
});
|
||||
await axiosPost("/utils/nukeOneCamera", "delete a camera", { cameraUniqueName: cameraUniqueName });
|
||||
deletingCamera.value = null;
|
||||
};
|
||||
|
||||
const cameraConnected = (uniquePath: string): boolean => {
|
||||
return (
|
||||
useStateStore().vsmState.allConnectedCameras.find((it) => cameraInfoFor(it).uniquePath === uniquePath) !== undefined
|
||||
);
|
||||
const cameraConnected = (uniquePath: string | undefined): boolean => {
|
||||
if (!uniquePath) return false;
|
||||
return useStateStore().vsmState.allConnectedCameras.find((it) => it.uniquePath === uniquePath) !== undefined;
|
||||
};
|
||||
|
||||
const unmatchedCameras = computed(() => {
|
||||
const activeVmPaths = Object.values(useCameraSettingsStore().cameras).map(
|
||||
(it) => cameraInfoFor(it.matchedCameraInfo).uniquePath
|
||||
);
|
||||
const disabledVmPaths = useStateStore().vsmState.disabledConfigs.map(
|
||||
(it) => cameraInfoFor(it.matchedCameraInfo).uniquePath
|
||||
);
|
||||
const activeVmPaths = Object.values(useCameraSettingsStore().cameras).map((it) => it.matchedCameraInfo.uniquePath);
|
||||
const disabledVmPaths = useStateStore().vsmState.disabledConfigs.map((it) => it.matchedCameraInfo.uniquePath);
|
||||
|
||||
return useStateStore().vsmState.allConnectedCameras.filter(
|
||||
(it) =>
|
||||
!activeVmPaths.includes(cameraInfoFor(it).uniquePath) && !disabledVmPaths.includes(cameraInfoFor(it).uniquePath)
|
||||
(it) => !activeVmPaths.includes(it.uniquePath) && !disabledVmPaths.includes(it.uniquePath)
|
||||
);
|
||||
});
|
||||
|
||||
@@ -91,8 +78,8 @@ const activeVisionModules = computed(() =>
|
||||
// Display connected cameras first
|
||||
.sort(
|
||||
(first, second) =>
|
||||
(cameraConnected(cameraInfoFor(second.matchedCameraInfo).uniquePath) ? 1 : 0) -
|
||||
(cameraConnected(cameraInfoFor(first.matchedCameraInfo).uniquePath) ? 1 : 0)
|
||||
(cameraConnected(second.matchedCameraInfo.uniquePath) ? 1 : 0) -
|
||||
(cameraConnected(first.matchedCameraInfo.uniquePath) ? 1 : 0)
|
||||
)
|
||||
);
|
||||
|
||||
@@ -105,41 +92,24 @@ 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 {
|
||||
PVFileCameraInfo: undefined,
|
||||
PVCSICameraInfo: undefined,
|
||||
PVUsbCameraInfo: undefined
|
||||
type: "PVFileCameraInfo",
|
||||
path: "",
|
||||
name: "",
|
||||
uniquePath: ""
|
||||
};
|
||||
}
|
||||
return (
|
||||
useStateStore().vsmState.allConnectedCameras.find(
|
||||
(it) => cameraInfoFor(it).uniquePath === cameraInfoFor(info).uniquePath
|
||||
) || {
|
||||
PVFileCameraInfo: undefined,
|
||||
PVCSICameraInfo: undefined,
|
||||
PVUsbCameraInfo: undefined
|
||||
useStateStore().vsmState.allConnectedCameras.find((it) => it.uniquePath === info.uniquePath) || {
|
||||
type: "PVFileCameraInfo",
|
||||
path: "",
|
||||
name: "",
|
||||
uniquePath: ""
|
||||
}
|
||||
);
|
||||
};
|
||||
@@ -158,12 +128,11 @@ const getMatchedDevice = (info: PVCameraInfo | undefined): PVCameraInfo => {
|
||||
class="pr-0"
|
||||
>
|
||||
<v-card color="surface" class="rounded-12">
|
||||
<v-card-title>{{ cameraInfoFor(module.matchedCameraInfo).name }}</v-card-title>
|
||||
<v-card-subtitle v-if="!cameraConnected(cameraInfoFor(module.matchedCameraInfo).uniquePath)"
|
||||
<v-card-title>{{ module.matchedCameraInfo.name }}</v-card-title>
|
||||
<v-card-subtitle v-if="!cameraConnected(module.matchedCameraInfo.uniquePath)"
|
||||
>Status: <span class="inactive-status">Disconnected</span></v-card-subtitle
|
||||
>
|
||||
<v-card-subtitle
|
||||
v-else-if="cameraConnected(cameraInfoFor(module.matchedCameraInfo).uniquePath) && !module.mismatch"
|
||||
<v-card-subtitle v-else-if="cameraConnected(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>
|
||||
@@ -172,7 +141,7 @@ const getMatchedDevice = (info: PVCameraInfo | undefined): PVCameraInfo => {
|
||||
<tbody>
|
||||
<tr
|
||||
v-if="
|
||||
cameraConnected(cameraInfoFor(module.matchedCameraInfo).uniquePath) &&
|
||||
cameraConnected(module.matchedCameraInfo.uniquePath) &&
|
||||
useStateStore().backendResults[module.uniqueName]
|
||||
"
|
||||
>
|
||||
@@ -214,7 +183,7 @@ const getMatchedDevice = (info: PVCameraInfo | undefined): PVCameraInfo => {
|
||||
</tbody>
|
||||
</v-table>
|
||||
<div
|
||||
v-if="cameraConnected(cameraInfoFor(module.matchedCameraInfo).uniquePath)"
|
||||
v-if="cameraConnected(module.matchedCameraInfo.uniquePath)"
|
||||
:id="`stream-container-${index}`"
|
||||
class="d-flex flex-column justify-center align-center mt-3"
|
||||
style="height: 250px"
|
||||
@@ -232,13 +201,8 @@ const getMatchedDevice = (info: PVCameraInfo | undefined): PVCameraInfo => {
|
||||
<v-btn
|
||||
color="buttonPassive"
|
||||
style="width: 100%"
|
||||
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
|
||||
@click="
|
||||
setCameraView(
|
||||
module.matchedCameraInfo,
|
||||
cameraConnected(cameraInfoFor(module.matchedCameraInfo).uniquePath)
|
||||
)
|
||||
"
|
||||
:variant="theme.global.current.value.dark ? 'outlined' : 'elevated'"
|
||||
@click="setCameraView(module.matchedCameraInfo, cameraConnected(module.matchedCameraInfo.uniquePath))"
|
||||
>
|
||||
<span>Details</span>
|
||||
</v-btn>
|
||||
@@ -248,7 +212,7 @@ const getMatchedDevice = (info: PVCameraInfo | undefined): PVCameraInfo => {
|
||||
class="text-black"
|
||||
color="buttonActive"
|
||||
style="width: 100%"
|
||||
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
|
||||
:variant="theme.global.current.value.dark ? 'outlined' : 'elevated'"
|
||||
:loading="deactivatingModule"
|
||||
@click="deactivateModule(module.uniqueName)"
|
||||
>
|
||||
@@ -261,7 +225,7 @@ const getMatchedDevice = (info: PVCameraInfo | undefined): PVCameraInfo => {
|
||||
color="error"
|
||||
style="width: 100%"
|
||||
:loading="module.uniqueName === deletingCamera"
|
||||
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
|
||||
:variant="theme.global.current.value.dark ? 'outlined' : 'elevated'"
|
||||
@click="
|
||||
() =>
|
||||
(confirmDeleteDialog = {
|
||||
@@ -315,7 +279,7 @@ const getMatchedDevice = (info: PVCameraInfo | undefined): PVCameraInfo => {
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Connected</td>
|
||||
<td>{{ cameraConnected(cameraInfoFor(module.matchedCameraInfo).uniquePath) }}</td>
|
||||
<td>{{ cameraConnected(module.matchedCameraInfo.uniquePath) }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</v-table>
|
||||
@@ -326,13 +290,8 @@ const getMatchedDevice = (info: PVCameraInfo | undefined): PVCameraInfo => {
|
||||
<v-btn
|
||||
color="buttonPassive"
|
||||
style="width: 100%"
|
||||
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
|
||||
@click="
|
||||
setCameraView(
|
||||
module.matchedCameraInfo,
|
||||
cameraConnected(cameraInfoFor(module.matchedCameraInfo).uniquePath)
|
||||
)
|
||||
"
|
||||
:variant="theme.global.current.value.dark ? 'outlined' : 'elevated'"
|
||||
@click="setCameraView(module.matchedCameraInfo, cameraConnected(module.matchedCameraInfo.uniquePath))"
|
||||
>
|
||||
<span>Details</span>
|
||||
</v-btn>
|
||||
@@ -342,7 +301,7 @@ const getMatchedDevice = (info: PVCameraInfo | undefined): PVCameraInfo => {
|
||||
class="text-black"
|
||||
color="buttonActive"
|
||||
style="width: 100%"
|
||||
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
|
||||
:variant="theme.global.current.value.dark ? 'outlined' : 'elevated'"
|
||||
:loading="activatingModule"
|
||||
@click="activateModule(module.uniqueName)"
|
||||
>
|
||||
@@ -355,7 +314,7 @@ const getMatchedDevice = (info: PVCameraInfo | undefined): PVCameraInfo => {
|
||||
color="error"
|
||||
style="width: 100%"
|
||||
:loading="module.uniqueName === deletingCamera"
|
||||
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
|
||||
:variant="theme.global.current.value.dark ? 'outlined' : 'elevated'"
|
||||
@click="
|
||||
() =>
|
||||
(confirmDeleteDialog = {
|
||||
@@ -377,15 +336,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.PVUsbCameraInfo">USB Camera:</span>
|
||||
<span v-else-if="camera.PVCSICameraInfo">CSI Camera:</span>
|
||||
<span v-else-if="camera.PVFileCameraInfo">File Camera:</span>
|
||||
<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-else>Unknown Camera:</span>
|
||||
<span>{{ cameraInfoFor(camera)?.name ?? cameraInfoFor(camera)?.baseName }}</span>
|
||||
<span>{{ camera.name }}</span>
|
||||
</v-card-title>
|
||||
<v-card-subtitle>Status: Unassigned</v-card-subtitle>
|
||||
<v-card-text class="pt-3">
|
||||
<span style="word-break: break-all">{{ cameraInfoFor(camera)?.path }}</span>
|
||||
<span style="word-break: break-all">{{ camera?.path }}</span>
|
||||
</v-card-text>
|
||||
<v-card-text class="pt-0">
|
||||
<v-row>
|
||||
@@ -393,7 +352,7 @@ const getMatchedDevice = (info: PVCameraInfo | undefined): PVCameraInfo => {
|
||||
<v-btn
|
||||
color="buttonPassive"
|
||||
style="width: 100%"
|
||||
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
|
||||
:variant="theme.global.current.value.dark ? 'outlined' : 'elevated'"
|
||||
@click="setCameraView(camera, false)"
|
||||
>
|
||||
<span>Details</span>
|
||||
@@ -405,7 +364,7 @@ const getMatchedDevice = (info: PVCameraInfo | undefined): PVCameraInfo => {
|
||||
color="buttonActive"
|
||||
style="width: 100%"
|
||||
:loading="assigningCamera"
|
||||
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
|
||||
:variant="theme.global.current.value.dark ? 'outlined' : 'elevated'"
|
||||
@click="assignCamera(camera)"
|
||||
>
|
||||
Activate
|
||||
@@ -436,7 +395,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>{{ cameraInfoFor(viewingCamera[0])?.name ?? cameraInfoFor(viewingCamera[0])?.baseName }}</span>
|
||||
<span>{{ viewingCamera[0].name }}</span>
|
||||
<v-btn variant="text" @click="setCameraView(null, null)">
|
||||
<v-icon size="x-large">mdi-close</v-icon>
|
||||
</v-btn>
|
||||
@@ -446,9 +405,7 @@ const getMatchedDevice = (info: PVCameraInfo | undefined): PVCameraInfo => {
|
||||
</v-card-text>
|
||||
<v-card-text
|
||||
v-else-if="
|
||||
activeVisionModules.find(
|
||||
(it) => cameraInfoFor(it.matchedCameraInfo).uniquePath === cameraInfoFor(viewingCamera[0]).uniquePath
|
||||
)?.mismatch
|
||||
activeVisionModules.find((it) => it.matchedCameraInfo.uniquePath === viewingCamera[0]?.uniquePath)?.mismatch
|
||||
"
|
||||
>
|
||||
<v-alert
|
||||
@@ -457,7 +414,7 @@ const getMatchedDevice = (info: PVCameraInfo | undefined): PVCameraInfo => {
|
||||
density="compact"
|
||||
text="A different camera may have been connected to this device! Compare the following information carefully."
|
||||
icon="mdi-information-outline"
|
||||
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'tonal'"
|
||||
:variant="theme.global.current.value.dark ? 'tonal' : 'elevated'"
|
||||
/>
|
||||
<PvCameraMatchCard :saved="viewingCamera[0]" :current="getMatchedDevice(viewingCamera[0])" />
|
||||
</v-card-text>
|
||||
|
||||
@@ -77,8 +77,18 @@ const conflictingCameraShown = computed<boolean>(() => {
|
||||
return useSettingsStore().general.conflictingCameras.length > 0;
|
||||
});
|
||||
|
||||
const fpsLimitWarningShown = computed<boolean>(() => {
|
||||
return Object.values(useCameraSettingsStore().cameras).some((c) => c.fpsLimit > 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 showCameraSetupDialog = ref(useCameraSettingsStore().needsCameraConfiguration);
|
||||
@@ -92,7 +102,7 @@ const showCameraSetupDialog = ref(useCameraSettingsStore().needsCameraConfigurat
|
||||
color="error"
|
||||
density="compact"
|
||||
icon="mdi-alert-circle-outline"
|
||||
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'tonal'"
|
||||
:variant="theme.global.current.value.dark ? 'tonal' : 'elevated'"
|
||||
>
|
||||
<span>
|
||||
Arducam camera detected! Please configure the camera model in the <a href="#/cameras">Camera tab</a>!
|
||||
@@ -104,32 +114,44 @@ const showCameraSetupDialog = ref(useCameraSettingsStore().needsCameraConfigurat
|
||||
color="error"
|
||||
density="compact"
|
||||
icon="mdi-alert-circle-outline"
|
||||
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'tonal'"
|
||||
:variant="theme.global.current.value.dark ? 'tonal' : 'elevated'"
|
||||
>
|
||||
<span>
|
||||
Conflicting hostname detected! Please change the hostname in the <a href="#/settings">Settings tab</a>!
|
||||
</span>
|
||||
</v-alert>
|
||||
<v-alert
|
||||
v-if="fpsLimitWarningShown"
|
||||
v-if="fpsLimitedCameras"
|
||||
class="mb-3"
|
||||
color="error"
|
||||
density="compact"
|
||||
icon="mdi-alert-circle-outline"
|
||||
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'tonal'"
|
||||
:variant="theme.global.current.value.dark ? 'tonal' : 'elevated'"
|
||||
>
|
||||
<span
|
||||
>One or more cameras have an FPS limit set! This may cause performance issues. Check your logs for more
|
||||
>{{ fpsLimitedCameras }} 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"
|
||||
color="error"
|
||||
density="compact"
|
||||
icon="mdi-alert-circle-outline"
|
||||
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'tonal'"
|
||||
:variant="theme.global.current.value.dark ? 'tonal' : 'elevated'"
|
||||
>
|
||||
<span
|
||||
>Conflicting camera name(s) detected! Please change the name(s) of
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<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%">
|
||||
@@ -21,7 +22,7 @@ const devMode = process.env.NODE_ENV === "development";
|
||||
</div>
|
||||
<div v-else style="width: 100%; height: 100%">
|
||||
<!--suppress HtmlUnknownTarget -->
|
||||
<iframe src="docs/index.html" style="overflow: hidden; height: 100%; width: 100%; border: 0" />
|
||||
<iframe :src="docsSrc" style="overflow: hidden; height: 100%; width: 100%; border: 0" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
{
|
||||
"extends": "@vue/tsconfig/tsconfig.json",
|
||||
"include": ["vite.config.*"],
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"types": ["node"]
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,10 @@
|
||||
{
|
||||
"extends": "@vue/tsconfig/tsconfig.dom.json",
|
||||
"include": ["env.d.ts", "src/**/*", "src/**/*.vue"],
|
||||
"include": ["env.d.ts", "vite.config.ts", "playwright.config.ts", "src/**/*", "src/**/*.vue", "tests/**/*"],
|
||||
"compilerOptions": {
|
||||
"moduleResolution": "node",
|
||||
"noImplicitAny": false,
|
||||
"lib": ["ES2022", "DOM", "DOM.Iterable"],
|
||||
"moduleResolution": "bundler",
|
||||
"noImplicitAny": true,
|
||||
"strict": true,
|
||||
"removeComments": true,
|
||||
"sourceMap": true,
|
||||
@@ -14,9 +15,4 @@
|
||||
"@/*": ["./src/*"]
|
||||
}
|
||||
},
|
||||
"references": [
|
||||
{
|
||||
"path": "./tsconfig.config.json"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -25,7 +25,7 @@ export default defineConfig({
|
||||
}
|
||||
},
|
||||
build: {
|
||||
rollupOptions: {
|
||||
rolldownOptions: {
|
||||
external: ["html2canvas", "dompurify", "canvg"]
|
||||
},
|
||||
sourcemap: true
|
||||
|
||||
@@ -6,7 +6,7 @@ ext.licenseFile = file("$rootDir/LICENSE")
|
||||
ext.externalLicensesFolder = file("$rootDir/ExternalLicenses")
|
||||
apply from: "${rootDir}/shared/common.gradle"
|
||||
|
||||
wpilibTools.deps.wpilibVersion = wpi.versions.wpilibVersion.get()
|
||||
wpilibTools.deps.wpilibVersion = wpilibVersion
|
||||
|
||||
def nativeConfigName = 'wpilibNatives'
|
||||
configurations {
|
||||
@@ -23,11 +23,12 @@ 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("frc" + openCVYear, wpi.versions.opencvVersion.get())
|
||||
wpilibNatives wpilibTools.deps.wpilibOpenCv(openCVversion)
|
||||
|
||||
// These stay as implementation dependencies since they don't have native code that gets packaged
|
||||
implementation 'org.zeroturnaround:zt-zip:1.14'
|
||||
@@ -40,7 +41,7 @@ dependencies {
|
||||
wpilibNatives("org.photonvision:rknn_jni-jni:$rknnVersion:$jniPlatform") {
|
||||
transitive = false
|
||||
}
|
||||
wpilibNatives("org.photonvision:rubik_jni-jni:$rubikVersion:$jniPlatform") {
|
||||
wpilibNatives("org.photonvision:tflite_jni-jni:$tfliteVersion:$jniPlatform") {
|
||||
transitive = false
|
||||
}
|
||||
wpilibNatives("org.photonvision:photon-libcamera-gl-driver-jni:$libcameraDriverVersion:$jniPlatform") {
|
||||
@@ -48,11 +49,17 @@ 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:rubik_jni-java:$rubikVersion") {
|
||||
implementation("org.photonvision:tflite_jni-java:$tfliteVersion") {
|
||||
transitive = false
|
||||
}
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@ public class LoadJNI {
|
||||
private static HashMap<JNITypes, Boolean> loadedMap = new HashMap<>();
|
||||
|
||||
public enum JNITypes {
|
||||
RUBIK_DETECTOR("tensorflowlite", "tensorflowlite_c", "external_delegate", "rubik_jni"),
|
||||
RUBIK_DETECTOR("tflite_jni"),
|
||||
RKNN_DETECTOR("rga", "rknnrt", "rknn_jni"),
|
||||
MRCAL("mrcal_jni"),
|
||||
LIBCAMERA("photonlibcamera");
|
||||
|
||||
@@ -17,10 +17,7 @@
|
||||
|
||||
package org.photonvision.common.configuration;
|
||||
|
||||
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 io.avaje.jsonb.Json;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
@@ -35,7 +32,9 @@ 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);
|
||||
|
||||
@@ -62,11 +61,8 @@ public class CameraConfiguration {
|
||||
|
||||
public int streamIndex = 0; // 0 index means ports [1181, 1182], 1 means [1183, 1184], etc...
|
||||
|
||||
// Ignore the pipes, as we serialize them to their own column to hack around
|
||||
// polymorphic lists
|
||||
@JsonIgnore public List<CVPipelineSettings> pipelineSettings = new ArrayList<>();
|
||||
public List<CVPipelineSettings> pipelineSettings = new ArrayList<>();
|
||||
|
||||
@JsonIgnore
|
||||
public DriverModePipelineSettings driveModeSettings = new DriverModePipelineSettings();
|
||||
|
||||
public CameraConfiguration(PVCameraInfo cameraInfo, String uniqueName, String nickname) {
|
||||
@@ -78,24 +74,22 @@ public class CameraConfiguration {
|
||||
logger.debug("Creating USB camera configuration for " + this.toShortString());
|
||||
}
|
||||
|
||||
// Shiny new constructor
|
||||
@JsonCreator
|
||||
// JSON Constructor (can't be marked with @Json.Creator due to public fields that aren't part of
|
||||
// the parameters)
|
||||
public CameraConfiguration(
|
||||
@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) {
|
||||
String uniqueName,
|
||||
PVCameraInfo matchedCameraInfo,
|
||||
String nickname,
|
||||
boolean deactivated,
|
||||
QuirkyCamera cameraQuirks,
|
||||
double FOV,
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -120,14 +114,14 @@ public class CameraConfiguration {
|
||||
PVCameraInfo matchedCameraInfo;
|
||||
|
||||
/** Legacy constructor for compat with 2024.3.1 */
|
||||
@JsonCreator
|
||||
@Json.Creator
|
||||
public LegacyCameraConfigStruct(
|
||||
@JsonProperty("baseName") String baseName,
|
||||
@JsonProperty("path") String path,
|
||||
@JsonProperty("otherPaths") String[] otherPaths,
|
||||
@JsonProperty("cameraType") CameraType cameraType,
|
||||
@JsonProperty("usbVID") int usbVID,
|
||||
@JsonProperty("usbPID") int usbPID) {
|
||||
String baseName,
|
||||
String path,
|
||||
String[] otherPaths,
|
||||
CameraType cameraType,
|
||||
int usbVID,
|
||||
int usbPID) {
|
||||
if (cameraType == CameraType.UsbCamera) {
|
||||
this.matchedCameraInfo =
|
||||
PVCameraInfo.fromUsbCameraInfo(
|
||||
@@ -171,16 +165,16 @@ public class CameraConfiguration {
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
* 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.
|
||||
*
|
||||
* @param calibration The calibration to add.
|
||||
*/
|
||||
public void addCalibration(CameraCalibrationCoefficients calibration) {
|
||||
logger.info("adding calibration " + calibration.unrotatedImageSize);
|
||||
logger.info("adding calibration " + calibration.resolution);
|
||||
calibrations.stream()
|
||||
.filter(it -> it.unrotatedImageSize.equals(calibration.unrotatedImageSize))
|
||||
.filter(it -> it.resolution.equals(calibration.resolution))
|
||||
.findAny()
|
||||
.ifPresent(
|
||||
(it) -> {
|
||||
@@ -194,12 +188,12 @@ public class CameraConfiguration {
|
||||
* Remove a calibration from our list. If found, the calibration will be "released". If not found,
|
||||
* no-op.
|
||||
*
|
||||
* @param unrotatedImageSize The resolution to remove.
|
||||
* @param resolution The resolution to remove.
|
||||
*/
|
||||
public void removeCalibration(Size unrotatedImageSize) {
|
||||
logger.info("deleting calibration " + unrotatedImageSize);
|
||||
public void removeCalibration(Size resolution) {
|
||||
logger.info("deleting calibration " + resolution);
|
||||
calibrations.stream()
|
||||
.filter(it -> it.unrotatedImageSize.equals(unrotatedImageSize))
|
||||
.filter(it -> it.resolution.equals(resolution))
|
||||
.findAny()
|
||||
.ifPresent(
|
||||
(it) -> {
|
||||
@@ -215,7 +209,6 @@ 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,7 +17,10 @@
|
||||
|
||||
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;
|
||||
@@ -34,7 +37,6 @@ 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;
|
||||
|
||||
@@ -233,14 +235,15 @@ public class ConfigManager {
|
||||
Path.of(getModelsDirectory().toString(), "photonvision-object-detection-models.json")
|
||||
.toFile();
|
||||
try {
|
||||
JacksonUtils.serialize(
|
||||
tempProperties.toPath(), this.getConfig().neuralNetworkPropertyManager());
|
||||
Jsonb.instance()
|
||||
.type(NeuralNetworkModelsSettings.class)
|
||||
.toJson(this.getConfig().getNeuralNetworkProperties(), new FileWriter(tempProperties));
|
||||
ZipUtil.pack(getModelsDirectory(), out);
|
||||
// Now delete the tempProperties
|
||||
if (tempProperties.exists()) {
|
||||
Files.delete(tempProperties.toPath());
|
||||
}
|
||||
} catch (Exception e) {
|
||||
} catch (IOException | IllegalStateException | JsonException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
return out;
|
||||
|
||||
@@ -17,44 +17,50 @@
|
||||
|
||||
package org.photonvision.common.configuration;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||
import io.avaje.jsonb.Json;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import org.photonvision.common.hardware.statusLED.StatusLEDType;
|
||||
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
@Json
|
||||
public class HardwareConfig {
|
||||
public final String deviceName;
|
||||
public final String deviceLogoPath;
|
||||
public final String supportURL;
|
||||
public String deviceName;
|
||||
|
||||
// LED control
|
||||
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;
|
||||
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;
|
||||
|
||||
// Custom GPIO
|
||||
public final String getGPIOCommand;
|
||||
public final String setGPIOCommand;
|
||||
public final String setPWMCommand;
|
||||
public final String setPWMFrequencyCommand;
|
||||
public final String releaseGPIOCommand;
|
||||
public String getGPIOCommand;
|
||||
public String setGPIOCommand;
|
||||
public String setPWMCommand;
|
||||
public String setPWMFrequencyCommand;
|
||||
public String releaseGPIOCommand;
|
||||
|
||||
// Device stuff
|
||||
public final String restartHardwareCommand;
|
||||
public final double vendorFOV; // -1 for unmanaged
|
||||
public String restartHardwareCommand;
|
||||
public double vendorFOV; // -1 for unmanaged
|
||||
|
||||
public HardwareConfig(
|
||||
String deviceName,
|
||||
String deviceLogoPath,
|
||||
String supportURL,
|
||||
ArrayList<Integer> ledPins,
|
||||
List<Integer> ledPins,
|
||||
boolean ledsCanDim,
|
||||
ArrayList<Integer> ledBrightnessRange,
|
||||
List<Integer> ledBrightnessRange,
|
||||
int ledPwmFrequency,
|
||||
ArrayList<Integer> statusRGBPins,
|
||||
boolean statusRGBActiveHigh,
|
||||
StatusLEDType statusLEDType,
|
||||
List<Integer> statusLEDPins,
|
||||
boolean statusLEDActiveHigh,
|
||||
String getGPIOCommand,
|
||||
String setGPIOCommand,
|
||||
String setPWMCommand,
|
||||
@@ -63,14 +69,13 @@ public class HardwareConfig {
|
||||
String restartHardwareCommand,
|
||||
double vendorFOV) {
|
||||
this.deviceName = deviceName;
|
||||
this.deviceLogoPath = deviceLogoPath;
|
||||
this.supportURL = supportURL;
|
||||
this.ledPins = ledPins;
|
||||
this.ledsCanDim = ledsCanDim;
|
||||
this.ledBrightnessRange = ledBrightnessRange;
|
||||
this.ledPWMFrequency = ledPwmFrequency;
|
||||
this.statusRGBPins = statusRGBPins;
|
||||
this.statusRGBActiveHigh = statusRGBActiveHigh;
|
||||
this.statusLEDType = statusLEDType;
|
||||
this.statusLEDPins = statusLEDPins;
|
||||
this.statusLEDActiveHigh = statusLEDActiveHigh;
|
||||
this.getGPIOCommand = getGPIOCommand;
|
||||
this.setGPIOCommand = setGPIOCommand;
|
||||
this.setPWMCommand = setPWMCommand;
|
||||
@@ -82,14 +87,13 @@ public class HardwareConfig {
|
||||
|
||||
public HardwareConfig() {
|
||||
deviceName = "";
|
||||
deviceLogoPath = "";
|
||||
supportURL = "";
|
||||
ledPins = new ArrayList<>();
|
||||
ledsCanDim = false;
|
||||
ledBrightnessRange = new ArrayList<>();
|
||||
ledPWMFrequency = 0;
|
||||
statusRGBPins = new ArrayList<>();
|
||||
statusRGBActiveHigh = false;
|
||||
statusLEDType = StatusLEDType.RGB;
|
||||
statusLEDPins = new ArrayList<>();
|
||||
statusLEDActiveHigh = false;
|
||||
getGPIOCommand = "";
|
||||
setGPIOCommand = "";
|
||||
setPWMCommand = "";
|
||||
@@ -121,10 +125,6 @@ public class HardwareConfig {
|
||||
public String toString() {
|
||||
return "HardwareConfig[deviceName="
|
||||
+ deviceName
|
||||
+ ", deviceLogoPath="
|
||||
+ deviceLogoPath
|
||||
+ ", supportURL="
|
||||
+ supportURL
|
||||
+ ", ledPins="
|
||||
+ ledPins
|
||||
+ ", ledsCanDim="
|
||||
@@ -133,10 +133,12 @@ public class HardwareConfig {
|
||||
+ ledBrightnessRange
|
||||
+ ", ledPWMFrequency="
|
||||
+ ledPWMFrequency
|
||||
+ ", statusRGBPins="
|
||||
+ statusRGBPins
|
||||
+ ", statusRGBActiveHigh"
|
||||
+ statusRGBActiveHigh
|
||||
+ ", statusLEDType="
|
||||
+ statusLEDType
|
||||
+ ", statusLEDPins="
|
||||
+ statusLEDPins
|
||||
+ ", statusLEDActiveHigh"
|
||||
+ statusLEDActiveHigh
|
||||
+ ", getGPIOCommand="
|
||||
+ getGPIOCommand
|
||||
+ ", setGPIOCommand="
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user