mirror of
https://github.com/PhotonVision/photonvision
synced 2026-06-19 00:41:41 +00:00
Compare commits
65 Commits
v2026.1.0-
...
py-docs
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f022130bfa | ||
|
|
5457db947e | ||
|
|
a7329c48a3 | ||
|
|
ce0b00ee03 | ||
|
|
99ca8228a1 | ||
|
|
46e71703ef | ||
|
|
6fbb41fb76 | ||
|
|
05fcf876cd | ||
|
|
1637be6044 | ||
|
|
6f2fd19351 | ||
|
|
892e240b18 | ||
|
|
326c77fa38 | ||
|
|
8cf48bee57 | ||
|
|
26f08a6fdf | ||
|
|
abb8ccf4e9 | ||
|
|
50adef1672 | ||
|
|
cf68403182 | ||
|
|
dc0985dfb5 | ||
|
|
8fb29ff5c4 | ||
|
|
476cd6df8b | ||
|
|
783ed82d50 | ||
|
|
416e2f7607 | ||
|
|
ebd1071553 | ||
|
|
fa8b60fe27 | ||
|
|
c2581f3e99 | ||
|
|
96b0938dc0 | ||
|
|
697e52f886 | ||
|
|
87b219d9be | ||
|
|
abcd6b8f50 | ||
|
|
b22371d7c0 | ||
|
|
3eea79f0d4 | ||
|
|
0147a44100 | ||
|
|
0bec1f239c | ||
|
|
44b46cf117 | ||
|
|
ffdda9ddfa | ||
|
|
a5bc63878d | ||
|
|
a5b1cc0ded | ||
|
|
aacbdf5010 | ||
|
|
3547d0584b | ||
|
|
40815020de | ||
|
|
e522642a48 | ||
|
|
3cdda8a84e | ||
|
|
228caf47f2 | ||
|
|
d1761d07e9 | ||
|
|
331f4f0218 | ||
|
|
eb85834180 | ||
|
|
871ca61c8d | ||
|
|
358f5747ab | ||
|
|
e334d26459 | ||
|
|
77d5388a35 | ||
|
|
88a1e789ad | ||
|
|
abc67bdd95 | ||
|
|
05309b1e25 | ||
|
|
ddacff7079 | ||
|
|
32e4f0029b | ||
|
|
34057f223d | ||
|
|
55303ccd9c | ||
|
|
3c73b68ba3 | ||
|
|
6d816b5053 | ||
|
|
7c6bab1dfa | ||
|
|
462a2aa629 | ||
|
|
47b799a0ce | ||
|
|
35c72e8446 | ||
|
|
c440ce57ce | ||
|
|
8452527589 |
1
.github/pull_request_template.md
vendored
1
.github/pull_request_template.md
vendored
@@ -11,6 +11,7 @@
|
||||
Merge checklist:
|
||||
- [ ] Pull Request title is [short, imperative summary](https://cbea.ms/git-commit/) of proposed changes
|
||||
- [ ] The description documents the _what_ and _why_
|
||||
- [ ] This PR has been [linted](https://docs.photonvision.org/en/latest/docs/contributing/linting.html).
|
||||
- [ ] If this PR changes behavior or adds a feature, user documentation is updated
|
||||
- [ ] If this PR touches photon-serde, all messages have been regenerated and hashes have not changed unexpectedly
|
||||
- [ ] If this PR touches configuration, this is backwards compatible with settings back to v2025.3.2
|
||||
|
||||
546
.github/workflows/build.yml
vendored
546
.github/workflows/build.yml
vendored
@@ -10,7 +10,7 @@ concurrency:
|
||||
cancel-in-progress: true
|
||||
|
||||
env:
|
||||
IMAGE_VERSION: v2026.1.1
|
||||
IMAGE_VERSION: v2026.0.4
|
||||
|
||||
jobs:
|
||||
|
||||
@@ -18,9 +18,8 @@ jobs:
|
||||
name: "Validation"
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: gradle/actions/wrapper-validation@v5
|
||||
|
||||
- uses: actions/checkout@v4
|
||||
- uses: gradle/actions/wrapper-validation@v4
|
||||
build-examples:
|
||||
|
||||
strategy:
|
||||
@@ -28,125 +27,52 @@ jobs:
|
||||
matrix:
|
||||
include:
|
||||
- os: windows-2022
|
||||
artifact-name: Win64
|
||||
architecture: x64
|
||||
- os: macos-14
|
||||
artifact-name: macOS
|
||||
- os: ubuntu-24.04
|
||||
artifact-name: Linux
|
||||
architecture: aarch64
|
||||
- os: ubuntu-22.04
|
||||
|
||||
name: "Photonlib - Build Examples - ${{ matrix.os }}"
|
||||
runs-on: ${{ matrix.os }}
|
||||
needs: [build-photonlib-host, build-photonlib-docker]
|
||||
needs: [validation]
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v4
|
||||
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@v4
|
||||
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%/}"
|
||||
|
||||
# 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
|
||||
# Need to publish to maven local first, so that C++ sim can pick it up
|
||||
- name: Publish photonlib to maven local
|
||||
run: ./gradlew photon-targeting:publishtomavenlocal photon-lib:publishtomavenlocal -x check
|
||||
- name: Build Java examples
|
||||
working-directory: photonlib-java-examples
|
||||
run: |
|
||||
./gradlew build
|
||||
./gradlew clean
|
||||
run: ./gradlew build
|
||||
- name: Build C++ examples
|
||||
working-directory: photonlib-cpp-examples
|
||||
run: |
|
||||
./gradlew build
|
||||
./gradlew clean
|
||||
|
||||
playwright-tests:
|
||||
name: "Playwright E2E tests"
|
||||
runs-on: ubuntu-24.04
|
||||
needs: [validation]
|
||||
steps:
|
||||
# Checkout code.
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v6
|
||||
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
|
||||
with:
|
||||
version: 10
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: 22
|
||||
- name: Setup tests
|
||||
working-directory: photon-client
|
||||
run: |
|
||||
pnpm install
|
||||
pnpm test-setup
|
||||
- name: Prebuild Gradle
|
||||
run: ./gradlew photon-targeting:build photon-core:build photon-server:build -x check
|
||||
- name: Run Playwright tests
|
||||
working-directory: photon-client
|
||||
run: pnpm test
|
||||
- uses: actions/upload-artifact@v6
|
||||
if: ${{ !cancelled() }}
|
||||
with:
|
||||
name: playwright-report
|
||||
path: photon-client/playwright-report/
|
||||
retention-days: 30
|
||||
run: ./gradlew build
|
||||
build-gradle:
|
||||
name: "Gradle Build"
|
||||
runs-on: ubuntu-24.04
|
||||
runs-on: ubuntu-22.04
|
||||
needs: [validation]
|
||||
steps:
|
||||
# Checkout code.
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v4
|
||||
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@v4
|
||||
with:
|
||||
java-version: 17
|
||||
distribution: temurin
|
||||
@@ -155,21 +81,23 @@ jobs:
|
||||
with:
|
||||
version: 10
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v6
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 22
|
||||
- name: Install mrcal deps
|
||||
run: sudo apt-get update && sudo apt-get install -y libcholmod3 liblapack3 libsuitesparseconfig5
|
||||
- name: Gradle Build
|
||||
run: ./gradlew photon-targeting:build photon-core:build photon-server:build -x check
|
||||
- name: Gradle Tests and Coverage
|
||||
run: ./gradlew test jacocoTestReport --stacktrace
|
||||
build-offline-docs:
|
||||
name: "Build Offline Docs"
|
||||
runs-on: ubuntu-24.04
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/setup-python@v6
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: 3.14
|
||||
python-version: '3.11'
|
||||
- name: Install graphviz
|
||||
run: |
|
||||
sudo apt-get update
|
||||
@@ -184,22 +112,22 @@ jobs:
|
||||
working-directory: docs
|
||||
run: |
|
||||
make html
|
||||
- uses: actions/upload-artifact@v6
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: built-docs
|
||||
path: docs/build/html
|
||||
|
||||
build-photonlib-vendorjson:
|
||||
name: "Build Vendor JSON"
|
||||
runs-on: ubuntu-24.04
|
||||
runs-on: ubuntu-22.04
|
||||
needs: [validation]
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Install Java 17
|
||||
uses: actions/setup-java@v5
|
||||
uses: actions/setup-java@v4
|
||||
with:
|
||||
java-version: 17
|
||||
distribution: temurin
|
||||
@@ -214,37 +142,40 @@ 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@v4
|
||||
with:
|
||||
name: photonlib-vendor-json
|
||||
path: photon-lib/build/generated/vendordeps/photonlib-*.json
|
||||
|
||||
build-photonlib-host:
|
||||
env:
|
||||
MACOSX_DEPLOYMENT_TARGET: 14
|
||||
MACOSX_DEPLOYMENT_TARGET: 13
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- os: windows-2022
|
||||
artifact-name: Win64
|
||||
- os: macos-26
|
||||
architecture: x64
|
||||
- os: macos-14
|
||||
artifact-name: macOS
|
||||
- os: ubuntu-24.04
|
||||
architecture: aarch64
|
||||
- os: ubuntu-22.04
|
||||
artifact-name: Linux
|
||||
|
||||
name: "Photonlib - Build Host - ${{ matrix.artifact-name }}"
|
||||
runs-on: ${{ matrix.os }}
|
||||
needs: [validation]
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Install Java 17
|
||||
uses: actions/setup-java@v5
|
||||
uses: actions/setup-java@v4
|
||||
with:
|
||||
java-version: 17
|
||||
distribution: temurin
|
||||
architecture: ${{ matrix.architecture }}
|
||||
- run: git fetch --tags --force
|
||||
- run: ./gradlew photon-targeting:build photon-lib:build
|
||||
name: Build with Gradle
|
||||
@@ -255,7 +186,7 @@ jobs:
|
||||
if: github.event_name == 'push' && github.repository_owner == 'photonvision'
|
||||
# Copy artifacts to build/outputs/maven
|
||||
- run: ./gradlew photon-lib:publish photon-targeting:publish -PcopyOfflineArtifacts
|
||||
- uses: actions/upload-artifact@v6
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: maven-${{ matrix.artifact-name }}
|
||||
path: build/outputs
|
||||
@@ -275,12 +206,12 @@ jobs:
|
||||
artifact-name: Aarch64
|
||||
build-options: "-Ponlylinuxarm64"
|
||||
|
||||
runs-on: ubuntu-24.04
|
||||
runs-on: ubuntu-22.04
|
||||
container: ${{ matrix.container }}
|
||||
name: "Photonlib - Build Docker - ${{ matrix.artifact-name }}"
|
||||
needs: [validation]
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Config Git
|
||||
@@ -296,7 +227,7 @@ jobs:
|
||||
if: github.event_name == 'push' && github.repository_owner == 'photonvision'
|
||||
# Copy artifacts to build/outputs/maven
|
||||
- run: ./gradlew photon-lib:publish photon-targeting:publish -PcopyOfflineArtifacts ${{ matrix.build-options }}
|
||||
- uses: actions/upload-artifact@v6
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: maven-${{ matrix.artifact-name }}
|
||||
path: build/outputs
|
||||
@@ -304,14 +235,14 @@ jobs:
|
||||
combine:
|
||||
name: Combine
|
||||
needs: [build-photonlib-docker, build-photonlib-host]
|
||||
runs-on: ubuntu-24.04
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- run: git fetch --tags --force
|
||||
# download all maven-* artifacts to outputs/
|
||||
- uses: actions/download-artifact@v7
|
||||
- uses: actions/download-artifact@v4
|
||||
with:
|
||||
merge-multiple: true
|
||||
path: output
|
||||
@@ -321,87 +252,12 @@ jobs:
|
||||
name: ZIP stuff up
|
||||
working-directory: output
|
||||
- run: ls output
|
||||
- uses: actions/upload-artifact@v6
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: photonlib-offline
|
||||
path: output/*.zip
|
||||
|
||||
build-package-linux:
|
||||
needs: [build-gradle, build-offline-docs]
|
||||
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- os: ubuntu-24.04
|
||||
artifact-name: Linux
|
||||
arch-override: linuxx64
|
||||
- os: ubuntu-24.04
|
||||
artifact-name: LinuxArm64
|
||||
arch-override: linuxarm64
|
||||
|
||||
runs-on: ${{ matrix.os }}
|
||||
name: "Build fat JAR - ${{ matrix.artifact-name }}"
|
||||
|
||||
steps: &build-package-steps
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Install Java 17
|
||||
uses: actions/setup-java@v5
|
||||
with:
|
||||
java-version: 17
|
||||
distribution: temurin
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: 10
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: 22
|
||||
cache: pnpm
|
||||
cache-dependency-path: photon-client/pnpm-lock.yaml
|
||||
- name: Install Arm64 Toolchain
|
||||
run: ./gradlew installArm64Toolchain
|
||||
if: ${{ (matrix.artifact-name) == 'LinuxArm64' }}
|
||||
- uses: actions/download-artifact@v7
|
||||
with:
|
||||
name: built-docs
|
||||
path: photon-server/src/main/resources/web/docs
|
||||
- run: ./gradlew photon-targeting:jar photon-server:shadowJar -PArchOverride=${{ matrix.arch-override }}
|
||||
if: ${{ (matrix.arch-override != 'none') }}
|
||||
- run: ./gradlew photon-server:shadowJar
|
||||
if: ${{ (matrix.arch-override == 'none') }}
|
||||
- uses: actions/upload-artifact@v6
|
||||
with:
|
||||
name: jar-${{ matrix.artifact-name }}
|
||||
path: photon-server/build/libs
|
||||
- uses: actions/upload-artifact@v6
|
||||
with:
|
||||
name: photon-targeting_jar-${{ matrix.artifact-name }}
|
||||
path: photon-targeting/build/libs
|
||||
|
||||
build-package-macos:
|
||||
needs: [build-gradle, build-offline-docs]
|
||||
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- os: macos-latest
|
||||
artifact-name: macOSArm
|
||||
arch-override: macarm64
|
||||
- os: macos-latest
|
||||
artifact-name: macOS
|
||||
arch-override: macx64
|
||||
|
||||
runs-on: ${{ matrix.os }}
|
||||
name: "Build fat JAR - ${{ matrix.artifact-name }}"
|
||||
|
||||
steps: *build-package-steps
|
||||
|
||||
build-package-windows:
|
||||
build-package:
|
||||
needs: [build-gradle, build-offline-docs]
|
||||
|
||||
strategy:
|
||||
@@ -410,216 +266,314 @@ jobs:
|
||||
include:
|
||||
- os: windows-latest
|
||||
artifact-name: Win64
|
||||
architecture: x64
|
||||
arch-override: winx64
|
||||
- os: macos-latest
|
||||
artifact-name: macOS
|
||||
architecture: x64
|
||||
arch-override: macx64
|
||||
- os: macos-latest
|
||||
artifact-name: macOSArm
|
||||
architecture: x64
|
||||
arch-override: macarm64
|
||||
- os: ubuntu-22.04
|
||||
artifact-name: Linux
|
||||
architecture: x64
|
||||
arch-override: linuxx64
|
||||
- os: ubuntu-22.04
|
||||
artifact-name: LinuxArm64
|
||||
architecture: x64
|
||||
arch-override: linuxarm64
|
||||
|
||||
runs-on: ${{ matrix.os }}
|
||||
name: "Build fat JAR - ${{ matrix.artifact-name }}"
|
||||
|
||||
steps: *build-package-steps
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Install Java 17
|
||||
uses: actions/setup-java@v4
|
||||
with:
|
||||
java-version: 17
|
||||
distribution: temurin
|
||||
architecture: ${{ matrix.architecture }}
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: 10
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 22
|
||||
cache: pnpm
|
||||
cache-dependency-path: photon-client/pnpm-lock.yaml
|
||||
- name: Install Arm64 Toolchain
|
||||
run: ./gradlew installArm64Toolchain
|
||||
if: ${{ (matrix.artifact-name) == 'LinuxArm64' }}
|
||||
- uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: built-docs
|
||||
path: photon-server/src/main/resources/web/docs
|
||||
- run: ./gradlew photon-targeting:jar photon-server:shadowJar -PArchOverride=${{ matrix.arch-override }}
|
||||
if: ${{ (matrix.arch-override != 'none') }}
|
||||
- run: ./gradlew photon-server:shadowJar
|
||||
if: ${{ (matrix.arch-override == 'none') }}
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: jar-${{ matrix.artifact-name }}
|
||||
path: photon-server/build/libs
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: photon-targeting_jar-${{ matrix.artifact-name }}
|
||||
path: photon-targeting/build/libs
|
||||
|
||||
run-smoketest-native:
|
||||
needs: [build-package-linux, build-package-macos, build-package-windows]
|
||||
needs: [build-package]
|
||||
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- os: ubuntu-22.04
|
||||
artifact-name: jar-Linux
|
||||
extraOpts: -Djdk.lang.Process.launchMechanism=vfork
|
||||
- os: windows-latest
|
||||
artifact-name: jar-Win64
|
||||
extraOpts: ""
|
||||
- os: macos-latest
|
||||
artifact-name: jar-macOS
|
||||
architecture: x64
|
||||
|
||||
runs-on: ${{ matrix.os }}
|
||||
|
||||
steps:
|
||||
- name: Install Java 17
|
||||
uses: actions/setup-java@v4
|
||||
with:
|
||||
java-version: 17
|
||||
distribution: temurin
|
||||
- uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: ${{ matrix.artifact-name }}
|
||||
# On linux, install mrcal packages
|
||||
- run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install --yes libcholmod3 liblapack3 libsuitesparseconfig5
|
||||
if: ${{ (matrix.os) == 'ubuntu-24.04' }}
|
||||
# and actually run the jar
|
||||
- run: java -jar ${{ matrix.extraOpts }} *.jar --smoketest
|
||||
if: ${{ (matrix.os) != 'windows-latest' }}
|
||||
- run: ls *.jar | %{ Write-Host "Running $($_.Name)"; Start-Process "java" -ArgumentList "-jar `"$($_.FullName)`" --smoketest" -NoNewWindow -Wait; break }
|
||||
if: ${{ (matrix.os) == 'windows-latest' }}
|
||||
|
||||
run-smoketest-chroot:
|
||||
needs: [build-package]
|
||||
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- os: ubuntu-24.04
|
||||
artifact-name: jar-Linux
|
||||
artifact-name: LinuxArm64
|
||||
image_suffix: RaspberryPi
|
||||
image_url: https://github.com/PhotonVision/photon-image-modifier/releases/download/$IMAGE_VERSION/photonvision_raspi.img.xz
|
||||
cpu: cortex-a7
|
||||
image_additional_mb: 0
|
||||
extraOpts: -Djdk.lang.Process.launchMechanism=vfork
|
||||
- os: windows-latest
|
||||
artifact-name: jar-Win64
|
||||
- os: macos-latest
|
||||
artifact-name: jar-macOS
|
||||
|
||||
runs-on: ${{ matrix.os }}
|
||||
name: smoketest-${{ matrix.image_suffix }}
|
||||
|
||||
steps:
|
||||
- name: Install Java 17
|
||||
uses: actions/setup-java@v5
|
||||
- uses: actions/download-artifact@v4
|
||||
with:
|
||||
java-version: 17
|
||||
distribution: temurin
|
||||
- uses: actions/download-artifact@v7
|
||||
name: jar-${{ matrix.artifact-name }}
|
||||
|
||||
- uses: pguyot/arm-runner-action@v2
|
||||
name: Run photon smoketest
|
||||
id: generate_image
|
||||
with:
|
||||
name: ${{ matrix.artifact-name }}
|
||||
- run: java -jar ${{ matrix.extraOpts }} *.jar --smoketest
|
||||
if: ${{ (matrix.os) != 'windows-latest' }}
|
||||
- run: ls *.jar | %{ Write-Host "Running $($_.Name)"; Start-Process "java" -ArgumentList "-jar `"$($_.FullName)`" --smoketest" -NoNewWindow -Wait; break }
|
||||
if: ${{ (matrix.os) == 'windows-latest' }}
|
||||
base_image: ${{ matrix.image_url }}
|
||||
image_additional_mb: ${{ matrix.image_additional_mb }}
|
||||
optimize_image: yes
|
||||
cpu: ${{ matrix.cpu }}
|
||||
# We do _not_ wanna copy photon into the image. Bind mount instead
|
||||
bind_mount_repository: true
|
||||
# our image better have java installed already
|
||||
commands: |
|
||||
java -jar ${{ matrix.extraOpts }} *.jar --smoketest
|
||||
|
||||
build-image:
|
||||
needs: [build-package-linux]
|
||||
needs: [build-package]
|
||||
|
||||
if: ${{ github.event_name != 'pull_request' }}
|
||||
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- os: ubuntu-24.04-arm
|
||||
- os: ubuntu-24.04
|
||||
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
|
||||
cpu: cortex-a7
|
||||
image_additional_mb: 0
|
||||
- os: ubuntu-24.04
|
||||
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
|
||||
cpu: cortex-a7
|
||||
image_additional_mb: 0
|
||||
- os: ubuntu-24.04
|
||||
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
|
||||
cpu: cortex-a7
|
||||
image_additional_mb: 0
|
||||
- os: ubuntu-24.04
|
||||
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
|
||||
cpu: cortex-a7
|
||||
image_additional_mb: 0
|
||||
- os: ubuntu-24.04
|
||||
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
|
||||
cpu: cortex-a76
|
||||
image_additional_mb: 0
|
||||
- os: ubuntu-24.04
|
||||
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
|
||||
cpu: cortex-a76
|
||||
image_additional_mb: 0
|
||||
- os: ubuntu-24.04
|
||||
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
|
||||
cpu: cortex-a8
|
||||
image_additional_mb: 1024
|
||||
- os: ubuntu-24.04
|
||||
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
|
||||
cpu: cortex-a8
|
||||
image_additional_mb: 1024
|
||||
- os: ubuntu-24.04
|
||||
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
|
||||
cpu: cortex-a8
|
||||
image_additional_mb: 1024
|
||||
- os: ubuntu-24.04
|
||||
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
|
||||
cpu: cortex-a8
|
||||
image_additional_mb: 1024
|
||||
- os: ubuntu-24.04
|
||||
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
|
||||
cpu: cortex-a8
|
||||
image_additional_mb: 1024
|
||||
- os: ubuntu-24.04
|
||||
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: rubikpi3
|
||||
plat_override: LINUX_QCS6490
|
||||
image_url: https://github.com/PhotonVision/photon-image-modifier/releases/download/$IMAGE_VERSION/photonvision_rubikpi3.tar.xz
|
||||
minimum_free_mb: 1024
|
||||
root_location: 'offset=569376768'
|
||||
shrink_image: 'no'
|
||||
cpu: cortex-a8
|
||||
image_additional_mb: 1024
|
||||
|
||||
runs-on: ${{ matrix.os }}
|
||||
name: "Build image - ${{ matrix.image_suffix }}"
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- uses: actions/download-artifact@v7
|
||||
- uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: jar-${{ matrix.artifact-name }}
|
||||
- uses: photonvision/photon-image-runner@HEAD
|
||||
- uses: pguyot/arm-runner-action@HEAD
|
||||
name: Generate image
|
||||
id: generate_image
|
||||
with:
|
||||
image_url: ${{ matrix.image_url }}
|
||||
minimum_free_mb: ${{ matrix.minimum_free_mb }}
|
||||
root_location: ${{ matrix.root_location || 'partition=2' }}
|
||||
shrink_image: ${{ matrix.shrink_image || 'yes' }}
|
||||
base_image: ${{ matrix.image_url }}
|
||||
image_additional_mb: ${{ matrix.image_additional_mb }}
|
||||
optimize_image: yes
|
||||
cpu: ${{ matrix.cpu }}
|
||||
# We do _not_ wanna copy photon into the image. Bind mount instead
|
||||
bind_mount_repository: true
|
||||
commands: |
|
||||
chmod +x scripts/armrunner.sh
|
||||
./scripts/armrunner.sh
|
||||
java -jar *.jar --smoketest --platform=${{ matrix.plat_override }}
|
||||
|
||||
- name: Compress image
|
||||
# Compress the standard images
|
||||
if: ${{ ! startsWith(matrix.image_suffix, 'rubik') }}
|
||||
run: |
|
||||
new_jar=$(realpath $(find . -name photonvision\*-linuxarm64.jar))
|
||||
new_image_name=$(basename "${new_jar/.jar/_${{ matrix.image_suffix }}.img}")
|
||||
sudo mv ${{ steps.generate_image.outputs.image }} $new_image_name
|
||||
mv ${{ steps.generate_image.outputs.image }} $new_image_name
|
||||
sudo xz -T 0 -v $new_image_name
|
||||
|
||||
- name: Tar built image
|
||||
# Build the RubikPi3-specific tar file
|
||||
if: ${{ startsWith(matrix.image_suffix, 'rubik') }}
|
||||
run: |
|
||||
new_jar=$(realpath $(find . -name photonvision\*-linuxarm64.jar))
|
||||
new_image_name=$(basename "${new_jar/.jar/_${{ matrix.image_suffix }}.img}")
|
||||
|
||||
imagedir=$(dirname ${{ steps.generate_image.outputs.image }})
|
||||
tardir=${new_image_name}
|
||||
sudo mkdir --parents ${tardir}
|
||||
sudo mv ${imagedir}/* ${tardir}/
|
||||
sudo tar -I 'xz -T0' -cf ${new_image_name}.tar.xz ${tardir} --checkpoint=10000 --checkpoint-action=echo='%T'
|
||||
|
||||
- uses: actions/upload-artifact@v6
|
||||
- uses: actions/upload-artifact@v4
|
||||
name: Upload image
|
||||
with:
|
||||
name: image-${{ matrix.image_suffix }}
|
||||
path: photonvision*.xz
|
||||
build-rubik-image:
|
||||
needs: [build-package]
|
||||
|
||||
matrix-checker:
|
||||
# This job always runs last to set the overall result based on the matrix jobs. If any matrix job failed, this job will fail.
|
||||
# This makes it so that we don't need to add each matrix job individually to CI checks.
|
||||
runs-on: ubuntu-latest
|
||||
needs: [build-image]
|
||||
if: always()
|
||||
steps:
|
||||
- run: ${{!contains(needs.*.result, 'failure')}}
|
||||
if: ${{ github.event_name != 'pull_request' }}
|
||||
|
||||
release:
|
||||
# Require smoketest-native so that if those fail, we don't release broken artifacts
|
||||
needs: [build-photonlib-vendorjson, build-image, combine, build-package-linux, build-package-macos, build-package-windows, run-smoketest-native]
|
||||
if: github.ref == ('refs/heads/main' || startsWith(github.ref, 'refs/tags/v')) && github.repository == 'PhotonVision/photonvision'
|
||||
runs-on: ubuntu-24.04
|
||||
name: "Build image - Rubik Pi 3"
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: jar-LinuxArm64
|
||||
- name: Generate image
|
||||
run: |
|
||||
wget https://raw.githubusercontent.com/PhotonVision/photon-image-modifier/refs/tags/$IMAGE_VERSION/mount_rubikpi3.sh
|
||||
chmod +x mount_rubikpi3.sh
|
||||
./mount_rubikpi3.sh https://github.com/PhotonVision/photon-image-modifier/releases/download/$IMAGE_VERSION/photonvision_rubikpi3.tar.xz /tmp/build/scripts/armrunner.sh
|
||||
- name: Compress image
|
||||
run: |
|
||||
new_jar=$(realpath $(find . -name photonvision\*-linuxarm64.jar))
|
||||
new_image_name=$(basename "${new_jar/.jar/_rubikpi3.img}")
|
||||
mv photonvision_rubikpi3 $new_image_name
|
||||
tar -I 'xz -T0' -cf ${new_image_name}.tar.xz $new_image_name --checkpoint=10000 --checkpoint-action=echo='%T'
|
||||
- uses: actions/upload-artifact@v4
|
||||
name: Upload image
|
||||
with:
|
||||
name: image-rubikpi3
|
||||
path: photonvision*.xz
|
||||
release:
|
||||
needs: [build-photonlib-vendorjson, build-package, build-image, build-rubik-image, combine]
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
# Download all fat JARs
|
||||
- uses: actions/download-artifact@v7
|
||||
- uses: actions/download-artifact@v4
|
||||
with:
|
||||
merge-multiple: true
|
||||
pattern: jar-*
|
||||
# Download offline photonlib
|
||||
- uses: actions/download-artifact@v7
|
||||
- uses: actions/download-artifact@v4
|
||||
with:
|
||||
merge-multiple: true
|
||||
pattern: photonlib-offline
|
||||
# Download vendor json
|
||||
- uses: actions/download-artifact@v7
|
||||
- uses: actions/download-artifact@v4
|
||||
with:
|
||||
merge-multiple: true
|
||||
pattern: photonlib-vendor-json
|
||||
# Download all images
|
||||
- uses: actions/download-artifact@v7
|
||||
- uses: actions/download-artifact@v4
|
||||
with:
|
||||
merge-multiple: true
|
||||
pattern: image-*
|
||||
@@ -640,7 +594,7 @@ jobs:
|
||||
**/photonlib*.zip
|
||||
if: github.event_name == 'push'
|
||||
- name: Create Vendor JSON Repo PR
|
||||
uses: wpilibsuite/vendor-json-repo/.github/actions/add_vendordep@HEAD
|
||||
uses: wpilibsuite/vendor-json-repo/.github/actions/add_vendordep@main
|
||||
with:
|
||||
repo: PhotonVision/vendor-json-repo
|
||||
token: ${{ secrets.VENDOR_JSON_REPO_PUSH_TOKEN }}
|
||||
|
||||
22
.github/workflows/dependency-submission.yml
vendored
22
.github/workflows/dependency-submission.yml
vendored
@@ -1,22 +0,0 @@
|
||||
name: Dependency Submission
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ 'main' ]
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
dependency-submission:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout sources
|
||||
uses: actions/checkout@v6
|
||||
- name: Setup Java
|
||||
uses: actions/setup-java@v5
|
||||
with:
|
||||
distribution: 'temurin'
|
||||
java-version: 17
|
||||
- name: Generate and submit dependency graph
|
||||
uses: gradle/actions/dependency-submission@v5
|
||||
2
.github/workflows/labeler.yml
vendored
2
.github/workflows/labeler.yml
vendored
@@ -9,6 +9,6 @@ jobs:
|
||||
pull-requests: write
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/labeler@v6
|
||||
- uses: actions/labeler@v5
|
||||
with:
|
||||
sync-labels: true
|
||||
|
||||
67
.github/workflows/lint-format.yml
vendored
67
.github/workflows/lint-format.yml
vendored
@@ -14,39 +14,32 @@ jobs:
|
||||
name: "Validation"
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: gradle/actions/wrapper-validation@v5
|
||||
- uses: actions/checkout@v4
|
||||
- uses: gradle/actions/wrapper-validation@v4
|
||||
wpiformat:
|
||||
name: "wpiformat"
|
||||
runs-on: ubuntu-24.04
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/checkout@v4
|
||||
- name: Fetch all history and metadata
|
||||
run: |
|
||||
git fetch --prune --unshallow
|
||||
git checkout -b pr
|
||||
git branch -f main origin/main
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v6
|
||||
- name: Set up Python 3.8
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: 3.14
|
||||
python-version: 3.11
|
||||
- name: Install wpiformat
|
||||
run: pip3 install wpiformat==2025.79
|
||||
run: pip3 install wpiformat==2025.75
|
||||
- name: Run
|
||||
run: wpiformat
|
||||
- name: Check output
|
||||
run: |
|
||||
set +e
|
||||
git --no-pager diff --exit-code HEAD
|
||||
exit_code=$?
|
||||
if test "$exit_code" -ne "0"; then
|
||||
echo "::error ::Linting failed. See https://docs.photonvision.org/en/latest/docs/contributing/linting.html"
|
||||
exit $exit_code
|
||||
fi
|
||||
run: git --no-pager diff --exit-code HEAD
|
||||
- name: Generate diff
|
||||
run: git diff HEAD > wpiformat-fixes.patch
|
||||
if: ${{ failure() }}
|
||||
- uses: actions/upload-artifact@v6
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: wpiformat fixes
|
||||
path: wpiformat-fixes.patch
|
||||
@@ -54,38 +47,32 @@ jobs:
|
||||
javaformat:
|
||||
name: "Java Formatting"
|
||||
needs: [validation]
|
||||
runs-on: ubuntu-24.04
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- uses: actions/setup-java@v5
|
||||
- uses: actions/setup-java@v4
|
||||
with:
|
||||
java-version: 17
|
||||
distribution: temurin
|
||||
- run: |
|
||||
set +e
|
||||
./gradlew spotlessCheck
|
||||
exit_code=$?
|
||||
if test "$exit_code" -ne "0"; then
|
||||
echo "::error ::Linting failed. See https://docs.photonvision.org/en/latest/docs/contributing/linting.html"
|
||||
exit $exit_code
|
||||
fi
|
||||
- run: ./gradlew spotlessCheck
|
||||
name: Run spotless
|
||||
|
||||
client-lint-format:
|
||||
name: "PhotonClient Lint and Formatting"
|
||||
defaults:
|
||||
run:
|
||||
working-directory: photon-client
|
||||
runs-on: ubuntu-24.04
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/checkout@v4
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: 10
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v6
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 22
|
||||
cache: pnpm
|
||||
@@ -93,20 +80,6 @@ jobs:
|
||||
- name: Install Dependencies
|
||||
run: pnpm i --frozen-lockfile
|
||||
- name: Check Linting
|
||||
run: |
|
||||
set +e
|
||||
pnpm run lint-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"
|
||||
exit $exit_code
|
||||
fi
|
||||
run: pnpm run lint-ci
|
||||
- name: Check Formatting
|
||||
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"
|
||||
exit $exit_code
|
||||
fi
|
||||
run: pnpm run format-ci
|
||||
|
||||
66
.github/workflows/photon-api-docs.yml
vendored
66
.github/workflows/photon-api-docs.yml
vendored
@@ -20,22 +20,22 @@ jobs:
|
||||
name: "Validation"
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: gradle/actions/wrapper-validation@v5
|
||||
- uses: actions/checkout@v4
|
||||
- uses: gradle/actions/wrapper-validation@v4
|
||||
build_demo:
|
||||
name: Build PhotonClient Demo
|
||||
defaults:
|
||||
run:
|
||||
working-directory: photon-client
|
||||
runs-on: ubuntu-24.04
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/checkout@v4
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: 10
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v6
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 22
|
||||
cache: pnpm
|
||||
@@ -44,24 +44,24 @@ jobs:
|
||||
run: pnpm i --frozen-lockfile
|
||||
- name: Build Production Client
|
||||
run: pnpm run build-demo
|
||||
- uses: actions/upload-artifact@v6
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: built-demo
|
||||
name: demo
|
||||
path: photon-client/dist/
|
||||
|
||||
run_java_cpp_docs:
|
||||
name: Build Java and C++ API Docs
|
||||
needs: [validation]
|
||||
runs-on: "ubuntu-24.04"
|
||||
runs-on: "ubuntu-22.04"
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v4
|
||||
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@v4
|
||||
with:
|
||||
java-version: 17
|
||||
distribution: temurin
|
||||
@@ -69,23 +69,53 @@ jobs:
|
||||
run: |
|
||||
chmod +x gradlew
|
||||
./gradlew photon-docs:generateJavaDocs photon-docs:doxygen
|
||||
- uses: actions/upload-artifact@v6
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: docs-java-cpp
|
||||
path: photon-docs/build/docs
|
||||
|
||||
run_py_docs:
|
||||
name: Build Python API Docs
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.10'
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip install -r photon-lib/py/docs/requirements.txt
|
||||
|
||||
- name: Build Sphinx site
|
||||
run: |
|
||||
sphinx-apidoc -o docs/source photonlibpy
|
||||
make -C docs html
|
||||
working-directory: photon-lib/py
|
||||
|
||||
- name: Upload built site as artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: docs-python
|
||||
path: photon-lib/py/docs/build/html
|
||||
|
||||
publish_api_docs:
|
||||
name: Publish API Docs
|
||||
needs: [run_java_cpp_docs]
|
||||
runs-on: ubuntu-24.04
|
||||
needs: [ run_java_cpp_docs, run_py_docs ]
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
# Download docs artifact
|
||||
- uses: actions/download-artifact@v7
|
||||
- uses: actions/download-artifact@v4
|
||||
with:
|
||||
pattern: docs-*
|
||||
- run: find .
|
||||
- name: Publish Docs To Development
|
||||
if: github.ref == 'refs/heads/main'
|
||||
# if: github.ref == 'refs/heads/main'
|
||||
uses: up9cloud/action-rsync@v1.4
|
||||
env:
|
||||
HOST: ${{ secrets.WEBMASTER_SSH_HOST }}
|
||||
@@ -104,11 +134,11 @@ jobs:
|
||||
publish_demo:
|
||||
name: Publish PhotonClient Demo
|
||||
needs: [build_demo]
|
||||
runs-on: ubuntu-24.04
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- uses: actions/download-artifact@v7
|
||||
- uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: built-demo
|
||||
name: demo
|
||||
- run: find .
|
||||
- name: Publish demo
|
||||
if: github.ref == 'refs/heads/main'
|
||||
|
||||
8
.github/workflows/photonvision-rtd.yml
vendored
8
.github/workflows/photonvision-rtd.yml
vendored
@@ -14,14 +14,14 @@ env:
|
||||
jobs:
|
||||
build:
|
||||
name: Build and Check Docs
|
||||
runs-on: ubuntu-24.04
|
||||
runs-on: ubuntu-22.04
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- uses: actions/setup-python@v6
|
||||
- uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: 3.14
|
||||
python-version: '3.11'
|
||||
|
||||
- name: Install and upgrade pip
|
||||
run: python -m pip install --upgrade pip
|
||||
|
||||
83
.github/workflows/python.yml
vendored
83
.github/workflows/python.yml
vendored
@@ -12,28 +12,29 @@ concurrency:
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
build-and-deploy:
|
||||
runs-on: ubuntu-24.04
|
||||
buildAndDeploy:
|
||||
runs-on: ubuntu-22.04
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v6
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: 3.14
|
||||
python-version: 3.11
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip install setuptools wheel pytest mypy
|
||||
pip install setuptools wheel pytest mypy mkdocs mkdocs-gen-files
|
||||
|
||||
- name: Build wheel
|
||||
working-directory: ./photon-lib/py
|
||||
run: python setup.py sdist bdist_wheel
|
||||
run: |
|
||||
python setup.py sdist bdist_wheel
|
||||
|
||||
- name: Run Unit Tests
|
||||
working-directory: ./photon-lib/py
|
||||
@@ -41,11 +42,17 @@ jobs:
|
||||
pip install --no-cache-dir dist/*.whl
|
||||
pytest
|
||||
|
||||
- name: Run mypy type checking
|
||||
run: mypy --show-column-numbers --config-file photon-lib/py/pyproject.toml photon-lib
|
||||
# Disable due to robotpy issue. See
|
||||
# https://github.com/PhotonVision/photonvision/issues/1968
|
||||
# - name: Run mypy type checking
|
||||
# uses: liskin/gh-problem-matcher-wrap@v3
|
||||
# with:
|
||||
# linters: mypy
|
||||
# run: |
|
||||
# mypy --show-column-numbers --config-file photon-lib/py/pyproject.toml photon-lib
|
||||
|
||||
- name: Upload artifacts
|
||||
uses: actions/upload-artifact@v6
|
||||
uses: actions/upload-artifact@master
|
||||
with:
|
||||
name: dist
|
||||
path: ./photon-lib/py/dist/
|
||||
@@ -55,61 +62,7 @@ jobs:
|
||||
if: startsWith(github.ref, 'refs/tags/v')
|
||||
uses: pypa/gh-action-pypi-publish@release/v1
|
||||
with:
|
||||
packages-dir: ./photon-lib/py/dist/
|
||||
packages_dir: ./photon-lib/py/dist/
|
||||
|
||||
permissions:
|
||||
id-token: write # IMPORTANT: this permission is mandatory for trusted publishing
|
||||
|
||||
build-python-examples:
|
||||
strategy:
|
||||
matrix:
|
||||
os: [ubuntu-24.04, windows-2022, macos-14]
|
||||
runs-on: ${{ matrix.os }}
|
||||
|
||||
steps:
|
||||
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: 3.14
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip install setuptools wheel pytest mypy
|
||||
|
||||
- name: Build wheel
|
||||
working-directory: ./photon-lib/py
|
||||
run: python setup.py sdist bdist_wheel
|
||||
|
||||
- name: Build and configure PhotonLibPy
|
||||
working-directory: ./photon-lib/py
|
||||
shell: bash
|
||||
run: |
|
||||
./buildAndTest.sh
|
||||
./enableUsingDevBuilds.sh
|
||||
|
||||
- name: Run Unit Tests
|
||||
working-directory: ./photon-lib/py
|
||||
shell: bash
|
||||
run: |
|
||||
pip install --no-cache-dir dist/*.whl
|
||||
pytest
|
||||
|
||||
- name: Run mypy type checking
|
||||
run: mypy --show-column-numbers --config-file photon-lib/py/pyproject.toml photon-lib
|
||||
|
||||
- name: Build Python examples
|
||||
working-directory: photonlib-python-examples
|
||||
shell: bash
|
||||
run: |
|
||||
for folder in */;
|
||||
do
|
||||
echo $folder
|
||||
./run.sh $folder
|
||||
done
|
||||
|
||||
8
.github/workflows/website.yml
vendored
8
.github/workflows/website.yml
vendored
@@ -9,13 +9,13 @@ jobs:
|
||||
name: Build and Sync Files
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/checkout@v4
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: 10
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v6
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 22
|
||||
cache: pnpm
|
||||
@@ -39,13 +39,13 @@ jobs:
|
||||
name: Check Formatting
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/checkout@v4
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: 10
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v6
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 22
|
||||
cache: pnpm
|
||||
|
||||
12
.gitignore
vendored
12
.gitignore
vendored
@@ -142,17 +142,13 @@ venv
|
||||
.venv/*
|
||||
.venv
|
||||
networktables.json
|
||||
|
||||
# Web stuff
|
||||
photon-server/src/main/resources/web/*
|
||||
node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
components.d.ts
|
||||
photon-server/src/main/resources/web/index.html
|
||||
|
||||
# Playwright
|
||||
photon-client/test-results/
|
||||
photon-client/playwright-report/
|
||||
photon-client/blob-report/
|
||||
photon-client/playwright/.cache/
|
||||
photon-client/playwright/.auth/
|
||||
# Py docs stuff
|
||||
photon-lib/py/docs/build
|
||||
photon-server/src/main/resources/web/index.html
|
||||
|
||||
@@ -1 +1 @@
|
||||
3.14
|
||||
3.11
|
||||
|
||||
@@ -6,9 +6,9 @@ sphinx:
|
||||
fail_on_warning: true
|
||||
|
||||
build:
|
||||
os: ubuntu-24.04
|
||||
os: ubuntu-22.04
|
||||
tools:
|
||||
python: "3.12"
|
||||
python: "3.11"
|
||||
apt_packages:
|
||||
- graphviz
|
||||
jobs:
|
||||
|
||||
3
.vscode/settings.json
vendored
3
.vscode/settings.json
vendored
@@ -1,6 +1,5 @@
|
||||
{
|
||||
"python.testing.unittestEnabled": false,
|
||||
"python.testing.pytestEnabled": true,
|
||||
"python.testing.cwd": "photon-lib/py",
|
||||
"java.configuration.updateBuildConfiguration": "automatic"
|
||||
"python.testing.cwd": "photon-lib/py"
|
||||
}
|
||||
16
.wpiformat
16
.wpiformat
@@ -3,6 +3,22 @@ cppHeaderFileInclude {
|
||||
}
|
||||
|
||||
modifiableFileExclude {
|
||||
\.dll$
|
||||
\.gif$
|
||||
\.ico$
|
||||
\.jpeg$
|
||||
\.jpg$
|
||||
\.mp4$
|
||||
\.pdf$
|
||||
\.png$
|
||||
\.rknn$
|
||||
\.so$
|
||||
\.svg$
|
||||
\.tflite$
|
||||
\.ttf$
|
||||
\.webp$
|
||||
\.woff2$
|
||||
gradlew
|
||||
photon-lib/py/photonlibpy/generated/
|
||||
photon-targeting/src/generated/
|
||||
photon-targeting/src/main/native/cpp/photon/constrained_solvepnp/generate/
|
||||
|
||||
13
README.md
13
README.md
@@ -17,9 +17,10 @@ If you are interested in contributing code or documentation to the project, plea
|
||||
## Documentation
|
||||
|
||||
- Our main documentation page: [docs.photonvision.org](https://docs.photonvision.org)
|
||||
- Photon UI demo: [demo.photonvision.org](https://demo.photonvision.org)
|
||||
- Photon UI demo: [http://photonvision.global/](http://photonvision.global/)
|
||||
- Javadocs: [javadocs.photonvision.org](https://javadocs.photonvision.org)
|
||||
- C++ Doxygen [cppdocs.photonvision.org](https://cppdocs.photonvision.org)
|
||||
- Python Documentation [pydocs.photonvision.org](https://pydocs.photonvision.org)
|
||||
|
||||
## Building
|
||||
|
||||
@@ -58,6 +59,14 @@ PhotonVision uses the following additional out-of-source repositories for buildi
|
||||
- Custom build of OpenCV with GStreamer/Protobuf/other custom flags: https://github.com/PhotonVision/thirdparty-opencv
|
||||
- JNI code for aruco-nano: https://github.com/PhotonVision/aruconano-jni
|
||||
|
||||
## Additional packages
|
||||
|
||||
For now, using mrcal requires installing these additional packages on Linux systems:
|
||||
|
||||
```
|
||||
sudo apt install libcholmod3 liblapack3 libsuitesparseconfig5
|
||||
```
|
||||
|
||||
## Acknowledgments
|
||||
|
||||
PhotonVision was forked from [Chameleon Vision](https://github.com/Chameleon-Vision/chameleon-vision/). Thank you to everyone who worked on the original project.
|
||||
@@ -72,8 +81,6 @@ PhotonVision was forked from [Chameleon Vision](https://github.com/Chameleon-Vis
|
||||
|
||||
* [FasterXML](https://github.com/FasterXML) - Specifically [jackson](https://github.com/FasterXML/jackson)
|
||||
|
||||
* [OSHI](https://github.com/oshi/oshi)
|
||||
|
||||
## License
|
||||
|
||||
PhotonVision is licensed under the [GNU General Public License](https://www.gnu.org/licenses/gpl-3.0.html).
|
||||
|
||||
30
build.gradle
30
build.gradle
@@ -2,10 +2,10 @@ import edu.wpi.first.toolchain.*
|
||||
|
||||
plugins {
|
||||
id "cpp"
|
||||
id "com.diffplug.spotless" version "8.1.0"
|
||||
id "com.diffplug.spotless" version "6.24.0"
|
||||
id "edu.wpi.first.wpilib.repositories.WPILibRepositoriesPlugin" version "2020.2"
|
||||
id "edu.wpi.first.GradleRIO" version "2026.1.1"
|
||||
id 'org.photonvision.tools.WpilibTools' version '2.3.3-photon'
|
||||
id "edu.wpi.first.GradleRIO" version "2025.3.2"
|
||||
id 'edu.wpi.first.WpilibTools' version '1.3.0'
|
||||
id 'com.google.protobuf' version '0.9.3' apply false
|
||||
id 'edu.wpi.first.GradleJni' version '1.1.0'
|
||||
id "org.ysb33r.doxygen" version "2.0.0" apply false
|
||||
@@ -15,11 +15,11 @@ plugins {
|
||||
|
||||
allprojects {
|
||||
repositories {
|
||||
maven { url = "https://frcmaven.wpi.edu/artifactory/ex-mvn/" }
|
||||
mavenCentral()
|
||||
mavenLocal()
|
||||
maven { url = "https://maven.photonvision.org/releases" }
|
||||
maven { url = "https://maven.photonvision.org/snapshots" }
|
||||
maven { url = "https://jogamp.org/deployment/maven/" }
|
||||
}
|
||||
wpilibRepositories.addAllReleaseRepositories(it)
|
||||
wpilibRepositories.addAllDevelopmentRepositories(it)
|
||||
@@ -32,22 +32,29 @@ ext.allOutputsFolder = file("$project.buildDir/outputs")
|
||||
apply from: "versioningHelper.gradle"
|
||||
|
||||
ext {
|
||||
wpilibVersion = "2026.1.1"
|
||||
wpilibVersion = "2025.3.2"
|
||||
wpimathVersion = wpilibVersion
|
||||
openCVYear = "2025"
|
||||
openCVversion = "4.10.0-3"
|
||||
javalinVersion = "6.7.0"
|
||||
libcameraDriverVersion = "v2026.0.0"
|
||||
rknnVersion = "v2026.0.1"
|
||||
rubikVersion = "v2026.0.1"
|
||||
frcYear = "2026"
|
||||
mrcalVersion = "v2026.0.0";
|
||||
libcameraDriverVersion = "v2025.0.4"
|
||||
rknnVersion = "dev-v2025.0.0-5-g666c0c6"
|
||||
rubikVersion = "dev-v2025.1.0-6-g4a5e508"
|
||||
frcYear = "2025"
|
||||
mrcalVersion = "v2025.0.0";
|
||||
|
||||
|
||||
pubVersion = versionString
|
||||
isDev = pubVersion.startsWith("dev")
|
||||
|
||||
// A list, for legacy reasons, with only the current platform contained
|
||||
wpilibNativeName = wpilibTools.platformMapper.currentPlatform.platformName;
|
||||
jniPlatform = wpilibTools.platformMapper.wpilibClassifier;
|
||||
def nativeName = wpilibNativeName
|
||||
if (wpilibNativeName == "linuxx64") nativeName = "linuxx86-64";
|
||||
if (wpilibNativeName == "winx64") nativeName = "windowsx86-64";
|
||||
if (wpilibNativeName == "macx64") nativeName = "osxx86-64";
|
||||
if (wpilibNativeName == "macarm64") nativeName = "osxarm64";
|
||||
jniPlatform = nativeName
|
||||
|
||||
println("Building for platform " + jniPlatform + " wpilib: " + wpilibNativeName)
|
||||
println("Using Wpilib: " + wpilibVersion)
|
||||
@@ -112,6 +119,5 @@ subprojects {
|
||||
options.addStringOption("charset", "utf-8")
|
||||
options.addStringOption("docencoding", "utf-8")
|
||||
options.addStringOption("encoding", "utf-8")
|
||||
options.addBooleanOption("Xdoclint/package:-org.photonvision.proto,-org.photonvision.struct,-org.photonvision.targeting.proto,-org.photonvision.jni", true)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,165 +0,0 @@
|
||||
-- PhotonVision Time Synchronization Protocol Dissector
|
||||
-- Protocol runs on UDP port 5810
|
||||
-- Reference: https://docs.photonvision.org/en/v2026.0.0-alpha-1/docs/contributing/design-descriptions/time-sync.html
|
||||
|
||||
photon_timesync_proto = Proto("photon_timesync", "PhotonVision Time Sync Protocol")
|
||||
|
||||
-- Protocol fields
|
||||
local pf_version = ProtoField.uint8("photon_timesync.version", "Version", base.DEC)
|
||||
local pf_message_id = ProtoField.uint8("photon_timesync.message_id", "Message ID", base.DEC, {
|
||||
[0] = "Ping",
|
||||
[1] = "Pong"
|
||||
})
|
||||
local pf_client_time = ProtoField.uint64("photon_timesync.client_time", "Client Time (μs)", base.DEC)
|
||||
local pf_server_time = ProtoField.uint64("photon_timesync.server_time", "Server Time (μs)", base.DEC)
|
||||
local pf_response_in = ProtoField.framenum("photon_timesync.response_in", "Response In Frame", base.NONE,
|
||||
frametype.RESPONSE)
|
||||
local pf_response_to = ProtoField.framenum("photon_timesync.response_to", "Response To Frame", base.NONE,
|
||||
frametype.REQUEST)
|
||||
local pf_response_time = ProtoField.relative_time("photon_timesync.response_time", "Response Time")
|
||||
|
||||
-- Register fields
|
||||
photon_timesync_proto.fields = {
|
||||
pf_version,
|
||||
pf_message_id,
|
||||
pf_client_time,
|
||||
pf_server_time,
|
||||
pf_response_in,
|
||||
pf_response_to,
|
||||
pf_response_time
|
||||
}
|
||||
|
||||
-- Table to track ping/pong relationships
|
||||
-- Key: client_time as string, Value: frame number of ping
|
||||
local ping_table = {}
|
||||
-- Table to store pong responses for pings
|
||||
-- Key: ping frame number, Value: pong frame number
|
||||
local pong_table = {}
|
||||
|
||||
-- Dissector function
|
||||
function photon_timesync_proto.dissector(buffer, pinfo, tree)
|
||||
-- Check if buffer has minimum length (TspPing = 10 bytes)
|
||||
local length = buffer:len()
|
||||
if length < 10 then
|
||||
return 0
|
||||
end
|
||||
|
||||
-- Set protocol column
|
||||
pinfo.cols.protocol = photon_timesync_proto.name
|
||||
|
||||
-- Create protocol tree
|
||||
local subtree = tree:add(photon_timesync_proto, buffer(), "PhotonVision Time Sync Protocol Data")
|
||||
|
||||
-- Parse version (1 byte)
|
||||
local version = buffer(0, 1):uint()
|
||||
subtree:add(pf_version, buffer(0, 1))
|
||||
|
||||
-- Parse message_id (1 byte)
|
||||
local msg_id = buffer(1, 1):uint()
|
||||
subtree:add(pf_message_id, buffer(1, 1))
|
||||
|
||||
-- Parse client_time (8 bytes, little-endian uint64)
|
||||
local client_time = buffer(2, 8):le_uint64()
|
||||
subtree:add_le(pf_client_time, buffer(2, 8))
|
||||
|
||||
-- Convert client_time to string for use as key
|
||||
local client_time_key = tostring(client_time)
|
||||
local frame_num = pinfo.number
|
||||
|
||||
-- Track relationships between ping and pong
|
||||
if not pinfo.visited then
|
||||
-- First pass: build the relationship tables
|
||||
if msg_id == 1 then
|
||||
-- This is a Ping - store it
|
||||
ping_table[client_time_key] = frame_num
|
||||
elseif msg_id == 2 then
|
||||
-- This is a Pong - find matching Ping
|
||||
local ping_frame = ping_table[client_time_key]
|
||||
if ping_frame then
|
||||
pong_table[ping_frame] = frame_num
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- Update info column and parse based on message type
|
||||
if msg_id == 1 then
|
||||
-- TspPing: version(1) + message_id(1) + client_time(8) = 10 bytes
|
||||
pinfo.cols.info = string.format("Time Sync Ping (client_time: %s μs)", tostring(client_time))
|
||||
|
||||
-- Check if we have a response for this ping
|
||||
local pong_frame = pong_table[frame_num]
|
||||
if pong_frame then
|
||||
local response_item = subtree:add(pf_response_in, pong_frame)
|
||||
response_item:set_generated()
|
||||
end
|
||||
elseif msg_id == 2 then
|
||||
-- TspPong: TspPing + server_time(8) = 18 bytes
|
||||
pinfo.cols.info = "Time Sync Pong"
|
||||
|
||||
if length >= 18 then
|
||||
local server_time = buffer(10, 8):le_uint64()
|
||||
subtree:add_le(pf_server_time, buffer(10, 8))
|
||||
pinfo.cols.info = string.format("Time Sync Pong (client: %s, server: %s μs)",
|
||||
tostring(client_time), tostring(server_time))
|
||||
|
||||
-- Find the matching ping frame
|
||||
local ping_frame = ping_table[client_time_key]
|
||||
if ping_frame then
|
||||
local request_item = subtree:add(pf_response_to, ping_frame)
|
||||
request_item:set_generated()
|
||||
|
||||
-- Calculate response time if we can get the ping packet
|
||||
local ping_time = pinfo.abs_ts - pinfo.rel_ts
|
||||
-- Note: This is an approximation. For accurate timing, we'd need to
|
||||
-- store the timestamp of the ping packet
|
||||
end
|
||||
end
|
||||
else
|
||||
pinfo.cols.info = string.format("Time Sync Unknown (ID: %d)", msg_id)
|
||||
end
|
||||
|
||||
return length
|
||||
end
|
||||
|
||||
-- Register dissector on UDP port 5810
|
||||
local udp_port = DissectorTable.get("udp.port")
|
||||
udp_port:add(5810, photon_timesync_proto)
|
||||
|
||||
-- Heuristic dissector function
|
||||
local function heuristic_checker(buffer, pinfo, tree)
|
||||
local length = buffer:len()
|
||||
|
||||
-- Check minimum length (TspPing = 10 bytes)
|
||||
if length < 10 then
|
||||
return false
|
||||
end
|
||||
|
||||
local version = buffer(0, 1):uint()
|
||||
local msg_id = buffer(1, 1):uint()
|
||||
|
||||
-- Check if this looks like our protocol
|
||||
-- Version should be reasonable (0-10), message_id should be 1 or 2
|
||||
if version <= 10 and (msg_id == 1 or msg_id == 2) then
|
||||
-- Validate packet structure
|
||||
if msg_id == 1 and length == 10 then
|
||||
-- TspPing is exactly 10 bytes
|
||||
photon_timesync_proto.dissector(buffer, pinfo, tree)
|
||||
return true
|
||||
elseif msg_id == 2 and length == 18 then
|
||||
-- TspPong is exactly 18 bytes
|
||||
photon_timesync_proto.dissector(buffer, pinfo, tree)
|
||||
return true
|
||||
end
|
||||
end
|
||||
|
||||
return false
|
||||
end
|
||||
|
||||
-- Register heuristic dissector
|
||||
photon_timesync_proto:register_heuristic("udp", heuristic_checker)
|
||||
|
||||
-- Initialize function to reset tables on new capture
|
||||
function photon_timesync_proto.init()
|
||||
ping_table = {}
|
||||
pong_table = {}
|
||||
end
|
||||
@@ -21,6 +21,7 @@ mdurl==0.1.2
|
||||
myst-parser==4.0.1
|
||||
packaging==25.0
|
||||
pbr==6.1.1
|
||||
pipreqs==0.5.0
|
||||
Pygments==2.19.1
|
||||
PyYAML==6.0.2
|
||||
requests==2.32.4
|
||||
@@ -53,6 +54,6 @@ stevedore==5.4.1
|
||||
typing_extensions==4.13.2
|
||||
urllib3==2.5.0
|
||||
uvicorn==0.34.2
|
||||
watchfiles==1.1.1
|
||||
watchfiles==1.0.5
|
||||
websockets==15.0.1
|
||||
yarg==0.1.9
|
||||
|
||||
@@ -3,15 +3,13 @@
|
||||
## About
|
||||
|
||||
:::{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 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!
|
||||
:::
|
||||
|
||||
## 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.**
|
||||
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 recommend using PhotonLib.
|
||||
:::
|
||||
|
||||
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.
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
"supportURL" : "https://limelightvision.io",
|
||||
"ledPins" : [ 13, 18 ],
|
||||
"ledsCanDim" : true,
|
||||
"ledPWMRange" : [ 0, 100 ],
|
||||
"ledPWMFrequency" : 30000,
|
||||
"vendorFOV" : 75.76079874010732
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ VERY Limited macOS support is available.
|
||||
|
||||
## 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).
|
||||
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 2025+. 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.
|
||||
|
||||
@@ -12,7 +12,15 @@ Bonjour provides more stable networking when using Windows PCs. Install [Bonjour
|
||||
|
||||
## Installing Java
|
||||
|
||||
PhotonVision requires a JDK installed and on the system path. **JDK 17 is needed.** You may already have it if you installed WPILib, but ensure that running `java -version` shows JDK 17. You will likely have to add WPILib's JDK to JAVA_HOME and the JDK's `bin` directory to PATH. If you do not have a JDK 17 install, [download and install it from here.](https://adoptium.net/temurin/releases?version=17)
|
||||
PhotonVision requires a JDK installed and on the system path. **JDK 17 is needed. Windows Users must use the JDK that ships with WPILib.** [Download and install it from here.](https://github.com/wpilibsuite/allwpilib/releases/tag/v2025.3.2) Either ensure the only Java on your PATH is the WPILIB Java or specify it to gradle with `-Dorg.gradle.java.home=C:\Users\Public\wpilib\2025\jdk`:
|
||||
|
||||
```
|
||||
> ./gradlew run "-Dorg.gradle.java.home=C:\Users\Public\wpilib\2025\jdk"
|
||||
```
|
||||
|
||||
:::{warning}
|
||||
Using a JDK other than WPILIB's JDK17 will cause issues when running PhotonVision and is not supported.
|
||||
:::
|
||||
|
||||
## Downloading the Latest Stable Release of PhotonVision
|
||||
|
||||
|
||||
@@ -23,7 +23,7 @@ AprilTag pipelines come with reasonable defaults to get you up and running with
|
||||
|
||||
Target families are defined by two numbers (before and after the h). The first number is the number of bits the tag is able to encode (which means more tags are available in the respective family) and the second is the hamming distance. Hamming distance describes the ability for error correction while identifying tag ids. A high hamming distance generally means that it will be easier for a tag to be identified even if there are errors. However, as hamming distance increases, the number of available tags decreases.
|
||||
|
||||
The 2026 FRC game will be using 36h11 tags, which can be found [here](https://github.com/AprilRobotics/apriltag-imgs/tree/2bc821edb4eb7b408d13c6a590d326d8a9ec98f3/tag36h11).
|
||||
The 2025 FRC game will be using 36h11 tags, which can be found [here](https://github.com/AprilRobotics/apriltag-imgs/tree/main/tag36h11).
|
||||
|
||||
### Decimate
|
||||
|
||||
|
||||
@@ -10,5 +10,5 @@ AprilTags are a common type of visual fiducial marker. Visual fiducial markers a
|
||||
A more technical explanation can be found in the [WPILib documentation](https://docs.wpilib.org/en/latest/docs/software/vision-processing/apriltag/apriltag-intro.html).
|
||||
|
||||
:::{note}
|
||||
You can get FIRST's [official PDF of the targets used in 2026 here](https://firstfrc.blob.core.windows.net/frc2026/FieldAssets/2026-apriltag-images-user-guide.pdf).
|
||||
You can get FIRST's [official PDF of the targets used in 2025 here](https://firstfrc.blob.core.windows.net/frc2025/FieldAssets/Apriltag_Images_and_User_Guide.pdf).
|
||||
:::
|
||||
|
||||
@@ -7,7 +7,7 @@ MultiTag requires an accurate field layout JSON to be uploaded! Differences betw
|
||||
:::
|
||||
|
||||
:::{warning}
|
||||
For the 2026 Rebuilt Season, there are two different field layouts. The first is the [welded field layout](https://github.com/wpilibsuite/allwpilib/blob/main/apriltag/src/main/native/resources/edu/wpi/first/apriltag/2026-rebuilt-welded.json), which photonvision ships with. The second is the [Andymark field layout](https://github.com/wpilibsuite/allwpilib/blob/main/apriltag/src/main/native/resources/edu/wpi/first/apriltag/2026-rebuilt-andymark.json). It is very important to ensure that you use the correct field layout, both in the [PhotonPoseEstimator](https://docs.photonvision.org/en/latest/docs/programming/photonlib/robot-pose-estimator.html#apriltags-and-photonposeestimator) and on the [coprocessor](https://docs.photonvision.org/en/latest/docs/apriltag-pipelines/multitag.html#updating-the-field-layout).
|
||||
For the 2025 Reefscape Season, there are two different field layouts. The first is the [welded field layout](https://github.com/wpilibsuite/allwpilib/blob/main/apriltag/src/main/native/resources/edu/wpi/first/apriltag/2025-reefscape-welded.json), which photonvision ships with. The second is the [Andymark field layout](https://github.com/wpilibsuite/allwpilib/blob/main/apriltag/src/main/native/resources/edu/wpi/first/apriltag/2025-reefscape-andymark.json). It is very important to ensure that you use the correct field layout, both in the [PhotonPoseEstimator](https://docs.photonvision.org/en/latest/docs/programming/photonlib/robot-pose-estimator.html#apriltags-and-photonposeestimator) and on the [coprocessor](https://docs.photonvision.org/en/latest/docs/apriltag-pipelines/multitag.html#updating-the-field-layout).
|
||||
:::
|
||||
|
||||
## Enabling MultiTag
|
||||
@@ -66,7 +66,7 @@ The returned field to camera transform is a transform from the fixed field origi
|
||||
|
||||
## Updating the Field Layout
|
||||
|
||||
PhotonVision ships by default with the [2026 welded field layout JSON](https://github.com/wpilibsuite/allwpilib/blob/main/apriltag/src/main/native/resources/edu/wpi/first/apriltag/2026-rebuilt-welded.json). The layout can be inspected by navigating to the settings tab and scrolling down to the "AprilTag Field Layout" card, as shown below.
|
||||
PhotonVision ships by default with the [2025 welded field layout JSON](https://github.com/wpilibsuite/allwpilib/blob/main/apriltag/src/main/native/resources/edu/wpi/first/apriltag/2025-reefscape-welded.json). The layout can be inspected by navigating to the settings tab and scrolling down to the "AprilTag Field Layout" card, as shown below.
|
||||
|
||||
```{image} images/field-layout.png
|
||||
:alt: The currently saved field layout in the Photon UI
|
||||
|
||||
@@ -8,7 +8,7 @@ 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) 17 to be compiled. This is the same Java version that comes with WPILib for 2025+. **Windows Users must use the JDK that ships with WPILib.** For other platforms, you can follow the instructions to install JDK 17 for your platform [here](https://bell-sw.com/pages/downloads/#jdk-17-lts).
|
||||
|
||||
**Node JS:**
|
||||
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 512 KiB |
@@ -109,13 +109,3 @@ Clients may publish statistics to NetworkTables. If they do, they shall publish
|
||||
| rtt2_us | Integer | The time in us from last complete (ping transmission to pong reception) |
|
||||
|
||||
PhotonVision has chosen to publish to the sub-table `/photonvision/.timesync/{DEVICE_HOSTNAME}`. Future implementations of this protocol may decide to implement this as a structured data type.
|
||||
|
||||
## Wireshark Dissector
|
||||
|
||||

|
||||
|
||||
A [WireShark dissector](https://raw.githubusercontent.com/PhotonVision/photonvision/refs/heads/main/devTools/photon.lua) created for Wireshark ~=4.6 can be used to inspect Time Syncronization messages. Copy the dissector to your Wireshark plugin directory (for me, that's `C:\Users\Me\AppData\Roaming\Wireshark\plugins`), and open the capture. Because TSP uses UDP Unicast, data must be collected on the coprocessor or robot processor using a command similar to:
|
||||
|
||||
```
|
||||
sudo tcpdump -i any port 5810 -w tsp_capture.pcap
|
||||
```
|
||||
|
||||
@@ -7,7 +7,7 @@ The following example is from the PhotonLib example repository ([Java](https://g
|
||||
- A Robot
|
||||
- A camera mounted rigidly to the robot's frame, centered and pointed forward.
|
||||
- A coprocessor running PhotonVision with an AprilTag or ArUco 2D Pipeline.
|
||||
- [A printout of AprilTag 7](https://firstfrc.blob.core.windows.net/frc2026/FieldAssets/2026-apriltag-images-user-guide.pdf), mounted on a rigid and flat surface.
|
||||
- [A printout of AprilTag 7](https://firstfrc.blob.core.windows.net/frc2025/FieldAssets/Apriltag_Images_and_User_Guide.pdf), mounted on a rigid and flat surface.
|
||||
|
||||
## Code
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ By default, PhotonVision attempts to make minimal assumptions of the hardware it
|
||||
|
||||
## LED Support
|
||||
|
||||
When running on Linux, PhotonVision can use [diozero](https://www.diozero.com) to control IO pins. The mapping of which pins control which LED's is part of the hardware config. The illumination LED pins are active-high: set high when LED's are commanded on, and set low when commanded off.
|
||||
For Raspberry-Pi based hardware, PhotonVision can use [PiGPIO](https://abyz.me.uk/rpi/pigpio/) to control IO pins. The mapping of which pins control which LED's is part of the hardware config. The pins are active-high: set high when LED's are commanded on, and set low when commanded off.
|
||||
|
||||
```{eval-rst}
|
||||
.. tab-set-code::
|
||||
@@ -16,11 +16,14 @@ When running on Linux, PhotonVision can use [diozero](https://www.diozero.com) t
|
||||
|
||||
{
|
||||
"ledPins" : [ 13 ],
|
||||
"ledSetCommand" : "",
|
||||
"ledsCanDim" : true,
|
||||
"ledBrightnessRange" : [ 0, 100 ],
|
||||
"ledPWMRange" : [ 0, 100 ],
|
||||
"ledPWMSetRange" : "",
|
||||
"ledPWMFrequency" : 0,
|
||||
"ledDimCommand" : "",
|
||||
"ledBlinkCommand" : "",
|
||||
"statusRGBPins" : [ ],
|
||||
"statusRGBActiveHigh" : false,
|
||||
}
|
||||
```
|
||||
|
||||
@@ -28,49 +31,6 @@ When running on Linux, PhotonVision can use [diozero](https://www.diozero.com) t
|
||||
No hardware boards with status RGB LED pins or non-dimming LED's have been tested yet. Please reach out to the development team if these features are desired, they can assist with configuration and testing.
|
||||
:::
|
||||
|
||||
### GPIO Pinout
|
||||
|
||||
::::{tab-set}
|
||||
|
||||
:::{tab-item} Raspberry Pi
|
||||
|
||||
The following diagram shows the GPIO pin numbering of the 40-pin header on Raspberry Pi hardware, courtesy of [pinout.xyz](https://pinout.xyz). Compute modules use the pin numbering from their respective datasheet.
|
||||
|
||||
```{image} https://raw.githubusercontent.com/pinout-xyz/Pinout.xyz/master/resources/raspberry-pi-pinout.png
|
||||
:alt: Raspberry Pi GPIO Pinout
|
||||
```
|
||||
|
||||
:::
|
||||
::::
|
||||
|
||||
### Custom GPIO
|
||||
|
||||
If your hardware does not support diozero's default provider, custom commands can be provided to interact with the GPIO lines. The examples below show what parameters are provided to each command, which can be used in any order or multiple times as needed.
|
||||
|
||||
```{eval-rst}
|
||||
.. tab-set-code::
|
||||
.. code-block:: json
|
||||
|
||||
{
|
||||
"getGPIOCommand" : "getGPIO {p}",
|
||||
"setGPIOCommand" : "setGPIO {p} {s}",
|
||||
"setPWMCommand" : "setPWM {p} {v}",
|
||||
"setPWMFrequencyCommand" : "setPWMFrequency {p} {f}",
|
||||
"releaseGPIOCommand" : "releseGPIO {p}",
|
||||
}
|
||||
```
|
||||
|
||||
The following template strings are used to input parameters to the commands:
|
||||
|
||||
| Template | Parameter | Values |
|
||||
| -------- | ---------- | ---------- |
|
||||
| `{p}` | pin number | integers |
|
||||
| `{s}` | state | true/false |
|
||||
| `{v}` | value | 0.0-1.0 |
|
||||
| `{f}` | frequency | integers |
|
||||
|
||||
If you were using custom LED commands from 2025 or earlier and still need custom GPIO commands, they can likely be copied over. `ledSetCommand` can be reused as `setGPIOCommand`. `ledDimCommand` can be reused with edits as `setPWMCommand`, replacing any occurrences of `{v}` with `$(awk 'BEGIN{ print int({v}*100) }')` if your command requires integer percentages.
|
||||
|
||||
## Hardware Interaction Commands
|
||||
|
||||
For Non-Raspberry-Pi hardware, users must provide valid hardware-specific commands for some parts of the UI interaction (including performance metrics, and executing system restarts).
|
||||
@@ -141,16 +101,14 @@ Here is a complete example `hardwareConfig.json`:
|
||||
"deviceLogoPath" : "",
|
||||
"supportURL" : "https://www.youtube.com/watch?v=b-CvLWbfZhU",
|
||||
"ledPins" : [2, 13],
|
||||
"ledSetCommand" : "",
|
||||
"ledsCanDim" : true,
|
||||
"ledBrightnessRange" : [ 0, 100 ],
|
||||
"ledPWMRange" : [ 0, 100 ],
|
||||
"ledPWMSetRange" : "",
|
||||
"ledPWMFrequency" : 0,
|
||||
"ledDimCommand" : "",
|
||||
"ledBlinkCommand" : "",
|
||||
"statusRGBPins" : [ ],
|
||||
"statusRGBActiveHigh" : false,
|
||||
"getGPIOCommand" : "getGPIO {p}",
|
||||
"setGPIOCommand" : "setGPIO {p} {s}",
|
||||
"setPWMCommand" : "setPWM {p} {v}",
|
||||
"setPWMFrequencyCommand" : "setPWMFrequency {p} {f}",
|
||||
"releaseGPIOCommand" : "releseGPIO {p}",
|
||||
"cpuTempCommand" : "",
|
||||
"cpuMemoryCommand" : "",
|
||||
"cpuUtilCommand" : "",
|
||||
|
||||
@@ -88,7 +88,7 @@ Cameras capable of capturing a good image with very short exposures will also he
|
||||
|
||||
### Using Multiple Cameras
|
||||
|
||||
Keeping the target(s) in view of the robot often requires more than one camera. PhotonVision has no hardcoded limit on the number of cameras supported. The limit is usually dependent on CPU (can all frames be processed fast enough?) and USB bandwidth (Can all cameras send their images without overwhelming the bus?).
|
||||
Keeping the target(s) in view of the robot often requires more than one camera. PhotonVision has no hardcoded limit on the number of cameras supported. The limit is usually dependant on CPU (can all frames be processed fast enough?) and USB bandwidth (Can all cameras send their images without overwhelming the bus?).
|
||||
|
||||
Note that cameras are not synchronized together. Frames are captured and processed asynchronously. Robot Code must fuse estimates together. For more information, see {ref}`the programming reference. <docs/programming/index:programming reference>`.
|
||||
|
||||
|
||||
@@ -1,37 +0,0 @@
|
||||
# FPS Limiter
|
||||
|
||||
:::{warning}
|
||||
When using the FPS limiter, it's important to disable it before a match begins.
|
||||
:::
|
||||
|
||||
The FPS limiter can be used to lower the frames processed per second for a given camera. This is intended to be used for power-saving, particularly in the case of high FPS cameras with powerful coprocessors. The value passed to the function will indicate the frames per second that should be processed. A value of -1 should be passed to indicate that the FPS limiter should not restrict processing; this is the default behavior.
|
||||
|
||||
```{eval-rst}
|
||||
.. tab-set-code::
|
||||
.. code-block:: java
|
||||
|
||||
int limit = camera.getFPSLimit();
|
||||
|
||||
camera.setFPSLimit(10);
|
||||
|
||||
// This removes any previously set FPS limit.
|
||||
camera.setFPSLimit(-1);
|
||||
|
||||
.. code-block:: c++
|
||||
|
||||
int limit = camera.GetFPSLimit();
|
||||
|
||||
camera.SetFPSLimit(10);
|
||||
|
||||
// This removes any previously set FPS limit.
|
||||
camera.SetFPSLimit(-1);
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
limit = camera.getFPSLimit()
|
||||
|
||||
camera.setFPSLimit(10)
|
||||
|
||||
# This removes any previously set FPS limit.
|
||||
camera.setFPSLimit(-1)
|
||||
```
|
||||
@@ -9,5 +9,4 @@ using-target-data
|
||||
robot-pose-estimator
|
||||
driver-mode-pipeline-index
|
||||
controlling-led
|
||||
fps-limiter
|
||||
```
|
||||
|
||||
@@ -48,88 +48,91 @@ Another necessary argument for creating a `PhotonPoseEstimator` is the `Transfor
|
||||
|
||||
## Creating a `PhotonPoseEstimator`
|
||||
|
||||
The PhotonPoseEstimator has a constructor that takes an `AprilTagFieldLayout` (see above) and `Transform3d`.
|
||||
The PhotonPoseEstimator has a constructor that takes an `AprilTagFieldLayout` (see above), `PoseStrategy`, `PhotonCamera`, and `Transform3d`. `PoseStrategy` has nine possible values:
|
||||
|
||||
- MULTI_TAG_PNP_ON_COPROCESSOR
|
||||
- Calculates a new robot position estimate by combining all visible tag corners. Recommended for all teams as it will be the most accurate.
|
||||
- Must configure the AprilTagFieldLayout properly in the UI, please see {ref}`here <docs/apriltag-pipelines/multitag:multitag localization>` for more information.
|
||||
- LOWEST_AMBIGUITY
|
||||
- Choose the Pose with the lowest ambiguity.
|
||||
- CLOSEST_TO_CAMERA_HEIGHT
|
||||
- Choose the Pose which is closest to the camera height.
|
||||
- CLOSEST_TO_REFERENCE_POSE
|
||||
- Choose the Pose which is closest to the pose from setReferencePose().
|
||||
- CLOSEST_TO_LAST_POSE
|
||||
- Choose the Pose which is closest to the last pose calculated.
|
||||
- AVERAGE_BEST_TARGETS
|
||||
- Choose the Pose which is the average of all the poses from each tag.
|
||||
- MULTI_TAG_PNP_ON_RIO
|
||||
- A slower, older version of MULTI_TAG_PNP_ON_COPROCESSOR, not recommended for use.
|
||||
- PNP_DISTANCE_TRIG_SOLVE
|
||||
- Use distance data from best visible tag to compute a Pose. This runs on the RoboRIO in order
|
||||
to access the robot's yaw heading, and MUST have addHeadingData called every frame so heading
|
||||
data is up-to-date. Based on a reference implementation by [FRC Team 6328 Mechanical Advantage](https://www.chiefdelphi.com/t/frc-6328-mechanical-advantage-2025-build-thread/477314/98).
|
||||
- CONSTRAINED_SOLVEPNP
|
||||
- Solve a constrained version of the Perspective-n-Point problem with the robot's drivebase
|
||||
flat on the floor. This computation takes place on the RoboRIO, and should not take more than 2ms.
|
||||
This also requires addHeadingData to be called every frame so heading data is up to date.
|
||||
If Multi-Tag PNP is enabled on the coprocessor, it will be used to provide an initial seed to
|
||||
the optimization algorithm -- otherwise, the multi-tag fallback strategy will be used as the
|
||||
seed.
|
||||
|
||||
```{eval-rst}
|
||||
.. tab-set-code::
|
||||
.. rli:: https://raw.githubusercontent.com/PhotonVision/photonvision/refs/heads/main/photonlib-java-examples/poseest/src/main/java/frc/robot/Vision.java
|
||||
:language: java
|
||||
:lines: 63
|
||||
:lines: 65-66
|
||||
|
||||
.. rli:: https://raw.githubusercontent.com/PhotonVision/photonvision/refs/heads/main/photonlib-cpp-examples/poseest/src/main/include/Vision.h
|
||||
:language: c++
|
||||
:lines: 149-150
|
||||
:lines: 150-153
|
||||
|
||||
.. rli:: https://raw.githubusercontent.com/PhotonVision/photonvision/refs/heads/main/photonlib-python-examples/poseest/robot.py
|
||||
:language: python
|
||||
:lines: 45-48
|
||||
:lines: 45-50
|
||||
```
|
||||
|
||||
:::{note}
|
||||
Python still takes a `PhotonCamera` in the constructor, so you must create the camera as shown in the next section and then return and use it to create the `PhotonPoseEstimator`.
|
||||
:::
|
||||
|
||||
## Using a `PhotonPoseEstimator`
|
||||
|
||||
To use your `PhotonPoseEstimator`, you must create a `PhotonCamera` and feed the results into your `PhotonPoseEstimator`. To do this, you must first set the name of your camera in Photon Client. From there you can define the camera in code.
|
||||
The final prerequisite to using your `PhotonPoseEstimator` is creating a `PhotonCamera`. To do this, you must set the name of your camera in Photon Client. From there you can define the camera in code.
|
||||
|
||||
```{eval-rst}
|
||||
.. tab-set-code::
|
||||
.. rli:: https://raw.githubusercontent.com/PhotonVision/photonvision/refs/heads/main/photonlib-java-examples/poseest/src/main/java/frc/robot/Vision.java
|
||||
:language: java
|
||||
:lines: 62
|
||||
:lines: 63
|
||||
|
||||
.. rli:: https://raw.githubusercontent.com/PhotonVision/photonvision/refs/heads/main/photonlib-cpp-examples/poseest/src/main/include/Vision.h
|
||||
.. rli:: https://raw.githubusercontent.com/PhotonVision/photonvision/refs/heads/main/photonlib-cpp-examples/aimattarget/src/main/include/Robot.h
|
||||
:language: c++
|
||||
:lines: 151
|
||||
:lines: 55
|
||||
|
||||
.. rli:: https://raw.githubusercontent.com/PhotonVision/photonvision/refs/heads/main/photonlib-python-examples/poseest/robot.py
|
||||
:language: python
|
||||
:lines: 44
|
||||
```
|
||||
|
||||
When taking in a result from a `PhotonCamera`, PhotonPoseEstimator offers nine possible "strategies" for calculating a pose from a pipeline result in the form of methods that you can call, following the pattern `estimate<strategy name>Pose`:
|
||||
|
||||
- Coprocessor MultiTag (`estimateCoprocMultiTagPose`)
|
||||
- Calculates a new robot position estimate by combining all visible tag corners. Recommended for all teams as it will be the most accurate.
|
||||
- Must configure the AprilTagFieldLayout properly in the UI, please see {ref}`here <docs/apriltag-pipelines/multitag:multitag localization>` for more information.
|
||||
- Lowest Ambiguity (`estimateLowestAmbiguityPose`)
|
||||
- Choose the Pose with the lowest ambiguity.
|
||||
- Closest to Camera Height (`estimateClosestToCameraHeightPose`)
|
||||
- Choose the Pose which is closest to the camera height.
|
||||
- Closest to Reference Pose (`estimateClosestToReferencePose`)
|
||||
- Choose the Pose which is closest to the pose that is passed into the function.
|
||||
- Average Best Targets (`estimateAverageBestTargetsPose`)
|
||||
- Choose the Pose which is the average of all the poses from each tag.
|
||||
- roboRio MultiTag (`estimateRioMultiTagPose`)
|
||||
- A slower, older version of Coprocessor MultiTag, not recommended for use.
|
||||
- PnP Distance Trig Solve (`estimatePnpDistanceTrigSolvePose`)
|
||||
- Use distance data from best visible tag to compute a Pose. This runs on the RoboRIO in order
|
||||
to access the robot's yaw heading, and MUST have addHeadingData called every frame so heading
|
||||
data is up-to-date. Based on a reference implementation by [FRC Team 6328 Mechanical Advantage](https://www.chiefdelphi.com/t/frc-6328-mechanical-advantage-2025-build-thread/477314/98).
|
||||
- Constrained SolvePnP (`estimateConstrainedSolvepnpPose`)
|
||||
- Solve a constrained version of the Perspective-n-Point problem with the robot's drivebase
|
||||
flat on the floor. This computation takes place on the RoboRIO, and should not take more than 2ms.
|
||||
This also requires addHeadingData to be called every frame so heading data is up to date.
|
||||
|
||||
Calling one of the `estimate<strategy>Pose()` methods on your `PhotonPoseEstimator` will return an `Optional<EstimatedRobotPose>`, which includes a `Pose3d` of the latest estimated pose (using the selected strategy) along with a `double` of the timestamp when the robot pose was estimated. The recommended way to use the estimatePose methods is to
|
||||
1. do estimation with one of MultiTag methods, check if the result is empty, then
|
||||
2. fallback to single tag estimation using a method like `estimateLowestAmbiguityPose`.
|
||||
Calling `update()` on your `PhotonPoseEstimator` will return an `EstimatedRobotPose`, which includes a `Pose3d` of the latest estimated pose (using the selected strategy) along with a `double` of the timestamp when the robot pose was estimated.
|
||||
|
||||
```{eval-rst}
|
||||
.. tab-set-code::
|
||||
.. rli:: https://raw.githubusercontent.com/PhotonVision/photonvision/refs/heads/main/photonlib-java-examples/poseest/src/main/java/frc/robot/Vision.java
|
||||
:language: java
|
||||
:lines: 91-94
|
||||
:lines: 93-116
|
||||
|
||||
.. rli:: https://raw.githubusercontent.com/PhotonVision/photonvision/refs/heads/main/photonlib-cpp-examples/poseest/src/main/include/Vision.h
|
||||
:language: c++
|
||||
:lines: 79-82
|
||||
:lines: 80-100
|
||||
|
||||
.. rli:: https://raw.githubusercontent.com/PhotonVision/photonvision/refs/heads/main/photonlib-python-examples/poseest/robot.py
|
||||
:language: python
|
||||
:lines: 52-54
|
||||
:lines: 53
|
||||
```
|
||||
|
||||
For Constrained SolvePnP, it's recommended to do the previously mentioned steps, and then feed the pose (if it exists) into `estimateConstrainedSolvepnpPose`, and if the Constrained SolvePnP result is empty, simply feed the seed pose into your drivetrain pose estimator.
|
||||
|
||||
Once you have the `Optional<EstimatedRobotPose>`, you can check to see if there's an actual pose inside, and act accordingly. You should be updating your [drivetrain pose estimator](https://docs.wpilib.org/en/latest/docs/software/advanced-controls/state-space/state-space-pose-estimators.html) with the result from the `PhotonPoseEstimator` every loop using `addVisionMeasurement()`. For Java and C++, the examples pass a method from the drivetrain to a `Vision` object, with the parameter being called `estConsumer`. Python calls the drivetrain directly.
|
||||
You should be updating your [drivetrain pose estimator](https://docs.wpilib.org/en/latest/docs/software/advanced-controls/state-space/state-space-pose-estimators.html) with the result from the `PhotonPoseEstimator` every loop using `addVisionMeasurement()`.
|
||||
|
||||
```{eval-rst}
|
||||
.. tab-set-code::
|
||||
@@ -143,22 +146,7 @@ Once you have the `Optional<EstimatedRobotPose>`, you can check to see if there'
|
||||
|
||||
.. rli:: https://raw.githubusercontent.com/PhotonVision/photonvision/refs/heads/main/photonlib-python-examples/poseest/robot.py
|
||||
:language: python
|
||||
:lines: 56-58
|
||||
```
|
||||
|
||||
```{eval-rst}
|
||||
.. tab-set-code::
|
||||
.. rli:: https://raw.githubusercontent.com/PhotonVision/photonvision/refs/heads/main/photonlib-java-examples/poseest/src/main/java/frc/robot/Vision.java
|
||||
:language: java
|
||||
:lines: 89-115
|
||||
|
||||
.. rli:: https://raw.githubusercontent.com/PhotonVision/photonvision/refs/heads/main/photonlib-cpp-examples/poseest/src/main/include/Vision.h
|
||||
:language: c++
|
||||
:lines: 77-100
|
||||
|
||||
.. rli:: https://raw.githubusercontent.com/PhotonVision/photonvision/refs/heads/main/photonlib-python-examples/poseest/robot.py
|
||||
:language: python
|
||||
:lines: 51-54
|
||||
:lines: 54-57
|
||||
```
|
||||
|
||||
## Complete Examples
|
||||
|
||||
@@ -8,7 +8,7 @@ If you’re not using cameras in 3D mode, calibration is optional, but it can st
|
||||
|
||||
## Print the Calibration Target
|
||||
|
||||
- Downloaded from our [demo site](https://demo.photonvision.org/#/cameras), or directly from your coprocessors cameras tab.
|
||||
- Downloaded from our [demo site](http://photonvision.global/#/cameras), or directly from your coprocessors cameras tab.
|
||||
- Use the ChArUco calibration board:
|
||||
- Board Type: ChAruCo
|
||||
- Tag Family: 4x4
|
||||
|
||||
@@ -29,7 +29,7 @@ Unless otherwise noted in release notes or if updating from the prior years vers
|
||||
Use the [Raspberry Pi Imager](https://www.raspberrypi.com/software/) to flash the image onto the coprocessors microSD card. Select the downloaded `.img.xz` file, select your microSD card, and flash.
|
||||
|
||||
:::{warning}
|
||||
Balena Etcher has been recommended in the past, but should no longer be used due to instability and lack of ongoing support from developers.
|
||||
Balena Etcher can also be used, but historically has had issues such as bootlooping (the system will repeatedly boot and restart) when imaging your device. Use at your own risk.
|
||||
:::
|
||||
|
||||
## Limelight Installation
|
||||
@@ -54,32 +54,3 @@ The Qualcomm Launcher caches files. If you flash multiple times, you may need to
|
||||
:::
|
||||
|
||||
To flash the Rubik Pi 3 coprocessor, it's necessary to use the [Qualcomm Launcher](https://softwarecenter.qualcomm.com/catalog/item/Qualcomm_Launcher). Upload a custom image by selecting the *Custom* option in the launcher. Choose the downloaded PhotonVision `.tar.xz` file and follow the prompts to complete the installation. It is recommended to skip the *Configure Login* process, as PhotonVision will handle the necessary settings.
|
||||
|
||||
### Alternative Flashing Method (advanced users only)
|
||||
|
||||
Follow the specific steps listed below from the [Rubik Pi 3 Docs](https://www.thundercomm.com/rubik-pi-3/en/docs/rubik-pi-3-user-manual/1.0.0-u/Troubleshooting/11.1.flash-over-android/).
|
||||
|
||||
[Step 1](https://www.thundercomm.com/rubik-pi-3/en/docs/rubik-pi-3-user-manual/1.0.0-u/Troubleshooting/11.1.flash-over-android/#1%EF%B8%8F%E2%83%A3-setup-qdl-tool) should be completed once per computer. [Step 2](https://www.thundercomm.com/rubik-pi-3/en/docs/rubik-pi-3-user-manual/1.0.0-u/Troubleshooting/11.1.flash-over-android/#2%EF%B8%8F%E2%83%A3-ufs-provisioning) and [Step 3](https://www.thundercomm.com/rubik-pi-3/en/docs/rubik-pi-3-user-manual/1.0.0-u/Troubleshooting/11.1.flash-over-android/#3%EF%B8%8F%E2%83%A3-flash-renesas-firmware) should be completed once per Rubik Pi 3.
|
||||
|
||||
After completing these steps, unzip your downloaded PhotonVision image to a folder. Navigate to that folder in your terminal or command prompt. After putting your Rubik Pi 3 into EDL mode, run the command below to flash PhotonVision. There is no need to complete any further steps from the Rubik Pi 3 documentation after running this command.
|
||||
|
||||
|
||||
::::{tab-set}
|
||||
:::{tab-item} Ubuntu host
|
||||
```shell
|
||||
qdl --storage ufs prog_firehose_ddr.elf rawprogram*.xml patch*.xml
|
||||
```
|
||||
:::
|
||||
|
||||
:::{tab-item} Windows host
|
||||
```shell
|
||||
QDL.exe prog_firehose_ddr.elf rawprogram0.xml rawprogram1.xml rawprogram2.xml rawprogram3.xml rawprogram4.xml rawprogram5.xml rawprogram6.xml patch1.xml patch2.xml patch3.xml patch4.xml patch5.xml patch6.xml
|
||||
```
|
||||
:::
|
||||
|
||||
:::{tab-item} macOS host
|
||||
```shell
|
||||
qdl prog_firehose_ddr.elf rawprogram*.xml patch*.xml
|
||||
```
|
||||
:::
|
||||
::::
|
||||
|
||||
@@ -4,15 +4,13 @@
|
||||
|
||||
1. **IT IS STRONGLY RECOMMENDED** to use one of the recommended power regulators to prevent vision from cutting out from voltage drops while operating the robot. We recommend wiring the regulator directly to the power header pins or using a locking USB C cable. In any case we recommend hot gluing the connector.
|
||||
|
||||
2. Run an ethernet cable from your coprocessor to your network switch / radio.
|
||||
2. Run an ethernet cable from your Pi to your network switch / radio.
|
||||
|
||||
## Raspberry Pi and Orange Pi
|
||||
|
||||
This diagram shows how to use the recommended regulator to power a Raspberry Pi or Orange Pi.
|
||||
This diagram shows how to use the recommended regulator to power a coprocessor.
|
||||
|
||||
::::{tab-set}
|
||||
|
||||
:::{tab-item} Orange Pi 5 Zinc V USB C
|
||||
:::{tab-item} Orange Pi Zinc V USB C
|
||||
|
||||
```{image} images/OrangePiZincUSBC.png
|
||||
:alt: Wiring the opi5 to the pdp using the Redux Robotics Zinc V and usb c
|
||||
@@ -80,12 +78,6 @@ This diagram shows how to use the recommended regulator to power a Raspberry Pi
|
||||
|
||||
Pigtails can be purchased from many sources we recommend [(USB C)](https://ctr-electronics.com/products/usb-type-c-wire-breakout?_pos=19&_sid=bf06b6a6b&_ss=r) [(Micro USB)](https://ctr-electronics.com/products/usb-micro-power-wire-breakout?pr_prod_strat=e5_desc&pr_rec_id=10bf36ce7&pr_rec_pid=7863771070637&pr_ref_pid=7863771103405&pr_seq=uniform)
|
||||
|
||||
## RUBIK Pi
|
||||
|
||||
The RUBIK Pi has very different power requirements than the Orange Pi (or standard Raspberry Pi). In particular it requires 12V inputs, and has
|
||||
a higher maximum power draw than those coprocessors. [First Rubik](https://first-rubik.github.io/docs/power/) has recommendations for both
|
||||
on-robot and off-robot scenarios.
|
||||
|
||||
## Limelight
|
||||
|
||||
Follow the wiring instructions located in the [Limelight Documentation](https://docs.limelightvision.io/) for your Limelight model.
|
||||
|
||||
@@ -11,8 +11,8 @@ A few issues make up the majority of support requests. Run through this checklis
|
||||
- Even if there's a switch between your laptop and coprocessor, you'll still want a radio or router in the loop somehow.
|
||||
- The FRC radio is the _only_ router we will officially support due to the innumerable variations between routers.
|
||||
- (Raspberry Pi, Orange Pi & Limelight only) have you flashed the correct image, and is it [up to date](https://github.com/PhotonVision/photonvision/releases/latest)?
|
||||
- Is your robot code using a **2026** version of WPILib, and is your coprocessor using the most up to date **2026** release?
|
||||
- 2022, 2023, 2024, 2025, and 2026 versions of either cannot be mix-and-matched!
|
||||
- Is your robot code using a **2025** version of WPILib, and is your coprocessor using the most up to date **2025** release?
|
||||
- 2022, 2023, 2024, and 2025 versions of either cannot be mix-and-matched!
|
||||
- Your PhotonVision version can be checked on the settings tab.
|
||||
- Is your team number correctly set on the settings tab?
|
||||
|
||||
@@ -30,7 +30,7 @@ Please check that:
|
||||
1\. You don't have the NetworkTables Server on (toggleable in the settings tab). Turn this off when doing work on a robot.
|
||||
2\. You have your team number set properly in the settings tab.
|
||||
3\. Your camera name in the `PhotonCamera` constructor matches the name in the UI.
|
||||
4\. You are using the 2026 version of WPILib and RoboRIO image.
|
||||
4\. You are using the 2025 version of WPILib and RoboRIO image.
|
||||
5\. Your robot is on.
|
||||
|
||||
If all of the above are met and you still have issues, feel free to {ref}`contact us <index:contact us>` and provide the following information:
|
||||
|
||||
@@ -138,5 +138,7 @@ docs/contributing/index
|
||||
|
||||
Java <https://javadocs.photonvision.org>
|
||||
|
||||
C++ <https://cppdocs.photonvision.org/>
|
||||
C++ <https://cppdocs.photonvision.org>
|
||||
|
||||
Python <https://pydocs.photonvision.org>
|
||||
```
|
||||
|
||||
8
photon-client/.gitignore
vendored
Normal file
8
photon-client/.gitignore
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
node_modules
|
||||
.DS_Store
|
||||
dist
|
||||
dist-ssr
|
||||
|
||||
# Editor directories and files
|
||||
.idea
|
||||
components.d.ts
|
||||
@@ -8,7 +8,7 @@ export default defineConfigWithVueTs(
|
||||
vueTsConfigs.recommended,
|
||||
skipFormattingConfig,
|
||||
{
|
||||
ignores: ["**/dist/**", "playwright-report"]
|
||||
ignores: ["**/dist/**"]
|
||||
},
|
||||
{
|
||||
//extends: ["js/recommended"],
|
||||
|
||||
@@ -9,20 +9,15 @@
|
||||
"build": "vite build",
|
||||
"build-demo": "vite build --mode demo",
|
||||
"lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix",
|
||||
"format": "prettier --write src/ tests/",
|
||||
"format": "prettier --write src/",
|
||||
"lint-ci": "eslint . --max-warnings 0 --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts",
|
||||
"format-ci": "prettier --check src/",
|
||||
"test": "playwright test",
|
||||
"test-ui": "playwright test --ui",
|
||||
"test-setup": "playwright install --with-deps"
|
||||
|
||||
"format-ci": "prettier --check src/"
|
||||
},
|
||||
"dependencies": {
|
||||
"@fontsource/prompt": "^5.2.6",
|
||||
"@mdi/font": "^7.4.47",
|
||||
"@msgpack/msgpack": "^3.1.2",
|
||||
"axios": "^1.11.0",
|
||||
"echarts": "^6.0.0",
|
||||
"jspdf": "^3.0.1",
|
||||
"pinia": "^3.0.2",
|
||||
"three": "^0.178.0",
|
||||
@@ -33,7 +28,6 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.31.0",
|
||||
"@playwright/test": "^1.56.1",
|
||||
"@types/node": "^22.15.14",
|
||||
"@types/three": "^0.178.0",
|
||||
"@vitejs/plugin-vue": "^6.0.0",
|
||||
|
||||
@@ -1,83 +0,0 @@
|
||||
import { defineConfig, devices } from "@playwright/test";
|
||||
import path from "path";
|
||||
|
||||
/**
|
||||
* Read environment variables from file.
|
||||
* https://github.com/motdotla/dotenv
|
||||
*/
|
||||
// import dotenv from 'dotenv';
|
||||
// import path from 'path';
|
||||
// dotenv.config({ path: path.resolve(__dirname, '.env') });
|
||||
|
||||
/**
|
||||
* See https://playwright.dev/docs/test-configuration.
|
||||
*/
|
||||
export default defineConfig({
|
||||
globalSetup: "./tests/global-setup",
|
||||
testDir: "./tests",
|
||||
/* Run tests in files in parallel */
|
||||
fullyParallel: false,
|
||||
/* Fail the build on CI if you accidentally left test.only in the source code. */
|
||||
forbidOnly: !!process.env.CI,
|
||||
/* Retry on CI only */
|
||||
retries: process.env.CI ? 2 : 0,
|
||||
/* Opt out of parallel tests on CI. */
|
||||
workers: 1,
|
||||
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
|
||||
reporter: "html",
|
||||
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
|
||||
use: {
|
||||
/* Base URL to use in actions like `await page.goto('')`. */
|
||||
baseURL: "http://localhost:5800",
|
||||
|
||||
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
|
||||
trace: "on-first-retry"
|
||||
},
|
||||
|
||||
/* Configure projects for major browsers */
|
||||
projects: [
|
||||
{
|
||||
name: "chromium",
|
||||
use: { ...devices["Desktop Chrome"] }
|
||||
},
|
||||
|
||||
{
|
||||
name: "firefox",
|
||||
use: { ...devices["Desktop Firefox"] }
|
||||
},
|
||||
|
||||
{
|
||||
name: "webkit",
|
||||
use: { ...devices["Desktop Safari"] }
|
||||
},
|
||||
|
||||
/* Test against mobile viewports. */
|
||||
// {
|
||||
// name: 'Mobile Chrome',
|
||||
// use: { ...devices['Pixel 5'] },
|
||||
// },
|
||||
// {
|
||||
// name: 'Mobile Safari',
|
||||
// use: { ...devices['iPhone 12'] },
|
||||
// },
|
||||
|
||||
/* Test against branded browsers. */
|
||||
{
|
||||
name: "Microsoft Edge",
|
||||
use: { ...devices["Desktop Edge"], channel: "msedge" }
|
||||
}
|
||||
// {
|
||||
// name: 'Google Chrome',
|
||||
// use: { ...devices['Desktop Chrome'], channel: 'chrome' },
|
||||
// },
|
||||
],
|
||||
|
||||
/* Run your local dev server before starting the tests */
|
||||
webServer: {
|
||||
command: process.platform == "win32" ? "" : "./" + "gradlew run",
|
||||
url: "http://localhost:5800",
|
||||
timeout: 300 * 1000,
|
||||
reuseExistingServer: !process.env.CI,
|
||||
cwd: path.normalize("../")
|
||||
}
|
||||
});
|
||||
65
photon-client/pnpm-lock.yaml
generated
65
photon-client/pnpm-lock.yaml
generated
@@ -20,9 +20,6 @@ importers:
|
||||
axios:
|
||||
specifier: ^1.11.0
|
||||
version: 1.11.0
|
||||
echarts:
|
||||
specifier: ^6.0.0
|
||||
version: 6.0.0
|
||||
jspdf:
|
||||
specifier: ^3.0.1
|
||||
version: 3.0.1
|
||||
@@ -48,9 +45,6 @@ importers:
|
||||
'@eslint/js':
|
||||
specifier: ^9.31.0
|
||||
version: 9.31.0
|
||||
'@playwright/test':
|
||||
specifier: ^1.56.1
|
||||
version: 1.56.1
|
||||
'@types/node':
|
||||
specifier: ^22.15.14
|
||||
version: 22.15.14
|
||||
@@ -436,11 +430,6 @@ packages:
|
||||
resolution: {integrity: sha512-ROFF39F6ZrnzSUEmQQZUar0Jt4xVoP9WnDRdWwF4NNcXs3xBTLgBUDoOwW141y1jP+S8nahIbdxbFC7IShw9Iw==}
|
||||
engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0}
|
||||
|
||||
'@playwright/test@1.56.1':
|
||||
resolution: {integrity: sha512-vSMYtL/zOcFpvJCW71Q/OEGQb7KYBPAdKh35WNSkaZA75JlAO8ED8UN6GUNTm3drWomcbcqRPFqQbLae8yBTdg==}
|
||||
engines: {node: '>=18'}
|
||||
hasBin: true
|
||||
|
||||
'@rolldown/pluginutils@1.0.0-beta.19':
|
||||
resolution: {integrity: sha512-3FL3mnMbPu0muGOCaKAhhFEYmqv9eTfPSJRJmANrCwtgK8VuxpsZDGK+m0LYAGoyO8+0j5uRe4PeyPDK1yA/hA==}
|
||||
|
||||
@@ -851,9 +840,6 @@ packages:
|
||||
resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
echarts@6.0.0:
|
||||
resolution: {integrity: sha512-Tte/grDQRiETQP4xz3iZWSvoHrkCQtwqd6hs+mifXcjrCuo2iKWbajFObuLJVBlDIJlOzgQPd1hsaKt/3+OMkQ==}
|
||||
|
||||
entities@4.5.0:
|
||||
resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==}
|
||||
engines: {node: '>=0.12'}
|
||||
@@ -1033,11 +1019,6 @@ packages:
|
||||
resolution: {integrity: sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==}
|
||||
engines: {node: '>= 6'}
|
||||
|
||||
fsevents@2.3.2:
|
||||
resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==}
|
||||
engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
|
||||
os: [darwin]
|
||||
|
||||
fsevents@2.3.3:
|
||||
resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==}
|
||||
engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
|
||||
@@ -1266,16 +1247,6 @@ packages:
|
||||
typescript:
|
||||
optional: true
|
||||
|
||||
playwright-core@1.56.1:
|
||||
resolution: {integrity: sha512-hutraynyn31F+Bifme+Ps9Vq59hKuUCz7H1kDOcBs+2oGguKkWTU50bBWrtz34OUWmIwpBTWDxaRPXrIXkgvmQ==}
|
||||
engines: {node: '>=18'}
|
||||
hasBin: true
|
||||
|
||||
playwright@1.56.1:
|
||||
resolution: {integrity: sha512-aFi5B0WovBHTEvpM3DzXTUaeN6eN0qWnTkKx4NQaH4Wvcmc153PdaY2UBdSYKaGYw+UyWXSVyxDUg5DoPEttjw==}
|
||||
engines: {node: '>=18'}
|
||||
hasBin: true
|
||||
|
||||
postcss-selector-parser@6.1.2:
|
||||
resolution: {integrity: sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==}
|
||||
engines: {node: '>=4'}
|
||||
@@ -1414,9 +1385,6 @@ packages:
|
||||
peerDependencies:
|
||||
typescript: '>=4.8.4'
|
||||
|
||||
tslib@2.3.0:
|
||||
resolution: {integrity: sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==}
|
||||
|
||||
tslib@2.8.1:
|
||||
resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==}
|
||||
|
||||
@@ -1557,9 +1525,6 @@ packages:
|
||||
resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
zrender@6.0.0:
|
||||
resolution: {integrity: sha512-41dFXEEXuJpNecuUQq6JlbybmnHaqqpGlbH1yxnA5V9MMP4SbohSVZsJIwz+zdjQXSSlR1Vc34EgH1zxyTDvhg==}
|
||||
|
||||
snapshots:
|
||||
|
||||
'@babel/helper-string-parser@7.27.1': {}
|
||||
@@ -1794,10 +1759,6 @@ snapshots:
|
||||
|
||||
'@pkgr/core@0.2.4': {}
|
||||
|
||||
'@playwright/test@1.56.1':
|
||||
dependencies:
|
||||
playwright: 1.56.1
|
||||
|
||||
'@rolldown/pluginutils@1.0.0-beta.19': {}
|
||||
|
||||
'@rollup/rollup-android-arm-eabi@4.40.2':
|
||||
@@ -2074,7 +2035,7 @@ snapshots:
|
||||
typescript: 5.8.3
|
||||
vue: 3.5.13(typescript@5.8.3)
|
||||
|
||||
'@vuetify/loader-shared@2.1.0(vue@3.5.13(typescript@5.8.3))(vuetify@3.8.3(typescript@5.8.3)(vite-plugin-vuetify@2.1.1)(vue@3.5.13(typescript@5.8.3)))':
|
||||
'@vuetify/loader-shared@2.1.0(vue@3.5.13(typescript@5.8.3))(vuetify@3.8.3)':
|
||||
dependencies:
|
||||
upath: 2.0.1
|
||||
vue: 3.5.13(typescript@5.8.3)
|
||||
@@ -2227,11 +2188,6 @@ snapshots:
|
||||
es-errors: 1.3.0
|
||||
gopd: 1.2.0
|
||||
|
||||
echarts@6.0.0:
|
||||
dependencies:
|
||||
tslib: 2.3.0
|
||||
zrender: 6.0.0
|
||||
|
||||
entities@4.5.0: {}
|
||||
|
||||
es-define-property@1.0.1: {}
|
||||
@@ -2443,9 +2399,6 @@ snapshots:
|
||||
hasown: 2.0.2
|
||||
mime-types: 2.1.35
|
||||
|
||||
fsevents@2.3.2:
|
||||
optional: true
|
||||
|
||||
fsevents@2.3.3:
|
||||
optional: true
|
||||
|
||||
@@ -2652,14 +2605,6 @@ snapshots:
|
||||
optionalDependencies:
|
||||
typescript: 5.8.3
|
||||
|
||||
playwright-core@1.56.1: {}
|
||||
|
||||
playwright@1.56.1:
|
||||
dependencies:
|
||||
playwright-core: 1.56.1
|
||||
optionalDependencies:
|
||||
fsevents: 2.3.2
|
||||
|
||||
postcss-selector-parser@6.1.2:
|
||||
dependencies:
|
||||
cssesc: 3.0.0
|
||||
@@ -2801,8 +2746,6 @@ snapshots:
|
||||
dependencies:
|
||||
typescript: 5.8.3
|
||||
|
||||
tslib@2.3.0: {}
|
||||
|
||||
tslib@2.8.1: {}
|
||||
|
||||
type-check@0.4.0:
|
||||
@@ -2838,7 +2781,7 @@ snapshots:
|
||||
|
||||
vite-plugin-vuetify@2.1.1(vite@7.0.5(@types/node@22.15.14)(sass@1.89.2))(vue@3.5.13(typescript@5.8.3))(vuetify@3.8.3):
|
||||
dependencies:
|
||||
'@vuetify/loader-shared': 2.1.0(vue@3.5.13(typescript@5.8.3))(vuetify@3.8.3(typescript@5.8.3)(vite-plugin-vuetify@2.1.1)(vue@3.5.13(typescript@5.8.3)))
|
||||
'@vuetify/loader-shared': 2.1.0(vue@3.5.13(typescript@5.8.3))(vuetify@3.8.3)
|
||||
debug: 4.4.0
|
||||
upath: 2.0.1
|
||||
vite: 7.0.5(@types/node@22.15.14)(sass@1.89.2)
|
||||
@@ -2908,7 +2851,3 @@ snapshots:
|
||||
xml-name-validator@4.0.0: {}
|
||||
|
||||
yocto-queue@0.1.0: {}
|
||||
|
||||
zrender@6.0.0:
|
||||
dependencies:
|
||||
tslib: 2.3.0
|
||||
|
||||
@@ -8,10 +8,8 @@ import { onBeforeUnmount, onMounted, watchEffect } from "vue";
|
||||
const {
|
||||
ArrowHelper,
|
||||
BoxGeometry,
|
||||
CameraHelper,
|
||||
Color,
|
||||
ConeGeometry,
|
||||
Group,
|
||||
Mesh,
|
||||
MeshNormalMaterial,
|
||||
PerspectiveCamera,
|
||||
@@ -22,18 +20,6 @@ const {
|
||||
} = await import("three");
|
||||
const { TrackballControls } = await import("three/examples/jsm/controls/TrackballControls");
|
||||
|
||||
import { useCameraSettingsStore } from "@/stores/settings/CameraSettingsStore";
|
||||
import { createPerspectiveCamera } from "@/lib/ThreeUtils";
|
||||
import { useTheme } from "vuetify";
|
||||
|
||||
const theme = useTheme();
|
||||
|
||||
const calibrationCoeffs = useCameraSettingsStore().getCalibrationCoeffs(
|
||||
useCameraSettingsStore().currentCameraSettings.validVideoFormats[
|
||||
useCameraSettingsStore().currentPipelineSettings.cameraVideoModeIndex
|
||||
].resolution
|
||||
);
|
||||
|
||||
const props = defineProps<{
|
||||
targets: PhotonTarget[];
|
||||
}>();
|
||||
@@ -46,18 +32,15 @@ let controls: TrackballControls | undefined;
|
||||
let previousTargets: Object3D[] = [];
|
||||
const drawTargets = (targets: PhotonTarget[]) => {
|
||||
// Check here, since if we check in watchEffect this never gets called
|
||||
if (!scene || !camera || !renderer || !controls) {
|
||||
if (scene === undefined || camera === undefined || renderer === undefined || controls === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (theme.global.name.value === "LightTheme") scene.background = new Color(0xa9a9a9);
|
||||
else scene.background = new Color(0x000000);
|
||||
|
||||
scene.remove(...previousTargets);
|
||||
previousTargets = [];
|
||||
|
||||
targets.forEach((target) => {
|
||||
if (!target.pose) return;
|
||||
if (target.pose === undefined) return;
|
||||
|
||||
const geometry = new BoxGeometry(0.3 / 5, 0.2, 0.2);
|
||||
const material = new MeshNormalMaterial();
|
||||
@@ -87,18 +70,6 @@ const drawTargets = (targets: PhotonTarget[]) => {
|
||||
previousTargets.push(arrow);
|
||||
});
|
||||
|
||||
if (calibrationCoeffs) {
|
||||
// And show camera frustum
|
||||
const calibCamera = createPerspectiveCamera(calibrationCoeffs.resolution, calibrationCoeffs.cameraIntrinsics, 10);
|
||||
const helper = new CameraHelper(calibCamera);
|
||||
const helperGroup = new Group();
|
||||
helperGroup.add(helper);
|
||||
// Flip to +Z forward
|
||||
helperGroup.rotateX(-Math.PI / 2.0);
|
||||
helperGroup.rotateY(-Math.PI / 2.0);
|
||||
previousTargets.push(helperGroup);
|
||||
}
|
||||
|
||||
if (previousTargets.length > 0) {
|
||||
scene.add(...previousTargets);
|
||||
}
|
||||
@@ -107,7 +78,7 @@ const onWindowResize = () => {
|
||||
const container = document.getElementById("container");
|
||||
const canvas = document.getElementById("view");
|
||||
|
||||
if (!container || !canvas || !camera || !renderer) {
|
||||
if (container === null || canvas === null || camera === undefined || renderer === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -118,7 +89,7 @@ const onWindowResize = () => {
|
||||
renderer.setSize(canvas.clientWidth, canvas.clientHeight);
|
||||
};
|
||||
const resetCamFirstPerson = () => {
|
||||
if (!scene || !camera || !controls) {
|
||||
if (scene === undefined || camera === undefined || controls === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -132,7 +103,7 @@ const resetCamFirstPerson = () => {
|
||||
}
|
||||
};
|
||||
const resetCamThirdPerson = () => {
|
||||
if (!scene || !camera || !controls) {
|
||||
if (scene === undefined || camera === undefined || controls === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -151,11 +122,10 @@ onMounted(async () => {
|
||||
camera = new PerspectiveCamera(75, 800 / 800, 0.1, 1000);
|
||||
|
||||
const canvas = document.getElementById("view");
|
||||
if (!canvas) return;
|
||||
if (canvas === null) return;
|
||||
renderer = new WebGLRenderer({ canvas: canvas });
|
||||
|
||||
if (theme.global.name.value === "LightTheme") scene.background = new Color(0xa9a9a9);
|
||||
else scene.background = new Color(0x000000);
|
||||
scene.background = new Color(0xa9a9a9);
|
||||
|
||||
onWindowResize();
|
||||
window.addEventListener("resize", onWindowResize);
|
||||
@@ -199,7 +169,7 @@ onMounted(async () => {
|
||||
controls.update();
|
||||
|
||||
const animate = () => {
|
||||
if (!scene || !camera || !renderer || !controls) {
|
||||
if (scene === undefined || camera === undefined || renderer === undefined || controls === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -222,31 +192,18 @@ watchEffect(() => {
|
||||
|
||||
<template>
|
||||
<div id="container" style="width: 100%">
|
||||
<div class="d-flex flex-wrap pt-0 pb-2">
|
||||
<v-col cols="12" md="6" class="pl-0">
|
||||
<v-card-title class="pa-0"> Target Visualization </v-card-title>
|
||||
<v-row>
|
||||
<v-col align-self="stretch" style="display: flex; justify-content: center">
|
||||
<canvas id="view" />
|
||||
</v-col>
|
||||
<v-col cols="6" md="3" class="d-flex align-center pt-0 pt-md-3 pl-6 pl-md-3">
|
||||
<v-btn
|
||||
style="width: 100%"
|
||||
color="buttonActive"
|
||||
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
|
||||
@click="resetCamFirstPerson"
|
||||
>
|
||||
First Person
|
||||
</v-btn>
|
||||
</v-row>
|
||||
<v-row style="margin-bottom: 24px">
|
||||
<v-col style="display: flex; justify-content: center">
|
||||
<v-btn color="secondary" @click="resetCamFirstPerson"> First Person </v-btn>
|
||||
</v-col>
|
||||
<v-col cols="6" md="3" class="d-flex align-center pt-0 pt-md-3 pr-0">
|
||||
<v-btn
|
||||
style="width: 100%"
|
||||
color="buttonActive"
|
||||
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
|
||||
@click="resetCamThirdPerson"
|
||||
>
|
||||
Third Person
|
||||
</v-btn>
|
||||
<v-col style="display: flex; justify-content: center">
|
||||
<v-btn color="secondary" @click="resetCamThirdPerson"> Third Person </v-btn>
|
||||
</v-col>
|
||||
</div>
|
||||
<canvas id="view" class="w-100" />
|
||||
</v-row>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -1,371 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import { onBeforeUnmount, onMounted, ref, watch, watchEffect, type Ref } from "vue";
|
||||
const {
|
||||
AmbientLight,
|
||||
AxesHelper,
|
||||
BoxGeometry,
|
||||
CameraHelper,
|
||||
Color,
|
||||
ConeGeometry,
|
||||
Group,
|
||||
Mesh,
|
||||
MeshNormalMaterial,
|
||||
MeshPhongMaterial,
|
||||
PerspectiveCamera,
|
||||
Scene,
|
||||
SphereGeometry,
|
||||
WebGLRenderer
|
||||
} = await import("three");
|
||||
const { TrackballControls } = await import("three/examples/jsm/controls/TrackballControls");
|
||||
import type { BoardObservation, CameraCalibrationResult } from "@/types/SettingTypes";
|
||||
import axios from "axios";
|
||||
import { useCameraSettingsStore } from "@/stores/settings/CameraSettingsStore";
|
||||
import { useTheme } from "vuetify";
|
||||
import { createPerspectiveCamera } from "@/lib/ThreeUtils";
|
||||
|
||||
const theme = useTheme();
|
||||
|
||||
const props = defineProps<{
|
||||
cameraUniqueName: string;
|
||||
resolution: { width: number; height: number };
|
||||
title: string;
|
||||
}>();
|
||||
|
||||
let scene: Scene | undefined;
|
||||
let camera: PerspectiveCamera | undefined;
|
||||
let renderer: WebGLRenderer | undefined;
|
||||
let controls: TrackballControls | undefined;
|
||||
|
||||
const createChessboard = (obs: BoardObservation, cal: CameraCalibrationResult): Group => {
|
||||
const group = new Group();
|
||||
|
||||
if (obs.locationInImageSpace.length === 0) return group;
|
||||
|
||||
// Add corner spheres
|
||||
obs.locationInObjectSpace.forEach((corner, idx) => {
|
||||
if (corner.x < 0 || corner.y < 0) return;
|
||||
|
||||
const isOutlier = !obs.cornersUsed[idx];
|
||||
|
||||
const color = isOutlier ? 0xff3333 : 0x33ff33;
|
||||
|
||||
const sphereGeom = new SphereGeometry(cal.calobjectSpacing / 8, 8, 8);
|
||||
const sphereMat = new MeshPhongMaterial({
|
||||
color: color,
|
||||
opacity: 1,
|
||||
transparent: !isOutlier
|
||||
});
|
||||
const sphere = new Mesh(sphereGeom, sphereMat);
|
||||
sphere.position.set(corner.x, corner.y, corner.z);
|
||||
group.add(sphere);
|
||||
});
|
||||
|
||||
return group;
|
||||
};
|
||||
|
||||
let previousTargets: Object3D[] = [];
|
||||
let baseAspect: number | undefined;
|
||||
const drawCalibration = (cal: CameraCalibrationResult | null) => {
|
||||
// Check here, since if we check in watchEffect this never gets called
|
||||
if (!cal || !scene || !camera || !renderer || !controls) {
|
||||
return;
|
||||
}
|
||||
|
||||
scene.remove(...previousTargets);
|
||||
previousTargets = [];
|
||||
|
||||
// Draw all chessboards with transparency
|
||||
cal.observations.forEach((obs) => {
|
||||
const pose = obs.optimisedCameraToObject;
|
||||
|
||||
// Create chessboard
|
||||
const board = createChessboard(obs, cal);
|
||||
board.userData.isCalibrationObject = true;
|
||||
|
||||
// Apply transform from camera to chessboard
|
||||
const pos = pose.translation;
|
||||
board.position.set(pos.x, pos.y, pos.z);
|
||||
|
||||
if (pose.rotation.quaternion) {
|
||||
const q = pose.rotation.quaternion;
|
||||
board.quaternion.set(q.X, q.Y, q.Z, q.W);
|
||||
}
|
||||
|
||||
previousTargets.push(board);
|
||||
});
|
||||
|
||||
// And show camera frustum
|
||||
const calibCamera = createPerspectiveCamera(props.resolution, cal.cameraIntrinsics);
|
||||
const helper = new CameraHelper(calibCamera);
|
||||
|
||||
// Flip to +Z forward
|
||||
const helperGroup = new Group();
|
||||
helperGroup.add(helper);
|
||||
helperGroup.rotateY(Math.PI);
|
||||
|
||||
previousTargets.push(helperGroup);
|
||||
|
||||
if (previousTargets.length > 0) {
|
||||
scene.add(...previousTargets);
|
||||
}
|
||||
};
|
||||
|
||||
const calibrationData: Ref<CameraCalibrationResult | null> = ref(null);
|
||||
const isLoading: Ref<boolean> = ref(true);
|
||||
const error: Ref<string | null> = ref(null);
|
||||
|
||||
const fetchCalibrationData = async () => {
|
||||
isLoading.value = true;
|
||||
error.value = null;
|
||||
|
||||
try {
|
||||
const response = await axios.get("/settings/camera/getCalibration", {
|
||||
params: {
|
||||
cameraUniqueName: props.cameraUniqueName,
|
||||
width: props.resolution.width,
|
||||
height: props.resolution.height
|
||||
}
|
||||
});
|
||||
calibrationData.value = response.data;
|
||||
} catch (err) {
|
||||
console.error("Failed to fetch calibration data:", err);
|
||||
error.value = "Failed to load calibration data";
|
||||
} finally {
|
||||
isLoading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const onWindowResize = () => {
|
||||
const container = document.getElementById("container");
|
||||
const canvas = document.getElementById("view");
|
||||
|
||||
if (!container || !canvas || !camera || !renderer) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Compute a concrete width from the container and derive height from a
|
||||
// stable base aspect ratio (calculated on mount) to avoid feedback loops
|
||||
// where updating canvas size changes container size while resizing
|
||||
const width = Math.max(1, Math.floor(container.clientWidth));
|
||||
let height: number;
|
||||
if (baseAspect && baseAspect > 0) {
|
||||
height = Math.max(1, Math.floor(width / baseAspect));
|
||||
} else {
|
||||
height = Math.max(1, Math.floor(container.clientHeight));
|
||||
}
|
||||
|
||||
// Use updateStyle=false so Three.js does not write to canvas style,
|
||||
// which can affect layout and re-trigger resize events
|
||||
renderer.setSize(width, height, false);
|
||||
camera.aspect = width / height;
|
||||
camera.updateProjectionMatrix();
|
||||
};
|
||||
|
||||
const resetCamFirstPerson = () => {
|
||||
if (!scene || !camera || !controls) {
|
||||
return;
|
||||
}
|
||||
|
||||
controls.reset();
|
||||
camera.position.set(0, 0, 0.05);
|
||||
camera.up.set(0, -1, 0);
|
||||
controls.target.set(0.0, 0.0, 1.0);
|
||||
controls.update();
|
||||
if (previousTargets.length > 0) {
|
||||
scene.add(...previousTargets);
|
||||
}
|
||||
};
|
||||
|
||||
const resetCamThirdPerson = () => {
|
||||
if (!scene || !camera || !controls) {
|
||||
return;
|
||||
}
|
||||
|
||||
controls.reset();
|
||||
camera.position.set(-0.3, -0.2, -0.3);
|
||||
camera.up.set(0, -1, 0);
|
||||
controls.target.set(0.0, 0.0, 0.4);
|
||||
controls.update();
|
||||
if (previousTargets.length > 0) {
|
||||
scene.add(...previousTargets);
|
||||
}
|
||||
};
|
||||
|
||||
let animationFrameId: number | null = null;
|
||||
|
||||
onMounted(async () => {
|
||||
// Grab data first off
|
||||
fetchCalibrationData();
|
||||
|
||||
scene = new Scene();
|
||||
camera = new PerspectiveCamera(75, 800 / 800, 0.1, 1000);
|
||||
|
||||
const canvas = document.getElementById("view");
|
||||
if (!canvas) return;
|
||||
renderer = new WebGLRenderer({ canvas: canvas });
|
||||
|
||||
// Add lights
|
||||
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);
|
||||
|
||||
// Initialize a stable aspect ratio so subsequent resize events derive
|
||||
// height from width, avoiding layout feedback during continuous resizing
|
||||
try {
|
||||
const initWidth = Math.max(1, Math.floor(document.getElementById("container")?.clientWidth || 1));
|
||||
const initHeight = Math.max(1, Math.floor(document.getElementById("container")?.clientHeight || 1));
|
||||
baseAspect = initWidth / Math.max(1, initHeight);
|
||||
} catch {
|
||||
baseAspect = undefined;
|
||||
}
|
||||
|
||||
onWindowResize();
|
||||
window.addEventListener("resize", onWindowResize);
|
||||
|
||||
const referenceFrameCues: Object3D[] = [];
|
||||
|
||||
// Draw the reference frame
|
||||
referenceFrameCues.push(new AxesHelper(0.3));
|
||||
|
||||
// Draw the Camera Body
|
||||
const camSize = 0.04;
|
||||
const camBodyGeometry = new BoxGeometry(camSize, camSize, camSize);
|
||||
const camLensGeometry = new ConeGeometry(camSize * 0.4, camSize * 0.8, 30);
|
||||
const camMaterial = new MeshNormalMaterial();
|
||||
const camBody = new Mesh(camBodyGeometry, camMaterial);
|
||||
const camLens = new Mesh(camLensGeometry, camMaterial);
|
||||
camBody.position.set(0, 0, 0);
|
||||
camLens.rotateX(-Math.PI / 2);
|
||||
camLens.position.set(0, 0, camSize * 0.8);
|
||||
referenceFrameCues.push(camBody);
|
||||
referenceFrameCues.push(camLens);
|
||||
|
||||
controls = new TrackballControls(camera, renderer.domElement);
|
||||
controls.rotateSpeed = 1.0;
|
||||
controls.zoomSpeed = 1.2;
|
||||
controls.panSpeed = 0.8;
|
||||
controls.noZoom = false;
|
||||
controls.noPan = false;
|
||||
controls.staticMoving = true;
|
||||
controls.dynamicDampingFactor = 0.3;
|
||||
|
||||
scene.add(...referenceFrameCues);
|
||||
resetCamThirdPerson();
|
||||
|
||||
controls.update();
|
||||
|
||||
const animate = () => {
|
||||
if (!scene || !camera || !renderer || !controls) {
|
||||
return;
|
||||
}
|
||||
|
||||
animationFrameId = requestAnimationFrame(animate);
|
||||
|
||||
controls.update();
|
||||
renderer.render(scene, camera);
|
||||
};
|
||||
|
||||
animate();
|
||||
});
|
||||
|
||||
const cleanup = () => {
|
||||
window.removeEventListener("resize", onWindowResize);
|
||||
|
||||
if (animationFrameId) {
|
||||
cancelAnimationFrame(animationFrameId);
|
||||
}
|
||||
|
||||
if (controls) {
|
||||
controls.dispose();
|
||||
}
|
||||
|
||||
if (renderer) {
|
||||
renderer.dispose();
|
||||
renderer.forceContextLoss();
|
||||
}
|
||||
|
||||
if (scene) {
|
||||
scene.traverse((object) => {
|
||||
if (object instanceof Mesh) {
|
||||
object.geometry?.dispose();
|
||||
if (object.material) {
|
||||
if (Array.isArray(object.material)) {
|
||||
object.material.forEach((material) => material.dispose());
|
||||
} else {
|
||||
object.material.dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
scene = undefined;
|
||||
camera = undefined;
|
||||
renderer = undefined;
|
||||
controls = undefined;
|
||||
previousTargets = [];
|
||||
};
|
||||
|
||||
onBeforeUnmount(cleanup);
|
||||
|
||||
// If hot-reloading, cleanup on hot reload
|
||||
if (import.meta.hot) {
|
||||
import.meta.hot.dispose(() => {
|
||||
cleanup();
|
||||
});
|
||||
}
|
||||
|
||||
watchEffect(() => {
|
||||
drawCalibration(calibrationData.value);
|
||||
});
|
||||
|
||||
watch(
|
||||
() => [
|
||||
props.cameraUniqueName,
|
||||
props.resolution.width,
|
||||
props.resolution.height,
|
||||
useCameraSettingsStore().getCalibrationCoeffs(props.resolution)
|
||||
],
|
||||
() => {
|
||||
console.log("Camera or resolution changed, refetching calibration");
|
||||
fetchCalibrationData();
|
||||
}
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div style="width: 100%; height: 100%" class="d-flex flex-column">
|
||||
<div class="d-flex flex-wrap pt-0 pb-2">
|
||||
<v-col cols="12" md="6" class="pl-0">
|
||||
<v-card-title class="pa-0">
|
||||
{{ props.title }}
|
||||
</v-card-title>
|
||||
</v-col>
|
||||
<v-col cols="6" md="3" class="d-flex align-center pt-0 pt-md-3 pl-6 pl-md-3">
|
||||
<v-btn
|
||||
style="width: 100%"
|
||||
color="buttonActive"
|
||||
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
|
||||
@click="resetCamFirstPerson"
|
||||
>
|
||||
First Person
|
||||
</v-btn>
|
||||
</v-col>
|
||||
<v-col cols="6" md="3" class="d-flex align-center pt-0 pt-md-3 pr-0">
|
||||
<v-btn
|
||||
style="width: 100%"
|
||||
color="buttonActive"
|
||||
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
|
||||
@click="resetCamThirdPerson"
|
||||
>
|
||||
Third Person
|
||||
</v-btn>
|
||||
</v-col>
|
||||
</div>
|
||||
<div id="container" style="flex: 1 1 auto">
|
||||
<canvas id="view" class="w-100 h-100" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -25,7 +25,8 @@ const renderCompact = computed<boolean>(() => compact.value || !mdAndUp.value);
|
||||
<template>
|
||||
<v-navigation-drawer permanent :rail="renderCompact" color="sidebar">
|
||||
<v-list nav color="primary">
|
||||
<v-list-item class="pr-0 pl-0" style="display: flex; justify-content: center">
|
||||
<!-- List item for the heading; note that there are some tricks in setting padding and image width make things look right -->
|
||||
<v-list-item :class="renderCompact ? 'pr-0 pl-0' : ''" style="display: flex; justify-content: center">
|
||||
<template #prepend>
|
||||
<img v-if="!renderCompact" class="logo" src="@/assets/images/logoLarge.svg" alt="large logo" />
|
||||
<img v-else class="logo" src="@/assets/images/logoSmallTransparent.svg" alt="small logo" />
|
||||
|
||||
@@ -38,8 +38,7 @@ const getUniqueVideoFormatsByResolution = (): VideoFormat[] => {
|
||||
if (!skip) {
|
||||
const calib = useCameraSettingsStore().getCalibrationCoeffs(format.resolution);
|
||||
if (calib !== undefined) {
|
||||
// Mean overall reprojection error
|
||||
// Calculated as average of each observation's mean error
|
||||
// For each error, square it, sum the squares, and divide by total points N
|
||||
if (calib.meanErrors.length)
|
||||
format.mean = calib.meanErrors.reduce((a, b) => a + b, 0) / calib.meanErrors.length;
|
||||
else format.mean = NaN;
|
||||
@@ -250,31 +249,27 @@ const setSelectedVideoFormat = (format: VideoFormat) => {
|
||||
<th>Horizontal FOV</th>
|
||||
<th>Vertical FOV</th>
|
||||
<th>Diagonal FOV</th>
|
||||
<th>Info</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody style="cursor: pointer">
|
||||
<v-tooltip
|
||||
v-for="(value, index) in getUniqueVideoFormatsByResolution()"
|
||||
:key="index"
|
||||
transition=""
|
||||
location="bottom"
|
||||
:open-delay="100"
|
||||
>
|
||||
<template #activator="{ props }">
|
||||
<tr :key="index" v-bind="props" @click="setSelectedVideoFormat(value)">
|
||||
<td>{{ getResolutionString(value.resolution) }}</td>
|
||||
<td>
|
||||
{{
|
||||
value.mean !== undefined ? (isNaN(value.mean) ? "Unknown" : value.mean.toFixed(2) + "px") : "-"
|
||||
}}
|
||||
<tr v-for="(value, index) in getUniqueVideoFormatsByResolution()" :key="index">
|
||||
<td>{{ getResolutionString(value.resolution) }}</td>
|
||||
<td>
|
||||
{{ value.mean !== undefined ? (isNaN(value.mean) ? "Unknown" : value.mean.toFixed(2) + "px") : "-" }}
|
||||
</td>
|
||||
<td>{{ value.horizontalFOV !== undefined ? value.horizontalFOV.toFixed(2) + "°" : "-" }}</td>
|
||||
<td>{{ value.verticalFOV !== undefined ? value.verticalFOV.toFixed(2) + "°" : "-" }}</td>
|
||||
<td>{{ value.diagonalFOV !== undefined ? value.diagonalFOV.toFixed(2) + "°" : "-" }}</td>
|
||||
<v-tooltip location="bottom">
|
||||
<template #activator="{ props }">
|
||||
<td v-bind="props" @click="setSelectedVideoFormat(value)">
|
||||
<v-icon size="small" color="primary">mdi-information</v-icon>
|
||||
</td>
|
||||
<td>{{ value.horizontalFOV !== undefined ? value.horizontalFOV.toFixed(2) + "°" : "-" }}</td>
|
||||
<td>{{ value.verticalFOV !== undefined ? value.verticalFOV.toFixed(2) + "°" : "-" }}</td>
|
||||
<td>{{ value.diagonalFOV !== undefined ? value.diagonalFOV.toFixed(2) + "°" : "-" }}</td>
|
||||
</tr>
|
||||
</template>
|
||||
<span>View calibration information</span>
|
||||
</v-tooltip>
|
||||
</template>
|
||||
<span>View calibration information</span>
|
||||
</v-tooltip>
|
||||
</tr>
|
||||
</tbody>
|
||||
</v-table>
|
||||
</v-card-text>
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
<script setup lang="ts">
|
||||
import PhotonCalibrationVisualizer from "@/components/app/photon-calibration-visualizer.vue";
|
||||
import type { CameraCalibrationResult, VideoFormat } from "@/types/SettingTypes";
|
||||
import { useCameraSettingsStore } from "@/stores/settings/CameraSettingsStore";
|
||||
import { useStateStore } from "@/stores/StateStore";
|
||||
@@ -9,6 +8,7 @@ import { useTheme } from "vuetify";
|
||||
import PvDeleteModal from "@/components/common/pv-delete-modal.vue";
|
||||
|
||||
const theme = useTheme();
|
||||
|
||||
const props = defineProps<{
|
||||
videoFormat: VideoFormat;
|
||||
}>();
|
||||
@@ -79,10 +79,8 @@ const importCalibration = async () => {
|
||||
};
|
||||
|
||||
interface ObservationDetails {
|
||||
index: number;
|
||||
mean: number;
|
||||
numOutliers: number;
|
||||
numMissing: number;
|
||||
index: number;
|
||||
}
|
||||
|
||||
const currentCalibrationCoeffs = computed<CameraCalibrationResult | undefined>(() =>
|
||||
@@ -94,9 +92,7 @@ const getObservationDetails = (): ObservationDetails[] | undefined => {
|
||||
|
||||
return coefficients?.meanErrors.map((m, i) => ({
|
||||
index: i,
|
||||
mean: parseFloat(m.toFixed(2)),
|
||||
numOutliers: coefficients.numOutliers[i],
|
||||
numMissing: coefficients.numMissing[i]
|
||||
mean: parseFloat(m.toFixed(2))
|
||||
}));
|
||||
};
|
||||
|
||||
@@ -105,236 +101,213 @@ const exportCalibrationURL = computed<string>(() =>
|
||||
);
|
||||
const calibrationImageURL = (index: number) =>
|
||||
useCameraSettingsStore().getCalImageUrl(inject<string>("backendHost") as string, props.videoFormat.resolution, index);
|
||||
|
||||
const tab = ref("details");
|
||||
const viewingImg = ref(0);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<v-card color="surface" dark>
|
||||
<v-card-title class="pb-2">
|
||||
<div class="d-flex flex-wrap">
|
||||
<v-col cols="12" md="6" class="pa-0">
|
||||
<v-card-title class="pa-0"> Calibration Details </v-card-title>
|
||||
</v-col>
|
||||
<v-col cols="6" md="3" class="d-flex align-center pt-0 pb-0 pl-0">
|
||||
<div class="d-flex flex-wrap pt-2 pl-2 pr-2 align-center">
|
||||
<v-col cols="12" md="6">
|
||||
<v-card-title class="pa-0"> Calibration Details </v-card-title>
|
||||
</v-col>
|
||||
<v-col cols="12" md="6" class="d-flex align-center pt-0 pt-md-3">
|
||||
<v-btn
|
||||
color="buttonPassive"
|
||||
class="mr-2"
|
||||
style="flex: 1"
|
||||
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
|
||||
@click="openUploadPhotonCalibJsonPrompt"
|
||||
>
|
||||
<v-icon start size="large">mdi-import</v-icon>
|
||||
<span>Import</span>
|
||||
</v-btn>
|
||||
<input
|
||||
ref="importCalibrationFromPhotonJson"
|
||||
type="file"
|
||||
accept=".json"
|
||||
style="display: none"
|
||||
@change="importCalibration"
|
||||
/>
|
||||
<v-btn
|
||||
color="buttonPassive"
|
||||
class="mr-2"
|
||||
:disabled="!currentCalibrationCoeffs"
|
||||
style="flex: 1"
|
||||
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
|
||||
@click="openExportCalibrationPrompt"
|
||||
>
|
||||
<v-icon start size="large">mdi-export</v-icon>
|
||||
<span>Export</span>
|
||||
</v-btn>
|
||||
<a
|
||||
ref="exportCalibration"
|
||||
style="color: black; text-decoration: none; display: none"
|
||||
:href="exportCalibrationURL"
|
||||
target="_blank"
|
||||
/>
|
||||
<v-btn
|
||||
color="error"
|
||||
:disabled="!currentCalibrationCoeffs"
|
||||
style="flex: 1"
|
||||
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
|
||||
@click="() => (confirmRemoveDialog = { show: true, vf: props.videoFormat })"
|
||||
>
|
||||
<v-icon start size="large">mdi-delete</v-icon>
|
||||
<span>Delete</span>
|
||||
</v-btn>
|
||||
</v-col>
|
||||
</div>
|
||||
<v-card-title class="pt-0 pb-0"
|
||||
>{{ useCameraSettingsStore().currentCameraName }}@{{ getResolutionString(videoFormat.resolution) }}</v-card-title
|
||||
>
|
||||
<v-card-text v-if="!currentCalibrationCoeffs">
|
||||
<v-alert
|
||||
class="pt-3 pb-3"
|
||||
color="primary"
|
||||
density="compact"
|
||||
text="The selected video format has not been calibrated."
|
||||
icon="mdi-alert-circle-outline"
|
||||
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'tonal'"
|
||||
/>
|
||||
</v-card-text>
|
||||
<v-card-text class="pt-0">
|
||||
<v-table density="compact" style="width: 100%">
|
||||
<template #default>
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="text-left">Name</th>
|
||||
<th class="text-left">Value</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Fx</td>
|
||||
<td>
|
||||
{{
|
||||
useCameraSettingsStore()
|
||||
.getCalibrationCoeffs(props.videoFormat.resolution)
|
||||
?.cameraIntrinsics.data[0].toFixed(2) || 0.0
|
||||
}}
|
||||
mm
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Fy</td>
|
||||
<td>
|
||||
{{
|
||||
useCameraSettingsStore()
|
||||
.getCalibrationCoeffs(props.videoFormat.resolution)
|
||||
?.cameraIntrinsics.data[4].toFixed(2) || 0.0
|
||||
}}
|
||||
mm
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Cx</td>
|
||||
<td>
|
||||
{{
|
||||
useCameraSettingsStore()
|
||||
.getCalibrationCoeffs(props.videoFormat.resolution)
|
||||
?.cameraIntrinsics.data[2].toFixed(2) || 0.0
|
||||
}}
|
||||
px
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Cy</td>
|
||||
<td>
|
||||
{{
|
||||
useCameraSettingsStore()
|
||||
.getCalibrationCoeffs(props.videoFormat.resolution)
|
||||
?.cameraIntrinsics.data[5].toFixed(2) || 0.0
|
||||
}}
|
||||
px
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Distortion</td>
|
||||
<td>
|
||||
{{
|
||||
useCameraSettingsStore()
|
||||
.getCalibrationCoeffs(props.videoFormat.resolution)
|
||||
?.distCoeffs.data.map((it) => parseFloat(it.toFixed(3))) || []
|
||||
}}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Mean Err</td>
|
||||
<td>
|
||||
{{
|
||||
videoFormat.mean !== undefined
|
||||
? isNaN(videoFormat.mean)
|
||||
? "NaN"
|
||||
: videoFormat.mean.toFixed(2) + "px"
|
||||
: "-"
|
||||
}}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Horizontal FOV</td>
|
||||
<td>
|
||||
{{ videoFormat.horizontalFOV !== undefined ? videoFormat.horizontalFOV.toFixed(2) + "°" : "-" }}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Vertical FOV</td>
|
||||
<td>{{ videoFormat.verticalFOV !== undefined ? videoFormat.verticalFOV.toFixed(2) + "°" : "-" }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Diagonal FOV</td>
|
||||
<td>{{ videoFormat.diagonalFOV !== undefined ? videoFormat.diagonalFOV.toFixed(2) + "°" : "-" }}</td>
|
||||
</tr>
|
||||
<!-- Board warp, only shown for mrcal-calibrated cameras -->
|
||||
<tr v-if="currentCalibrationCoeffs?.calobjectWarp?.length === 2">
|
||||
<td>Board warp, X/Y</td>
|
||||
<td>
|
||||
{{
|
||||
useCameraSettingsStore()
|
||||
.getCalibrationCoeffs(props.videoFormat.resolution)
|
||||
?.calobjectWarp?.map((it) => (it * 1000).toFixed(2) + " mm")
|
||||
.join(" / ")
|
||||
}}
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</template>
|
||||
</v-table>
|
||||
</v-card-text>
|
||||
<v-card-title v-if="currentCalibrationCoeffs" class="pt-0 pb-0">Individual Observations</v-card-title>
|
||||
<v-card-text v-if="currentCalibrationCoeffs" class="pt-0">
|
||||
<v-data-table
|
||||
density="compact"
|
||||
style="width: 100%"
|
||||
:headers="[
|
||||
{ title: 'Observation Id', key: 'index' },
|
||||
{ title: 'Mean Reprojection Error', key: 'mean' },
|
||||
{ title: '', key: 'data-table-expand' }
|
||||
]"
|
||||
:items="getObservationDetails()"
|
||||
item-value="index"
|
||||
show-expand
|
||||
>
|
||||
<template #item.data-table-expand="{ internalItem, toggleExpand }">
|
||||
<v-btn
|
||||
color="buttonPassive"
|
||||
style="width: 100%"
|
||||
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
|
||||
@click="openUploadPhotonCalibJsonPrompt"
|
||||
>
|
||||
<v-icon start size="large"> mdi-import</v-icon>
|
||||
<span>Import</span>
|
||||
</v-btn>
|
||||
<input
|
||||
ref="importCalibrationFromPhotonJson"
|
||||
type="file"
|
||||
accept=".json"
|
||||
style="display: none"
|
||||
@change="importCalibration"
|
||||
/>
|
||||
</v-col>
|
||||
<v-col cols="6" md="3" class="d-flex align-center pt-0 pb-0 pr-0">
|
||||
<v-btn
|
||||
color="buttonPassive"
|
||||
:disabled="!currentCalibrationCoeffs"
|
||||
style="width: 100%"
|
||||
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
|
||||
@click="openExportCalibrationPrompt"
|
||||
>
|
||||
<v-icon start size="large">mdi-export</v-icon>
|
||||
<span>Export</span>
|
||||
</v-btn>
|
||||
<a
|
||||
ref="exportCalibration"
|
||||
style="color: black; text-decoration: none; display: none"
|
||||
:href="exportCalibrationURL"
|
||||
target="_blank"
|
||||
/>
|
||||
</v-col>
|
||||
</div>
|
||||
</v-card-title>
|
||||
icon="mdi-eye"
|
||||
class="text-none"
|
||||
color="medium-emphasis"
|
||||
size="small"
|
||||
variant="text"
|
||||
slim
|
||||
@click="toggleExpand(internalItem)"
|
||||
></v-btn>
|
||||
</template>
|
||||
|
||||
<v-card-text class="d-flex flex-row pt-0">
|
||||
<v-col cols="4" class="pa-0">
|
||||
<v-tabs v-model="tab" grow bg-color="surface" height="48" slider-color="buttonActive">
|
||||
<v-tab key="details" value="details">Details</v-tab>
|
||||
<v-tab key="observations" value="observations">Observations</v-tab>
|
||||
</v-tabs>
|
||||
<v-tabs-window v-model="tab" class="pt-3">
|
||||
<v-tabs-window-item key="details" value="details">
|
||||
<v-table style="width: 100%" density="compact">
|
||||
<template #default>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Camera</td>
|
||||
<td>
|
||||
{{ useCameraSettingsStore().currentCameraName }}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Resolution</td>
|
||||
<td>
|
||||
{{ getResolutionString(videoFormat.resolution) }}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Fx</td>
|
||||
<td>
|
||||
{{
|
||||
useCameraSettingsStore()
|
||||
.getCalibrationCoeffs(props.videoFormat.resolution)
|
||||
?.cameraIntrinsics.data[0].toFixed(2) || 0.0
|
||||
}}
|
||||
mm
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Fy</td>
|
||||
<td>
|
||||
{{
|
||||
useCameraSettingsStore()
|
||||
.getCalibrationCoeffs(props.videoFormat.resolution)
|
||||
?.cameraIntrinsics.data[4].toFixed(2) || 0.0
|
||||
}}
|
||||
mm
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Cx</td>
|
||||
<td>
|
||||
{{
|
||||
useCameraSettingsStore()
|
||||
.getCalibrationCoeffs(props.videoFormat.resolution)
|
||||
?.cameraIntrinsics.data[2].toFixed(2) || 0.0
|
||||
}}
|
||||
px
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Cy</td>
|
||||
<td>
|
||||
{{
|
||||
useCameraSettingsStore()
|
||||
.getCalibrationCoeffs(props.videoFormat.resolution)
|
||||
?.cameraIntrinsics.data[5].toFixed(2) || 0.0
|
||||
}}
|
||||
px
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Distortion</td>
|
||||
<td>
|
||||
{{
|
||||
useCameraSettingsStore()
|
||||
.getCalibrationCoeffs(props.videoFormat.resolution)
|
||||
?.distCoeffs.data.map((it) => parseFloat(it.toFixed(3))) || []
|
||||
}}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Mean Err</td>
|
||||
<td>
|
||||
{{
|
||||
videoFormat.mean !== undefined
|
||||
? isNaN(videoFormat.mean)
|
||||
? "NaN"
|
||||
: videoFormat.mean.toFixed(2) + "px"
|
||||
: "-"
|
||||
}}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Horizontal FOV</td>
|
||||
<td>
|
||||
{{ videoFormat.horizontalFOV !== undefined ? videoFormat.horizontalFOV.toFixed(2) + "°" : "-" }}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Vertical FOV</td>
|
||||
<td>
|
||||
{{ videoFormat.verticalFOV !== undefined ? videoFormat.verticalFOV.toFixed(2) + "°" : "-" }}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Diagonal FOV</td>
|
||||
<td>
|
||||
{{ videoFormat.diagonalFOV !== undefined ? videoFormat.diagonalFOV.toFixed(2) + "°" : "-" }}
|
||||
</td>
|
||||
</tr>
|
||||
<!-- Board warp, only shown for mrcal-calibrated cameras -->
|
||||
<tr v-if="currentCalibrationCoeffs?.calobjectWarp?.length === 2">
|
||||
<td>Board warp, X/Y</td>
|
||||
<td>
|
||||
{{
|
||||
currentCalibrationCoeffs?.calobjectWarp?.map((it) => (it * 1000).toFixed(2) + " mm").join(" / ")
|
||||
}}
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</template>
|
||||
</v-table>
|
||||
</v-tabs-window-item>
|
||||
<v-tabs-window-item key="observations" value="observations">
|
||||
<v-data-table
|
||||
id="observations-table"
|
||||
items-per-page-text="Page size:"
|
||||
density="compact"
|
||||
style="width: 100%"
|
||||
:headers="[
|
||||
{ title: 'Id', key: 'index' },
|
||||
{ title: 'Mean Reprojection Error', key: 'mean' }
|
||||
]"
|
||||
:items="getObservationDetails()"
|
||||
item-value="index"
|
||||
show-expand
|
||||
>
|
||||
<template #item.data-table-expand="{ internalItem }">
|
||||
<v-btn
|
||||
class="text-none"
|
||||
size="small"
|
||||
variant="text"
|
||||
slim
|
||||
rounded
|
||||
@click="viewingImg = internalItem.index"
|
||||
>
|
||||
<v-icon
|
||||
size="large"
|
||||
:color="viewingImg === internalItem.index ? 'buttonActive' : 'rgba(255, 255, 255, 0.7)'"
|
||||
>mdi-eye</v-icon
|
||||
>
|
||||
</v-btn>
|
||||
</template>
|
||||
</v-data-table>
|
||||
</v-tabs-window-item>
|
||||
</v-tabs-window>
|
||||
</v-col>
|
||||
<v-col cols="8" class="pa-0 pl-6">
|
||||
<v-card-text class="pa-0 fill-height d-flex justify-center align-center">
|
||||
<div v-if="!currentCalibrationCoeffs">
|
||||
<v-alert
|
||||
class="pt-3 pb-3"
|
||||
color="primary"
|
||||
text="The selected video format has not been calibrated."
|
||||
icon="mdi-alert-circle-outline"
|
||||
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'tonal'"
|
||||
/>
|
||||
</div>
|
||||
<Suspense v-else-if="tab === 'details'">
|
||||
<!-- Allows us to import three js when it's actually needed -->
|
||||
<PhotonCalibrationVisualizer
|
||||
:camera-unique-name="useCameraSettingsStore().currentCameraSettings.uniqueName"
|
||||
:resolution="props.videoFormat.resolution"
|
||||
title="Camera to Board Transforms"
|
||||
/>
|
||||
<template #fallback> Loading... </template>
|
||||
</Suspense>
|
||||
<div v-else style="display: flex; justify-content: center; width: 100%">
|
||||
<img :src="calibrationImageURL(viewingImg)" alt="observation image" class="snapshot-preview pt-2 pb-2" />
|
||||
</div>
|
||||
</v-card-text>
|
||||
</v-col>
|
||||
<template #expanded-row="{ columns, item }">
|
||||
<td :colspan="columns.length">
|
||||
<div style="display: flex; justify-content: center; width: 100%">
|
||||
<img :src="calibrationImageURL(item.index)" alt="observation image" class="snapshot-preview pt-2 pb-2" />
|
||||
</div>
|
||||
</td>
|
||||
</template>
|
||||
</v-data-table>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
|
||||
@@ -349,7 +322,11 @@ const viewingImg = ref(0);
|
||||
|
||||
<style scoped>
|
||||
.snapshot-preview {
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
max-width: 55%;
|
||||
}
|
||||
@media only screen and (max-width: 512px) {
|
||||
.snapshot-preview {
|
||||
max-width: 100%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -177,7 +177,7 @@ const wrappedCameras = computed<SelectItem[]>(() =>
|
||||
<v-btn
|
||||
block
|
||||
size="small"
|
||||
color="buttonActive"
|
||||
color="primary"
|
||||
:disabled="!settingsHaveChanged()"
|
||||
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
|
||||
@click="saveCameraSettings"
|
||||
|
||||
@@ -27,13 +27,7 @@ function debounce(func: (...args: any[]) => void, wait: number) {
|
||||
}
|
||||
|
||||
const debouncedEmit = debounce((v: number) => {
|
||||
if (v < props.min) {
|
||||
emit("update:modelValue", props.min);
|
||||
} else if (v > props.max) {
|
||||
emit("update:modelValue", props.max);
|
||||
} else {
|
||||
emit("update:modelValue", v);
|
||||
}
|
||||
emit("update:modelValue", v);
|
||||
}, 20);
|
||||
|
||||
const localValue = computed({
|
||||
|
||||
@@ -2,19 +2,14 @@
|
||||
defineProps<{
|
||||
label?: string;
|
||||
tooltip?: string;
|
||||
icon?: string;
|
||||
location?: "top" | "bottom" | "left" | "right";
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<v-tooltip :disabled="tooltip === undefined" :location="location ?? 'right'" open-delay="300">
|
||||
<v-tooltip :disabled="tooltip === undefined" location="right" open-delay="300">
|
||||
<template #activator="{ props }">
|
||||
<span style="cursor: text !important" class="text-white" v-bind="props">{{ label }}</span>
|
||||
<v-icon v-if="icon" small class="ml-2" color="primary" v-bind="props">
|
||||
{{ icon }}
|
||||
</v-icon>
|
||||
</template>
|
||||
<span>{{ tooltip }}</span>
|
||||
</v-tooltip>
|
||||
|
||||
@@ -158,16 +158,6 @@ const interactiveCols = computed(() =>
|
||||
(args) => useCameraSettingsStore().changeCurrentPipelineSetting({ cameraWhiteBalanceTemp: args }, false)
|
||||
"
|
||||
/>
|
||||
<pv-switch
|
||||
v-model="useCameraSettingsStore().currentPipelineSettings.blockForFrames"
|
||||
:disabled="!useCameraSettingsStore().currentCameraSettings.matchedCameraInfo.PVUsbCameraInfo"
|
||||
label="Low Latency Mode"
|
||||
:switch-cols="interactiveCols"
|
||||
tooltip="When enabled, USB cameras wait for the next camera frame for lowest latency. When disabled, uses the most recent available frame for higher FPS."
|
||||
@update:modelValue="
|
||||
(args) => useCameraSettingsStore().changeCurrentPipelineSetting({ blockForFrames: args }, false)
|
||||
"
|
||||
/>
|
||||
<pv-select
|
||||
v-model="useCameraSettingsStore().currentPipelineSettings.inputImageRotationMode"
|
||||
label="Orientation"
|
||||
|
||||
@@ -9,11 +9,20 @@ const trackedTargets = computed<PhotonTarget[]>(() => useStateStore().currentPip
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<Suspense>
|
||||
<!-- Allows us to import three js when it's actually needed -->
|
||||
<photon3d-visualizer :targets="trackedTargets" />
|
||||
<v-row style="width: 100%">
|
||||
<v-col>
|
||||
<span class="text-white">Target Visualization</span>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row style="width: 100%">
|
||||
<v-col style="display: flex; align-items: center; justify-content: center">
|
||||
<Suspense>
|
||||
<!-- Allows us to import three js when it's actually needed -->
|
||||
<photon3d-visualizer :targets="trackedTargets" />
|
||||
|
||||
<template #fallback> Loading... </template>
|
||||
</Suspense>
|
||||
<template #fallback> Loading... </template>
|
||||
</Suspense>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -1,489 +0,0 @@
|
||||
@ -0,0 +1,565 @@
|
||||
<script setup lang="ts">
|
||||
import { inject, computed, ref, watch } from "vue";
|
||||
import { useStateStore } from "@/stores/StateStore";
|
||||
import { useSettingsStore } from "@/stores/settings/GeneralSettingsStore";
|
||||
import PvSelect from "@/components/common/pv-select.vue";
|
||||
import PvDeleteModal from "@/components/common/pv-delete-modal.vue";
|
||||
import MetricsChart from "./MetricsChart.vue";
|
||||
import { useTheme } from "vuetify";
|
||||
import { axiosPost, forceReloadPage } from "@/lib/PhotonUtils";
|
||||
import TooltippedLabel from "@/components/common/pv-tooltipped-label.vue";
|
||||
import { metricsHistorySnapshot } from "@/stores/settings/GeneralSettingsStore";
|
||||
|
||||
const theme = useTheme();
|
||||
|
||||
const restartProgram = () => {
|
||||
axiosPost("/utils/restartProgram", "restart PhotonVision");
|
||||
forceReloadPage();
|
||||
};
|
||||
const restartDevice = () => {
|
||||
axiosPost("/utils/restartDevice", "restart the device");
|
||||
forceReloadPage();
|
||||
};
|
||||
|
||||
const address = inject<string>("backendHost");
|
||||
|
||||
const offlineUpdate = ref();
|
||||
const openOfflineUpdatePrompt = () => {
|
||||
offlineUpdate.value.click();
|
||||
};
|
||||
const handleOfflineUpdate = async () => {
|
||||
const files = offlineUpdate.value.files;
|
||||
if (files.length === 0) return;
|
||||
const formData = new FormData();
|
||||
formData.append("jarData", files[0]);
|
||||
useStateStore().showSnackbarMessage({
|
||||
message: "New Software Upload in Progress...",
|
||||
color: "secondary",
|
||||
timeout: -1
|
||||
});
|
||||
await axiosPost("/utils/offlineUpdate", "upload new software", formData, {
|
||||
headers: { "Content-Type": "multipart/form-data" },
|
||||
onUploadProgress: ({ progress }) => {
|
||||
const uploadPercentage = (progress || 0) * 100.0;
|
||||
if (uploadPercentage < 99.5) {
|
||||
useStateStore().showSnackbarMessage({
|
||||
message: "New Software Upload in Progress",
|
||||
color: "secondary",
|
||||
timeout: -1,
|
||||
progressBar: uploadPercentage,
|
||||
progressBarColor: "primary"
|
||||
});
|
||||
} else {
|
||||
useStateStore().showSnackbarMessage({
|
||||
message: "Installing uploaded software...",
|
||||
color: "secondary",
|
||||
timeout: -1
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
forceReloadPage();
|
||||
};
|
||||
|
||||
const exportLogFile = ref();
|
||||
const openExportLogsPrompt = () => {
|
||||
exportLogFile.value.click();
|
||||
};
|
||||
|
||||
const exportSettings = ref();
|
||||
const openExportSettingsPrompt = () => {
|
||||
exportSettings.value.click();
|
||||
};
|
||||
|
||||
enum ImportType {
|
||||
AllSettings,
|
||||
HardwareConfig,
|
||||
HardwareSettings,
|
||||
NetworkConfig,
|
||||
ApriltagFieldLayout
|
||||
}
|
||||
|
||||
const showImportDialog = ref(false);
|
||||
const importType = ref<ImportType | undefined>(undefined);
|
||||
const importFile = ref<File | null>(null);
|
||||
|
||||
const handleSettingsImport = () => {
|
||||
if (importType.value === undefined || importFile.value === null) return;
|
||||
const formData = new FormData();
|
||||
formData.append("data", importFile.value);
|
||||
let settingsEndpoint: string;
|
||||
switch (importType.value) {
|
||||
case ImportType.HardwareConfig:
|
||||
settingsEndpoint = "/hardwareConfig";
|
||||
break;
|
||||
case ImportType.HardwareSettings:
|
||||
settingsEndpoint = "/hardwareSettings";
|
||||
break;
|
||||
case ImportType.NetworkConfig:
|
||||
settingsEndpoint = "/networkConfig";
|
||||
break;
|
||||
case ImportType.ApriltagFieldLayout:
|
||||
settingsEndpoint = "/aprilTagFieldLayout";
|
||||
break;
|
||||
default:
|
||||
case ImportType.AllSettings:
|
||||
settingsEndpoint = "";
|
||||
break;
|
||||
}
|
||||
axiosPost(`/settings${settingsEndpoint}`, "import settings", formData, {
|
||||
headers: { "Content-Type": "multipart/form-data" }
|
||||
});
|
||||
showImportDialog.value = false;
|
||||
importType.value = undefined;
|
||||
importFile.value = null;
|
||||
};
|
||||
|
||||
const showFactoryReset = ref(false);
|
||||
const nukePhotonConfigDirectory = () => {
|
||||
axiosPost("/utils/nukeConfigDirectory", "delete the config directory");
|
||||
forceReloadPage();
|
||||
};
|
||||
|
||||
interface MetricItem {
|
||||
header: string;
|
||||
value?: string;
|
||||
}
|
||||
|
||||
const generalMetrics = computed<MetricItem[]>(() => {
|
||||
const stats = [
|
||||
{ header: "Version", value: useSettingsStore().general.version || "Unknown" },
|
||||
{ header: "Hardware Model", value: useSettingsStore().general.hardwareModel || "Unknown" },
|
||||
{ header: "Platform", value: useSettingsStore().general.hardwarePlatform || "Unknown" },
|
||||
{ header: "GPU Acceleration", value: useSettingsStore().general.gpuAcceleration || "None detected" }
|
||||
];
|
||||
|
||||
if (!useSettingsStore().network.networkingDisabled) {
|
||||
stats.push({ header: "IP Address", value: useSettingsStore().metrics.ipAddress || "Unknown" });
|
||||
}
|
||||
|
||||
return stats;
|
||||
});
|
||||
|
||||
// @ts-expect-error This uses Intl.DurationFormat which is newly implemented and not available in TS.
|
||||
const durationFormatter = new Intl.DurationFormat("en", { style: "narrow" });
|
||||
const platformMetrics = computed<MetricItem[]>(() => {
|
||||
const metrics = useSettingsStore().metrics;
|
||||
const stats = [
|
||||
{
|
||||
header: "Uptime",
|
||||
value: (() => {
|
||||
const seconds = metrics.uptime;
|
||||
if (seconds === undefined) return "Unknown";
|
||||
|
||||
const days = Math.floor(seconds / 86400);
|
||||
const hours = Math.floor((seconds % 86400) / 3600);
|
||||
const minutes = Math.floor((seconds % 3600) / 60);
|
||||
const secs = Math.floor(seconds % 60);
|
||||
|
||||
return durationFormatter.format({
|
||||
days: days,
|
||||
hours: hours,
|
||||
minutes: minutes,
|
||||
seconds: secs
|
||||
});
|
||||
})()
|
||||
}
|
||||
];
|
||||
|
||||
if (metrics.npuUsage && metrics.npuUsage.length > 0) {
|
||||
stats.push({
|
||||
header: "NPU Usage",
|
||||
value: metrics.npuUsage?.map((usage, index) => `Core${index} ${usage}%`).join(", ") || "Unknown"
|
||||
});
|
||||
}
|
||||
|
||||
if (metrics.gpuMem && metrics.gpuMemUtil && metrics.gpuMem > 0 && metrics.gpuMemUtil > 0) {
|
||||
stats.push({
|
||||
header: "GPU Memory Usage",
|
||||
value: `${metrics.gpuMemUtil}MB of ${metrics.gpuMem}MB`
|
||||
});
|
||||
}
|
||||
|
||||
if (metrics.cpuThr) {
|
||||
stats.push({
|
||||
header: "CPU Throttling",
|
||||
value: metrics.cpuThr.toString()
|
||||
});
|
||||
}
|
||||
|
||||
if (metrics.recvBitRate && metrics.recvBitRate !== -1) {
|
||||
stats.push({
|
||||
header: "Received Bit Rate",
|
||||
value: `${(metrics.recvBitRate / 1e6).toFixed(5)} Mb/s`
|
||||
});
|
||||
}
|
||||
|
||||
return stats;
|
||||
});
|
||||
|
||||
const cpuUsageData = ref<{ time: number; value: number }[]>([]);
|
||||
const cpuMemoryUsageData = ref<{ time: number; value: number }[]>([]);
|
||||
const cpuTempData = ref<{ time: number; value: number }[]>([]);
|
||||
const networkUsageData = ref<{ time: number; value: number }[]>([]);
|
||||
|
||||
watch(metricsHistorySnapshot, () => {
|
||||
cpuUsageData.value = metricsHistorySnapshot.value.map((entry) => ({
|
||||
time: entry.time,
|
||||
value: entry.metrics.cpuUtil ?? 0
|
||||
}));
|
||||
cpuMemoryUsageData.value = metricsHistorySnapshot.value.map((entry) => ({
|
||||
time: entry.time,
|
||||
value: entry.metrics.ramUtil === -1 ? -1 : ((entry.metrics.ramUtil ?? 0) / (entry.metrics.ramMem ?? -1.0)) * 100
|
||||
}));
|
||||
cpuTempData.value = metricsHistorySnapshot.value.map((entry) => ({
|
||||
time: entry.time,
|
||||
value: entry.metrics.cpuTemp ?? 0
|
||||
}));
|
||||
networkUsageData.value = metricsHistorySnapshot.value.map((entry) => ({
|
||||
time: entry.time,
|
||||
value: entry.metrics.sentBitRate === -1 ? -1 : (entry.metrics.sentBitRate ?? 0) / 1e6
|
||||
}));
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<v-row no-gutters>
|
||||
<!-- Device control card -->
|
||||
<v-col class="pr-3">
|
||||
<v-card class="mb-3 rounded-12 fill-height d-flex flex-column justify-space-between" color="surface">
|
||||
<v-card-title class="d-flex justify-space-between">
|
||||
<span>Device Control</span>
|
||||
</v-card-title>
|
||||
<v-card-text class="flex-0-0">
|
||||
<v-table>
|
||||
<tbody>
|
||||
<tr v-for="(item, itemIndex) in generalMetrics.concat(platformMetrics)" :key="itemIndex">
|
||||
<td :key="itemIndex">
|
||||
{{ item.header }}
|
||||
</td>
|
||||
<td :key="itemIndex">
|
||||
{{ item.value }}
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</v-table>
|
||||
</v-card-text>
|
||||
<v-card-text class="pt-0 flex-0-0">
|
||||
<v-row>
|
||||
<v-col>
|
||||
<v-btn
|
||||
color="buttonPassive"
|
||||
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
|
||||
@click="useStateStore().showLogModal = true"
|
||||
>
|
||||
<v-icon start class="open-icon" size="large"> mdi-eye </v-icon>
|
||||
<span class="open-label">View Logs</span>
|
||||
</v-btn>
|
||||
</v-col>
|
||||
<v-col>
|
||||
<v-btn
|
||||
color="buttonPassive"
|
||||
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
|
||||
@click="openExportLogsPrompt"
|
||||
>
|
||||
<v-icon start class="open-icon" size="large"> mdi-download </v-icon>
|
||||
<span class="open-label">Download Logs</span>
|
||||
|
||||
<!-- Special hidden link that gets 'clicked' when the user exports journalctl logs -->
|
||||
<a
|
||||
ref="exportLogFile"
|
||||
style="color: black; text-decoration: none; display: none"
|
||||
:href="`http://${address}/api/utils/photonvision-journalctl.txt`"
|
||||
download="photonvision-journalctl.txt"
|
||||
target="_blank"
|
||||
/>
|
||||
</v-btn>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-card-text>
|
||||
<v-card-text class="pt-0 flex-0-0">
|
||||
<v-row>
|
||||
<v-col>
|
||||
<v-btn
|
||||
color="buttonPassive"
|
||||
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
|
||||
@click="() => (showImportDialog = true)"
|
||||
>
|
||||
<v-icon start class="open-icon" size="large"> mdi-import </v-icon>
|
||||
<span class="open-label">Import Settings</span>
|
||||
</v-btn>
|
||||
</v-col>
|
||||
<v-col>
|
||||
<v-btn
|
||||
color="buttonPassive"
|
||||
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
|
||||
@click="openExportSettingsPrompt"
|
||||
>
|
||||
<v-icon start class="open-icon" size="large"> mdi-export </v-icon>
|
||||
<span class="open-label">Export Settings</span>
|
||||
</v-btn>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-card-text>
|
||||
<v-card-text class="pt-0 flex-0-0">
|
||||
<v-row>
|
||||
<v-col cols="12" sm="6"
|
||||
><v-btn
|
||||
color="buttonActive"
|
||||
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
|
||||
@click="restartProgram"
|
||||
>
|
||||
<v-icon start class="open-icon" size="large"> mdi-restart </v-icon>
|
||||
<span class="open-label">Restart Software</span>
|
||||
</v-btn>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6">
|
||||
<v-btn
|
||||
color="buttonPassive"
|
||||
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
|
||||
@click="openOfflineUpdatePrompt"
|
||||
>
|
||||
<v-icon start class="open-icon" size="large"> mdi-upload </v-icon>
|
||||
<span class="open-label">Offline Update</span>
|
||||
</v-btn>
|
||||
<input
|
||||
ref="offlineUpdate"
|
||||
type="file"
|
||||
accept=".jar"
|
||||
style="display: none"
|
||||
@change="handleOfflineUpdate"
|
||||
/>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-card-text>
|
||||
<v-card-text class="pt-0 flex-0-0">
|
||||
<v-row>
|
||||
<v-col cols="12" sm="6">
|
||||
<v-btn
|
||||
color="buttonActive"
|
||||
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
|
||||
@click="restartDevice"
|
||||
>
|
||||
<v-icon start class="open-icon" size="large"> mdi-restart-alert </v-icon>
|
||||
<span class="open-label">Reboot Device</span>
|
||||
</v-btn>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6">
|
||||
<v-btn
|
||||
color="error"
|
||||
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
|
||||
@click="() => (showFactoryReset = true)"
|
||||
>
|
||||
<v-icon start class="open-icon" size="large"> mdi-trash-can-outline </v-icon>
|
||||
<span class="open-icon"> Factory Reset </span>
|
||||
</v-btn>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-col>
|
||||
|
||||
<!-- Device metrics card -->
|
||||
<v-col>
|
||||
<v-card class="mb-3 rounded-12 fill-height d-flex flex-column justify-space-between" color="surface">
|
||||
<v-card-title class="d-flex justify-space-between">
|
||||
<span>Device Metrics</span>
|
||||
</v-card-title>
|
||||
<v-card-text class="pt-0 flex-0-0 pb-2">
|
||||
<div class="d-flex justify-space-between pb-3">
|
||||
<span>CPU Usage</span>
|
||||
<span>{{ Math.round(cpuUsageData.at(-1)?.value ?? 0) }}%</span>
|
||||
</div>
|
||||
<MetricsChart id="chart" :data="cpuUsageData" type="percentage" :min="0" :max="100" color="blue" />
|
||||
</v-card-text>
|
||||
<v-card-text class="pt-0 flex-0-0 pb-2">
|
||||
<div class="d-flex justify-space-between pb-3 pt-3">
|
||||
<span>CPU Memory Usage</span>
|
||||
<span>{{ Math.round(cpuMemoryUsageData.at(-1)?.value ?? 0) }}%</span>
|
||||
</div>
|
||||
<MetricsChart id="chart" :data="cpuMemoryUsageData" type="percentage" :min="0" :max="100" color="purple" />
|
||||
</v-card-text>
|
||||
<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>
|
||||
</div>
|
||||
<MetricsChart id="chart" :data="cpuTempData" type="temperature" color="red" />
|
||||
</v-card-text>
|
||||
<v-card-text class="pt-0 flex-0-0">
|
||||
<div class="d-flex justify-space-between pb-3 pt-3">
|
||||
<tooltipped-label
|
||||
label="Network Usage"
|
||||
icon="mdi-information"
|
||||
location="top"
|
||||
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
|
||||
>
|
||||
</div>
|
||||
<MetricsChart id="chart" :data="networkUsageData" type="mb" :min="0" :max="10" color="green" />
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<!-- Factory reset modal -->
|
||||
<pv-delete-modal
|
||||
v-model="showFactoryReset"
|
||||
title="Factory Reset PhotonVision"
|
||||
description="This will delete all settings and configurations stored on this device, including network settings. This action cannot be undone."
|
||||
expected-confirmation-text="Delete Everything"
|
||||
:on-confirm="nukePhotonConfigDirectory"
|
||||
:on-backup="openExportSettingsPrompt"
|
||||
delete-text="Factory reset"
|
||||
/>
|
||||
|
||||
<!-- Import settings modal -->
|
||||
<v-dialog
|
||||
v-model="showImportDialog"
|
||||
width="600"
|
||||
@update:modelValue="
|
||||
() => {
|
||||
importType = undefined;
|
||||
importFile = null;
|
||||
}
|
||||
"
|
||||
>
|
||||
<v-card color="surface" dark>
|
||||
<v-card-title class="pb-0">Import Settings</v-card-title>
|
||||
<v-card-text>
|
||||
Upload and apply previously saved or exported PhotonVision settings to this device
|
||||
<div class="pa-5 pb-0">
|
||||
<pv-select
|
||||
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']"
|
||||
:select-cols="10"
|
||||
style="width: 100%"
|
||||
/>
|
||||
<v-file-input
|
||||
v-model="importFile"
|
||||
class="pb-5"
|
||||
variant="underlined"
|
||||
:disabled="importType === undefined"
|
||||
:error-messages="importType === undefined ? 'Settings type not selected' : ''"
|
||||
:accept="importType === ImportType.AllSettings ? '.zip' : '.json'"
|
||||
/>
|
||||
<v-btn
|
||||
color="primary"
|
||||
:disabled="importFile === null"
|
||||
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
|
||||
@click="handleSettingsImport"
|
||||
>
|
||||
<v-icon start class="open-icon"> mdi-import </v-icon>
|
||||
<span class="open-label">Import Settings</span>
|
||||
</v-btn>
|
||||
</div>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
|
||||
<a
|
||||
ref="exportSettings"
|
||||
style="color: black; text-decoration: none; display: none"
|
||||
:href="`http://${address}/api/settings/photonvision_config.zip`"
|
||||
download="photonvision-settings.zip"
|
||||
target="_blank"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.v-btn:not(.refresh) {
|
||||
width: 100%;
|
||||
}
|
||||
.fill-height {
|
||||
height: calc(100% - 12px) !important;
|
||||
}
|
||||
@media only screen and (max-width: 351px) {
|
||||
.open-icon {
|
||||
margin: 0 !important;
|
||||
}
|
||||
.open-label {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
303
photon-client/src/components/settings/DeviceControlCard.vue
Normal file
303
photon-client/src/components/settings/DeviceControlCard.vue
Normal file
@@ -0,0 +1,303 @@
|
||||
<script setup lang="ts">
|
||||
import { inject, ref } from "vue";
|
||||
import { useStateStore } from "@/stores/StateStore";
|
||||
import PvSelect from "@/components/common/pv-select.vue";
|
||||
import PvDeleteModal from "@/components/common/pv-delete-modal.vue";
|
||||
import { useTheme } from "vuetify";
|
||||
import { axiosPost } from "@/lib/PhotonUtils";
|
||||
|
||||
const theme = useTheme();
|
||||
|
||||
const restartProgram = () => {
|
||||
axiosPost("/utils/restartProgram", "restart PhotonVision");
|
||||
};
|
||||
const restartDevice = () => {
|
||||
axiosPost("/utils/restartDevice", "restart the device");
|
||||
};
|
||||
|
||||
const address = inject<string>("backendHost");
|
||||
|
||||
const offlineUpdate = ref();
|
||||
const openOfflineUpdatePrompt = () => {
|
||||
offlineUpdate.value.click();
|
||||
};
|
||||
const handleOfflineUpdate = () => {
|
||||
const files = offlineUpdate.value.files;
|
||||
if (files.length === 0) return;
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append("jarData", files[0]);
|
||||
|
||||
useStateStore().showSnackbarMessage({
|
||||
message: "New Software Upload in Progress...",
|
||||
color: "secondary",
|
||||
timeout: -1
|
||||
});
|
||||
|
||||
axiosPost("/utils/offlineUpdate", "upload new software", formData, {
|
||||
headers: { "Content-Type": "multipart/form-data" },
|
||||
onUploadProgress: ({ progress }) => {
|
||||
const uploadPercentage = (progress || 0) * 100.0;
|
||||
if (uploadPercentage < 99.5) {
|
||||
useStateStore().showSnackbarMessage({
|
||||
message: "New Software Upload in Progress",
|
||||
color: "secondary",
|
||||
timeout: -1,
|
||||
progressBar: uploadPercentage,
|
||||
progressBarColor: "primary"
|
||||
});
|
||||
} else {
|
||||
useStateStore().showSnackbarMessage({
|
||||
message: "Installing uploaded software...",
|
||||
color: "secondary",
|
||||
timeout: -1
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const exportLogFile = ref();
|
||||
const openExportLogsPrompt = () => {
|
||||
exportLogFile.value.click();
|
||||
};
|
||||
|
||||
const exportSettings = ref();
|
||||
const openExportSettingsPrompt = () => {
|
||||
exportSettings.value.click();
|
||||
};
|
||||
|
||||
enum ImportType {
|
||||
AllSettings,
|
||||
HardwareConfig,
|
||||
HardwareSettings,
|
||||
NetworkConfig,
|
||||
ApriltagFieldLayout
|
||||
}
|
||||
const showImportDialog = ref(false);
|
||||
const importType = ref<ImportType | undefined>(undefined);
|
||||
const importFile = ref<File | null>(null);
|
||||
const handleSettingsImport = () => {
|
||||
if (importType.value === undefined || importFile.value === null) return;
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append("data", importFile.value);
|
||||
|
||||
let settingsEndpoint: string;
|
||||
switch (importType.value) {
|
||||
case ImportType.HardwareConfig:
|
||||
settingsEndpoint = "/hardwareConfig";
|
||||
break;
|
||||
case ImportType.HardwareSettings:
|
||||
settingsEndpoint = "/hardwareSettings";
|
||||
break;
|
||||
case ImportType.NetworkConfig:
|
||||
settingsEndpoint = "/networkConfig";
|
||||
break;
|
||||
case ImportType.ApriltagFieldLayout:
|
||||
settingsEndpoint = "/aprilTagFieldLayout";
|
||||
break;
|
||||
default:
|
||||
case ImportType.AllSettings:
|
||||
settingsEndpoint = "";
|
||||
break;
|
||||
}
|
||||
|
||||
axiosPost(`/settings${settingsEndpoint}`, "import settings", formData, {
|
||||
headers: { "Content-Type": "multipart/form-data" }
|
||||
});
|
||||
|
||||
showImportDialog.value = false;
|
||||
importType.value = undefined;
|
||||
importFile.value = null;
|
||||
};
|
||||
|
||||
const showFactoryReset = ref(false);
|
||||
const nukePhotonConfigDirectory = () => {
|
||||
axiosPost("/utils/nukeConfigDirectory", "delete the config directory");
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<v-card class="mb-3 rounded-12" color="surface">
|
||||
<v-card-title>Device Control</v-card-title>
|
||||
<div class="pa-5 pt-0">
|
||||
<v-row>
|
||||
<v-col cols="12" lg="4" md="6">
|
||||
<v-btn
|
||||
color="buttonActive"
|
||||
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
|
||||
@click="restartProgram"
|
||||
>
|
||||
<v-icon start class="open-icon" size="large"> mdi-restart </v-icon>
|
||||
<span class="open-label">Restart PhotonVision</span>
|
||||
</v-btn>
|
||||
</v-col>
|
||||
<v-col cols="12" lg="4" md="6">
|
||||
<v-btn
|
||||
color="buttonActive"
|
||||
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
|
||||
@click="restartDevice"
|
||||
>
|
||||
<v-icon start class="open-icon" size="large"> mdi-restart-alert </v-icon>
|
||||
<span class="open-label">Restart Device</span>
|
||||
</v-btn>
|
||||
</v-col>
|
||||
<v-col cols="12" lg="4">
|
||||
<v-btn
|
||||
color="buttonPassive"
|
||||
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
|
||||
@click="openOfflineUpdatePrompt"
|
||||
>
|
||||
<v-icon start class="open-icon" size="large"> mdi-upload </v-icon>
|
||||
<span class="open-label">Offline Update</span>
|
||||
</v-btn>
|
||||
<input ref="offlineUpdate" type="file" accept=".jar" style="display: none" @change="handleOfflineUpdate" />
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row>
|
||||
<v-col cols="12" sm="6">
|
||||
<v-btn
|
||||
color="buttonPassive"
|
||||
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
|
||||
@click="() => (showImportDialog = true)"
|
||||
>
|
||||
<v-icon start class="open-icon" size="large"> mdi-import </v-icon>
|
||||
<span class="open-label">Import Settings</span>
|
||||
</v-btn>
|
||||
<v-dialog
|
||||
v-model="showImportDialog"
|
||||
width="600"
|
||||
@update:modelValue="
|
||||
() => {
|
||||
importType = undefined;
|
||||
importFile = null;
|
||||
}
|
||||
"
|
||||
>
|
||||
<v-card color="surface" dark>
|
||||
<v-card-title class="pb-0">Import Settings</v-card-title>
|
||||
<v-card-text>
|
||||
Upload and apply previously saved or exported PhotonVision settings to this device
|
||||
<div class="pa-5 pb-0">
|
||||
<pv-select
|
||||
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'
|
||||
]"
|
||||
:select-cols="10"
|
||||
style="width: 100%"
|
||||
/>
|
||||
<v-file-input
|
||||
v-model="importFile"
|
||||
class="pb-5"
|
||||
variant="underlined"
|
||||
:disabled="importType === undefined"
|
||||
:error-messages="importType === undefined ? 'Settings type not selected' : ''"
|
||||
:accept="importType === ImportType.AllSettings ? '.zip' : '.json'"
|
||||
/>
|
||||
<v-btn
|
||||
color="primary"
|
||||
:disabled="importFile === null"
|
||||
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
|
||||
@click="handleSettingsImport"
|
||||
>
|
||||
<v-icon start class="open-icon"> mdi-import </v-icon>
|
||||
<span class="open-label">Import Settings</span>
|
||||
</v-btn>
|
||||
</div>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6">
|
||||
<v-btn
|
||||
color="buttonPassive"
|
||||
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
|
||||
@click="openExportSettingsPrompt"
|
||||
>
|
||||
<v-icon start class="open-icon" size="large"> mdi-export </v-icon>
|
||||
<span class="open-label">Export Settings</span>
|
||||
</v-btn>
|
||||
<a
|
||||
ref="exportSettings"
|
||||
style="color: black; text-decoration: none; display: none"
|
||||
:href="`http://${address}/api/settings/photonvision_config.zip`"
|
||||
download="photonvision-settings.zip"
|
||||
target="_blank"
|
||||
/>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6">
|
||||
<v-btn
|
||||
color="buttonPassive"
|
||||
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
|
||||
@click="openExportLogsPrompt"
|
||||
>
|
||||
<v-icon start class="open-icon" size="large"> mdi-download </v-icon>
|
||||
<span class="open-label">Download logs</span>
|
||||
|
||||
<!-- Special hidden link that gets 'clicked' when the user exports journalctl logs -->
|
||||
<a
|
||||
ref="exportLogFile"
|
||||
style="color: black; text-decoration: none; display: none"
|
||||
:href="`http://${address}/api/utils/photonvision-journalctl.txt`"
|
||||
download="photonvision-journalctl.txt"
|
||||
target="_blank"
|
||||
/>
|
||||
</v-btn>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6">
|
||||
<v-btn
|
||||
color="buttonPassive"
|
||||
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
|
||||
@click="useStateStore().showLogModal = true"
|
||||
>
|
||||
<v-icon start class="open-icon" size="large"> mdi-eye </v-icon>
|
||||
<span class="open-label">View logs</span>
|
||||
</v-btn>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row>
|
||||
<v-col cols="12">
|
||||
<v-btn
|
||||
color="error"
|
||||
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
|
||||
@click="() => (showFactoryReset = true)"
|
||||
>
|
||||
<v-icon start class="open-icon" size="large"> mdi-trash-can-outline </v-icon>
|
||||
<span class="open-icon"> Factory Reset PhotonVision </span>
|
||||
</v-btn>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</div>
|
||||
<pv-delete-modal
|
||||
v-model="showFactoryReset"
|
||||
title="Factory Reset PhotonVision"
|
||||
description="This will delete all settings and configurations stored on this device, including network settings. This action cannot be undone."
|
||||
expected-confirmation-text="Delete Everything"
|
||||
:on-confirm="nukePhotonConfigDirectory"
|
||||
:on-backup="openExportSettingsPrompt"
|
||||
delete-text="Factory reset"
|
||||
/>
|
||||
</v-card>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.v-btn {
|
||||
width: 100%;
|
||||
}
|
||||
@media only screen and (max-width: 351px) {
|
||||
.open-icon {
|
||||
margin: 0 !important;
|
||||
}
|
||||
.open-label {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
292
photon-client/src/components/settings/MetricsCard.vue
Normal file
292
photon-client/src/components/settings/MetricsCard.vue
Normal file
@@ -0,0 +1,292 @@
|
||||
<script setup lang="ts">
|
||||
import { useSettingsStore } from "@/stores/settings/GeneralSettingsStore";
|
||||
import { computed, onBeforeMount, ref } from "vue";
|
||||
import { useStateStore } from "@/stores/StateStore";
|
||||
|
||||
interface MetricItem {
|
||||
header: string;
|
||||
value?: string;
|
||||
}
|
||||
|
||||
const generalMetrics = computed<MetricItem[]>(() => {
|
||||
const stats = [
|
||||
{ header: "Version", value: useSettingsStore().general.version || "Unknown" },
|
||||
{ header: "Hardware Model", value: useSettingsStore().general.hardwareModel || "Unknown" },
|
||||
{ header: "Platform", value: useSettingsStore().general.hardwarePlatform || "Unknown" },
|
||||
{ header: "GPU Acceleration", value: useSettingsStore().general.gpuAcceleration || "Unknown" }
|
||||
];
|
||||
|
||||
if (!useSettingsStore().network.networkingDisabled) {
|
||||
stats.push({ header: "IP Address", value: useSettingsStore().metrics.ipAddress || "Unknown" });
|
||||
}
|
||||
|
||||
return stats;
|
||||
});
|
||||
|
||||
// @ts-expect-error This uses Intl.DurationFormat which is newly implemented and not available in TS.
|
||||
const durationFormatter = new Intl.DurationFormat("en", { style: "narrow" });
|
||||
const platformMetrics = computed<MetricItem[]>(() => {
|
||||
const metrics = useSettingsStore().metrics;
|
||||
const stats = [
|
||||
{
|
||||
header: "CPU Temp",
|
||||
value: metrics.cpuTemp === undefined || metrics.cpuTemp == -1 ? "Unknown" : `${metrics.cpuTemp}°C`
|
||||
},
|
||||
{
|
||||
header: "CPU Usage",
|
||||
value: metrics.cpuUtil === undefined ? "Unknown" : `${metrics.cpuUtil}%`
|
||||
},
|
||||
{
|
||||
header: "CPU Memory Usage",
|
||||
value:
|
||||
metrics.ramUtil && metrics.ramMem && metrics.ramUtil >= 0 && metrics.ramMem >= 0
|
||||
? `${metrics.ramUtil}MB of ${metrics.ramMem}MB`
|
||||
: "Unknown"
|
||||
},
|
||||
{
|
||||
header: "Uptime",
|
||||
value: (() => {
|
||||
const seconds = metrics.uptime;
|
||||
if (seconds === undefined) return "Unknown";
|
||||
|
||||
const days = Math.floor(seconds / 86400);
|
||||
const hours = Math.floor((seconds % 86400) / 3600);
|
||||
const minutes = Math.floor((seconds % 3600) / 60);
|
||||
const secs = Math.floor(seconds % 60);
|
||||
|
||||
return durationFormatter.format({
|
||||
days: days,
|
||||
hours: hours,
|
||||
minutes: minutes,
|
||||
seconds: secs
|
||||
});
|
||||
})()
|
||||
},
|
||||
{
|
||||
header: "Disk Usage",
|
||||
value: metrics.diskUtilPct === undefined ? "Unknown" : `${metrics.diskUtilPct}%`
|
||||
}
|
||||
];
|
||||
|
||||
if (metrics.npuUsage && metrics.npuUsage.length > 0) {
|
||||
stats.push({
|
||||
header: "NPU Usage",
|
||||
value: metrics.npuUsage?.map((usage, index) => `Core${index} ${usage}%`).join(", ") || "Unknown"
|
||||
});
|
||||
}
|
||||
|
||||
if (metrics.gpuMem && metrics.gpuMemUtil && metrics.gpuMem > 0 && metrics.gpuMemUtil > 0) {
|
||||
stats.push({
|
||||
header: "GPU Memory Usage",
|
||||
value: `${metrics.gpuMemUtil}MB of ${metrics.gpuMem}MB`
|
||||
});
|
||||
}
|
||||
|
||||
if (metrics.cpuThr) {
|
||||
stats.push({
|
||||
header: "CPU Throttling",
|
||||
value: metrics.cpuThr.toString()
|
||||
});
|
||||
}
|
||||
|
||||
return stats;
|
||||
});
|
||||
|
||||
const metricsLastFetched = ref("Never");
|
||||
const fetchMetrics = () => {
|
||||
useSettingsStore()
|
||||
.requestMetricsUpdate()
|
||||
.catch((error) => {
|
||||
if (error.request) {
|
||||
useStateStore().showSnackbarMessage({
|
||||
color: "error",
|
||||
message: "Unable to fetch metrics! The backend didn't respond."
|
||||
});
|
||||
} else {
|
||||
useStateStore().showSnackbarMessage({
|
||||
color: "error",
|
||||
message: "An error occurred while trying to fetch metrics."
|
||||
});
|
||||
}
|
||||
})
|
||||
.finally(() => {
|
||||
const pad = (num: number): string => {
|
||||
return String(num).padStart(2, "0");
|
||||
};
|
||||
|
||||
const date = new Date();
|
||||
metricsLastFetched.value = `${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}`;
|
||||
});
|
||||
};
|
||||
|
||||
onBeforeMount(() => {
|
||||
fetchMetrics();
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<v-card class="mb-3 rounded-12" color="surface">
|
||||
<v-card-title style="display: flex; justify-content: space-between">
|
||||
<span>Metrics</span>
|
||||
<v-btn variant="text" @click="fetchMetrics">
|
||||
<v-icon start class="open-icon" size="large">mdi-reload</v-icon>
|
||||
Last Fetched: {{ metricsLastFetched }}
|
||||
</v-btn>
|
||||
</v-card-title>
|
||||
<v-card-text class="pt-0 pb-3">
|
||||
<v-card-subtitle class="pa-0" style="font-size: 16px">General</v-card-subtitle>
|
||||
<v-table class="metrics-table mt-3">
|
||||
<thead>
|
||||
<tr>
|
||||
<th
|
||||
v-for="(item, itemIndex) in generalMetrics"
|
||||
:key="itemIndex"
|
||||
class="metric-item metric-item-title"
|
||||
:class="{
|
||||
tl: itemIndex === 0,
|
||||
tr: itemIndex === generalMetrics.length - 1,
|
||||
t: 0 < itemIndex && itemIndex < generalMetrics.length - 1
|
||||
}"
|
||||
>
|
||||
{{ item.header }}
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td
|
||||
v-for="(item, itemIndex) in generalMetrics"
|
||||
:key="itemIndex"
|
||||
class="metric-item"
|
||||
:class="{
|
||||
bl: itemIndex === 0,
|
||||
br: itemIndex === generalMetrics.length - 1,
|
||||
b: 0 < itemIndex && itemIndex < generalMetrics.length - 1
|
||||
}"
|
||||
>
|
||||
{{ item.value }}
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</v-table>
|
||||
</v-card-text>
|
||||
<v-card-text class="pt-4">
|
||||
<v-card-subtitle class="pa-0 pb-1" style="font-size: 16px">Hardware</v-card-subtitle>
|
||||
<v-table class="metrics-table mt-3">
|
||||
<thead>
|
||||
<tr>
|
||||
<th
|
||||
v-for="(item, itemIndex) in platformMetrics"
|
||||
:key="itemIndex"
|
||||
class="metric-item metric-item-title"
|
||||
:class="{
|
||||
tl: itemIndex === 0,
|
||||
tr: itemIndex === platformMetrics.length - 1,
|
||||
t: 0 < itemIndex && itemIndex < platformMetrics.length - 1
|
||||
}"
|
||||
>
|
||||
{{ item.header }}
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td
|
||||
v-for="(item, itemIndex) in platformMetrics"
|
||||
:key="itemIndex"
|
||||
class="metric-item"
|
||||
:class="{
|
||||
bl: itemIndex === 0,
|
||||
br: itemIndex === platformMetrics.length - 1,
|
||||
b: 0 < itemIndex && itemIndex < platformMetrics.length - 1
|
||||
}"
|
||||
>
|
||||
<span v-if="useSettingsStore().metrics.cpuUtil !== undefined">{{ item.value }}</span>
|
||||
<span v-else>---</span>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</v-table>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.metrics-table {
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
$stats-table-border: rgba(255, 255, 255, 0.5);
|
||||
$stats-table-inner: rgba(255, 255, 255, 0.1);
|
||||
|
||||
.t {
|
||||
border-top: 1px solid $stats-table-border;
|
||||
border-right: 1px solid $stats-table-border;
|
||||
border-bottom: 1px solid $stats-table-inner !important;
|
||||
}
|
||||
|
||||
.b {
|
||||
border-bottom: 1px solid $stats-table-border;
|
||||
border-right: 1px solid $stats-table-border;
|
||||
}
|
||||
|
||||
.tl {
|
||||
border-top: 1px solid $stats-table-border;
|
||||
border-left: 1px solid $stats-table-border;
|
||||
border-right: 1px solid $stats-table-border;
|
||||
border-bottom: 1px solid $stats-table-inner !important;
|
||||
border-top-left-radius: 5px;
|
||||
}
|
||||
|
||||
.tr {
|
||||
border-top: 1px solid $stats-table-border;
|
||||
border-right: 1px solid $stats-table-border;
|
||||
border-bottom: 1px solid $stats-table-inner !important;
|
||||
border-top-right-radius: 5px;
|
||||
}
|
||||
|
||||
.bl {
|
||||
border-bottom: 1px solid $stats-table-border;
|
||||
border-left: 1px solid $stats-table-border;
|
||||
border-right: 1px solid $stats-table-border;
|
||||
border-bottom-left-radius: 5px;
|
||||
}
|
||||
|
||||
.br {
|
||||
border-bottom: 1px solid $stats-table-border;
|
||||
border-right: 1px solid $stats-table-border;
|
||||
border-bottom-right-radius: 5px;
|
||||
}
|
||||
|
||||
.metric-item {
|
||||
font-size: 16px !important;
|
||||
padding: 1px 15px 1px 10px;
|
||||
border-right: 1px solid $stats-table-border;
|
||||
font-weight: normal;
|
||||
color: white !important;
|
||||
text-align: center !important;
|
||||
}
|
||||
|
||||
.metric-item-title {
|
||||
font-size: 18px !important;
|
||||
}
|
||||
|
||||
.v-table {
|
||||
::-webkit-scrollbar {
|
||||
width: 0;
|
||||
height: 0.55em;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
-webkit-box-shadow: inset 0 0 6px rgba(0, 0, 0, 0.3);
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background-color: rgb(var(--v-theme-accent));
|
||||
border-radius: 10px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,236 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import * as echarts from "echarts";
|
||||
import { onMounted, ref, onBeforeUnmount, watch } from "vue";
|
||||
import { useTheme } from "vuetify";
|
||||
|
||||
// Color - original (adjusted)
|
||||
// blue - 59, 130, 246 (r: 92, g: 154, b: 255)
|
||||
// purple - 154, 100, 180 (r: 167, g: 104, b: 196)
|
||||
// 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 }
|
||||
};
|
||||
const DEFAULT_COLOR = "blue";
|
||||
|
||||
const typeLabels = {
|
||||
percentage: "%",
|
||||
temperature: "°C",
|
||||
mb: " Mb/s"
|
||||
};
|
||||
|
||||
const theme = useTheme();
|
||||
const chartRef = ref(null);
|
||||
let chart: echarts.ECharts | null = null;
|
||||
|
||||
const getOptions = (data: ChartData[] = []) => {
|
||||
const now = Date.now();
|
||||
return {
|
||||
title: {
|
||||
show: false
|
||||
},
|
||||
tooltip: {
|
||||
trigger: "axis",
|
||||
formatter: (params: any) => {
|
||||
const p = params[0];
|
||||
const append = typeLabels[props.type];
|
||||
const fmsLimitLabel = "FMS Limit - 7.000 Mb/s";
|
||||
|
||||
// prettier-ignore
|
||||
let tooltip = "<div style=\"text-align: right;\">";
|
||||
const seriesData = `${new Date(p.value[0]).toLocaleTimeString([], { hour12: false })} - ${p.value[1].toFixed(props.type === "mb" ? 3 : 2)}${append}`;
|
||||
|
||||
if (props.type === "mb") {
|
||||
if (p.value[1] >= 7) tooltip += seriesData + `<br/>${fmsLimitLabel}`;
|
||||
else tooltip += fmsLimitLabel + `<br/>${seriesData}`;
|
||||
} else tooltip += seriesData;
|
||||
|
||||
return `${tooltip}</div>`;
|
||||
},
|
||||
backgroundColor: theme.themes.value[theme.global.name.value].colors.background,
|
||||
textStyle: {
|
||||
color: theme.themes.value[theme.global.name.value].colors.onBackground
|
||||
},
|
||||
axisPointer: {
|
||||
animation: false
|
||||
}
|
||||
},
|
||||
grid: {
|
||||
top: 0,
|
||||
bottom: 10,
|
||||
left: 0,
|
||||
right: 50,
|
||||
containLabel: false
|
||||
},
|
||||
xAxis: {
|
||||
type: "time",
|
||||
splitLine: {
|
||||
show: true,
|
||||
lineStyle: {
|
||||
color: "#ffffff18"
|
||||
}
|
||||
},
|
||||
splitNumber: 4,
|
||||
min: now - 55 * 1000,
|
||||
axisLine: {
|
||||
lineStyle: {
|
||||
color: theme.global.name.value === "LightTheme" ? "#aaa" : "#777"
|
||||
}
|
||||
},
|
||||
axisLabel: {
|
||||
align: "left",
|
||||
color: theme.global.name.value === "LightTheme" ? "#fff" : "#ddd",
|
||||
formatter: (value: number) => {
|
||||
const date = new Date(value);
|
||||
return date.toLocaleTimeString([], {
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
second: "2-digit",
|
||||
hour12: false
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
yAxis: {
|
||||
type: "value",
|
||||
position: "right",
|
||||
min:
|
||||
props.min ??
|
||||
function (value) {
|
||||
return Math.max(0, (value.min - 10) | 0);
|
||||
},
|
||||
max:
|
||||
props.max ??
|
||||
function (value) {
|
||||
return (value.max + 10) | 0;
|
||||
},
|
||||
splitNumber: 2,
|
||||
splitLine: {
|
||||
show: true,
|
||||
lineStyle: {
|
||||
color: "#ffffff18"
|
||||
}
|
||||
},
|
||||
axisLabel: {
|
||||
color: theme.global.name.value === "LightTheme" ? "#fff" : "#ddd"
|
||||
}
|
||||
},
|
||||
series: getSeries(data),
|
||||
animation: false
|
||||
};
|
||||
};
|
||||
|
||||
const getSeries = (data: ChartData[] = []) => {
|
||||
const color = colors[`${props.color ?? DEFAULT_COLOR}-${theme.global.name.value}`];
|
||||
return [
|
||||
{
|
||||
type: "line",
|
||||
showSymbol: false,
|
||||
data: data.map((d) => [d.time, d.value]),
|
||||
markLine:
|
||||
props.type === "mb"
|
||||
? {
|
||||
symbol: "none",
|
||||
lineStyle: {
|
||||
color: "red",
|
||||
width: 1
|
||||
},
|
||||
label: {
|
||||
show: false
|
||||
},
|
||||
data: [{ yAxis: 7 }]
|
||||
}
|
||||
: 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})`
|
||||
},
|
||||
areaStyle: {
|
||||
color: {
|
||||
type: "linear",
|
||||
x: 0,
|
||||
y: 0,
|
||||
x2: 0,
|
||||
y2: 1,
|
||||
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)`
|
||||
},
|
||||
{
|
||||
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)`
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
];
|
||||
};
|
||||
|
||||
interface ChartData {
|
||||
time: number;
|
||||
value: number;
|
||||
}
|
||||
|
||||
// Type options: "percentage", "temperature", "mb"
|
||||
const props = defineProps<{
|
||||
data: ChartData[];
|
||||
type: string;
|
||||
min?: number;
|
||||
max?: number;
|
||||
color?: string;
|
||||
}>();
|
||||
|
||||
onMounted(() => {
|
||||
chart = echarts.init(chartRef.value);
|
||||
chart.setOption(getOptions(props.data));
|
||||
|
||||
window.addEventListener("resize", resizeChart);
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
window.removeEventListener("resize", resizeChart);
|
||||
chart?.dispose();
|
||||
});
|
||||
|
||||
function resizeChart() {
|
||||
chart?.resize();
|
||||
}
|
||||
|
||||
watch(
|
||||
() => props.data,
|
||||
(data) => {
|
||||
chart?.setOption(getOptions(data));
|
||||
},
|
||||
{ deep: true }
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div ref="chartRef"></div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
div {
|
||||
width: calc(100% + 20px);
|
||||
height: 100px;
|
||||
margin-right: -20px;
|
||||
}
|
||||
</style>
|
||||
@@ -9,7 +9,6 @@ import { type ConfigurableNetworkSettings, NetworkConnectionType } from "@/types
|
||||
import { useStateStore } from "@/stores/StateStore";
|
||||
import { useTheme } from "vuetify";
|
||||
import { getThemeColor, setThemeColor, resetTheme } from "@/lib/ThemeManager";
|
||||
import { statusCheck } from "@/lib/PhotonUtils";
|
||||
|
||||
const theme = useTheme();
|
||||
|
||||
@@ -81,7 +80,9 @@ const settingsHaveChanged = (): boolean => {
|
||||
);
|
||||
};
|
||||
|
||||
const saveGeneralSettings = async () => {
|
||||
const saveGeneralSettings = () => {
|
||||
const changingStaticIp = useSettingsStore().network.connectionType === NetworkConnectionType.Static;
|
||||
|
||||
// replace undefined members with empty strings for backend
|
||||
const payload = {
|
||||
connectionType: tempSettingsStruct.value.connectionType,
|
||||
@@ -96,58 +97,42 @@ const saveGeneralSettings = async () => {
|
||||
staticIp: tempSettingsStruct.value.staticIp
|
||||
};
|
||||
|
||||
const changingStaticIP =
|
||||
useSettingsStore().network.connectionType === NetworkConnectionType.Static &&
|
||||
tempSettingsStruct.value.staticIp !== useSettingsStore().network.staticIp;
|
||||
useSettingsStore()
|
||||
.updateGeneralSettings(payload)
|
||||
.then((response) => {
|
||||
useStateStore().showSnackbarMessage({ message: response.data.text || response.data, color: "success" });
|
||||
|
||||
try {
|
||||
const response = await useSettingsStore().updateGeneralSettings(payload);
|
||||
useStateStore().showSnackbarMessage({ message: response.data.text || response.data, color: "success" });
|
||||
|
||||
// Update the local settings cause the backend checked their validity. Assign is to deref value
|
||||
useSettingsStore().network = { ...useSettingsStore().network, ...Object.assign({}, tempSettingsStruct.value) };
|
||||
} catch (error: any) {
|
||||
resetTempSettingsStruct();
|
||||
if (error.response) {
|
||||
useStateStore().showSnackbarMessage({
|
||||
color: "error",
|
||||
message: error.response.data.text || error.response.data
|
||||
});
|
||||
} else if (error.request) {
|
||||
useStateStore().showSnackbarMessage({
|
||||
color: "error",
|
||||
message: "Error while trying to process the request! The backend didn't respond."
|
||||
});
|
||||
} else {
|
||||
useStateStore().showSnackbarMessage({
|
||||
color: "error",
|
||||
message: "An error occurred while trying to process the request."
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (changingStaticIP) {
|
||||
const status = await statusCheck(5000, tempSettingsStruct.value.staticIp);
|
||||
|
||||
if (!status) {
|
||||
useStateStore().showSnackbarMessage({
|
||||
message:
|
||||
"Warning: Unable to verify new static IP address! You may need to manually navigate to the new address: http://" +
|
||||
tempSettingsStruct.value.staticIp +
|
||||
":5800",
|
||||
color: "warning"
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Keep current hash route (e.g., #/settings)
|
||||
const hash = window.location.hash || "";
|
||||
const url = `http://${tempSettingsStruct.value.staticIp}:5800/${hash}`;
|
||||
setTimeout(() => {
|
||||
window.location.href = url;
|
||||
}, 1000);
|
||||
}
|
||||
// Update the local settings cause the backend checked their validity. Assign is to deref value
|
||||
useSettingsStore().network = { ...useSettingsStore().network, ...Object.assign({}, tempSettingsStruct.value) };
|
||||
})
|
||||
.catch((error) => {
|
||||
resetTempSettingsStruct();
|
||||
if (error.response) {
|
||||
if (error.status === 504 || changingStaticIp) {
|
||||
useStateStore().showSnackbarMessage({
|
||||
color: "error",
|
||||
message: `Connection lost! Try the new static IP at ${useSettingsStore().network.staticIp}:5800 or ${
|
||||
useSettingsStore().network.hostname
|
||||
}:5800?`
|
||||
});
|
||||
} else {
|
||||
useStateStore().showSnackbarMessage({
|
||||
color: "error",
|
||||
message: error.response.data.text || error.response.data
|
||||
});
|
||||
}
|
||||
} else if (error.request) {
|
||||
useStateStore().showSnackbarMessage({
|
||||
color: "error",
|
||||
message: "Error while trying to process the request! The backend didn't respond."
|
||||
});
|
||||
} else {
|
||||
useStateStore().showSnackbarMessage({
|
||||
color: "error",
|
||||
message: "An error occurred while trying to process the request."
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const currentNetworkInterfaceIndex = computed<number | undefined>({
|
||||
@@ -86,7 +86,7 @@ const renameModel = async (model: ObjectDetectionModelProperties, newName: strin
|
||||
});
|
||||
|
||||
axiosPost("/objectdetection/rename", "rename an object detection model", {
|
||||
modelPath: model.modelPath,
|
||||
modelPath: model.modelPath.replace("file:", ""),
|
||||
newName: newName
|
||||
});
|
||||
showRenameDialog.value.show = false;
|
||||
@@ -224,7 +224,6 @@ const handleBulkImport = () => {
|
||||
v-model="importVersion"
|
||||
variant="underlined"
|
||||
label="Model Version"
|
||||
data-testid="import-version-select"
|
||||
:items="
|
||||
useSettingsStore().general.supportedBackends?.includes('RKNN')
|
||||
? ['YOLOv5', 'YOLOv8', 'YOLO11']
|
||||
@@ -325,7 +324,7 @@ const handleBulkImport = () => {
|
||||
<th>Info</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody data-testid="model-table">
|
||||
<tbody>
|
||||
<tr v-for="model in supportedModels" :key="model.modelPath">
|
||||
<td>{{ model.nickname }}</td>
|
||||
<td>{{ model.labels.join(", ") }}</td>
|
||||
@@ -418,7 +417,7 @@ const handleBulkImport = () => {
|
||||
<a
|
||||
ref="exportIndividualModel"
|
||||
style="color: black; text-decoration: none; display: none"
|
||||
:href="`http://${address}/api/objectdetection/exportIndividual?modelPath=${showInfo.model.modelPath}`"
|
||||
:href="`http://${address}/api/objectdetection/exportIndividual?modelPath=${showInfo.model.modelPath.replace('file:', '')}`"
|
||||
:download="`${showInfo.model.nickname}_${showInfo.model.family}_${showInfo.model.version}_${showInfo.model.resolutionWidth}x${showInfo.model.resolutionHeight}_${showInfo.model.labels.join('_')}.${showInfo.model.family.toLowerCase()}`"
|
||||
target="_blank"
|
||||
/>
|
||||
|
||||
@@ -6,49 +6,6 @@ export const resolutionsAreEqual = (a: Resolution, b: Resolution) => {
|
||||
return a.height === b.height && a.width === b.width;
|
||||
};
|
||||
|
||||
/**
|
||||
* Checks the status of the backend by polling the "/status" endpoint.
|
||||
*
|
||||
* This function will repeatedly attempt to send a GET request to the backend
|
||||
* until a successful response is received or the specified timeout is reached.
|
||||
*
|
||||
* @param timeout - The maximum time in milliseconds to wait for a successful response.
|
||||
* @param ip - Optional IP address of the backend server. If not provided, the default endpoint is used. This is meant for the case where the backend is running on a different IP than the frontend.
|
||||
* @returns A promise that resolves to a boolean indicating whether the backend is responsive (true) or not (false).
|
||||
*/
|
||||
export const statusCheck = async (timeout: number, ip?: string): Promise<boolean> => {
|
||||
// Poll the backend until it's responsive or we hit the timeout
|
||||
let pollLimit = Math.floor(timeout / 100);
|
||||
while (pollLimit > 0) {
|
||||
try {
|
||||
pollLimit--;
|
||||
await axios.get(ip ? `http://${ip}/status` : "/status");
|
||||
return true;
|
||||
} catch {
|
||||
// Backend not ready yet, wait and retry
|
||||
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
/**
|
||||
* Forces a page reload after a brief delay and a status check.
|
||||
*/
|
||||
export const forceReloadPage = async () => {
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||
|
||||
useStateStore().showSnackbarMessage({
|
||||
message: "Reloading the page to apply changes...",
|
||||
color: "success"
|
||||
});
|
||||
|
||||
await statusCheck(20000);
|
||||
|
||||
window.location.reload();
|
||||
};
|
||||
|
||||
export const getResolutionString = (resolution: Resolution): string => `${resolution.width}x${resolution.height}`;
|
||||
|
||||
export const parseJsonFile = async <T extends Record<string, any>>(file: File): Promise<T> => {
|
||||
|
||||
@@ -52,12 +52,11 @@ export const restoreThemeConfig = (theme: ThemeInstance) => {
|
||||
: (customSurface ?? defaultTheme.colors!.sidebar!);
|
||||
|
||||
theme.themes.value[theme.global.name.value].colors.primary = customPrimary ?? defaultTheme.colors!.primary!;
|
||||
theme.themes.value[theme.global.name.value].colors.buttonActive =
|
||||
(themeType === "light" ? customPrimary : customSecondary) ?? defaultTheme.colors!.buttonActive!;
|
||||
theme.themes.value[theme.global.name.value].colors.buttonActive = customPrimary ?? defaultTheme.colors!.buttonActive!;
|
||||
|
||||
theme.themes.value[theme.global.name.value].colors.secondary = customSecondary ?? defaultTheme.colors!.secondary!;
|
||||
theme.themes.value[theme.global.name.value].colors.buttonPassive =
|
||||
(themeType === "light" ? customSecondary : customPrimary) ?? defaultTheme.colors!.buttonPassive!;
|
||||
customSecondary ?? defaultTheme.colors!.buttonPassive!;
|
||||
|
||||
theme.themes.value[theme.global.name.value].colors.accent = customSecondary ?? defaultTheme.colors!.accent!;
|
||||
theme.themes.value[theme.global.name.value].colors.toggle = customSecondary ?? defaultTheme.colors!.toggle!;
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
import type { JsonMatOfDouble, Resolution } from "@/types/SettingTypes";
|
||||
const { PerspectiveCamera } = await import("three");
|
||||
|
||||
/**
|
||||
* Convert a camera intrinsics matrix and image resolution to a Three.js PerspectiveCamera. This assumes no skew and square pixels (same focal length in x and y), which is a sane assumption for most FRC cameras
|
||||
*
|
||||
* @param resolution video mode width/height
|
||||
* @param intrinsicsCore camera intrinsics from the backend, row-major
|
||||
* @returns a Three.js PerspectiveCamera matching the provided intrinsics
|
||||
*/
|
||||
export const createPerspectiveCamera = (
|
||||
resolution: Resolution,
|
||||
intrinsicsCore: JsonMatOfDouble,
|
||||
frustumMax: number = 1
|
||||
) => {
|
||||
const imageWidth = resolution.width;
|
||||
const imageHeight = resolution.height;
|
||||
const focalLengthY = intrinsicsCore.data[4];
|
||||
const fovY = 2 * Math.atan(imageHeight / (2 * focalLengthY)) * (180 / Math.PI);
|
||||
const aspect = imageWidth / imageHeight;
|
||||
|
||||
return new PerspectiveCamera(fovY, aspect, 0.1, frustumMax);
|
||||
};
|
||||
@@ -16,11 +16,6 @@ export interface NTConnectionStatus {
|
||||
clients?: number;
|
||||
}
|
||||
|
||||
interface NetworkUsageEntry {
|
||||
time: number;
|
||||
usage: number;
|
||||
}
|
||||
|
||||
interface StateStore {
|
||||
backendConnected: boolean;
|
||||
websocket?: AutoReconnectingWebsocket;
|
||||
@@ -29,7 +24,6 @@ interface StateStore {
|
||||
sidebarFolded: boolean;
|
||||
logMessages: LogMessage[];
|
||||
currentCameraUniqueName: string;
|
||||
networkUsageHistory: NetworkUsageEntry[];
|
||||
|
||||
backendResults: Record<number, PipelineResult>;
|
||||
multitagResultBuffer: Record<string, MultitagResult[]>;
|
||||
@@ -70,7 +64,6 @@ export const useStateStore = defineStore("state", {
|
||||
localStorage.getItem("sidebarFolded") === null ? false : localStorage.getItem("sidebarFolded") === "true",
|
||||
logMessages: [],
|
||||
currentCameraUniqueName: Object.keys(cameraStore.cameras)[0],
|
||||
networkUsageHistory: [],
|
||||
|
||||
backendResults: {
|
||||
0: {
|
||||
|
||||
@@ -27,7 +27,10 @@ export const useCameraSettingsStore = defineStore("cameraSettings", {
|
||||
}),
|
||||
getters: {
|
||||
needsCameraConfiguration(): boolean {
|
||||
return useCameraSettingsStore().cameras["Placeholder Name"] === PlaceholderCameraSettings;
|
||||
return (
|
||||
JSON.stringify(useCameraSettingsStore().cameras[PlaceholderCameraSettings.uniqueName]) ===
|
||||
JSON.stringify(PlaceholderCameraSettings)
|
||||
);
|
||||
},
|
||||
// TODO update types to update this value being undefined. This would be a decently large change.
|
||||
currentCameraSettings(): UiCameraConfiguration {
|
||||
@@ -91,9 +94,6 @@ export const useCameraSettingsStore = defineStore("cameraSettings", {
|
||||
maxWhiteBalanceTemp(): number {
|
||||
return this.currentCameraSettings.maxWhiteBalanceTemp;
|
||||
},
|
||||
fpsLimit(): number {
|
||||
return this.currentCameraSettings.fpsLimit;
|
||||
},
|
||||
isConnected(): boolean {
|
||||
return this.currentCameraSettings.isConnected;
|
||||
},
|
||||
@@ -144,7 +144,6 @@ export const useCameraSettingsStore = defineStore("cameraSettings", {
|
||||
minWhiteBalanceTemp: d.minWhiteBalanceTemp,
|
||||
maxWhiteBalanceTemp: d.maxWhiteBalanceTemp,
|
||||
matchedCameraInfo: d.matchedCameraInfo,
|
||||
fpsLimit: d.fpsLimit,
|
||||
isConnected: d.isConnected,
|
||||
hasConnected: d.hasConnected,
|
||||
mismatch: d.mismatch
|
||||
|
||||
@@ -10,7 +10,6 @@ import { NetworkConnectionType } from "@/types/SettingTypes";
|
||||
import { useStateStore } from "@/stores/StateStore";
|
||||
import axios from "axios";
|
||||
import type { WebsocketSettingsUpdate } from "@/types/WebsocketDataTypes";
|
||||
import { ref } from "vue";
|
||||
|
||||
interface GeneralSettingsStore {
|
||||
general: GeneralSettings;
|
||||
@@ -20,50 +19,6 @@ interface GeneralSettingsStore {
|
||||
currentFieldLayout;
|
||||
}
|
||||
|
||||
interface MetricsEntry {
|
||||
time: number;
|
||||
metrics: MetricData;
|
||||
}
|
||||
|
||||
class MetricsHistory {
|
||||
private MAX_METRIC_HISTORY = 60;
|
||||
private UPDATE_INTERVAL_MS = 900;
|
||||
|
||||
private buffer: (MetricsEntry | undefined)[];
|
||||
private size: number;
|
||||
private index = 0;
|
||||
private count = 0;
|
||||
private lastUpdate = 0;
|
||||
|
||||
constructor(size = this.MAX_METRIC_HISTORY) {
|
||||
this.size = size;
|
||||
this.buffer = new Array<MetricsEntry | undefined>(size);
|
||||
}
|
||||
|
||||
update(value: MetricsEntry): boolean {
|
||||
const now = Date.now();
|
||||
if (now - this.lastUpdate < this.UPDATE_INTERVAL_MS) return false;
|
||||
|
||||
this.lastUpdate = now;
|
||||
this.buffer[this.index] = value;
|
||||
this.index = (this.index + 1) % this.size;
|
||||
this.count = Math.min(this.count + 1, this.size);
|
||||
return true;
|
||||
}
|
||||
|
||||
getHistory(): MetricsEntry[] {
|
||||
const result: MetricsEntry[] = new Array(this.count);
|
||||
for (let i = 0; i < this.count; i++) {
|
||||
const idx = (this.index - this.count + i + this.size) % this.size;
|
||||
result[i] = this.buffer[idx]!;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
const metricsHistoryBuffer = new MetricsHistory();
|
||||
export const metricsHistorySnapshot = ref<MetricsEntry[]>([]);
|
||||
|
||||
export const useSettingsStore = defineStore("settings", {
|
||||
state: (): GeneralSettingsStore => ({
|
||||
general: {
|
||||
@@ -107,12 +62,9 @@ export const useSettingsStore = defineStore("settings", {
|
||||
gpuMem: undefined,
|
||||
gpuMemUtil: undefined,
|
||||
diskUtilPct: undefined,
|
||||
diskUsableSpace: undefined,
|
||||
npuUsage: undefined,
|
||||
ipAddress: undefined,
|
||||
uptime: undefined,
|
||||
sentBitRate: undefined,
|
||||
recvBitRate: undefined
|
||||
uptime: undefined
|
||||
},
|
||||
currentFieldLayout: {
|
||||
field: {
|
||||
@@ -131,6 +83,9 @@ export const useSettingsStore = defineStore("settings", {
|
||||
}
|
||||
},
|
||||
actions: {
|
||||
requestMetricsUpdate() {
|
||||
return axios.post("/utils/publishMetrics");
|
||||
},
|
||||
updateMetricsFromWebsocket(data: Required<MetricData>) {
|
||||
this.metrics = {
|
||||
cpuTemp: data.cpuTemp || undefined,
|
||||
@@ -141,16 +96,10 @@ export const useSettingsStore = defineStore("settings", {
|
||||
gpuMem: data.gpuMem || undefined,
|
||||
gpuMemUtil: data.gpuMemUtil || undefined,
|
||||
diskUtilPct: data.diskUtilPct || undefined,
|
||||
diskUsableSpace: data.diskUsableSpace || undefined,
|
||||
npuUsage: data.npuUsage || undefined,
|
||||
ipAddress: data.ipAddress || undefined,
|
||||
uptime: data.uptime || undefined,
|
||||
sentBitRate: data.sentBitRate || undefined,
|
||||
recvBitRate: data.recvBitRate || undefined
|
||||
uptime: data.uptime || undefined
|
||||
};
|
||||
if (metricsHistoryBuffer.update({ time: Date.now(), metrics: this.metrics })) {
|
||||
metricsHistorySnapshot.value = metricsHistoryBuffer.getHistory();
|
||||
}
|
||||
},
|
||||
updateGeneralSettingsFromWebsocket(data: WebsocketSettingsUpdate) {
|
||||
this.general = {
|
||||
|
||||
@@ -84,8 +84,6 @@ export interface PipelineSettings {
|
||||
|
||||
cameraAutoWhiteBalance: boolean;
|
||||
cameraWhiteBalanceTemp: number;
|
||||
|
||||
blockForFrames: boolean;
|
||||
}
|
||||
export type ConfigurablePipelineSettings = Partial<
|
||||
Omit<
|
||||
@@ -150,8 +148,7 @@ export const DefaultPipelineSettings: Omit<
|
||||
cameraAutoWhiteBalance: false,
|
||||
cameraWhiteBalanceTemp: 4000,
|
||||
cameraMinExposureRaw: 1,
|
||||
cameraMaxExposureRaw: 2,
|
||||
blockForFrames: true
|
||||
cameraMaxExposureRaw: 2
|
||||
};
|
||||
|
||||
export interface ReflectivePipelineSettings extends PipelineSettings {
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { type ActivePipelineSettings, DefaultAprilTagPipelineSettings } from "@/types/PipelineTypes";
|
||||
import type { Pose3d } from "@/types/PhotonTrackingTypes";
|
||||
import type { WebsocketCameraSettingsUpdate } from "./WebsocketDataTypes";
|
||||
import { reactive } from "vue";
|
||||
|
||||
export interface GeneralSettings {
|
||||
version?: string;
|
||||
@@ -34,12 +33,9 @@ export interface MetricData {
|
||||
gpuMem?: number;
|
||||
gpuMemUtil?: number;
|
||||
diskUtilPct?: number;
|
||||
diskUsableSpace?: number;
|
||||
npuUsage?: number[];
|
||||
ipAddress?: string;
|
||||
uptime?: number;
|
||||
sentBitRate?: number;
|
||||
recvBitRate?: number;
|
||||
}
|
||||
|
||||
export enum NetworkConnectionType {
|
||||
@@ -194,7 +190,7 @@ export interface BoardObservation {
|
||||
locationInImageSpace: CvPoint[];
|
||||
reprojectionErrors: CvPoint[];
|
||||
optimisedCameraToObject: Pose3d;
|
||||
cornersUsed: boolean[];
|
||||
includeObservationInCalibration: boolean;
|
||||
snapshotName: string;
|
||||
snapshotData: JsonImageMat;
|
||||
}
|
||||
@@ -205,15 +201,9 @@ export interface CameraCalibrationResult {
|
||||
distCoeffs: JsonMatOfDouble;
|
||||
observations: BoardObservation[];
|
||||
calobjectWarp?: number[];
|
||||
calobjectSize: { width: number; height: number };
|
||||
calobjectSpacing: number;
|
||||
lensModel: string;
|
||||
|
||||
// We have to omit observations for bandwidth, so backend will send us this from UICameraCalibrationCoefficients
|
||||
// We might have to omit observations for bandwidth, so backend will send us this
|
||||
numSnapshots: number;
|
||||
meanErrors: number[];
|
||||
numMissing: number[];
|
||||
numOutliers: number[];
|
||||
}
|
||||
|
||||
export enum ValidQuirks {
|
||||
@@ -273,8 +263,6 @@ export interface UiCameraConfiguration {
|
||||
minWhiteBalanceTemp: number;
|
||||
maxWhiteBalanceTemp: number;
|
||||
|
||||
fpsLimit: number;
|
||||
|
||||
matchedCameraInfo: PVCameraInfo;
|
||||
isConnected: boolean;
|
||||
hasConnected: boolean;
|
||||
@@ -286,7 +274,7 @@ export interface CameraSettingsChangeRequest {
|
||||
quirksToChange: Record<ValidQuirks, boolean>;
|
||||
}
|
||||
|
||||
export const PlaceholderCameraSettings: UiCameraConfiguration = reactive({
|
||||
export const PlaceholderCameraSettings: UiCameraConfiguration = {
|
||||
cameraPath: "/dev/null",
|
||||
|
||||
nickname: "Placeholder Camera",
|
||||
@@ -326,7 +314,7 @@ export const PlaceholderCameraSettings: UiCameraConfiguration = reactive({
|
||||
rows: 1,
|
||||
cols: 1,
|
||||
type: 1,
|
||||
data: [600, 0, 1920 / 2, 0, 600, 1080 / 2, 0, 0, 1]
|
||||
data: [1, 2, 3, 4, 5, 6, 7, 8, 9]
|
||||
},
|
||||
distCoeffs: {
|
||||
rows: 1,
|
||||
@@ -336,72 +324,28 @@ export const PlaceholderCameraSettings: UiCameraConfiguration = reactive({
|
||||
},
|
||||
observations: [
|
||||
{
|
||||
locationInObjectSpace: [
|
||||
{
|
||||
x: 0,
|
||||
y: 0,
|
||||
z: 0
|
||||
},
|
||||
{
|
||||
x: 0.02539999969303608,
|
||||
y: 0,
|
||||
z: 0
|
||||
},
|
||||
{
|
||||
x: 0.05079999938607216,
|
||||
y: 0,
|
||||
z: 0
|
||||
}
|
||||
],
|
||||
locationInImageSpace: [
|
||||
{
|
||||
x: 57.062007904052734,
|
||||
y: 108.12601470947266
|
||||
},
|
||||
{
|
||||
x: 108.72974395751953,
|
||||
y: 108.0336685180664
|
||||
},
|
||||
{
|
||||
x: 158.78118896484375,
|
||||
y: 107.8104019165039
|
||||
}
|
||||
{ x: 100, y: 100 },
|
||||
{ x: 210, y: 100 },
|
||||
{ x: 320, y: 101 }
|
||||
],
|
||||
locationInObjectSpace: [{ x: 0, y: 0, z: 0 }],
|
||||
optimisedCameraToObject: {
|
||||
translation: {
|
||||
x: -0.28942385915178886,
|
||||
y: -0.12895727420625547,
|
||||
z: 0.5933086404370699
|
||||
},
|
||||
rotation: {
|
||||
quaternion: {
|
||||
W: 0.9890028788589879,
|
||||
X: -0.0507354429843431,
|
||||
Y: -0.13458187019694584,
|
||||
Z: -0.034452004994036174
|
||||
}
|
||||
}
|
||||
translation: { x: 1, y: 2, z: 3 },
|
||||
rotation: { quaternion: { W: 1, X: 0, Y: 0, Z: 0 } }
|
||||
},
|
||||
reprojectionErrors: [
|
||||
{ x: 1, y: 1 },
|
||||
{ x: 2, y: 1 },
|
||||
{ x: 3, y: 1 }
|
||||
],
|
||||
cornersUsed: [true, true, false],
|
||||
includeObservationInCalibration: false,
|
||||
snapshotName: "img0.png",
|
||||
snapshotData: { rows: 480, cols: 640, type: CvType.CV_8U, data: "" }
|
||||
}
|
||||
],
|
||||
calobjectSize: {
|
||||
width: 10,
|
||||
height: 10
|
||||
},
|
||||
calobjectSpacing: 0.0254,
|
||||
lensModel: "opencv8",
|
||||
numSnapshots: 1,
|
||||
meanErrors: [123.45],
|
||||
numMissing: [0],
|
||||
numOutliers: [1]
|
||||
meanErrors: [123.45]
|
||||
}
|
||||
],
|
||||
pipelineNicknames: ["Placeholder Pipeline"],
|
||||
@@ -444,11 +388,10 @@ export const PlaceholderCameraSettings: UiCameraConfiguration = reactive({
|
||||
PVCSICameraInfo: undefined,
|
||||
PVUsbCameraInfo: undefined
|
||||
},
|
||||
fpsLimit: -1,
|
||||
isConnected: true,
|
||||
hasConnected: true,
|
||||
mismatch: false
|
||||
});
|
||||
};
|
||||
|
||||
export enum CalibrationBoardTypes {
|
||||
Chessboard = 0,
|
||||
|
||||
@@ -67,7 +67,6 @@ export interface WebsocketCameraSettingsUpdate {
|
||||
minWhiteBalanceTemp: number;
|
||||
maxWhiteBalanceTemp: number;
|
||||
matchedCameraInfo: PVCameraInfo;
|
||||
fpsLimit: number;
|
||||
isConnected: boolean;
|
||||
hasConnected: boolean;
|
||||
mismatch: boolean;
|
||||
|
||||
@@ -87,7 +87,7 @@ const unmatchedCameras = computed(() => {
|
||||
const activeVisionModules = computed(() =>
|
||||
Object.values(useCameraSettingsStore().cameras)
|
||||
// Ignore placeholder camera
|
||||
.filter((camera) => camera !== PlaceholderCameraSettings)
|
||||
.filter((camera) => JSON.stringify(camera) !== JSON.stringify(PlaceholderCameraSettings))
|
||||
// Display connected cameras first
|
||||
.sort(
|
||||
(first, second) =>
|
||||
|
||||
@@ -64,8 +64,10 @@ const cameraMismatchWarningShown = computed<boolean>(() => {
|
||||
return (
|
||||
Object.values(useCameraSettingsStore().cameras)
|
||||
// Ignore placeholder camera
|
||||
.filter((camera) => camera !== PlaceholderCameraSettings)
|
||||
.some((camera) => camera.mismatch)
|
||||
.filter((camera) => JSON.stringify(camera) !== JSON.stringify(PlaceholderCameraSettings))
|
||||
.some((camera) => {
|
||||
return camera.mismatch;
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
@@ -77,10 +79,6 @@ 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 showCameraSetupDialog = ref(useCameraSettingsStore().needsCameraConfiguration);
|
||||
</script>
|
||||
|
||||
@@ -110,19 +108,6 @@ const showCameraSetupDialog = ref(useCameraSettingsStore().needsCameraConfigurat
|
||||
Conflicting hostname detected! Please change the hostname in the <a href="#/settings">Settings tab</a>!
|
||||
</span>
|
||||
</v-alert>
|
||||
<v-alert
|
||||
v-if="fpsLimitWarningShown"
|
||||
class="mb-3"
|
||||
color="error"
|
||||
density="compact"
|
||||
icon="mdi-alert-circle-outline"
|
||||
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'tonal'"
|
||||
>
|
||||
<span
|
||||
>One or more cameras have an FPS limit set! This may cause performance issues. Check your logs for more
|
||||
information.
|
||||
</span>
|
||||
</v-alert>
|
||||
<v-alert
|
||||
v-if="conflictingCameraShown"
|
||||
class="mb-3"
|
||||
@@ -163,7 +148,6 @@ const showCameraSetupDialog = ref(useCameraSettingsStore().needsCameraConfigurat
|
||||
<PipelineConfigCard />
|
||||
|
||||
<!-- TODO - not sure this belongs here -->
|
||||
<!-- Need v-model to allow the dialog to be dismissed and v-if to only display when cameras need configuration -->
|
||||
<v-dialog
|
||||
v-if="useCameraSettingsStore().needsCameraConfiguration"
|
||||
v-model="showCameraSetupDialog"
|
||||
|
||||
@@ -1,21 +1,24 @@
|
||||
<script setup lang="ts">
|
||||
import MetricsCard from "@/components/settings/MetricsCard.vue";
|
||||
import DeviceControlCard from "@/components/settings/DeviceControlCard.vue";
|
||||
import ObjectDetectionCard from "@/components/settings/ObjectDetectionCard.vue";
|
||||
import GlobalSettingsCard from "@/components/settings/GlobalSettingsCard.vue";
|
||||
import NetworkingCard from "@/components/settings/NetworkingCard.vue";
|
||||
import LightingControlCard from "@/components/settings/LEDControlCard.vue";
|
||||
import { useSettingsStore } from "@/stores/settings/GeneralSettingsStore";
|
||||
import ApriltagControlCard from "@/components/settings/ApriltagControlCard.vue";
|
||||
import DeviceCard from "@/components/settings/DeviceCard.vue";
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="pa-3">
|
||||
<DeviceCard />
|
||||
<GlobalSettingsCard />
|
||||
<MetricsCard />
|
||||
<DeviceControlCard />
|
||||
<NetworkingCard />
|
||||
<ObjectDetectionCard v-if="useSettingsStore().general.supportedBackends.length > 0" />
|
||||
<LightingControlCard v-if="useSettingsStore().lighting.supported" />
|
||||
<Suspense>
|
||||
<!-- Allows us to import three js when it's actually needed -->
|
||||
<ApriltagControlCard />
|
||||
|
||||
<template #fallback> Loading... </template>
|
||||
</Suspense>
|
||||
</div>
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
import { test as base } from "@playwright/test";
|
||||
import axios from "axios";
|
||||
|
||||
export const test = base.extend({
|
||||
page: async ({ page }, use) => {
|
||||
// Use the page in the test (no per-test backend reset here)
|
||||
axios.defaults.baseURL = "http://localhost:5800/api/test";
|
||||
await use(page);
|
||||
}
|
||||
});
|
||||
|
||||
test.beforeAll(async () => {
|
||||
console.log("Running before all tests: Resetting backend state...");
|
||||
await axios.post("http://localhost:5800/api/test/resetBackend");
|
||||
await axios.post("http://localhost:5800/api/test/activateTestMode");
|
||||
});
|
||||
@@ -1,7 +0,0 @@
|
||||
async function globalSetup() {
|
||||
// You can perform global setup tasks here, such as starting a server or setting environment variables
|
||||
const path = await import("path");
|
||||
process.env.TESTS_DIR = path.resolve(process.cwd());
|
||||
}
|
||||
|
||||
export default globalSetup;
|
||||
@@ -1,39 +0,0 @@
|
||||
import { expect } from "@playwright/test";
|
||||
import { test } from "./fixtures.ts";
|
||||
|
||||
test("Camera Gain Slider won't go past max or min", async ({ page }) => {
|
||||
await page.goto("http://localhost:5800/#/dashboard");
|
||||
await page.locator("div").filter({ hasText: "Set up some cameras to get started!" }).nth(2).press("Escape");
|
||||
|
||||
// Fill in Camera Gain text field with 1000
|
||||
await page.locator("#input-v-44").fill("1000");
|
||||
await page.locator("#input-v-44").press("Enter");
|
||||
await expect(page.locator("#input-v-44")).toHaveValue("100");
|
||||
|
||||
// Try using buttons to go past the max
|
||||
await page.getByRole("button", { name: "appended action" }).nth(2).click();
|
||||
await expect(page.locator("#input-v-44")).toHaveValue("100");
|
||||
|
||||
// Make sure the value is actually properly limited, not just visually
|
||||
await page.getByRole("button", { name: "prepended action" }).nth(2).click();
|
||||
await expect(page.locator("#input-v-44")).toHaveValue("99");
|
||||
|
||||
await page.locator("#input-v-44").fill("-10");
|
||||
await page.locator("#input-v-44").press("Enter");
|
||||
await expect(page.locator("#input-v-44")).toHaveValue("0");
|
||||
|
||||
await page.getByRole("button", { name: "prepended action" }).nth(2).click();
|
||||
await expect(page.locator("#input-v-44")).toHaveValue("0");
|
||||
|
||||
// Make sure the value is actually properly limited, not just visually
|
||||
await page.getByRole("button", { name: "appended action" }).nth(2).click();
|
||||
await expect(page.locator("#input-v-44")).toHaveValue("1");
|
||||
|
||||
// Make sure that the guard actually prevents value setting, instead of just reverting the value
|
||||
// This can be ensured by making sure the Camera Gain field doesn't disappear (disappears when the value is -1)
|
||||
await page.getByRole("button", { name: "prepended action" }).nth(2).click();
|
||||
await page.getByRole("button", { name: "prepended action" }).nth(2).click();
|
||||
await expect(page.locator("#input-v-44")).toHaveValue("0");
|
||||
|
||||
await expect(page.getByText("Camera Gain", { exact: true })).toBeVisible();
|
||||
});
|
||||
@@ -1,79 +0,0 @@
|
||||
import { expect } from "@playwright/test";
|
||||
import { test } from "../fixtures";
|
||||
import axios from "axios";
|
||||
import path from "path";
|
||||
|
||||
const fakeModelName = "FAKE-MODEL";
|
||||
const fakeLabels = "test, 1, woof";
|
||||
const newModelName = "foo-bar";
|
||||
const platforms = ["LINUX_RK3588_64", "LINUX_QCS6490"];
|
||||
|
||||
for (const platform of platforms) {
|
||||
test.describe(`Platform: ${platform}`, () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto("/#/settings");
|
||||
await axios.post("/override/platform", { platform: platform });
|
||||
await page.reload();
|
||||
});
|
||||
|
||||
test("testSettingsPage", async ({ page }) => {
|
||||
if (platform.endsWith("RK3588_64")) {
|
||||
await expect(page.getByRole("main")).toContainText("Linux AARCH 64-bit with RK3588");
|
||||
} else if (platform.endsWith("QCS6490")) {
|
||||
await expect(page.getByRole("main")).toContainText("Linux AARCH 64-bit with QCS6490");
|
||||
}
|
||||
await expect(page.getByText("Object Detection")).toBeVisible();
|
||||
});
|
||||
|
||||
test("Upload model", async ({ page }) => {
|
||||
const testsDir = process.env.TESTS_DIR;
|
||||
if (!testsDir) {
|
||||
throw new Error("TESTS_DIR is not set");
|
||||
}
|
||||
|
||||
await page.getByRole("button", { name: "Import Model" }).click();
|
||||
await page.getByRole("textbox", { name: "Labels" }).fill(fakeLabels);
|
||||
await page.getByRole("spinbutton", { name: "Width" }).fill("640");
|
||||
await page.getByRole("spinbutton", { name: "Height" }).fill("640");
|
||||
await page.getByTestId("import-version-select").click();
|
||||
await page.getByRole("option", { name: "YOLOv8" }).click();
|
||||
|
||||
const modelFile = platform.endsWith("RK3588_64") ? `${fakeModelName}.rknn` : `${fakeModelName}.tflite`;
|
||||
await page
|
||||
.getByRole("button", { name: "Model File Model File" })
|
||||
.setInputFiles(path.join(testsDir, "tests/resources", modelFile));
|
||||
|
||||
await page.getByRole("button", { name: "Import Object Detection Model" }).click();
|
||||
|
||||
await page.goto("/#/settings");
|
||||
const tableRow = page.getByTestId("model-table").locator("tr", { hasText: fakeModelName });
|
||||
|
||||
await expect(tableRow).toBeVisible();
|
||||
await expect(tableRow).toContainText(fakeLabels);
|
||||
});
|
||||
|
||||
test("Rename model", async ({ page }) => {
|
||||
const tableRow = page.getByTestId("model-table").locator("tr", { hasText: fakeModelName });
|
||||
|
||||
await tableRow.getByRole("button", { name: "Rename Model" }).click();
|
||||
await page.getByRole("textbox", { name: "New Name New Name" }).fill(newModelName);
|
||||
await page.getByRole("button", { name: "Rename", exact: true }).click();
|
||||
|
||||
await page.reload();
|
||||
|
||||
const renamedRow = page.getByTestId("model-table").locator("tr", { hasText: newModelName });
|
||||
await expect(renamedRow).toContainText(fakeLabels);
|
||||
});
|
||||
|
||||
test("Delete model", async ({ page }) => {
|
||||
const tableRow = page.getByTestId("model-table").locator("tr", { hasText: newModelName });
|
||||
|
||||
await tableRow.getByRole("button", { name: "Delete Model" }).click();
|
||||
await page.getByRole("button", { name: "Delete model", exact: true }).click();
|
||||
|
||||
await page.reload();
|
||||
const deletedRow = page.getByTestId("model-table").locator("tr", { hasText: newModelName });
|
||||
await expect(deletedRow).toHaveCount(0);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
apply plugin: 'org.photonvision.tools.WpilibTools'
|
||||
apply plugin: 'edu.wpi.first.WpilibTools'
|
||||
|
||||
import java.nio.file.Path
|
||||
|
||||
@@ -28,39 +28,37 @@ dependencies {
|
||||
wpilibNatives wpilibTools.deps.wpilib("hal")
|
||||
wpilibNatives wpilibTools.deps.wpilibOpenCv("frc" + openCVYear, wpi.versions.opencvVersion.get())
|
||||
|
||||
// These stay as implementation dependencies since they don't have native code that gets packaged
|
||||
// Zip
|
||||
implementation 'org.zeroturnaround:zt-zip:1.14'
|
||||
|
||||
implementation "org.xerial:sqlite-jdbc:3.41.0.0"
|
||||
implementation 'com.diozero:diozero-core:1.4.1'
|
||||
implementation 'com.github.oshi:oshi-core:6.9.1'
|
||||
|
||||
// The JNI libraries use wpilibNatives, the java libraries use implementation
|
||||
if (jniPlatform == "linuxarm64") {
|
||||
wpilibNatives("org.photonvision:rknn_jni-jni:$rknnVersion:$jniPlatform") {
|
||||
transitive = false
|
||||
}
|
||||
wpilibNatives("org.photonvision:rubik_jni-jni:$rubikVersion:$jniPlatform") {
|
||||
transitive = false
|
||||
}
|
||||
wpilibNatives("org.photonvision:photon-libcamera-gl-driver-jni:$libcameraDriverVersion:$jniPlatform") {
|
||||
transitive = false
|
||||
}
|
||||
implementation("org.photonvision:rknn_jni-jni:$rknnVersion:linuxarm64") {
|
||||
transitive = false
|
||||
}
|
||||
|
||||
implementation("org.photonvision:rknn_jni-java:$rknnVersion") {
|
||||
transitive = false
|
||||
}
|
||||
|
||||
implementation("org.photonvision:rubik_jni-jni:$rubikVersion:linuxarm64") {
|
||||
transitive = false
|
||||
}
|
||||
implementation("org.photonvision:rubik_jni-java:$rubikVersion") {
|
||||
transitive = false
|
||||
}
|
||||
|
||||
implementation("org.photonvision:photon-libcamera-gl-driver-jni:$libcameraDriverVersion:linuxarm64") {
|
||||
transitive = false
|
||||
}
|
||||
implementation "org.photonvision:photon-libcamera-gl-driver-java:$libcameraDriverVersion"
|
||||
|
||||
implementation "org.photonvision:photon-mrcal-java:$mrcalVersion"
|
||||
|
||||
wpilibNatives("org.photonvision:photon-mrcal-jni:$mrcalVersion:$jniPlatform") {
|
||||
transitive = false
|
||||
// Only include mrcal natives on platforms that we build for
|
||||
if (!(jniPlatform in [
|
||||
"osxx86-64",
|
||||
"osxarm64"
|
||||
])) {
|
||||
implementation("org.photonvision:photon-mrcal-jni:$mrcalVersion:$wpilibNativeName") {
|
||||
transitive = false
|
||||
}
|
||||
}
|
||||
|
||||
testImplementation group: 'org.junit-pioneer' , name: 'junit-pioneer', version: '2.2.0'
|
||||
|
||||
@@ -1,59 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) Photon Vision.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.photonvision.common;
|
||||
|
||||
import edu.wpi.first.util.CombinedRuntimeLoader;
|
||||
import java.io.IOException;
|
||||
import java.util.HashMap;
|
||||
import org.photonvision.jni.LibraryLoader;
|
||||
|
||||
public class LoadJNI {
|
||||
private static HashMap<JNITypes, Boolean> loadedMap = new HashMap<>();
|
||||
|
||||
public enum JNITypes {
|
||||
RUBIK_DETECTOR("tensorflowlite", "tensorflowlite_c", "external_delegate", "rubik_jni"),
|
||||
RKNN_DETECTOR("rga", "rknnrt", "rknn_jni"),
|
||||
MRCAL("mrcal_jni"),
|
||||
LIBCAMERA("photonlibcamera");
|
||||
|
||||
public final String[] libraries;
|
||||
|
||||
JNITypes(String... libraries) {
|
||||
this.libraries = libraries;
|
||||
}
|
||||
}
|
||||
|
||||
public static synchronized void forceLoad(JNITypes type) throws IOException {
|
||||
loadLibraries();
|
||||
|
||||
if (loadedMap.getOrDefault(type, false)) {
|
||||
return;
|
||||
}
|
||||
|
||||
CombinedRuntimeLoader.loadLibraries(LoadJNI.class, type.libraries);
|
||||
loadedMap.put(type, true);
|
||||
}
|
||||
|
||||
public static boolean loadLibraries() {
|
||||
return LibraryLoader.loadWpiLibraries() && LibraryLoader.loadTargeting();
|
||||
}
|
||||
|
||||
public static boolean hasLoaded(JNITypes t) {
|
||||
return loadedMap.getOrDefault(t, false);
|
||||
}
|
||||
}
|
||||
@@ -191,10 +191,9 @@ public class CameraConfiguration {
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a calibration from our list. If found, the calibration will be "released". If not found,
|
||||
* no-op.
|
||||
* Remove a calibration from our list.
|
||||
*
|
||||
* @param unrotatedImageSize The resolution to remove.
|
||||
* @param calibration The calibration to remove
|
||||
*/
|
||||
public void removeCalibration(Size unrotatedImageSize) {
|
||||
logger.info("deleting calibration " + unrotatedImageSize);
|
||||
|
||||
@@ -28,18 +28,23 @@ public class HardwareConfig {
|
||||
|
||||
// LED control
|
||||
public final ArrayList<Integer> ledPins;
|
||||
public final String ledSetCommand;
|
||||
public final boolean ledsCanDim;
|
||||
public final ArrayList<Integer> ledBrightnessRange;
|
||||
public final int ledPWMFrequency;
|
||||
public final String ledDimCommand;
|
||||
public final String ledBlinkCommand;
|
||||
public final ArrayList<Integer> statusRGBPins;
|
||||
public final boolean statusRGBActiveHigh;
|
||||
|
||||
// Custom GPIO
|
||||
public final String getGPIOCommand;
|
||||
public final String setGPIOCommand;
|
||||
public final String setPWMCommand;
|
||||
public final String setPWMFrequencyCommand;
|
||||
public final String releaseGPIOCommand;
|
||||
// Metrics
|
||||
public final String cpuTempCommand;
|
||||
public final String cpuMemoryCommand;
|
||||
public final String cpuUtilCommand;
|
||||
public final String cpuThrottleReasonCmd;
|
||||
public final String cpuUptimeCommand;
|
||||
public final String gpuMemoryCommand;
|
||||
public final String ramUtilCommand;
|
||||
public final String gpuMemUsageCommand;
|
||||
public final String diskUsageCommand;
|
||||
|
||||
// Device stuff
|
||||
public final String restartHardwareCommand;
|
||||
@@ -50,32 +55,42 @@ public class HardwareConfig {
|
||||
String deviceLogoPath,
|
||||
String supportURL,
|
||||
ArrayList<Integer> ledPins,
|
||||
String ledSetCommand,
|
||||
boolean ledsCanDim,
|
||||
ArrayList<Integer> ledBrightnessRange,
|
||||
int ledPwmFrequency,
|
||||
String ledDimCommand,
|
||||
String ledBlinkCommand,
|
||||
ArrayList<Integer> statusRGBPins,
|
||||
boolean statusRGBActiveHigh,
|
||||
String getGPIOCommand,
|
||||
String setGPIOCommand,
|
||||
String setPWMCommand,
|
||||
String setPWMFrequencyCommand,
|
||||
String releaseGPIOCommand,
|
||||
String cpuTempCommand,
|
||||
String cpuMemoryCommand,
|
||||
String cpuUtilCommand,
|
||||
String cpuThrottleReasonCmd,
|
||||
String cpuUptimeCommand,
|
||||
String gpuMemoryCommand,
|
||||
String ramUtilCommand,
|
||||
String gpuMemUsageCommand,
|
||||
String diskUsageCommand,
|
||||
String restartHardwareCommand,
|
||||
double vendorFOV) {
|
||||
this.deviceName = deviceName;
|
||||
this.deviceLogoPath = deviceLogoPath;
|
||||
this.supportURL = supportURL;
|
||||
this.ledPins = ledPins;
|
||||
this.ledSetCommand = ledSetCommand;
|
||||
this.ledsCanDim = ledsCanDim;
|
||||
this.ledBrightnessRange = ledBrightnessRange;
|
||||
this.ledPWMFrequency = ledPwmFrequency;
|
||||
this.ledDimCommand = ledDimCommand;
|
||||
this.ledBlinkCommand = ledBlinkCommand;
|
||||
this.statusRGBPins = statusRGBPins;
|
||||
this.statusRGBActiveHigh = statusRGBActiveHigh;
|
||||
this.getGPIOCommand = getGPIOCommand;
|
||||
this.setGPIOCommand = setGPIOCommand;
|
||||
this.setPWMCommand = setPWMCommand;
|
||||
this.setPWMFrequencyCommand = setPWMFrequencyCommand;
|
||||
this.releaseGPIOCommand = releaseGPIOCommand;
|
||||
this.cpuTempCommand = cpuTempCommand;
|
||||
this.cpuMemoryCommand = cpuMemoryCommand;
|
||||
this.cpuUtilCommand = cpuUtilCommand;
|
||||
this.cpuThrottleReasonCmd = cpuThrottleReasonCmd;
|
||||
this.cpuUptimeCommand = cpuUptimeCommand;
|
||||
this.gpuMemoryCommand = gpuMemoryCommand;
|
||||
this.ramUtilCommand = ramUtilCommand;
|
||||
this.gpuMemUsageCommand = gpuMemUsageCommand;
|
||||
this.diskUsageCommand = diskUsageCommand;
|
||||
this.restartHardwareCommand = restartHardwareCommand;
|
||||
this.vendorFOV = vendorFOV;
|
||||
}
|
||||
@@ -85,16 +100,21 @@ public class HardwareConfig {
|
||||
deviceLogoPath = "";
|
||||
supportURL = "";
|
||||
ledPins = new ArrayList<>();
|
||||
ledSetCommand = "";
|
||||
ledsCanDim = false;
|
||||
ledBrightnessRange = new ArrayList<>();
|
||||
ledPWMFrequency = 0;
|
||||
ledDimCommand = "";
|
||||
ledBlinkCommand = "";
|
||||
statusRGBPins = new ArrayList<>();
|
||||
statusRGBActiveHigh = false;
|
||||
getGPIOCommand = "";
|
||||
setGPIOCommand = "";
|
||||
setPWMCommand = "";
|
||||
setPWMFrequencyCommand = "";
|
||||
releaseGPIOCommand = "";
|
||||
cpuTempCommand = "";
|
||||
cpuMemoryCommand = "";
|
||||
cpuUtilCommand = "";
|
||||
cpuThrottleReasonCmd = "";
|
||||
cpuUptimeCommand = "";
|
||||
gpuMemoryCommand = "";
|
||||
ramUtilCommand = "";
|
||||
gpuMemUsageCommand = "";
|
||||
diskUsageCommand = "";
|
||||
restartHardwareCommand = "";
|
||||
vendorFOV = -1;
|
||||
}
|
||||
@@ -107,14 +127,19 @@ public class HardwareConfig {
|
||||
}
|
||||
|
||||
/**
|
||||
* @return True if any gpio command has been configured to be non-empty, false otherwise
|
||||
* @return True if any command has been configured to a non-default empty, false otherwise
|
||||
*/
|
||||
public final boolean hasGPIOCommandsConfigured() {
|
||||
return getGPIOCommand != ""
|
||||
|| setGPIOCommand != ""
|
||||
|| setPWMCommand != ""
|
||||
|| setPWMFrequencyCommand != ""
|
||||
|| releaseGPIOCommand != "";
|
||||
public final boolean hasCommandsConfigured() {
|
||||
return cpuTempCommand != ""
|
||||
|| cpuMemoryCommand != ""
|
||||
|| cpuUtilCommand != ""
|
||||
|| cpuThrottleReasonCmd != ""
|
||||
|| cpuUptimeCommand != ""
|
||||
|| gpuMemoryCommand != ""
|
||||
|| ramUtilCommand != ""
|
||||
|| ledBlinkCommand != ""
|
||||
|| gpuMemUsageCommand != ""
|
||||
|| diskUsageCommand != "";
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -127,26 +152,36 @@ public class HardwareConfig {
|
||||
+ supportURL
|
||||
+ ", ledPins="
|
||||
+ ledPins
|
||||
+ ", ledSetCommand="
|
||||
+ ledSetCommand
|
||||
+ ", ledsCanDim="
|
||||
+ ledsCanDim
|
||||
+ ", ledBrightnessRange="
|
||||
+ ledBrightnessRange
|
||||
+ ", ledPWMFrequency="
|
||||
+ ledPWMFrequency
|
||||
+ ", ledDimCommand="
|
||||
+ ledDimCommand
|
||||
+ ", ledBlinkCommand="
|
||||
+ ledBlinkCommand
|
||||
+ ", statusRGBPins="
|
||||
+ statusRGBPins
|
||||
+ ", statusRGBActiveHigh"
|
||||
+ statusRGBActiveHigh
|
||||
+ ", getGPIOCommand="
|
||||
+ getGPIOCommand
|
||||
+ ", setGPIOCommand="
|
||||
+ setGPIOCommand
|
||||
+ ", setPWMCommand="
|
||||
+ setPWMCommand
|
||||
+ ", setPWMFrequencyCommand="
|
||||
+ setPWMFrequencyCommand
|
||||
+ ", releaseGPIOCommand="
|
||||
+ releaseGPIOCommand
|
||||
+ ", cpuTempCommand="
|
||||
+ cpuTempCommand
|
||||
+ ", cpuMemoryCommand="
|
||||
+ cpuMemoryCommand
|
||||
+ ", cpuUtilCommand="
|
||||
+ cpuUtilCommand
|
||||
+ ", cpuThrottleReasonCmd="
|
||||
+ cpuThrottleReasonCmd
|
||||
+ ", cpuUptimeCommand="
|
||||
+ cpuUptimeCommand
|
||||
+ ", gpuMemoryCommand="
|
||||
+ gpuMemoryCommand
|
||||
+ ", ramUtilCommand="
|
||||
+ ramUtilCommand
|
||||
+ ", gpuMemUsageCommand="
|
||||
+ gpuMemUsageCommand
|
||||
+ ", diskUsageCommand="
|
||||
+ diskUsageCommand
|
||||
+ ", restartHardwareCommand="
|
||||
+ restartHardwareCommand
|
||||
+ ", vendorFOV="
|
||||
|
||||
@@ -212,23 +212,12 @@ public class NeuralNetworkModelManager {
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the singleton instance of the NeuralNetworkModelManager. Call getInstance() to use the
|
||||
* default (no reset), or getInstance(true) to reset.
|
||||
* Returns the singleton instance of the NeuralNetworkModelManager
|
||||
*
|
||||
* @return The singleton instance
|
||||
*/
|
||||
public static NeuralNetworkModelManager getInstance() {
|
||||
return getInstance(false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the singleton instance of the NeuralNetworkModelManager, optionally resetting it.
|
||||
*
|
||||
* @param reset If true, resets the instance
|
||||
* @return The singleton instance
|
||||
*/
|
||||
public static NeuralNetworkModelManager getInstance(boolean reset) {
|
||||
if (INSTANCE == null || reset) {
|
||||
if (INSTANCE == null) {
|
||||
INSTANCE = new NeuralNetworkModelManager();
|
||||
}
|
||||
return INSTANCE;
|
||||
@@ -416,7 +405,9 @@ public class NeuralNetworkModelManager {
|
||||
for (ModelProperties model : getShippedProperties(modelsDirectory).getModels()) {
|
||||
if (supportedBackends.contains(model.family())) {
|
||||
supportedProperties.addModelProperties(model);
|
||||
logger.debug("Added shipped model: " + model.modelPath().getFileName().toString());
|
||||
} else {
|
||||
logger.warn(
|
||||
"Skipping model " + model.nickname() + " as it is not supported on this platform.");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -18,7 +18,6 @@
|
||||
package org.photonvision.common.configuration;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonCreator;
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import java.nio.file.Path;
|
||||
import java.util.HashMap;
|
||||
@@ -126,7 +125,6 @@ public class NeuralNetworkPropertyManager {
|
||||
*
|
||||
* @return A list of all models
|
||||
*/
|
||||
@JsonIgnore
|
||||
public ModelProperties[] getModels() {
|
||||
return modelPathToProperties.values().toArray(new ModelProperties[0]);
|
||||
}
|
||||
|
||||
@@ -30,7 +30,6 @@ import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.function.Supplier;
|
||||
import java.util.stream.Collectors;
|
||||
import org.photonvision.common.configuration.CameraConfiguration.LegacyCameraConfigStruct;
|
||||
import org.photonvision.common.configuration.DatabaseSchema.Columns;
|
||||
@@ -254,45 +253,6 @@ public class SqlConfigProvider extends ConfigProvider {
|
||||
return true;
|
||||
}
|
||||
|
||||
private <T> T loadConfigOrDefault(
|
||||
Connection conn, String key, Class<T> ref, Supplier<T> factory) {
|
||||
String configString = getOneConfigFile(conn, key);
|
||||
T configObj;
|
||||
if (!configString.isBlank()) {
|
||||
try {
|
||||
configObj = JacksonUtils.deserialize(configString, ref);
|
||||
logger.info("Loaded " + ref.getSimpleName() + " from database");
|
||||
return configObj;
|
||||
} catch (IOException e) {
|
||||
logger.error("Could not deserialize " + ref.getSimpleName() + " from database!", e);
|
||||
}
|
||||
} else {
|
||||
logger.debug("No " + ref.getSimpleName() + " in database");
|
||||
}
|
||||
// either the config entry is empty or Jackson threw and exception
|
||||
try {
|
||||
configObj = factory.get();
|
||||
logger.info("Loaded default " + ref.getSimpleName());
|
||||
return configObj;
|
||||
} catch (Exception e) {
|
||||
logger.error("Failed to construct a default instance of " + ref.getSimpleName(), e);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private AprilTagFieldLayout atflDefault() {
|
||||
AprilTagFieldLayout atfl;
|
||||
try {
|
||||
atfl = AprilTagFieldLayout.loadField(AprilTagFields.kDefaultField);
|
||||
logger.info("Loaded " + AprilTagFields.kDefaultField.toString() + " field");
|
||||
} catch (UncheckedIOException e) {
|
||||
logger.error("Error loading WPILib field", e);
|
||||
logger.info("Creating an empty field");
|
||||
atfl = new AprilTagFieldLayout(List.of(), 1, 1);
|
||||
}
|
||||
return atfl;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void load() {
|
||||
logger.debug("Loading config...");
|
||||
@@ -300,24 +260,68 @@ public class SqlConfigProvider extends ConfigProvider {
|
||||
if (conn == null) return;
|
||||
|
||||
synchronized (m_mutex) {
|
||||
var hardwareConfig =
|
||||
loadConfigOrDefault(
|
||||
conn, GlobalKeys.HARDWARE_CONFIG, HardwareConfig.class, HardwareConfig::new);
|
||||
var hardwareSettings =
|
||||
loadConfigOrDefault(
|
||||
conn, GlobalKeys.HARDWARE_SETTINGS, HardwareSettings.class, HardwareSettings::new);
|
||||
var networkConfig =
|
||||
loadConfigOrDefault(
|
||||
conn, GlobalKeys.NETWORK_CONFIG, NetworkConfig.class, NetworkConfig::new);
|
||||
var nnProps =
|
||||
loadConfigOrDefault(
|
||||
conn,
|
||||
GlobalKeys.NEURAL_NETWORK_PROPERTIES,
|
||||
NeuralNetworkPropertyManager.class,
|
||||
NeuralNetworkPropertyManager::new);
|
||||
var atfl =
|
||||
loadConfigOrDefault(
|
||||
conn, GlobalKeys.ATFL_CONFIG_FILE, AprilTagFieldLayout.class, this::atflDefault);
|
||||
HardwareConfig hardwareConfig;
|
||||
HardwareSettings hardwareSettings;
|
||||
NetworkConfig networkConfig;
|
||||
AprilTagFieldLayout atfl;
|
||||
NeuralNetworkPropertyManager nnProps;
|
||||
|
||||
try {
|
||||
hardwareConfig =
|
||||
JacksonUtils.deserialize(
|
||||
getOneConfigFile(conn, GlobalKeys.HARDWARE_CONFIG), HardwareConfig.class);
|
||||
} catch (IOException e) {
|
||||
logger.error("Could not deserialize hardware config! Loading defaults", e);
|
||||
hardwareConfig = new HardwareConfig();
|
||||
}
|
||||
|
||||
try {
|
||||
hardwareSettings =
|
||||
JacksonUtils.deserialize(
|
||||
getOneConfigFile(conn, GlobalKeys.HARDWARE_SETTINGS), HardwareSettings.class);
|
||||
} catch (IOException e) {
|
||||
logger.error("Could not deserialize hardware settings! Loading defaults", e);
|
||||
hardwareSettings = new HardwareSettings();
|
||||
}
|
||||
|
||||
try {
|
||||
networkConfig =
|
||||
JacksonUtils.deserialize(
|
||||
getOneConfigFile(conn, GlobalKeys.NETWORK_CONFIG), NetworkConfig.class);
|
||||
} catch (IOException e) {
|
||||
logger.error("Could not deserialize network config! Loading defaults", e);
|
||||
networkConfig = new NetworkConfig();
|
||||
}
|
||||
|
||||
try {
|
||||
atfl =
|
||||
JacksonUtils.deserialize(
|
||||
getOneConfigFile(conn, GlobalKeys.ATFL_CONFIG_FILE), AprilTagFieldLayout.class);
|
||||
} catch (IOException e) {
|
||||
logger.error("Could not deserialize apriltag layout! Loading defaults", e);
|
||||
try {
|
||||
atfl = AprilTagFieldLayout.loadField(AprilTagFields.kDefaultField);
|
||||
} catch (UncheckedIOException e2) {
|
||||
logger.error("Error loading WPILib field", e);
|
||||
atfl = null;
|
||||
}
|
||||
if (atfl == null) {
|
||||
// what do we even do here lmao -- wpilib should always work
|
||||
logger.error("Field layout is *still* null??????");
|
||||
atfl = new AprilTagFieldLayout(List.of(), 1, 1);
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
nnProps =
|
||||
JacksonUtils.deserialize(
|
||||
getOneConfigFile(conn, GlobalKeys.NEURAL_NETWORK_PROPERTIES),
|
||||
NeuralNetworkPropertyManager.class);
|
||||
} catch (IOException e) {
|
||||
logger.error("Could not deserialize neural network properties! Loading defaults", e);
|
||||
nnProps = new NeuralNetworkPropertyManager();
|
||||
}
|
||||
|
||||
var cams = loadCameraConfigs(conn);
|
||||
|
||||
try {
|
||||
|
||||
@@ -51,24 +51,16 @@ public class NTDataPublisher implements CVPipelineResultConsumer {
|
||||
private final BooleanSupplier driverModeSupplier;
|
||||
private final Consumer<Boolean> driverModeConsumer;
|
||||
|
||||
NTDataChangeListener fpsLimitListener;
|
||||
private final Consumer<Integer> fpsLimitConsumer;
|
||||
private final Supplier<Integer> fpsLimitSupplier;
|
||||
|
||||
public NTDataPublisher(
|
||||
String cameraNickname,
|
||||
Supplier<Integer> pipelineIndexSupplier,
|
||||
Consumer<Integer> pipelineIndexConsumer,
|
||||
BooleanSupplier driverModeSupplier,
|
||||
Consumer<Boolean> driverModeConsumer,
|
||||
Supplier<Integer> fpsLimitSupplier,
|
||||
Consumer<Integer> fpsLimitConsumer) {
|
||||
Consumer<Boolean> driverModeConsumer) {
|
||||
this.pipelineIndexSupplier = pipelineIndexSupplier;
|
||||
this.pipelineIndexConsumer = pipelineIndexConsumer;
|
||||
this.driverModeSupplier = driverModeSupplier;
|
||||
this.driverModeConsumer = driverModeConsumer;
|
||||
this.fpsLimitSupplier = fpsLimitSupplier;
|
||||
this.fpsLimitConsumer = fpsLimitConsumer;
|
||||
|
||||
updateCameraNickname(cameraNickname);
|
||||
updateEntries();
|
||||
@@ -111,19 +103,6 @@ public class NTDataPublisher implements CVPipelineResultConsumer {
|
||||
logger.debug("Set driver mode to " + newDriverMode);
|
||||
}
|
||||
|
||||
private void onFPSLimitChange(NetworkTableEvent entryNotification) {
|
||||
var newFPSLimit = (int) entryNotification.valueData.value.getInteger();
|
||||
var originalFPSLimit = fpsLimitSupplier.get();
|
||||
|
||||
if (newFPSLimit == originalFPSLimit) {
|
||||
logger.debug("FPS limit is already " + newFPSLimit);
|
||||
return;
|
||||
}
|
||||
|
||||
fpsLimitConsumer.accept(newFPSLimit);
|
||||
logger.debug("Set FPS limit to " + newFPSLimit);
|
||||
}
|
||||
|
||||
private void removeEntries() {
|
||||
if (pipelineIndexListener != null) pipelineIndexListener.remove();
|
||||
if (driverModeListener != null) driverModeListener.remove();
|
||||
@@ -133,7 +112,6 @@ public class NTDataPublisher implements CVPipelineResultConsumer {
|
||||
private void updateEntries() {
|
||||
if (pipelineIndexListener != null) pipelineIndexListener.remove();
|
||||
if (driverModeListener != null) driverModeListener.remove();
|
||||
if (fpsLimitListener != null) fpsLimitListener.remove();
|
||||
|
||||
ts.updateEntries();
|
||||
|
||||
@@ -144,10 +122,6 @@ public class NTDataPublisher implements CVPipelineResultConsumer {
|
||||
driverModeListener =
|
||||
new NTDataChangeListener(
|
||||
ts.subTable.getInstance(), ts.driverModeSubscriber, this::onDriverModeChange);
|
||||
|
||||
fpsLimitListener =
|
||||
new NTDataChangeListener(
|
||||
ts.subTable.getInstance(), ts.fpsLimitSubscriber, this::onFPSLimitChange);
|
||||
}
|
||||
|
||||
public void updateCameraNickname(String newCameraNickname) {
|
||||
@@ -196,7 +170,6 @@ public class NTDataPublisher implements CVPipelineResultConsumer {
|
||||
|
||||
ts.pipelineIndexPublisher.set(pipelineIndexSupplier.get());
|
||||
ts.driverModePublisher.set(driverModeSupplier.getAsBoolean());
|
||||
ts.fpsLimitPublisher.set(fpsLimitSupplier.get());
|
||||
ts.latencyMillisEntry.set(acceptedResult.getLatencyMillis());
|
||||
ts.fpsEntry.set(acceptedResult.fps);
|
||||
ts.hasTargetEntry.set(acceptedResult.hasTargets());
|
||||
|
||||
@@ -45,6 +45,8 @@ import org.photonvision.common.logging.LogGroup;
|
||||
import org.photonvision.common.logging.LogLevel;
|
||||
import org.photonvision.common.logging.Logger;
|
||||
import org.photonvision.common.networking.NetworkUtils;
|
||||
import org.photonvision.common.scripting.ScriptEventType;
|
||||
import org.photonvision.common.scripting.ScriptManager;
|
||||
import org.photonvision.common.util.TimedTaskManager;
|
||||
import org.photonvision.common.util.file.JacksonUtils;
|
||||
|
||||
@@ -181,6 +183,7 @@ public class NetworkTablesManager {
|
||||
logger.info(msg);
|
||||
HardwareManager.getInstance().setNTConnected(true);
|
||||
|
||||
ScriptManager.queueEvent(ScriptEventType.kNTConnected);
|
||||
getInstance().broadcastVersion();
|
||||
getInstance().broadcastConnectedStatus();
|
||||
|
||||
@@ -250,8 +253,6 @@ public class NetworkTablesManager {
|
||||
String mac = NetworkUtils.getMacAddress();
|
||||
if (!mac.equals(currentMacAddress)) {
|
||||
logger.debug("MAC address changed! New MAC address is " + mac + ", was " + currentMacAddress);
|
||||
kCoprocTable.getSubTable(currentMacAddress).getEntry("hostname").unpublish();
|
||||
kCoprocTable.getSubTable(currentMacAddress).getEntry("cameraNames").unpublish();
|
||||
currentMacAddress = mac;
|
||||
}
|
||||
if (mac.isEmpty()) {
|
||||
@@ -259,13 +260,7 @@ public class NetworkTablesManager {
|
||||
return;
|
||||
}
|
||||
|
||||
var config = ConfigManager.getInstance().getConfig();
|
||||
String hostname;
|
||||
if (config.getNetworkConfig().shouldManage) {
|
||||
hostname = config.getNetworkConfig().hostname;
|
||||
} else {
|
||||
hostname = CameraServerJNI.getHostname();
|
||||
}
|
||||
String hostname = ConfigManager.getInstance().getConfig().getNetworkConfig().hostname;
|
||||
if (hostname == null || hostname.isEmpty()) {
|
||||
logger.error("Cannot check hostname and camera names, hostname is not set!");
|
||||
return;
|
||||
|
||||
@@ -54,8 +54,6 @@ public class UICameraConfiguration {
|
||||
public PVCameraInfo matchedCameraInfo;
|
||||
public boolean mismatch;
|
||||
|
||||
public int fpsLimit;
|
||||
|
||||
// Status for if the underlying device is present and such
|
||||
public boolean isConnected;
|
||||
public boolean hasConnected;
|
||||
|
||||
@@ -19,14 +19,14 @@ package org.photonvision.common.dataflow.websocket;
|
||||
|
||||
import java.util.List;
|
||||
import org.photonvision.PhotonVersion;
|
||||
import org.photonvision.common.LoadJNI;
|
||||
import org.photonvision.common.LoadJNI.JNITypes;
|
||||
import org.photonvision.common.configuration.NeuralNetworkModelManager;
|
||||
import org.photonvision.common.configuration.PhotonConfiguration;
|
||||
import org.photonvision.common.dataflow.networktables.NetworkTablesManager;
|
||||
import org.photonvision.common.hardware.Platform;
|
||||
import org.photonvision.common.networking.NetworkManager;
|
||||
import org.photonvision.common.networking.NetworkUtils;
|
||||
import org.photonvision.mrcal.MrCalJNILoader;
|
||||
import org.photonvision.raspi.LibCameraJNILoader;
|
||||
import org.photonvision.vision.processes.VisionModule;
|
||||
import org.photonvision.vision.processes.VisionSourceManager;
|
||||
|
||||
@@ -53,8 +53,8 @@ public class UIPhotonConfiguration {
|
||||
new UIGeneralSettings(
|
||||
PhotonVersion.versionString,
|
||||
// TODO add support for other types of GPU accel
|
||||
LoadJNI.hasLoaded(JNITypes.LIBCAMERA) ? "Zerocopy Libcamera Working" : "",
|
||||
LoadJNI.hasLoaded(JNITypes.MRCAL),
|
||||
LibCameraJNILoader.getInstance().isSupported() ? "Zerocopy Libcamera Working" : "",
|
||||
MrCalJNILoader.getInstance().isLoaded(),
|
||||
c.neuralNetworkPropertyManager().getModels(),
|
||||
NeuralNetworkModelManager.getInstance().getSupportedBackends(),
|
||||
c.getHardwareConfig().deviceName.isEmpty()
|
||||
|
||||
@@ -0,0 +1,99 @@
|
||||
/*
|
||||
* Copyright (C) Photon Vision.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.photonvision.common.hardware.GPIO;
|
||||
|
||||
import org.photonvision.common.configuration.HardwareConfig;
|
||||
import org.photonvision.common.hardware.Platform;
|
||||
|
||||
public class CustomGPIO extends GPIOBase {
|
||||
private boolean currentState;
|
||||
private final int port;
|
||||
|
||||
public CustomGPIO(int port) {
|
||||
this.port = port;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void togglePin() {
|
||||
if (this.port != -1) {
|
||||
execute(
|
||||
commands
|
||||
.get("setState")
|
||||
.replace("{s}", String.valueOf(!currentState))
|
||||
.replace("{p}", String.valueOf(this.port)));
|
||||
currentState = !currentState;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getPinNumber() {
|
||||
return port;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setStateImpl(boolean state) {
|
||||
if (this.port != -1) {
|
||||
execute(
|
||||
commands
|
||||
.get("setState")
|
||||
.replace("{s}", String.valueOf(state))
|
||||
.replace("{p}", String.valueOf(port)));
|
||||
currentState = state;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean shutdown() {
|
||||
if (this.port != -1) {
|
||||
execute(commands.get("shutdown"));
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean getStateImpl() {
|
||||
return currentState;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void blinkImpl(int pulseTimeMillis, int blinks) {
|
||||
execute(
|
||||
commands
|
||||
.get("blink")
|
||||
.replace("{pulseTime}", String.valueOf(pulseTimeMillis))
|
||||
.replace("{blinks}", String.valueOf(blinks))
|
||||
.replace("{p}", String.valueOf(this.port)));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setBrightnessImpl(int brightness) {
|
||||
execute(
|
||||
commands
|
||||
.get("dim")
|
||||
.replace("{p}", String.valueOf(port))
|
||||
.replace("{v}", String.valueOf(brightness)));
|
||||
}
|
||||
|
||||
public static void setConfig(HardwareConfig config) {
|
||||
if (Platform.isRaspberryPi()) return;
|
||||
commands.replace("setState", config.ledSetCommand);
|
||||
commands.replace("dim", config.ledDimCommand);
|
||||
commands.replace("blink", config.ledBlinkCommand);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
/*
|
||||
* Copyright (C) Photon Vision.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.photonvision.common.hardware.GPIO;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import org.photonvision.common.logging.LogGroup;
|
||||
import org.photonvision.common.logging.Logger;
|
||||
import org.photonvision.common.util.ShellExec;
|
||||
|
||||
public abstract class GPIOBase {
|
||||
private static final Logger logger = new Logger(GPIOBase.class, LogGroup.General);
|
||||
private static final ShellExec runCommand = new ShellExec(true, true);
|
||||
|
||||
protected static HashMap<String, String> commands =
|
||||
new HashMap<>() {
|
||||
{
|
||||
put("setState", "");
|
||||
put("shutdown", "");
|
||||
put("dim", "");
|
||||
put("blink", "");
|
||||
}
|
||||
};
|
||||
|
||||
protected static String execute(String command) {
|
||||
try {
|
||||
runCommand.executeBashCommand(command);
|
||||
} catch (Exception e) {
|
||||
logger.error(Arrays.toString(e.getStackTrace()));
|
||||
return "";
|
||||
}
|
||||
return runCommand.getOutput();
|
||||
}
|
||||
|
||||
public abstract int getPinNumber();
|
||||
|
||||
public void setState(boolean state) {
|
||||
if (getPinNumber() != -1) {
|
||||
setStateImpl(state);
|
||||
}
|
||||
}
|
||||
|
||||
protected abstract void setStateImpl(boolean state);
|
||||
|
||||
public final void setOff() {
|
||||
setState(false);
|
||||
}
|
||||
|
||||
public final void setOn() {
|
||||
setState(true);
|
||||
}
|
||||
|
||||
public void togglePin() {
|
||||
setState(!getStateImpl());
|
||||
}
|
||||
|
||||
public abstract boolean shutdown();
|
||||
|
||||
public final boolean getState() {
|
||||
if (getPinNumber() != -1) {
|
||||
return getStateImpl();
|
||||
} else return false;
|
||||
}
|
||||
|
||||
public abstract boolean getStateImpl();
|
||||
|
||||
public final void blink(int pulseTimeMillis, int blinks) {
|
||||
if (getPinNumber() != -1) {
|
||||
blinkImpl(pulseTimeMillis, blinks);
|
||||
}
|
||||
}
|
||||
|
||||
protected abstract void blinkImpl(int pulseTimeMillis, int blinks);
|
||||
|
||||
public final void setBrightness(int brightness) {
|
||||
if (getPinNumber() != -1) {
|
||||
if (brightness > 100) brightness = 100;
|
||||
if (brightness < 0) brightness = 0;
|
||||
setBrightnessImpl(brightness);
|
||||
}
|
||||
}
|
||||
|
||||
protected abstract void setBrightnessImpl(int brightness);
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
/*
|
||||
* Copyright (C) Photon Vision.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.photonvision.common.hardware.GPIO.pi;
|
||||
|
||||
@SuppressWarnings("SpellCheckingInspection")
|
||||
public enum PigpioCommand {
|
||||
PCMD_READ(3), // int gpio_read(unsigned gpio)
|
||||
PCMD_WRITE(4), // int gpio_write(unsigned gpio, unsigned level)
|
||||
PCMD_WVCLR(27), // int wave_clear(void)
|
||||
PCMD_WVAG(28), // int wave_add_generic(unsigned numPulses, gpioPulse_t *pulses)
|
||||
PCMD_WVHLT(33), // int wave_tx_stop(void)
|
||||
PCMD_WVCRE(49), // int wave_create(void)
|
||||
PCMD_WVDEL(50), // int wave_delete(unsigned wave_id)
|
||||
PCMD_WVTX(51), // int wave_tx_send(unsigned wave_id) (once)
|
||||
PCMD_WVTXR(52), // int wave_tx_send(unsigned wave_id) (repeat)
|
||||
PCMD_GDC(83), // int get_duty_cycle(unsigned user_gpio)
|
||||
PCMD_HP(86), // int hardware_pwm(unsigned gpio, unsigned PWMfreq, unsigned PWMduty)
|
||||
PCMD_WVTXM(100); // int wave_tx_send(unsigned wave_id, unsigned wave_mode)
|
||||
|
||||
public final int value;
|
||||
|
||||
PigpioCommand(int value) {
|
||||
this.value = value;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,343 @@
|
||||
/*
|
||||
* Copyright (C) Photon Vision.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.photonvision.common.hardware.GPIO.pi;
|
||||
|
||||
import java.util.HashMap;
|
||||
|
||||
/**
|
||||
* A class that defines the exceptions that can be thrown by Pigpio.
|
||||
*
|
||||
* <p>Credit to nkolban
|
||||
* https://github.com/nkolban/jpigpio/blob/master/JPigpio/src/jpigpio/PigpioException.java
|
||||
*/
|
||||
@SuppressWarnings({"SpellCheckingInspection", "unused", "RedundantSuppression"})
|
||||
public class PigpioException extends Exception {
|
||||
private int rc = -99999999;
|
||||
private static final long serialVersionUID = 443595760654129068L;
|
||||
|
||||
public PigpioException() {
|
||||
super();
|
||||
}
|
||||
|
||||
public PigpioException(int rc) {
|
||||
super();
|
||||
this.rc = rc;
|
||||
}
|
||||
|
||||
public PigpioException(int rc, String msg) {
|
||||
super(msg);
|
||||
this.rc = rc;
|
||||
}
|
||||
|
||||
public PigpioException(String arg0, Throwable arg1, boolean arg2, boolean arg3) {
|
||||
super(arg0, arg1, arg2, arg3);
|
||||
}
|
||||
|
||||
public PigpioException(String arg0, Throwable arg1) {
|
||||
super(arg0, arg1);
|
||||
}
|
||||
|
||||
public PigpioException(String arg0) {
|
||||
super(arg0);
|
||||
}
|
||||
|
||||
public PigpioException(Throwable arg0) {
|
||||
super(arg0);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getMessage() {
|
||||
return "(" + rc + ") " + getMessageForError(rc);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the error code that was returned by the underlying Pigpio call.
|
||||
*
|
||||
* @return The error code that was returned by the underlying Pigpio call.
|
||||
*/
|
||||
public int getErrorCode() {
|
||||
return rc;
|
||||
} // End of getErrorCode
|
||||
|
||||
// Public constants for the error codes that can be thrown by Pigpio
|
||||
public static final int PI_INIT_FAILED = -1; // gpioInitialise failed
|
||||
public static final int PI_BAD_USER_GPIO = -2; // gpio not 0-31
|
||||
public static final int PI_BAD_GPIO = -3; // gpio not 0-53
|
||||
public static final int PI_BAD_MODE = -4; // mode not 0-7
|
||||
public static final int PI_BAD_LEVEL = -5; // level not 0-1
|
||||
public static final int PI_BAD_PUD = -6; // pud not 0-2
|
||||
public static final int PI_BAD_PULSEWIDTH = -7; // pulsewidth not 0 or 500-2500
|
||||
public static final int PI_BAD_DUTYCYCLE = -8; // dutycycle outside set range
|
||||
public static final int PI_BAD_TIMER = -9; // timer not 0-9
|
||||
public static final int PI_BAD_MS = -10; // ms not 10-60000
|
||||
public static final int PI_BAD_TIMETYPE = -11; // timetype not 0-1
|
||||
public static final int PI_BAD_SECONDS = -12; // seconds < 0
|
||||
public static final int PI_BAD_MICROS = -13; // micros not 0-999999
|
||||
public static final int PI_TIMER_FAILED = -14; // gpioSetTimerFunc failed
|
||||
public static final int PI_BAD_WDOG_TIMEOUT = -15; // timeout not 0-60000
|
||||
public static final int PI_NO_ALERT_FUNC = -16; // DEPRECATED
|
||||
public static final int PI_BAD_CLK_PERIPH = -17; // clock peripheral not 0-1
|
||||
public static final int PI_BAD_CLK_SOURCE = -18; // DEPRECATED
|
||||
public static final int PI_BAD_CLK_MICROS = -19; // clock micros not 1, 2, 4, 5, 8, or 10
|
||||
public static final int PI_BAD_BUF_MILLIS = -20; // buf millis not 100-10000
|
||||
public static final int PI_BAD_DUTYRANGE = -21; // dutycycle range not 25-40000
|
||||
public static final int PI_BAD_DUTY_RANGE = -21; // DEPRECATED (use PI_BAD_DUTYRANGE)
|
||||
public static final int PI_BAD_SIGNUM = -22; // signum not 0-63
|
||||
public static final int PI_BAD_PATHNAME = -23; // can't open pathname
|
||||
public static final int PI_NO_HANDLE = -24; // no handle available
|
||||
public static final int PI_BAD_HANDLE = -25; // unknown handle
|
||||
public static final int PI_BAD_IF_FLAGS = -26; // ifFlags > 3
|
||||
public static final int PI_BAD_CHANNEL = -27; // DMA channel not 0-14
|
||||
public static final int PI_BAD_PRIM_CHANNEL = -27; // DMA primary channel not 0-14
|
||||
public static final int PI_BAD_SOCKET_PORT = -28; // socket port not 1024-32000
|
||||
public static final int PI_BAD_FIFO_COMMAND = -29; // unrecognized fifo command
|
||||
public static final int PI_BAD_SECO_CHANNEL = -30; // DMA secondary channel not 0-6
|
||||
public static final int PI_NOT_INITIALISED = -31; // function called before gpioInitialise
|
||||
public static final int PI_INITIALISED = -32; // function called after gpioInitialise
|
||||
public static final int PI_BAD_WAVE_MODE = -33; // waveform mode not 0-1
|
||||
public static final int PI_BAD_CFG_INTERNAL = -34; // bad parameter in gpioCfgInternals call
|
||||
public static final int PI_BAD_WAVE_BAUD = -35; // baud rate not 50-250K(RX)/50-1M(TX)
|
||||
public static final int PI_TOO_MANY_PULSES = -36; // waveform has too many pulses
|
||||
public static final int PI_TOO_MANY_CHARS = -37; // waveform has too many chars
|
||||
public static final int PI_NOT_SERIAL_GPIO = -38; // no serial read in progress on gpio
|
||||
public static final int PI_BAD_SERIAL_STRUC = -39; // bad (null) serial structure parameter
|
||||
public static final int PI_BAD_SERIAL_BUF = -40; // bad (null) serial buf parameter
|
||||
public static final int PI_NOT_PERMITTED = -41; // gpio operation not permitted
|
||||
public static final int PI_SOME_PERMITTED = -42; // one or more gpios not permitted
|
||||
public static final int PI_BAD_WVSC_COMMND = -43; // bad WVSC subcommand
|
||||
public static final int PI_BAD_WVSM_COMMND = -44; // bad WVSM subcommand
|
||||
public static final int PI_BAD_WVSP_COMMND = -45; // bad WVSP subcommand
|
||||
public static final int PI_BAD_PULSELEN = -46; // trigger pulse length not 1-100
|
||||
public static final int PI_BAD_SCRIPT = -47; // invalid script
|
||||
public static final int PI_BAD_SCRIPT_ID = -48; // unknown script id
|
||||
public static final int PI_BAD_SER_OFFSET = -49; // add serial data offset > 30 minutes
|
||||
public static final int PI_GPIO_IN_USE = -50; // gpio already in use
|
||||
public static final int PI_BAD_SERIAL_COUNT = -51; // must read at least a byte at a time
|
||||
public static final int PI_BAD_PARAM_NUM = -52; // script parameter id not 0-9
|
||||
public static final int PI_DUP_TAG = -53; // script has duplicate tag
|
||||
public static final int PI_TOO_MANY_TAGS = -54; // script has too many tags
|
||||
public static final int PI_BAD_SCRIPT_CMD = -55; // illegal script command
|
||||
public static final int PI_BAD_VAR_NUM = -56; // script variable id not 0-149
|
||||
public static final int PI_NO_SCRIPT_ROOM = -57; // no more room for scripts
|
||||
public static final int PI_NO_MEMORY = -58; // can't allocate temporary memory
|
||||
public static final int PI_SOCK_READ_FAILED = -59; // socket read failed
|
||||
public static final int PI_SOCK_WRIT_FAILED = -60; // socket write failed
|
||||
public static final int PI_TOO_MANY_PARAM = -61; // too many script parameters (> 10)
|
||||
public static final int PI_NOT_HALTED = -62; // script already running or failed
|
||||
public static final int PI_BAD_TAG = -63; // script has unresolved tag
|
||||
public static final int PI_BAD_MICS_DELAY = -64; // bad MICS delay (too large)
|
||||
public static final int PI_BAD_MILS_DELAY = -65; // bad MILS delay (too large)
|
||||
public static final int PI_BAD_WAVE_ID = -66; // non existent wave id
|
||||
public static final int PI_TOO_MANY_CBS = -67; // No more CBs for waveform
|
||||
public static final int PI_TOO_MANY_OOL = -68; // No more OOL for waveform
|
||||
public static final int PI_EMPTY_WAVEFORM = -69; // attempt to create an empty waveform
|
||||
public static final int PI_NO_WAVEFORM_ID = -70; // no more waveforms
|
||||
public static final int PI_I2C_OPEN_FAILED = -71; // can't open I2C device
|
||||
public static final int PI_SER_OPEN_FAILED = -72; // can't open serial device
|
||||
public static final int PI_SPI_OPEN_FAILED = -73; // can't open SPI device
|
||||
public static final int PI_BAD_I2C_BUS = -74; // bad I2C bus
|
||||
public static final int PI_BAD_I2C_ADDR = -75; // bad I2C address
|
||||
public static final int PI_BAD_SPI_CHANNEL = -76; // bad SPI channel
|
||||
public static final int PI_BAD_FLAGS = -77; // bad i2c/spi/ser open flags
|
||||
public static final int PI_BAD_SPI_SPEED = -78; // bad SPI speed
|
||||
public static final int PI_BAD_SER_DEVICE = -79; // bad serial device name
|
||||
public static final int PI_BAD_SER_SPEED = -80; // bad serial baud rate
|
||||
public static final int PI_BAD_PARAM = -81; // bad i2c/spi/ser parameter
|
||||
public static final int PI_I2C_WRITE_FAILED = -82; // i2c write failed
|
||||
public static final int PI_I2C_READ_FAILED = -83; // i2c read failed
|
||||
public static final int PI_BAD_SPI_COUNT = -84; // bad SPI count
|
||||
public static final int PI_SER_WRITE_FAILED = -85; // ser write failed
|
||||
public static final int PI_SER_READ_FAILED = -86; // ser read failed
|
||||
public static final int PI_SER_READ_NO_DATA = -87; // ser read no data available
|
||||
public static final int PI_UNKNOWN_COMMAND = -88; // unknown command
|
||||
public static final int PI_SPI_XFER_FAILED = -89; // spi xfer/read/write failed
|
||||
public static final int PI_BAD_POINTER = -90; // bad (NULL) pointer
|
||||
public static final int PI_NO_AUX_SPI = -91; // need a A+/B+/Pi2 for auxiliary SPI
|
||||
public static final int PI_NOT_PWM_GPIO = -92; // gpio is not in use for PWM
|
||||
public static final int PI_NOT_SERVO_GPIO = -93; // gpio is not in use for servo pulses
|
||||
public static final int PI_NOT_HCLK_GPIO = -94; // gpio has no hardware clock
|
||||
public static final int PI_NOT_HPWM_GPIO = -95; // gpio has no hardware PWM
|
||||
public static final int PI_BAD_HPWM_FREQ = -96; // hardware PWM frequency not 1-125M
|
||||
public static final int PI_BAD_HPWM_DUTY = -97; // hardware PWM dutycycle not 0-1M
|
||||
public static final int PI_BAD_HCLK_FREQ = -98; // hardware clock frequency not 4689-250M
|
||||
public static final int PI_BAD_HCLK_PASS = -99; // need password to use hardware clock 1
|
||||
public static final int PI_HPWM_ILLEGAL = -100; // illegal, PWM in use for main clock
|
||||
public static final int PI_BAD_DATABITS = -101; // serial data bits not 1-32
|
||||
public static final int PI_BAD_STOPBITS = -102; // serial (half) stop bits not 2-8
|
||||
public static final int PI_MSG_TOOBIG = -103; // socket/pipe message too big
|
||||
public static final int PI_BAD_MALLOC_MODE = -104; // bad memory allocation mode
|
||||
public static final int PI_TOO_MANY_SEGS = -105; // too many I2C transaction parts
|
||||
public static final int PI_BAD_I2C_SEG = -106; // a combined I2C transaction failed
|
||||
public static final int PI_BAD_SMBUS_CMD = -107;
|
||||
public static final int PI_NOT_I2C_GPIO = -108;
|
||||
public static final int PI_BAD_I2C_WLEN = -109;
|
||||
public static final int PI_BAD_I2C_RLEN = -110;
|
||||
public static final int PI_BAD_I2C_CMD = -111;
|
||||
public static final int PI_BAD_I2C_BAUD = -112;
|
||||
public static final int PI_CHAIN_LOOP_CNT = -113;
|
||||
public static final int PI_BAD_CHAIN_LOOP = -114;
|
||||
public static final int PI_CHAIN_COUNTER = -115;
|
||||
public static final int PI_BAD_CHAIN_CMD = -116;
|
||||
public static final int PI_BAD_CHAIN_DELAY = -117;
|
||||
public static final int PI_CHAIN_NESTING = -118;
|
||||
public static final int PI_CHAIN_TOO_BIG = -119;
|
||||
public static final int PI_DEPRECATED = -120;
|
||||
public static final int PI_BAD_SER_INVERT = -121;
|
||||
public static final int PI_BAD_EDGE = -122;
|
||||
public static final int PI_BAD_ISR_INIT = -123;
|
||||
public static final int PI_BAD_FOREVER = -124;
|
||||
public static final int PI_BAD_FILTER = -125;
|
||||
|
||||
public static final int PI_PIGIF_ERR_0 = -2000;
|
||||
public static final int PI_PIGIF_ERR_99 = -2099;
|
||||
|
||||
public static final int PI_CUSTOM_ERR_0 = -3000;
|
||||
public static final int PI_CUSTOM_ERR_999 = -3999;
|
||||
|
||||
private static final HashMap<Integer, String> errorMessages = new HashMap<>();
|
||||
|
||||
static {
|
||||
errorMessages.put(PI_INIT_FAILED, "pigpio initialisation failed");
|
||||
errorMessages.put(PI_BAD_USER_GPIO, "GPIO not 0-31");
|
||||
errorMessages.put(PI_BAD_GPIO, "GPIO not 0-53");
|
||||
errorMessages.put(PI_BAD_MODE, "mode not 0-7");
|
||||
errorMessages.put(PI_BAD_LEVEL, "level not 0-1");
|
||||
errorMessages.put(PI_BAD_PUD, "pud not 0-2");
|
||||
errorMessages.put(PI_BAD_PULSEWIDTH, "pulsewidth not 0 or 500-2500");
|
||||
errorMessages.put(PI_BAD_DUTYCYCLE, "dutycycle not 0-range (default 255)");
|
||||
errorMessages.put(PI_BAD_TIMER, "timer not 0-9");
|
||||
errorMessages.put(PI_BAD_MS, "ms not 10-60000");
|
||||
errorMessages.put(PI_BAD_TIMETYPE, "timetype not 0-1");
|
||||
errorMessages.put(PI_BAD_SECONDS, "seconds < 0");
|
||||
errorMessages.put(PI_BAD_MICROS, "micros not 0-999999");
|
||||
errorMessages.put(PI_TIMER_FAILED, "gpioSetTimerFunc failed");
|
||||
errorMessages.put(PI_BAD_WDOG_TIMEOUT, "timeout not 0-60000");
|
||||
errorMessages.put(PI_NO_ALERT_FUNC, "DEPRECATED");
|
||||
errorMessages.put(PI_BAD_CLK_PERIPH, "clock peripheral not 0-1");
|
||||
errorMessages.put(PI_BAD_CLK_SOURCE, "DEPRECATED");
|
||||
errorMessages.put(PI_BAD_CLK_MICROS, "clock micros not 1, 2, 4, 5, 8, or 10");
|
||||
errorMessages.put(PI_BAD_BUF_MILLIS, "buf millis not 100-10000");
|
||||
errorMessages.put(PI_BAD_DUTYRANGE, "dutycycle range not 25-40000");
|
||||
errorMessages.put(PI_BAD_SIGNUM, "signum not 0-63");
|
||||
errorMessages.put(PI_BAD_PATHNAME, "can't open pathname");
|
||||
errorMessages.put(PI_NO_HANDLE, "no handle available");
|
||||
errorMessages.put(PI_BAD_HANDLE, "unknown handle");
|
||||
errorMessages.put(PI_BAD_IF_FLAGS, "ifFlags > 3");
|
||||
errorMessages.put(PI_BAD_CHANNEL, "DMA channel not 0-14");
|
||||
errorMessages.put(PI_BAD_SOCKET_PORT, "socket port not 1024-30000");
|
||||
errorMessages.put(PI_BAD_FIFO_COMMAND, "unknown fifo command");
|
||||
errorMessages.put(PI_BAD_SECO_CHANNEL, "DMA secondary channel not 0-14");
|
||||
errorMessages.put(PI_NOT_INITIALISED, "function called before gpioInitialise");
|
||||
errorMessages.put(PI_INITIALISED, "function called after gpioInitialise");
|
||||
errorMessages.put(PI_BAD_WAVE_MODE, "waveform mode not 0-1");
|
||||
errorMessages.put(PI_BAD_CFG_INTERNAL, "bad parameter in gpioCfgInternals call");
|
||||
errorMessages.put(PI_BAD_WAVE_BAUD, "baud rate not 50-250000(RX)/1000000(TX)");
|
||||
errorMessages.put(PI_TOO_MANY_PULSES, "waveform has too many pulses");
|
||||
errorMessages.put(PI_TOO_MANY_CHARS, "waveform has too many chars");
|
||||
errorMessages.put(PI_NOT_SERIAL_GPIO, "no bit bang serial read in progress on GPIO");
|
||||
errorMessages.put(PI_NOT_PERMITTED, "no permission to update GPIO");
|
||||
errorMessages.put(PI_SOME_PERMITTED, "no permission to update one or more GPIO");
|
||||
errorMessages.put(PI_BAD_WVSC_COMMND, "bad WVSC subcommand");
|
||||
errorMessages.put(PI_BAD_WVSM_COMMND, "bad WVSM subcommand");
|
||||
errorMessages.put(PI_BAD_WVSP_COMMND, "bad WVSP subcommand");
|
||||
errorMessages.put(PI_BAD_PULSELEN, "trigger pulse length not 1-100");
|
||||
errorMessages.put(PI_BAD_SCRIPT, "invalid script");
|
||||
errorMessages.put(PI_BAD_SCRIPT_ID, "unknown script id");
|
||||
errorMessages.put(PI_BAD_SER_OFFSET, "add serial data offset > 30 minute");
|
||||
errorMessages.put(PI_GPIO_IN_USE, "GPIO already in use");
|
||||
errorMessages.put(PI_BAD_SERIAL_COUNT, "must read at least a byte at a time");
|
||||
errorMessages.put(PI_BAD_PARAM_NUM, "script parameter id not 0-9");
|
||||
errorMessages.put(PI_DUP_TAG, "script has duplicate tag");
|
||||
errorMessages.put(PI_TOO_MANY_TAGS, "script has too many tags");
|
||||
errorMessages.put(PI_BAD_SCRIPT_CMD, "illegal script command");
|
||||
errorMessages.put(PI_BAD_VAR_NUM, "script variable id not 0-149");
|
||||
errorMessages.put(PI_NO_SCRIPT_ROOM, "no more room for scripts");
|
||||
errorMessages.put(PI_NO_MEMORY, "can't allocate temporary memory");
|
||||
errorMessages.put(PI_SOCK_READ_FAILED, "socket read failed");
|
||||
errorMessages.put(PI_SOCK_WRIT_FAILED, "socket write failed");
|
||||
errorMessages.put(PI_TOO_MANY_PARAM, "too many script parameters (> 10)");
|
||||
errorMessages.put(PI_NOT_HALTED, "script already running or failed");
|
||||
errorMessages.put(PI_BAD_TAG, "script has unresolved tag");
|
||||
errorMessages.put(PI_BAD_MICS_DELAY, "bad MICS delay (too large)");
|
||||
errorMessages.put(PI_BAD_MILS_DELAY, "bad MILS delay (too large)");
|
||||
errorMessages.put(PI_BAD_WAVE_ID, "non existent wave id");
|
||||
errorMessages.put(PI_TOO_MANY_CBS, "No more CBs for waveform");
|
||||
errorMessages.put(PI_TOO_MANY_OOL, "No more OOL for waveform");
|
||||
errorMessages.put(PI_EMPTY_WAVEFORM, "attempt to create an empty waveform");
|
||||
errorMessages.put(PI_NO_WAVEFORM_ID, "No more waveform ids");
|
||||
errorMessages.put(PI_I2C_OPEN_FAILED, "can't open I2C device");
|
||||
errorMessages.put(PI_SER_OPEN_FAILED, "can't open serial device");
|
||||
errorMessages.put(PI_SPI_OPEN_FAILED, "can't open SPI device");
|
||||
errorMessages.put(PI_BAD_I2C_BUS, "bad I2C bus");
|
||||
errorMessages.put(PI_BAD_I2C_ADDR, "bad I2C address");
|
||||
errorMessages.put(PI_BAD_SPI_CHANNEL, "bad SPI channel");
|
||||
errorMessages.put(PI_BAD_FLAGS, "bad i2c/spi/ser open flags");
|
||||
errorMessages.put(PI_BAD_SPI_SPEED, "bad SPI speed");
|
||||
errorMessages.put(PI_BAD_SER_DEVICE, "bad serial device name");
|
||||
errorMessages.put(PI_BAD_SER_SPEED, "bad serial baud rate");
|
||||
errorMessages.put(PI_BAD_PARAM, "bad i2c/spi/ser parameter");
|
||||
errorMessages.put(PI_I2C_WRITE_FAILED, "I2C write failed");
|
||||
errorMessages.put(PI_I2C_READ_FAILED, "I2C read failed");
|
||||
errorMessages.put(PI_BAD_SPI_COUNT, "bad SPI count");
|
||||
errorMessages.put(PI_SER_WRITE_FAILED, "ser write failed");
|
||||
errorMessages.put(PI_SER_READ_FAILED, "ser read failed");
|
||||
errorMessages.put(PI_SER_READ_NO_DATA, "ser read no data available");
|
||||
errorMessages.put(PI_UNKNOWN_COMMAND, "unknown command");
|
||||
errorMessages.put(PI_SPI_XFER_FAILED, "SPI xfer/read/write failed");
|
||||
errorMessages.put(PI_BAD_POINTER, "bad (NULL) pointer");
|
||||
errorMessages.put(PI_NO_AUX_SPI, "no auxiliary SPI on Pi A or B");
|
||||
errorMessages.put(PI_NOT_PWM_GPIO, "GPIO is not in use for PWM");
|
||||
errorMessages.put(PI_NOT_SERVO_GPIO, "GPIO is not in use for servo pulses");
|
||||
errorMessages.put(PI_NOT_HCLK_GPIO, "GPIO has no hardware clock");
|
||||
errorMessages.put(PI_NOT_HPWM_GPIO, "GPIO has no hardware PWM");
|
||||
errorMessages.put(PI_BAD_HPWM_FREQ, "hardware PWM frequency not 1-125M");
|
||||
errorMessages.put(PI_BAD_HPWM_DUTY, "hardware PWM dutycycle not 0-1M");
|
||||
errorMessages.put(PI_BAD_HCLK_FREQ, "hardware clock frequency not 4689-250M");
|
||||
errorMessages.put(PI_BAD_HCLK_PASS, "need password to use hardware clock 1");
|
||||
errorMessages.put(PI_HPWM_ILLEGAL, "illegal, PWM in use for main clock");
|
||||
errorMessages.put(PI_BAD_DATABITS, "serial data bits not 1-32");
|
||||
errorMessages.put(PI_BAD_STOPBITS, "serial (half) stop bits not 2-8");
|
||||
errorMessages.put(PI_MSG_TOOBIG, "socket/pipe message too big");
|
||||
errorMessages.put(PI_BAD_MALLOC_MODE, "bad memory allocation mode");
|
||||
errorMessages.put(PI_TOO_MANY_SEGS, "too many I2C transaction segments");
|
||||
errorMessages.put(PI_BAD_I2C_SEG, "an I2C transaction segment failed");
|
||||
errorMessages.put(PI_BAD_SMBUS_CMD, "SMBus command not supported");
|
||||
errorMessages.put(PI_NOT_I2C_GPIO, "no bit bang I2C in progress on GPIO");
|
||||
errorMessages.put(PI_BAD_I2C_WLEN, "bad I2C write length");
|
||||
errorMessages.put(PI_BAD_I2C_RLEN, "bad I2C read length");
|
||||
errorMessages.put(PI_BAD_I2C_CMD, "bad I2C command");
|
||||
errorMessages.put(PI_BAD_I2C_BAUD, "bad I2C baud rate, not 50-500k");
|
||||
errorMessages.put(PI_CHAIN_LOOP_CNT, "bad chain loop count");
|
||||
errorMessages.put(PI_BAD_CHAIN_LOOP, "empty chain loop");
|
||||
errorMessages.put(PI_CHAIN_COUNTER, "too many chain counters");
|
||||
errorMessages.put(PI_BAD_CHAIN_CMD, "bad chain command");
|
||||
errorMessages.put(PI_BAD_CHAIN_DELAY, "bad chain delay micros");
|
||||
errorMessages.put(PI_CHAIN_NESTING, "chain counters nested too deeply");
|
||||
errorMessages.put(PI_CHAIN_TOO_BIG, "chain is too long");
|
||||
errorMessages.put(PI_DEPRECATED, "deprecated function removed");
|
||||
errorMessages.put(PI_BAD_SER_INVERT, "bit bang serial invert not 0 or 1");
|
||||
errorMessages.put(PI_BAD_EDGE, "bad ISR edge value, not 0-2");
|
||||
errorMessages.put(PI_BAD_ISR_INIT, "bad ISR initialisation");
|
||||
errorMessages.put(PI_BAD_FOREVER, "loop forever must be last chain command");
|
||||
errorMessages.put(PI_BAD_FILTER, "bad filter parameter");
|
||||
}
|
||||
|
||||
public static String getMessageForError(int errorCode) {
|
||||
return errorMessages.get(errorCode);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
/*
|
||||
* Copyright (C) Photon Vision.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.photonvision.common.hardware.GPIO.pi;
|
||||
|
||||
import org.photonvision.common.hardware.GPIO.GPIOBase;
|
||||
import org.photonvision.common.logging.LogGroup;
|
||||
import org.photonvision.common.logging.Logger;
|
||||
|
||||
public class PigpioPin extends GPIOBase {
|
||||
public static final Logger logger = new Logger(PigpioPin.class, LogGroup.General);
|
||||
private static final PigpioSocket piSocket = new PigpioSocket();
|
||||
|
||||
private final boolean isHardwarePWMPin;
|
||||
private final int pinNo;
|
||||
|
||||
private boolean hasFailedHardwarePWM;
|
||||
|
||||
public PigpioPin(int pinNo) {
|
||||
isHardwarePWMPin = pinNo == 12 || pinNo == 13 || pinNo == 17 || pinNo == 18;
|
||||
this.pinNo = pinNo;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getPinNumber() {
|
||||
return pinNo;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void setStateImpl(boolean state) {
|
||||
try {
|
||||
piSocket.gpioWrite(pinNo, state);
|
||||
} catch (PigpioException e) {
|
||||
logger.error("gpioWrite FAIL - " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean shutdown() {
|
||||
setState(false);
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean getStateImpl() {
|
||||
try {
|
||||
return piSocket.gpioRead(pinNo);
|
||||
} catch (PigpioException e) {
|
||||
logger.error("gpioRead FAIL - " + e.getMessage());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void blinkImpl(int pulseTimeMillis, int blinks) {
|
||||
try {
|
||||
piSocket.generateAndSendWaveform(pulseTimeMillis, blinks, pinNo);
|
||||
} catch (PigpioException e) {
|
||||
logger.error("Could not set blink - " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void setBrightnessImpl(int brightness) {
|
||||
if (isHardwarePWMPin) {
|
||||
try {
|
||||
piSocket.hardwarePWM(pinNo, 22000, (int) (1000000 * (brightness / 100.0)));
|
||||
} catch (PigpioException e) {
|
||||
logger.error("Failed to hardPWM - " + e.getMessage());
|
||||
}
|
||||
} else if (!hasFailedHardwarePWM) {
|
||||
logger.warn(
|
||||
"Specified pin ("
|
||||
+ pinNo
|
||||
+ ") is not capable of hardware PWM - no action will be taken.");
|
||||
hasFailedHardwarePWM = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
/*
|
||||
* Copyright (C) Photon Vision.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.photonvision.common.hardware.GPIO.pi;
|
||||
|
||||
public class PigpioPulse {
|
||||
int gpioOn;
|
||||
int gpioOff;
|
||||
int delayMicros;
|
||||
|
||||
/**
|
||||
* Initialises a pulse.
|
||||
*
|
||||
* @param gpioOn GPIO number to switch on at the start of the pulse. If zero, then no GPIO will be
|
||||
* switched on.
|
||||
* @param gpioOff GPIO number to switch off at the start of the pulse. If zero, then no GPIO will
|
||||
* be switched off.
|
||||
* @param delayMicros the delay in microseconds before the next pulse.
|
||||
*/
|
||||
public PigpioPulse(int gpioOn, int gpioOff, int delayMicros) {
|
||||
this.gpioOn = gpioOn != 0 ? 1 << gpioOn : 0;
|
||||
this.gpioOff = gpioOff != 0 ? 1 << gpioOff : 0;
|
||||
this.delayMicros = delayMicros;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,357 @@
|
||||
/*
|
||||
* Copyright (C) Photon Vision.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.photonvision.common.hardware.GPIO.pi;
|
||||
|
||||
import static org.photonvision.common.hardware.GPIO.pi.PigpioException.*;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.ByteOrder;
|
||||
import java.util.ArrayList;
|
||||
import org.photonvision.common.logging.LogGroup;
|
||||
import org.photonvision.common.logging.Logger;
|
||||
|
||||
@SuppressWarnings({"SpellCheckingInspection", "unused"})
|
||||
public class PigpioSocket {
|
||||
private static final Logger logger = new Logger(PigpioSocket.class, LogGroup.General);
|
||||
private static final int PIGPIOD_MESSAGE_SIZE = 12;
|
||||
|
||||
private PigpioSocketLock commandSocket;
|
||||
private int activeWaveformID = -1;
|
||||
|
||||
/** Creates and starts a socket connection to a pigpio daemon on localhost */
|
||||
public PigpioSocket() {
|
||||
this("127.0.0.1", 8888);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates and starts a socket connection to a pigpio daemon on a remote host with the specified
|
||||
* address and port
|
||||
*
|
||||
* @param addr Address of remote pigpio daemon
|
||||
* @param port Port of remote pigpio daemon
|
||||
*/
|
||||
public PigpioSocket(String addr, int port) {
|
||||
try {
|
||||
commandSocket = new PigpioSocketLock(addr, port);
|
||||
} catch (IOException e) {
|
||||
logger.error("Failed to create or connect to Pigpio Daemon socket", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reconnects to the pigpio daemon
|
||||
*
|
||||
* @throws PigpioException on failure
|
||||
*/
|
||||
public void reconnect() throws PigpioException {
|
||||
try {
|
||||
commandSocket.reconnect();
|
||||
} catch (IOException e) {
|
||||
logger.error("Failed to reconnect to Pigpio Daemon socket", e);
|
||||
throw new PigpioException("reconnect", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Terminates the connection to the pigpio daemon
|
||||
*
|
||||
* @throws PigpioException on failure
|
||||
*/
|
||||
public void gpioTerminate() throws PigpioException {
|
||||
try {
|
||||
commandSocket.terminate();
|
||||
} catch (IOException e) {
|
||||
logger.error("Failed to terminate connection to Pigpio Daemon socket", e);
|
||||
throw new PigpioException("gpioTerminate", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Read the GPIO level
|
||||
*
|
||||
* @param pin Pin to read from
|
||||
* @return Value of the pin
|
||||
* @throws PigpioException on failure
|
||||
*/
|
||||
public boolean gpioRead(int pin) throws PigpioException {
|
||||
try {
|
||||
int retCode = commandSocket.sendCmd(PigpioCommand.PCMD_READ.value, pin);
|
||||
if (retCode < 0) throw new PigpioException(retCode);
|
||||
return retCode != 0;
|
||||
} catch (IOException e) {
|
||||
logger.error("Failed to read GPIO pin: " + pin, e);
|
||||
throw new PigpioException("gpioRead", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Write the GPIO level
|
||||
*
|
||||
* @param pin Pin to write to
|
||||
* @param value Value to write
|
||||
* @throws PigpioException on failure
|
||||
*/
|
||||
public void gpioWrite(int pin, boolean value) throws PigpioException {
|
||||
try {
|
||||
int retCode = commandSocket.sendCmd(PigpioCommand.PCMD_WRITE.value, pin, value ? 1 : 0);
|
||||
if (retCode < 0) throw new PigpioException(retCode);
|
||||
} catch (IOException e) {
|
||||
logger.error("Failed to write to GPIO pin: " + pin, e);
|
||||
throw new PigpioException("gpioWrite", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears all waveforms and any data added by calls to {@link #waveAddGeneric(ArrayList)}
|
||||
*
|
||||
* @throws PigpioException on failure
|
||||
*/
|
||||
public void waveClear() throws PigpioException {
|
||||
try {
|
||||
int retCode = commandSocket.sendCmd(PigpioCommand.PCMD_WVCLR.value);
|
||||
if (retCode < 0) throw new PigpioException(retCode);
|
||||
} catch (IOException e) {
|
||||
logger.error("Failed to clear waveforms", e);
|
||||
throw new PigpioException("waveClear", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a number of pulses to the current waveform
|
||||
*
|
||||
* @param pulses ArrayList of pulses to add
|
||||
* @return the new total number of pulses in the current waveform
|
||||
* @throws PigpioException on failure
|
||||
*/
|
||||
private int waveAddGeneric(ArrayList<PigpioPulse> pulses) throws PigpioException {
|
||||
// pigpio wave message format
|
||||
|
||||
// I p1 0
|
||||
// I p2 0
|
||||
// I p3 pulses * 12
|
||||
// ## extension ##
|
||||
// III on/off/delay * pulses
|
||||
|
||||
if (pulses == null || pulses.isEmpty()) return 0;
|
||||
|
||||
try {
|
||||
ByteBuffer bb = ByteBuffer.allocate(pulses.size() * 12);
|
||||
bb.order(ByteOrder.LITTLE_ENDIAN);
|
||||
for (var pulse : pulses) {
|
||||
bb.putInt(pulse.gpioOn).putInt(pulse.gpioOff).putInt(pulse.delayMicros);
|
||||
}
|
||||
|
||||
int retCode =
|
||||
commandSocket.sendCmd(
|
||||
PigpioCommand.PCMD_WVAG.value,
|
||||
0,
|
||||
0,
|
||||
pulses.size() * PIGPIOD_MESSAGE_SIZE,
|
||||
bb.array());
|
||||
if (retCode < 0) throw new PigpioException(retCode);
|
||||
|
||||
return retCode;
|
||||
} catch (IOException e) {
|
||||
logger.error("Failed to add pulse(s) to waveform", e);
|
||||
throw new PigpioException("waveAddGeneric", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates pulses and adds them to the current waveform
|
||||
*
|
||||
* @param pulseTimeMillis Pulse length in milliseconds
|
||||
* @param blinks Number of times to pulse. -1 for repeat
|
||||
* @param pinNo Pin to pulse
|
||||
*/
|
||||
private void addBlinkPulsesToWaveform(int pulseTimeMillis, int blinks, int pinNo) {
|
||||
boolean repeat = blinks == -1;
|
||||
|
||||
if (blinks == 0) return;
|
||||
|
||||
if (repeat) {
|
||||
blinks = 1;
|
||||
}
|
||||
|
||||
try {
|
||||
ArrayList<PigpioPulse> pulses = new ArrayList<>();
|
||||
var startPulse = new PigpioPulse(pinNo, 0, pulseTimeMillis * 1000);
|
||||
var endPulse = new PigpioPulse(0, pinNo, pulseTimeMillis * 1000);
|
||||
|
||||
for (int i = 0; i < blinks; i++) {
|
||||
pulses.add(startPulse);
|
||||
pulses.add(endPulse);
|
||||
}
|
||||
|
||||
waveAddGeneric(pulses);
|
||||
pulses.clear();
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates and sends a waveform to the given pins with the specified parameters.
|
||||
*
|
||||
* @param pulseTimeMillis Pulse length in milliseconds
|
||||
* @param blinks Number of times to pulse. -1 for repeat
|
||||
* @param pins Pins to pulse
|
||||
* @throws PigpioException on failure
|
||||
*/
|
||||
public void generateAndSendWaveform(int pulseTimeMillis, int blinks, int... pins)
|
||||
throws PigpioException {
|
||||
if (pins.length == 0) return;
|
||||
boolean repeat = blinks == -1;
|
||||
if (blinks == 0) return;
|
||||
|
||||
// stop any active waves
|
||||
waveTxStop();
|
||||
waveClear();
|
||||
|
||||
if (activeWaveformID != -1) {
|
||||
waveDelete(activeWaveformID);
|
||||
activeWaveformID = -1;
|
||||
}
|
||||
|
||||
for (int pin : pins) {
|
||||
addBlinkPulsesToWaveform(pulseTimeMillis, blinks, pin);
|
||||
}
|
||||
|
||||
int waveformId = waveCreate();
|
||||
|
||||
if (waveformId >= 0) {
|
||||
if (repeat) {
|
||||
waveSendRepeat(waveformId);
|
||||
} else {
|
||||
waveSendOnce(waveformId);
|
||||
}
|
||||
} else {
|
||||
logger.error("Failed to send wave: " + getMessageForError(waveformId));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Stops the transmission of the current waveform
|
||||
*
|
||||
* @return success
|
||||
* @throws PigpioException on failure
|
||||
*/
|
||||
public boolean waveTxStop() throws PigpioException {
|
||||
try {
|
||||
int retCode = commandSocket.sendCmd(PigpioCommand.PCMD_WVHLT.value);
|
||||
if (retCode < 0) throw new PigpioException(retCode);
|
||||
return retCode == 0;
|
||||
} catch (IOException e) {
|
||||
logger.error("Failed to stop waveform", e);
|
||||
throw new PigpioException("waveTxStop", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a waveform from the data provided by the prior calls to {@link
|
||||
* #waveAddGeneric(ArrayList)} Upon success a wave ID greater than or equal to 0 is returned
|
||||
*
|
||||
* @return ID of the created waveform
|
||||
* @throws PigpioException on failure
|
||||
*/
|
||||
public int waveCreate() throws PigpioException {
|
||||
try {
|
||||
int retCode = commandSocket.sendCmd(PigpioCommand.PCMD_WVCRE.value);
|
||||
if (retCode < 0) throw new PigpioException(retCode);
|
||||
return retCode;
|
||||
} catch (IOException e) {
|
||||
logger.error("Failed to create new waveform", e);
|
||||
throw new PigpioException("waveCreate", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes the waveform with specified wave ID
|
||||
*
|
||||
* @param waveId ID of the waveform to delete
|
||||
* @throws PigpioException on failure
|
||||
*/
|
||||
public void waveDelete(int waveId) throws PigpioException {
|
||||
try {
|
||||
int retCode = commandSocket.sendCmd(PigpioCommand.PCMD_WVDEL.value, waveId);
|
||||
if (retCode < 0) throw new PigpioException(retCode);
|
||||
} catch (IOException e) {
|
||||
logger.error("Failed to delete wave: " + waveId, e);
|
||||
throw new PigpioException("waveDelete", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Transmits the waveform with specified wave ID. The waveform is sent once
|
||||
*
|
||||
* @param waveId ID of the waveform to transmit
|
||||
* @return The number of DMA control blocks in the waveform
|
||||
* @throws PigpioException on failure
|
||||
*/
|
||||
public int waveSendOnce(int waveId) throws PigpioException {
|
||||
try {
|
||||
int retCode = commandSocket.sendCmd(PigpioCommand.PCMD_WVTX.value, waveId);
|
||||
if (retCode < 0) throw new PigpioException(retCode);
|
||||
return retCode;
|
||||
} catch (IOException e) {
|
||||
throw new PigpioException("waveSendOnce", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Transmits the waveform with specified wave ID. The waveform cycles until cancelled (either by
|
||||
* the sending of a new waveform or {@link #waveTxStop()}
|
||||
*
|
||||
* @param waveId ID of the waveform to transmit
|
||||
* @return The number of DMA control blocks in the waveform
|
||||
* @throws PigpioException on failure
|
||||
*/
|
||||
public int waveSendRepeat(int waveId) throws PigpioException {
|
||||
try {
|
||||
int retCode = commandSocket.sendCmd(PigpioCommand.PCMD_WVTXR.value, waveId);
|
||||
if (retCode < 0) throw new PigpioException(retCode);
|
||||
return retCode;
|
||||
} catch (IOException e) {
|
||||
throw new PigpioException("waveSendRepeat", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts hardware PWM on a GPIO at the specified frequency and dutycycle
|
||||
*
|
||||
* @param pin GPIO pin to start PWM on
|
||||
* @param pwmFrequency Frequency to run at (1Hz-125MHz). Frequencies above 30MHz are unlikely to
|
||||
* work
|
||||
* @param pwmDuty Duty cycle to run at (0-1,000,000)
|
||||
* @throws PigpioException on failure
|
||||
*/
|
||||
public void hardwarePWM(int pin, int pwmFrequency, int pwmDuty) throws PigpioException {
|
||||
try {
|
||||
ByteBuffer bb = ByteBuffer.allocate(4);
|
||||
bb.order(ByteOrder.LITTLE_ENDIAN);
|
||||
bb.putInt(pwmDuty);
|
||||
|
||||
int retCode =
|
||||
commandSocket.sendCmd(PigpioCommand.PCMD_HP.value, pin, pwmFrequency, 4, bb.array());
|
||||
if (retCode < 0) throw new PigpioException(retCode);
|
||||
} catch (IOException e) {
|
||||
throw new PigpioException("hardwarePWM", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,147 @@
|
||||
/*
|
||||
* Copyright (C) Photon Vision.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.photonvision.common.hardware.GPIO.pi;
|
||||
|
||||
import java.io.DataInputStream;
|
||||
import java.io.DataOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.net.Socket;
|
||||
import java.nio.ByteBuffer;
|
||||
|
||||
/**
|
||||
* Credit to nkolban
|
||||
* https://github.com/nkolban/jpigpio/blob/master/JPigpio/src/jpigpio/SocketLock.java
|
||||
*/
|
||||
final class PigpioSocketLock {
|
||||
private static final int replyTimeoutMillis = 1000;
|
||||
|
||||
private final String addr;
|
||||
private final int port;
|
||||
|
||||
private Socket socket;
|
||||
private DataInputStream in;
|
||||
private DataOutputStream out;
|
||||
|
||||
public PigpioSocketLock(String addr, int port) throws IOException {
|
||||
this.addr = addr;
|
||||
this.port = port;
|
||||
reconnect();
|
||||
}
|
||||
|
||||
public void reconnect() throws IOException {
|
||||
socket = new Socket(addr, port);
|
||||
out = new DataOutputStream(socket.getOutputStream());
|
||||
in = new DataInputStream(socket.getInputStream());
|
||||
}
|
||||
|
||||
public void terminate() throws IOException {
|
||||
in.close();
|
||||
in = null;
|
||||
|
||||
out.flush();
|
||||
out.close();
|
||||
out = null;
|
||||
|
||||
socket.close();
|
||||
socket = null;
|
||||
}
|
||||
|
||||
public synchronized int sendCmd(int cmd) throws IOException {
|
||||
byte[] b = {};
|
||||
return sendCmd(cmd, 0, 0, 0, b);
|
||||
}
|
||||
|
||||
public synchronized int sendCmd(int cmd, int p1) throws IOException {
|
||||
byte[] b = {};
|
||||
return sendCmd(cmd, p1, 0, 0, b);
|
||||
}
|
||||
|
||||
public synchronized int sendCmd(int cmd, int p1, int p2) throws IOException {
|
||||
byte[] b = {};
|
||||
return sendCmd(cmd, p1, p2, 0, b);
|
||||
}
|
||||
|
||||
public synchronized int sendCmd(int cmd, int p1, int p2, int p3) throws IOException {
|
||||
byte[] b = {};
|
||||
return sendCmd(cmd, p1, p2, p3, b);
|
||||
}
|
||||
|
||||
/**
|
||||
* Send extended command to pigpiod and return result code
|
||||
*
|
||||
* @param cmd Command to send
|
||||
* @param p1 Command parameter 1
|
||||
* @param p2 Command parameter 2
|
||||
* @param p3 Command parameter 3 (usually length of extended data - see paramater ext)
|
||||
* @param ext Array of bytes containing extended data
|
||||
* @return Command result code
|
||||
* @throws IOException in case of network connection error
|
||||
*/
|
||||
@SuppressWarnings("UnusedAssignment")
|
||||
public synchronized int sendCmd(int cmd, int p1, int p2, int p3, byte[] ext) throws IOException {
|
||||
ByteBuffer bb = ByteBuffer.allocate(16 + ext.length);
|
||||
|
||||
bb.putInt(Integer.reverseBytes(cmd));
|
||||
bb.putInt(Integer.reverseBytes(p1));
|
||||
bb.putInt(Integer.reverseBytes(p2));
|
||||
bb.putInt(Integer.reverseBytes(p3));
|
||||
|
||||
if (ext.length > 0) {
|
||||
bb.put(ext);
|
||||
}
|
||||
|
||||
out.write(bb.array());
|
||||
out.flush();
|
||||
|
||||
int w = replyTimeoutMillis;
|
||||
int a = in.available();
|
||||
|
||||
// if by any chance there is no response from pigpiod, then wait up to
|
||||
// specified timeout
|
||||
while (w > 0 && a < 16) {
|
||||
w -= 10;
|
||||
try {
|
||||
Thread.sleep(10);
|
||||
} catch (InterruptedException ignored) {
|
||||
}
|
||||
a = in.available();
|
||||
}
|
||||
|
||||
// throw exception if response from pigpiod has not arrived yet
|
||||
if (in.available() < 16) {
|
||||
throw new IOException(
|
||||
"Timeout: No response from pigpio daemon within " + replyTimeoutMillis + " ms.");
|
||||
}
|
||||
|
||||
int resp = Integer.reverseBytes(in.readInt()); // ignore response
|
||||
resp = Integer.reverseBytes(in.readInt()); // ignore response
|
||||
resp = Integer.reverseBytes(in.readInt()); // ignore response
|
||||
resp = Integer.reverseBytes(in.readInt()); // contains error or response
|
||||
return resp;
|
||||
}
|
||||
|
||||
/**
|
||||
* Read all remaining bytes coming from pigpiod
|
||||
*
|
||||
* @param data Array to store read bytes.
|
||||
* @throws IOException if unable to read from network
|
||||
*/
|
||||
public void readBytes(byte[] data) throws IOException {
|
||||
in.readFully(data);
|
||||
}
|
||||
}
|
||||
@@ -17,27 +17,23 @@
|
||||
|
||||
package org.photonvision.common.hardware;
|
||||
|
||||
import com.diozero.api.DeviceMode;
|
||||
import com.diozero.internal.spi.NativeDeviceFactoryInterface;
|
||||
import com.diozero.sbc.BoardPinInfo;
|
||||
import com.diozero.sbc.DeviceFactoryHelper;
|
||||
import edu.wpi.first.networktables.IntegerPublisher;
|
||||
import edu.wpi.first.networktables.IntegerSubscriber;
|
||||
import java.io.IOException;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.function.Supplier;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import org.photonvision.common.configuration.ConfigManager;
|
||||
import org.photonvision.common.configuration.HardwareConfig;
|
||||
import org.photonvision.common.configuration.HardwareSettings;
|
||||
import org.photonvision.common.dataflow.networktables.NTDataChangeListener;
|
||||
import org.photonvision.common.dataflow.networktables.NetworkTablesManager;
|
||||
import org.photonvision.common.hardware.gpio.CustomAdapter;
|
||||
import org.photonvision.common.hardware.gpio.CustomDeviceFactory;
|
||||
import org.photonvision.common.hardware.GPIO.CustomGPIO;
|
||||
import org.photonvision.common.hardware.GPIO.pi.PigpioSocket;
|
||||
import org.photonvision.common.hardware.metrics.MetricsManager;
|
||||
import org.photonvision.common.logging.LogGroup;
|
||||
import org.photonvision.common.logging.Logger;
|
||||
import org.photonvision.common.util.ShellExec;
|
||||
import org.photonvision.common.util.TimedTaskManager;
|
||||
|
||||
public class HardwareManager {
|
||||
private static HardwareManager instance;
|
||||
@@ -48,6 +44,8 @@ public class HardwareManager {
|
||||
private final HardwareConfig hardwareConfig;
|
||||
private final HardwareSettings hardwareSettings;
|
||||
|
||||
private final MetricsManager metricsManager;
|
||||
|
||||
@SuppressWarnings({"FieldCanBeLocal", "unused"})
|
||||
private final StatusLED statusLED;
|
||||
|
||||
@@ -61,6 +59,8 @@ public class HardwareManager {
|
||||
|
||||
public final VisionLED visionLED; // May be null if no LED is specified
|
||||
|
||||
private final PigpioSocket pigpioSocket; // will be null unless on Raspi
|
||||
|
||||
public static HardwareManager getInstance() {
|
||||
if (instance == null) {
|
||||
var conf = ConfigManager.getInstance().getConfig();
|
||||
@@ -73,6 +73,12 @@ public class HardwareManager {
|
||||
this.hardwareConfig = hardwareConfig;
|
||||
this.hardwareSettings = hardwareSettings;
|
||||
|
||||
this.metricsManager = new MetricsManager();
|
||||
this.metricsManager.setConfig(hardwareConfig);
|
||||
|
||||
TimedTaskManager.getInstance()
|
||||
.addTask("Metrics Publisher", this.metricsManager::publishMetrics, 5000);
|
||||
|
||||
ledModeRequest =
|
||||
NetworkTablesManager.getInstance()
|
||||
.kRootTable
|
||||
@@ -82,44 +88,32 @@ public class HardwareManager {
|
||||
NetworkTablesManager.getInstance().kRootTable.getIntegerTopic("ledModeState").publish();
|
||||
ledModeState.set(VisionLEDMode.kDefault.value);
|
||||
|
||||
// Device factory is lazy to prevent creating one if it will go unused.
|
||||
Supplier<NativeDeviceFactoryInterface> lazyDeviceFactory =
|
||||
new Supplier<NativeDeviceFactoryInterface>() {
|
||||
NativeDeviceFactoryInterface deviceFactory = null;
|
||||
CustomGPIO.setConfig(hardwareConfig);
|
||||
|
||||
@Override
|
||||
public NativeDeviceFactoryInterface get() {
|
||||
if (deviceFactory == null) {
|
||||
if (hardwareConfig.hasGPIOCommandsConfigured()) {
|
||||
deviceFactory = HardwareManager.configureCustomGPIO(hardwareConfig);
|
||||
} else {
|
||||
deviceFactory = DeviceFactoryHelper.getNativeDeviceFactory();
|
||||
}
|
||||
}
|
||||
|
||||
return deviceFactory;
|
||||
}
|
||||
};
|
||||
if (Platform.isRaspberryPi()) {
|
||||
pigpioSocket = new PigpioSocket();
|
||||
} else {
|
||||
pigpioSocket = null;
|
||||
}
|
||||
|
||||
statusLED =
|
||||
hardwareConfig.statusRGBPins.size() == 3
|
||||
? new StatusLED(
|
||||
lazyDeviceFactory.get(),
|
||||
hardwareConfig.statusRGBPins,
|
||||
hardwareConfig.statusRGBActiveHigh)
|
||||
? new StatusLED(hardwareConfig.statusRGBPins)
|
||||
: null;
|
||||
|
||||
if (statusLED != null) {
|
||||
TimedTaskManager.getInstance().addTask("StatusLEDUpdate", this::statusLEDUpdate, 150);
|
||||
}
|
||||
|
||||
var hasBrightnessRange = hardwareConfig.ledBrightnessRange.size() == 2;
|
||||
visionLED =
|
||||
hardwareConfig.ledPins.isEmpty()
|
||||
? null
|
||||
: new VisionLED(
|
||||
lazyDeviceFactory.get(),
|
||||
hardwareConfig.ledPins,
|
||||
hardwareConfig.ledsCanDim,
|
||||
hasBrightnessRange ? hardwareConfig.ledBrightnessRange.get(0) : 0,
|
||||
hasBrightnessRange ? hardwareConfig.ledBrightnessRange.get(1) : 100,
|
||||
hardwareConfig.ledPWMFrequency,
|
||||
pigpioSocket,
|
||||
ledModeState::set);
|
||||
|
||||
ledModeListener =
|
||||
@@ -141,33 +135,6 @@ public class HardwareManager {
|
||||
// if (Platform.isLinux()) MetricsPublisher.getInstance().startTask();
|
||||
}
|
||||
|
||||
public static NativeDeviceFactoryInterface configureCustomGPIO(HardwareConfig hardwareConfig) {
|
||||
// Create a new adapter and device factory using the commands from hardwareConfig
|
||||
CustomAdapter adapter =
|
||||
new CustomAdapter(
|
||||
hardwareConfig.getGPIOCommand,
|
||||
hardwareConfig.setGPIOCommand,
|
||||
hardwareConfig.setPWMCommand,
|
||||
hardwareConfig.setPWMFrequencyCommand,
|
||||
hardwareConfig.setPWMFrequencyCommand);
|
||||
CustomDeviceFactory deviceFactory = new CustomDeviceFactory(adapter);
|
||||
BoardPinInfo pinInfo = deviceFactory.getBoardPinInfo();
|
||||
|
||||
// Populate pin info according to hardware config
|
||||
for (int pin : hardwareConfig.ledPins) {
|
||||
if (hardwareConfig.ledsCanDim) {
|
||||
pinInfo.addGpioPinInfo(pin, pin, List.of(DeviceMode.PWM_OUTPUT, DeviceMode.DIGITAL_OUTPUT));
|
||||
} else {
|
||||
pinInfo.addGpioPinInfo(pin, pin, List.of(DeviceMode.DIGITAL_OUTPUT));
|
||||
}
|
||||
}
|
||||
for (int pin : hardwareConfig.statusRGBPins) {
|
||||
pinInfo.addGpioPinInfo(pin, pin, List.of(DeviceMode.DIGITAL_OUTPUT));
|
||||
}
|
||||
|
||||
return deviceFactory;
|
||||
}
|
||||
|
||||
public void setBrightnessPercent(int percent) {
|
||||
if (percent != hardwareSettings.ledBrightnessPercentage) {
|
||||
hardwareSettings.ledBrightnessPercentage = percent;
|
||||
@@ -203,50 +170,62 @@ public class HardwareManager {
|
||||
|
||||
// API's supporting status LEDs
|
||||
|
||||
private Set<String> pipelineTargets = new HashSet<String>();
|
||||
private Map<String, Boolean> pipelineTargets = new HashMap<String, Boolean>();
|
||||
private boolean ntConnected = false;
|
||||
private boolean systemRunning = false;
|
||||
private int blinkCounter = 0;
|
||||
|
||||
public void setTargetsVisibleStatus(String uniqueName, boolean hasTargets) {
|
||||
if (hasTargets) {
|
||||
pipelineTargets.add(uniqueName);
|
||||
} else {
|
||||
pipelineTargets.remove(uniqueName);
|
||||
}
|
||||
updateStatus();
|
||||
pipelineTargets.put(uniqueName, hasTargets);
|
||||
}
|
||||
|
||||
public void setNTConnected(boolean isConnected) {
|
||||
ntConnected = isConnected;
|
||||
updateStatus();
|
||||
this.ntConnected = isConnected;
|
||||
}
|
||||
|
||||
public void setError(PhotonStatus status) {
|
||||
if (status == null || !status.isError()) {
|
||||
updateStatus();
|
||||
} else if (statusLED != null) {
|
||||
statusLED.setStatus(status);
|
||||
}
|
||||
public void setRunning(boolean isRunning) {
|
||||
this.systemRunning = isRunning;
|
||||
}
|
||||
|
||||
private void updateStatus() {
|
||||
if (statusLED == null) {
|
||||
return;
|
||||
private void statusLEDUpdate() {
|
||||
// make blinky
|
||||
boolean blinky = ((blinkCounter % 3) > 0);
|
||||
|
||||
// check if any pipeline has a visible target
|
||||
boolean anyTarget = false;
|
||||
for (var t : this.pipelineTargets.values()) {
|
||||
if (t) {
|
||||
anyTarget = true;
|
||||
}
|
||||
}
|
||||
PhotonStatus status;
|
||||
boolean anyTarget = !pipelineTargets.isEmpty();
|
||||
if (ntConnected) {
|
||||
if (anyTarget) {
|
||||
status = PhotonStatus.NT_CONNECTED_TARGETS_VISIBLE;
|
||||
|
||||
if (this.systemRunning) {
|
||||
if (!this.ntConnected) {
|
||||
if (anyTarget) {
|
||||
// Blue Flashing
|
||||
statusLED.setRGB(false, false, blinky);
|
||||
} else {
|
||||
// Yellow flashing
|
||||
statusLED.setRGB(blinky, blinky, false);
|
||||
}
|
||||
} else {
|
||||
status = PhotonStatus.NT_CONNECTED_TARGETS_MISSING;
|
||||
if (anyTarget) {
|
||||
// Blue
|
||||
statusLED.setRGB(false, false, blinky);
|
||||
} else {
|
||||
// blinky green
|
||||
statusLED.setRGB(false, blinky, false);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (anyTarget) {
|
||||
status = PhotonStatus.NT_DISCONNECTED_TARGETS_VISIBLE;
|
||||
} else {
|
||||
status = PhotonStatus.NT_DISCONNECTED_TARGETS_MISSING;
|
||||
}
|
||||
// Faulted, not running... blinky red
|
||||
statusLED.setRGB(blinky, false, false);
|
||||
}
|
||||
statusLED.setStatus(status);
|
||||
|
||||
blinkCounter++;
|
||||
}
|
||||
|
||||
public void publishMetrics() {
|
||||
metricsManager.publishMetrics();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,97 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) Photon Vision.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.photonvision.common.hardware;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.databind.PropertyNamingStrategies;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Optional;
|
||||
import org.photonvision.common.logging.LogGroup;
|
||||
import org.photonvision.common.logging.Logger;
|
||||
|
||||
/**
|
||||
* Our blessed images inject the current version via the build process in
|
||||
* https://github.com/PhotonVision/photon-image-modifier
|
||||
*
|
||||
* <p>This class provides a convenient abstraction around this
|
||||
*/
|
||||
public class OsImageData {
|
||||
private static final Logger logger = new Logger(OsImageData.class, LogGroup.General);
|
||||
|
||||
private static Path imageVersionFile = Path.of("/opt/photonvision/image-version");
|
||||
private static Path imageMetadataFile = Path.of("/opt/photonvision/image-version.json");
|
||||
|
||||
/** The OS image version string, if available. This is legacy, use {@link ImageMetadata}. */
|
||||
public static final Optional<String> IMAGE_VERSION = getImageVersion();
|
||||
|
||||
private static Optional<String> getImageVersion() {
|
||||
if (!imageVersionFile.toFile().exists()) {
|
||||
logger.warn("Photon cannot locate base OS image version at " + imageVersionFile.toString());
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
try {
|
||||
return Optional.of(Files.readString(imageVersionFile).strip());
|
||||
} catch (IOException e) {
|
||||
logger.error("Couldn't read image-version file", e);
|
||||
}
|
||||
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
public static final Optional<ImageMetadata> IMAGE_METADATA = getImageMetadata();
|
||||
|
||||
public static record ImageMetadata(
|
||||
String buildDate, String commitSha, String commitTag, String imageName, String imageSource) {}
|
||||
|
||||
private static Optional<ImageMetadata> getImageMetadata() {
|
||||
if (!imageMetadataFile.toFile().exists()) {
|
||||
logger.warn("Photon cannot locate OS image metadata at " + imageMetadataFile.toString());
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
try {
|
||||
String content = Files.readString(imageMetadataFile).strip();
|
||||
|
||||
ObjectMapper mapper =
|
||||
new ObjectMapper().setPropertyNamingStrategy(PropertyNamingStrategies.SNAKE_CASE);
|
||||
|
||||
ImageMetadata md = mapper.readValue(content, ImageMetadata.class);
|
||||
|
||||
if (md.buildDate() == null
|
||||
&& md.commitSha() == null
|
||||
&& md.commitTag() == null
|
||||
&& md.imageName() == null
|
||||
&& md.imageSource() == null) {
|
||||
logger.warn(
|
||||
"OS image metadata JSON did not contain recognized fields; preserving legacy behavior");
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
return Optional.of(md);
|
||||
} catch (IOException e) {
|
||||
logger.error("Couldn't read image metadata file", e);
|
||||
} catch (Exception e) {
|
||||
logger.error("Failed to parse image metadata", e);
|
||||
}
|
||||
|
||||
return Optional.empty();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
/*
|
||||
* Copyright (C) Photon Vision.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.photonvision.common.hardware;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Optional;
|
||||
import org.photonvision.common.logging.LogGroup;
|
||||
import org.photonvision.common.logging.Logger;
|
||||
|
||||
/**
|
||||
* Our blessed images inject the current version via this build workflow:
|
||||
* https://github.com/PhotonVision/photon-image-modifier/blob/2e5ddb6b599df0be921c12c8dbe7b939ecd7f615/.github/workflows/main.yml#L67
|
||||
*
|
||||
* <p>This class provides a convenient abstraction around this
|
||||
*/
|
||||
public class OsImageVersion {
|
||||
private static final Logger logger = new Logger(OsImageVersion.class, LogGroup.General);
|
||||
|
||||
private static Path imageVersionFile = Path.of("/opt/photonvision/image-version");
|
||||
|
||||
public static final Optional<String> IMAGE_VERSION = getImageVersion();
|
||||
|
||||
private static Optional<String> getImageVersion() {
|
||||
if (!imageVersionFile.toFile().exists()) {
|
||||
logger.warn(
|
||||
"Photon cannot locate base OS image version metadata at " + imageVersionFile.toString());
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
try {
|
||||
return Optional.of(Files.readString(imageVersionFile).strip());
|
||||
} catch (IOException e) {
|
||||
logger.error("Couldn't read image-version file", e);
|
||||
}
|
||||
|
||||
return Optional.empty();
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user