mirror of
https://github.com/PhotonVision/photonvision
synced 2026-06-25 01:41:40 +00:00
Compare commits
25 Commits
v2025.0.0-
...
v2025.0.1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d9ada4c26c | ||
|
|
535e5d02f9 | ||
|
|
38f40bf03d | ||
|
|
7cc491536b | ||
|
|
66ccc35840 | ||
|
|
812dc61b33 | ||
|
|
dbbdc14c7c | ||
|
|
cf73f981b7 | ||
|
|
a99a8e750b | ||
|
|
a0b22cd8a3 | ||
|
|
5d55d215ec | ||
|
|
625dacb020 | ||
|
|
fc8ecac376 | ||
|
|
75e2498f53 | ||
|
|
7a4ea3dd56 | ||
|
|
5e1a93950e | ||
|
|
380546cee0 | ||
|
|
d7a7610917 | ||
|
|
37aaa49b32 | ||
|
|
937bafa8e2 | ||
|
|
3d18ded3f6 | ||
|
|
daa5842fb5 | ||
|
|
6f52267c26 | ||
|
|
acbae88d34 | ||
|
|
986c7020c3 |
520
.github/workflows/build.yml
vendored
520
.github/workflows/build.yml
vendored
@@ -18,519 +18,47 @@ on:
|
||||
- '.github/**'
|
||||
|
||||
jobs:
|
||||
build-client:
|
||||
name: "PhotonClient Build"
|
||||
defaults:
|
||||
run:
|
||||
working-directory: photon-client
|
||||
build-photonlib-vendorjson:
|
||||
name: "Build Vendor JSON"
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 18
|
||||
- name: Install Dependencies
|
||||
run: npm ci
|
||||
- name: Build Production Client
|
||||
run: npm run build
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: built-client
|
||||
path: photon-client/dist/
|
||||
build-examples:
|
||||
name: "Build Examples"
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Fetch tags
|
||||
run: git fetch --tags --force
|
||||
- name: Install RoboRIO Toolchain
|
||||
run: ./gradlew installRoboRioToolchain
|
||||
- name: Install Java 17
|
||||
uses: actions/setup-java@v4
|
||||
with:
|
||||
java-version: 17
|
||||
distribution: temurin
|
||||
# Need to publish to maven local first, so that C++ sim can pick it up
|
||||
# Still haven't figured out how to make the vendordep file be copied before trying to build examples
|
||||
- name: Publish photonlib to maven local
|
||||
run: |
|
||||
chmod +x gradlew
|
||||
./gradlew publishtomavenlocal -x check
|
||||
- name: Build Java examples
|
||||
working-directory: photonlib-java-examples
|
||||
run: |
|
||||
chmod +x gradlew
|
||||
./gradlew copyPhotonlib -x check
|
||||
./gradlew build -x check
|
||||
- name: Build C++ examples
|
||||
working-directory: photonlib-cpp-examples
|
||||
run: |
|
||||
chmod +x gradlew
|
||||
./gradlew copyPhotonlib -x check
|
||||
./gradlew build -x check
|
||||
build-gradle:
|
||||
name: "Gradle Build"
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
# Checkout code.
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Fetch tags
|
||||
run: git fetch --tags --force
|
||||
- name: Install Java 17
|
||||
uses: actions/setup-java@v3
|
||||
with:
|
||||
java-version: 17
|
||||
distribution: temurin
|
||||
- name: Install mrcal deps
|
||||
run: sudo apt-get update && sudo apt-get install -y libcholmod3 liblapack3 libsuitesparseconfig5
|
||||
- name: Gradle Build
|
||||
run: |
|
||||
chmod +x gradlew
|
||||
./gradlew photon-targeting:build photon-core:build photon-server:build -x check
|
||||
- name: Gradle Tests
|
||||
run: ./gradlew testHeadless -i --stacktrace
|
||||
- name: Gradle Coverage
|
||||
run: ./gradlew jacocoTestReport
|
||||
- name: Publish Coverage Report
|
||||
uses: codecov/codecov-action@v3
|
||||
with:
|
||||
file: ./photon-server/build/reports/jacoco/test/jacocoTestReport.xml
|
||||
- name: Publish Core Coverage Report
|
||||
uses: codecov/codecov-action@v3
|
||||
with:
|
||||
file: ./photon-core/build/reports/jacoco/test/jacocoTestReport.xml
|
||||
build-offline-docs:
|
||||
name: "Build Offline Docs"
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.11'
|
||||
- name: Install dependencies
|
||||
working-directory: docs
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip install sphinx sphinx_rtd_theme sphinx-tabs sphinxext-opengraph doc8
|
||||
pip install -r requirements.txt
|
||||
- name: Build the docs
|
||||
working-directory: docs
|
||||
run: |
|
||||
make html
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: built-docs
|
||||
path: docs/build/html
|
||||
build-photonlib-host:
|
||||
env:
|
||||
MACOSX_DEPLOYMENT_TARGET: 13
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- os: windows-2022
|
||||
artifact-name: Win64
|
||||
architecture: x64
|
||||
- os: macos-14
|
||||
artifact-name: macOS
|
||||
architecture: aarch64
|
||||
- os: ubuntu-22.04
|
||||
artifact-name: Linux
|
||||
|
||||
name: "Photonlib - Build Host - ${{ matrix.artifact-name }}"
|
||||
runs-on: ${{ matrix.os }}
|
||||
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 }}
|
||||
|
||||
# grab all tags
|
||||
- run: git fetch --tags --force
|
||||
|
||||
# Generate the JSON and give it the ""standard""" name maven gives it
|
||||
- run: |
|
||||
chmod +x gradlew
|
||||
./gradlew photon-targeting:build photon-lib:build -i
|
||||
- run: ./gradlew photon-lib:publish photon-targeting:publish
|
||||
name: Publish
|
||||
env:
|
||||
ARTIFACTORY_API_KEY: ${{ secrets.ARTIFACTORY_API_KEY }}
|
||||
if: github.event_name == 'push' && github.repository_owner == 'photonvision'
|
||||
# Copy artifacts to build/outputs/maven
|
||||
- run: ./gradlew photon-lib:publish photon-targeting:publish -PcopyOfflineArtifacts
|
||||
./gradlew photon-lib:generateVendorJson
|
||||
export VERSION=$(git describe --tags --match=v*)
|
||||
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@v4
|
||||
with:
|
||||
name: maven-${{ matrix.artifact-name }}
|
||||
path: build/outputs
|
||||
name: photonlib-vendor-json
|
||||
path: photon-lib/build/generated/vendordeps/photonlib-*.json
|
||||
|
||||
build-photonlib-docker:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- container: wpilib/roborio-cross-ubuntu:2024-22.04
|
||||
artifact-name: Athena
|
||||
build-options: "-Ponlylinuxathena"
|
||||
- container: wpilib/raspbian-cross-ubuntu:bullseye-22.04
|
||||
artifact-name: Raspbian
|
||||
build-options: "-Ponlylinuxarm32"
|
||||
- container: wpilib/aarch64-cross-ubuntu:bullseye-22.04
|
||||
artifact-name: Aarch64
|
||||
build-options: "-Ponlylinuxarm64"
|
||||
|
||||
runs-on: ubuntu-22.04
|
||||
container: ${{ matrix.container }}
|
||||
name: "Photonlib - Build Docker - ${{ matrix.artifact-name }}"
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Config Git
|
||||
run: |
|
||||
git config --global --add safe.directory /__w/photonvision/photonvision
|
||||
- name: Build PhotonLib
|
||||
# We don't need to run tests, since we specify only non-native platforms
|
||||
run: |
|
||||
chmod +x gradlew
|
||||
./gradlew photon-targeting:build photon-lib:build ${{ matrix.build-options }} -i -x test
|
||||
- name: Publish
|
||||
run: |
|
||||
chmod +x gradlew
|
||||
./gradlew photon-lib:publish photon-targeting:publish ${{ matrix.build-options }}
|
||||
env:
|
||||
ARTIFACTORY_API_KEY: ${{ secrets.ARTIFACTORY_API_KEY }}
|
||||
if: github.event_name == 'push' && github.repository_owner == 'photonvision'
|
||||
# Copy artifacts to build/outputs/maven
|
||||
- run: ./gradlew photon-lib:publish photon-targeting:publish -PcopyOfflineArtifacts ${{ matrix.build-options }}
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: maven-${{ matrix.artifact-name }}
|
||||
path: build/outputs
|
||||
|
||||
combine:
|
||||
name: Combine
|
||||
needs: [build-photonlib-docker, build-photonlib-host]
|
||||
dispatch:
|
||||
name: dispatch
|
||||
needs: [build-photonlib-vendorjson]
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: peter-evans/repository-dispatch@v3
|
||||
# if: |
|
||||
# github.repository == 'mcm001/photonvision' &&
|
||||
# startsWith(github.ref, 'refs/tags/v')
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- run: git fetch --tags --force
|
||||
# download all maven-* artifacts to outputs/
|
||||
- uses: actions/download-artifact@v4
|
||||
with:
|
||||
merge-multiple: true
|
||||
path: output
|
||||
pattern: maven-*
|
||||
- run: find .
|
||||
- run: zip -r photonlib-$(git describe --tags --match=v*).zip .
|
||||
name: ZIP stuff up
|
||||
working-directory: output
|
||||
- run: ls output
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: photonlib-offline
|
||||
path: output/*.zip
|
||||
|
||||
build-package:
|
||||
needs: [build-client, build-gradle, build-offline-docs]
|
||||
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
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:
|
||||
- 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 }}
|
||||
- run: |
|
||||
rm -rf photon-server/src/main/resources/web/*
|
||||
mkdir -p photon-server/src/main/resources/web/docs
|
||||
if: ${{ (matrix.os) != 'windows-latest' }}
|
||||
- run: |
|
||||
del photon-server\src\main\resources\web\*.*
|
||||
mkdir photon-server\src\main\resources\web\docs
|
||||
if: ${{ (matrix.os) == 'windows-latest' }}
|
||||
- uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: built-client
|
||||
path: photon-server/src/main/resources/web/
|
||||
- uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: built-docs
|
||||
path: photon-server/src/main/resources/web/docs
|
||||
- run: |
|
||||
chmod +x gradlew
|
||||
./gradlew photon-server:shadowJar -PArchOverride=${{ matrix.arch-override }}
|
||||
if: ${{ (matrix.arch-override != 'none') }}
|
||||
- run: |
|
||||
chmod +x gradlew
|
||||
./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
|
||||
|
||||
run-smoketest-native:
|
||||
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-latest' }}
|
||||
# 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-22.04
|
||||
artifact-name: LinuxArm64
|
||||
image_suffix: RaspberryPi
|
||||
image_url: https://github.com/PhotonVision/photon-image-modifier/releases/download/v2025.0.0-beta-4/photonvision_raspi.img.xz
|
||||
cpu: cortex-a7
|
||||
image_additional_mb: 0
|
||||
extraOpts: -Djdk.lang.Process.launchMechanism=vfork
|
||||
|
||||
runs-on: ${{ matrix.os }}
|
||||
name: smoketest-${{ matrix.image_suffix }}
|
||||
|
||||
steps:
|
||||
- uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: jar-${{ matrix.artifact-name }}
|
||||
|
||||
- uses: pguyot/arm-runner-action@v2
|
||||
name: Run photon smoketest
|
||||
id: generate_image
|
||||
with:
|
||||
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]
|
||||
|
||||
if: ${{ github.event_name != 'pull_request' }}
|
||||
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- os: ubuntu-22.04
|
||||
artifact-name: LinuxArm64
|
||||
image_suffix: RaspberryPi
|
||||
image_url: https://github.com/PhotonVision/photon-image-modifier/releases/download/v2025.0.0-beta-6/photonvision_raspi.img.xz
|
||||
cpu: cortex-a7
|
||||
image_additional_mb: 0
|
||||
- os: ubuntu-22.04
|
||||
artifact-name: LinuxArm64
|
||||
image_suffix: limelight2
|
||||
image_url: https://github.com/PhotonVision/photon-image-modifier/releases/download/v2025.0.0-beta-6/photonvision_limelight.img.xz
|
||||
cpu: cortex-a7
|
||||
image_additional_mb: 0
|
||||
- os: ubuntu-22.04
|
||||
artifact-name: LinuxArm64
|
||||
image_suffix: limelight3
|
||||
image_url: https://github.com/PhotonVision/photon-image-modifier/releases/download/v2025.0.0-beta-6/photonvision_limelight3.img.xz
|
||||
cpu: cortex-a7
|
||||
image_additional_mb: 0
|
||||
- os: ubuntu-22.04
|
||||
artifact-name: LinuxArm64
|
||||
image_suffix: orangepi5
|
||||
image_url: https://github.com/PhotonVision/photon-image-modifier/releases/download/v2025.0.0-beta-6/photonvision_opi5.img.xz
|
||||
cpu: cortex-a8
|
||||
image_additional_mb: 1024
|
||||
- os: ubuntu-22.04
|
||||
artifact-name: LinuxArm64
|
||||
image_suffix: orangepi5b
|
||||
image_url: https://github.com/PhotonVision/photon-image-modifier/releases/download/v2025.0.0-beta-6/photonvision_opi5b.img.xz
|
||||
cpu: cortex-a8
|
||||
image_additional_mb: 1024
|
||||
- os: ubuntu-22.04
|
||||
artifact-name: LinuxArm64
|
||||
image_suffix: orangepi5plus
|
||||
image_url: https://github.com/PhotonVision/photon-image-modifier/releases/download/v2025.0.0-beta-6/photonvision_opi5plus.img.xz
|
||||
cpu: cortex-a8
|
||||
image_additional_mb: 1024
|
||||
- os: ubuntu-22.04
|
||||
artifact-name: LinuxArm64
|
||||
image_suffix: orangepi5pro
|
||||
image_url: https://github.com/PhotonVision/photon-image-modifier/releases/download/v2025.0.0-beta-6/photonvision_opi5pro.img.xz
|
||||
cpu: cortex-a8
|
||||
image_additional_mb: 1024
|
||||
- os: ubuntu-latest
|
||||
artifact-name: LinuxArm64
|
||||
image_suffix: orangepi5max
|
||||
image_url: https://github.com/PhotonVision/photon-image-modifier/releases/download/v2025.0.0-beta-6/photonvision_opi5max.img.xz
|
||||
cpu: cortex-a8
|
||||
image_additional_mb: 1024
|
||||
|
||||
runs-on: ${{ matrix.os }}
|
||||
name: "Build image - ${{ matrix.image_url }}"
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: jar-${{ matrix.artifact-name }}
|
||||
- uses: pguyot/arm-runner-action@HEAD
|
||||
name: Generate image
|
||||
id: generate_image
|
||||
with:
|
||||
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
|
||||
- name: Compress image
|
||||
run: |
|
||||
new_jar=$(realpath $(find . -name photonvision\*-linuxarm64.jar))
|
||||
new_image_name=$(basename "${new_jar/.jar/_${{ matrix.image_suffix }}.img}")
|
||||
mv ${{ steps.generate_image.outputs.image }} $new_image_name
|
||||
sudo xz -T 0 -v $new_image_name
|
||||
- uses: actions/upload-artifact@v4
|
||||
name: Upload image
|
||||
with:
|
||||
name: image-${{ matrix.image_suffix }}
|
||||
path: photonvision*.xz
|
||||
release:
|
||||
needs: [build-package, build-image, combine]
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
# Download all fat JARs
|
||||
- uses: actions/download-artifact@v4
|
||||
with:
|
||||
merge-multiple: true
|
||||
pattern: jar-*
|
||||
# Download offline photonlib
|
||||
- uses: actions/download-artifact@v4
|
||||
with:
|
||||
merge-multiple: true
|
||||
pattern: photonlib-offline
|
||||
# Download all images
|
||||
- uses: actions/download-artifact@v4
|
||||
with:
|
||||
merge-multiple: true
|
||||
pattern: image-*
|
||||
|
||||
- run: find
|
||||
# Push to dev release
|
||||
- uses: pyTooling/Actions/releaser@r0
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
tag: 'Dev'
|
||||
rm: true
|
||||
files: |
|
||||
**/*.xz
|
||||
**/*.jar
|
||||
**/photonlib*.json
|
||||
**/photonlib*.zip
|
||||
if: github.event_name == 'push'
|
||||
# Upload all jars and xz archives
|
||||
# Split into two uploads to work around max size limits in action-gh-releases
|
||||
# https://github.com/softprops/action-gh-release/issues/353
|
||||
- uses: softprops/action-gh-release@v2.0.8
|
||||
with:
|
||||
files: |
|
||||
**/*orangepi5*.xz
|
||||
if: startsWith(github.ref, 'refs/tags/v')
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
- uses: softprops/action-gh-release@v2.0.8
|
||||
with:
|
||||
files: |
|
||||
**/!(*orangepi5*).xz
|
||||
**/*.jar
|
||||
**/photonlib*.json
|
||||
**/photonlib*.zip
|
||||
if: startsWith(github.ref, 'refs/tags/v')
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
token: ${{ secrets.VENDOR_JSON_REPO_PUSH_TOKEN }}
|
||||
repository: PhotonVision/vendor-json-repo
|
||||
event-type: tag
|
||||
client-payload: '{"run_id": "${{ github.run_id }}", "package_version": "${{ github.ref_name }}"}'
|
||||
|
||||
97
.github/workflows/lint-format.yml
vendored
97
.github/workflows/lint-format.yml
vendored
@@ -1,97 +0,0 @@
|
||||
name: Lint and Format
|
||||
|
||||
on:
|
||||
# Run on pushes to master and pushed tags, and on pull requests against master, but ignore the docs folder
|
||||
push:
|
||||
branches: [ master ]
|
||||
tags:
|
||||
- 'v*'
|
||||
paths:
|
||||
- '**'
|
||||
- '!docs/**'
|
||||
- '.github/**'
|
||||
pull_request:
|
||||
branches: [ master ]
|
||||
paths:
|
||||
- '**'
|
||||
- '!docs/**'
|
||||
- '.github/**'
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.head_ref || github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
wpiformat:
|
||||
name: "wpiformat"
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Fetch all history and metadata
|
||||
run: |
|
||||
git fetch --prune --unshallow
|
||||
git checkout -b pr
|
||||
git branch -f master origin/master
|
||||
- name: Set up Python 3.8
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: 3.11
|
||||
- name: Install wpiformat
|
||||
run: pip3 install wpiformat==2024.41
|
||||
- name: Run
|
||||
run: wpiformat
|
||||
- name: Check output
|
||||
run: git --no-pager diff --exit-code HEAD
|
||||
- name: Generate diff
|
||||
run: git diff HEAD > wpiformat-fixes.patch
|
||||
if: ${{ failure() }}
|
||||
- uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: wpiformat fixes
|
||||
path: wpiformat-fixes.patch
|
||||
if: ${{ failure() }}
|
||||
javaformat:
|
||||
name: "Java Formatting"
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- uses: actions/setup-java@v3
|
||||
with:
|
||||
java-version: 17
|
||||
distribution: temurin
|
||||
- run: |
|
||||
chmod +x gradlew
|
||||
./gradlew spotlessCheck
|
||||
|
||||
client-lint-format:
|
||||
name: "PhotonClient Lint and Formatting"
|
||||
defaults:
|
||||
run:
|
||||
working-directory: photon-client
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 18
|
||||
- name: Install Dependencies
|
||||
run: npm ci
|
||||
- name: Check Linting
|
||||
run: npm run lint-ci
|
||||
- name: Check Formatting
|
||||
run: npm run format-ci
|
||||
server-index:
|
||||
name: "Check server index.html not changed"
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Fetch all history and metadata
|
||||
run: |
|
||||
git fetch --prune --unshallow
|
||||
git checkout -b pr
|
||||
git branch -f master origin/master
|
||||
- name: Check index.html not changed
|
||||
run: git --no-pager diff --exit-code origin/master photon-server/src/main/resources/web/index.html
|
||||
96
.github/workflows/photon-code-docs.yml
vendored
96
.github/workflows/photon-code-docs.yml
vendored
@@ -1,96 +0,0 @@
|
||||
name: Photon Code Documentation
|
||||
|
||||
on:
|
||||
# Run on pushes to master and pushed tags, and on pull requests against master, but ignore the docs folder
|
||||
push:
|
||||
branches: [ master ]
|
||||
tags:
|
||||
- 'v*'
|
||||
paths:
|
||||
- '**'
|
||||
- '!docs/**'
|
||||
- '.github/**'
|
||||
pull_request:
|
||||
branches: [ master ]
|
||||
paths:
|
||||
- '**'
|
||||
- '!docs/**'
|
||||
- '.github/**'
|
||||
|
||||
# Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages
|
||||
permissions:
|
||||
contents: read
|
||||
pages: write
|
||||
id-token: write
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
build-client:
|
||||
name: "PhotonClient Build"
|
||||
defaults:
|
||||
run:
|
||||
working-directory: photon-client
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 18
|
||||
- name: Install Dependencies
|
||||
run: npm ci
|
||||
- name: Build Production Client
|
||||
run: npm run build-demo
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: built-client
|
||||
path: photon-client/dist/
|
||||
|
||||
run_docs:
|
||||
runs-on: "ubuntu-22.04"
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Fetch tags
|
||||
run: git fetch --tags --force
|
||||
- name: Install Java 17
|
||||
uses: actions/setup-java@v3
|
||||
with:
|
||||
java-version: 17
|
||||
distribution: temurin
|
||||
|
||||
- name: Build javadocs/doxygen
|
||||
run: |
|
||||
chmod +x gradlew
|
||||
./gradlew photon-docs:generateJavaDocs photon-docs:doxygen
|
||||
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: built-docs
|
||||
path: photon-docs/build/docs
|
||||
|
||||
release:
|
||||
needs: [build-client, run_docs]
|
||||
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
|
||||
# Download literally every single artifact.
|
||||
- uses: actions/download-artifact@v4
|
||||
|
||||
- run: find .
|
||||
- name: copy file via ssh password
|
||||
if: github.ref == 'refs/heads/master'
|
||||
uses: appleboy/scp-action@v0.1.7
|
||||
with:
|
||||
host: ${{ secrets.WEBMASTER_SSH_HOST }}
|
||||
username: ${{ secrets.WEBMASTER_SSH_USERNAME }}
|
||||
password: ${{ secrets.WEBMASTER_SSH_KEY }}
|
||||
port: ${{ secrets.WEBMASTER_SSH_PORT }}
|
||||
source: "*"
|
||||
target: /var/www/html/photonvision-docs/
|
||||
46
.github/workflows/photonvision-docs.yml
vendored
46
.github/workflows/photonvision-docs.yml
vendored
@@ -1,46 +0,0 @@
|
||||
name: PhotonVision Sphinx Documentation Checks
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ master ]
|
||||
paths:
|
||||
- 'docs/**'
|
||||
- '.github/**'
|
||||
pull_request:
|
||||
branches: [ master ]
|
||||
paths:
|
||||
- 'docs/**'
|
||||
- '.github/**'
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-22.04
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: '3.11'
|
||||
|
||||
- name: Install and upgrade pip
|
||||
run: python -m pip install --upgrade pip
|
||||
|
||||
- name: Install Python dependencies
|
||||
working-directory: docs
|
||||
run: |
|
||||
pip install sphinx sphinx_rtd_theme sphinx-tabs sphinxext-opengraph doc8
|
||||
pip install -r requirements.txt
|
||||
|
||||
- name: Check links
|
||||
working-directory: docs
|
||||
run: make linkcheck
|
||||
continue-on-error: true
|
||||
|
||||
- name: Check lint
|
||||
working-directory: docs
|
||||
run: make lint
|
||||
|
||||
- name: Compile HTML
|
||||
working-directory: docs
|
||||
run: make html
|
||||
68
.github/workflows/python.yml
vendored
68
.github/workflows/python.yml
vendored
@@ -1,68 +0,0 @@
|
||||
name: Build and Distribute PhotonLibPy
|
||||
|
||||
permissions:
|
||||
id-token: write # IMPORTANT: this permission is mandatory for trusted publishing
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ master ]
|
||||
tags:
|
||||
- 'v*'
|
||||
paths:
|
||||
- '**'
|
||||
- '!docs/**'
|
||||
- '.github/**'
|
||||
pull_request:
|
||||
branches: [ master ]
|
||||
paths:
|
||||
- '**'
|
||||
- '!docs/**'
|
||||
- '.github/**'
|
||||
|
||||
jobs:
|
||||
buildAndDeploy:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: 3.11
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip install setuptools wheel pytest
|
||||
|
||||
- name: Build wheel
|
||||
working-directory: ./photon-lib/py
|
||||
run: |
|
||||
python setup.py sdist bdist_wheel
|
||||
|
||||
- name: Run Unit Tests
|
||||
working-directory: ./photon-lib/py
|
||||
run: |
|
||||
pip install --no-cache-dir dist/*.whl
|
||||
pytest
|
||||
|
||||
|
||||
- name: Upload artifacts
|
||||
uses: actions/upload-artifact@master
|
||||
with:
|
||||
name: dist
|
||||
path: ./photon-lib/py/dist/
|
||||
|
||||
- name: Publish package distributions to TestPyPI
|
||||
# Only upload on tags
|
||||
if: startsWith(github.ref, 'refs/tags/v')
|
||||
uses: pypa/gh-action-pypi-publish@release/v1
|
||||
with:
|
||||
packages_dir: ./photon-lib/py/dist/
|
||||
|
||||
permissions:
|
||||
id-token: write # IMPORTANT: this permission is mandatory for trusted publishing
|
||||
@@ -9,6 +9,8 @@ build:
|
||||
os: ubuntu-22.04
|
||||
tools:
|
||||
python: "3.11"
|
||||
apt_packages:
|
||||
- graphviz
|
||||
jobs:
|
||||
post_checkout:
|
||||
# Cancel building pull requests when there aren't changed in the docs directory or YAML file.
|
||||
|
||||
@@ -25,7 +25,7 @@ If you are interested in contributing code or documentation to the project, plea
|
||||
|
||||
Gradle is used for all C++ and Java code, and NPM is used for the web UI. Instructions to compile PhotonVision yourself can be found [in our docs](https://docs.photonvision.org/en/latest/docs/contributing/building-photon.html#compiling-instructions).
|
||||
|
||||
You can run one of the many built in examples straight from the command line, too! They contain a fully featured robot project, and some include simulation support. The projects can be found inside the [`photonlib-java-examples`](photonlib-java-examples) and [`photonlib-cpp-examples`](photonlib-cpp-examples) subdirectories, respectively. Instructions for running these examples directly from the repo are found [in the docs](https://docs.photonvision.org/en/latest/docs/contributing/photonvision/build-instructions.html#running-examples).
|
||||
You can run one of the many built in examples straight from the command line, too! They contain a fully featured robot project, and some include simulation support. The projects can be found inside the [`photonlib-java-examples`](photonlib-java-examples) and [`photonlib-cpp-examples`](photonlib-cpp-examples) subdirectories, respectively. Instructions for running these examples directly from the repo are found [in the docs](https://docs.photonvision.org/en/latest/docs/contributing/building-photon.html#running-examples).
|
||||
|
||||
## Gradle Arguments
|
||||
|
||||
@@ -47,7 +47,7 @@ If you're cross-compiling, you'll need the wpilib toolchain installed. This can
|
||||
|
||||
## Out-of-Source Dependencies
|
||||
|
||||
PhotonVision uses the following additonal out-of-source repositories for building code.
|
||||
PhotonVision uses the following additional out-of-source repositories for building code.
|
||||
|
||||
- Base system images for Raspberry Pi & Orange Pi: https://github.com/PhotonVision/photon-image-modifier
|
||||
- C++ driver for Raspberry Pi CSI cameras: https://github.com/PhotonVision/photon-libcamera-gl-driver
|
||||
|
||||
12
build.gradle
12
build.gradle
@@ -4,11 +4,10 @@ plugins {
|
||||
id "java"
|
||||
id "cpp"
|
||||
id "com.diffplug.spotless" version "6.24.0"
|
||||
id "edu.wpi.first.NativeUtils" version "2024.6.1" apply false
|
||||
id "edu.wpi.first.wpilib.repositories.WPILibRepositoriesPlugin" version "2020.2"
|
||||
id "edu.wpi.first.GradleRIO" version "2024.3.2"
|
||||
id "edu.wpi.first.GradleRIO" version "2025.1.1-beta-1"
|
||||
id 'edu.wpi.first.WpilibTools' version '1.3.0'
|
||||
id 'com.google.protobuf' version '0.9.4' apply false
|
||||
id 'com.google.protobuf' version '0.9.3' apply false
|
||||
id 'edu.wpi.first.GradleJni' version '1.1.0'
|
||||
}
|
||||
|
||||
@@ -31,14 +30,15 @@ ext.allOutputsFolder = file("$project.buildDir/outputs")
|
||||
apply from: "versioningHelper.gradle"
|
||||
|
||||
ext {
|
||||
wpilibVersion = "2024.3.2"
|
||||
wpilibVersion = "2025.1.1-beta-1"
|
||||
wpimathVersion = wpilibVersion
|
||||
openCVversion = "4.8.0-2"
|
||||
openCVYear = "2024"
|
||||
openCVversion = "4.8.0-4"
|
||||
joglVersion = "2.4.0"
|
||||
javalinVersion = "5.6.2"
|
||||
libcameraDriverVersion = "dev-v2023.1.0-14-g787ab59"
|
||||
rknnVersion = "dev-v2024.0.1-4-g0db16ac"
|
||||
frcYear = "2024"
|
||||
frcYear = "2025"
|
||||
mrcalVersion = "dev-v2024.0.0-24-gc1efcf0";
|
||||
|
||||
|
||||
|
||||
@@ -37,6 +37,7 @@ extensions = [
|
||||
"sphinx_design",
|
||||
"myst_parser",
|
||||
"sphinx.ext.mathjax",
|
||||
"sphinx.ext.graphviz",
|
||||
]
|
||||
|
||||
# Configure OpenGraph support
|
||||
|
||||
@@ -26,7 +26,7 @@ The built documentation is located at `docs/build/html/index.html` relative to t
|
||||
|
||||
## Docs Builds on Pull Requests
|
||||
|
||||
Pre-merge builds of docs can be found at: `https://photonvision-docs--PRNUMBER.org.readthedocs.build/en/PRNUMBER/index.html`. These docs are republished on every commit to a pull request made to PhotonVision/photonvision-docs. For example, PR 325 would have pre-merge documentation published to `https://photonvision-docs--325.org.readthedocs.build/en/325/index.html`. Additionally, the pull requrest will have a link directly to the pre-release build of the docs. This build only runs when there is a change to files in the docs sub-folder.
|
||||
Pre-merge builds of docs can be found at: `https://photonvision-docs--PRNUMBER.org.readthedocs.build/en/PRNUMBER/index.html`. These docs are republished on every commit to a pull request made to PhotonVision/photonvision-docs. For example, PR 325 would have pre-merge documentation published to `https://photonvision-docs--325.org.readthedocs.build/en/325/index.html`. Additionally, the pull request will have a link directly to the pre-release build of the docs. This build only runs when there is a change to files in the docs sub-folder.
|
||||
|
||||
## Style Guide
|
||||
|
||||
|
||||
@@ -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 2025+. If you don't have this JDK with WPILib, 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:**
|
||||
|
||||
|
||||
@@ -3,4 +3,5 @@
|
||||
```{toctree}
|
||||
:maxdepth: 1
|
||||
image-rotation
|
||||
time-sync
|
||||
```
|
||||
|
||||
111
docs/source/docs/contributing/design-descriptions/time-sync.md
Normal file
111
docs/source/docs/contributing/design-descriptions/time-sync.md
Normal file
@@ -0,0 +1,111 @@
|
||||
# Time Synchronization Protocol Specification, Version 1.0
|
||||
|
||||
Protocol Revision 1.0, 08/25/2024
|
||||
|
||||
## Background
|
||||
|
||||
In a distributed compute environment like robots, time synchronization between computers is increasingly important. Currently, [NetworkTables Version 4.1](https://github.com/wpilibsuite/allwpilib/blob/main/ntcore/doc/networktables4.adoc) provides support for time synchronization of clients with the NetworkTables server using binary PING/PONG messages sent over WebSockets. This approach, while fundamentally the same as is described in this memo, has demonstrated some opportunities for improvement:
|
||||
|
||||
- PING/PONG messages are processed in the same queue as other NetworkTables messages. Depending on the underlying implementation and processor speed, this can incur message processing delays and increase client-calculated Round-Trip Time (RTT), and cause messages to arrive at the server timestamped in the future.
|
||||
- Messages use WebSockets over TCP for their transport layer. We don't need the robustness guarantees of TCP as our connection is stateless.
|
||||
|
||||
For these reasons, a time synchronization solution separate from NetworkTables communication was desired. Architecture decisions made to address these issues are:
|
||||
|
||||
- Use the User Datagram Protocol (UDP) transport layer, as we don't need the robustness guarantees afforded by TCP. As a Client, if a PING isn't replied to, we'll just try again at the start of the next PING window. As a bonus, we are free to use UDP port 5810 as NetworkTables only uses TCP Port 5810/5811 as of Version 4.1.
|
||||
- Use a separate thread from the current NetworkTables libUV runner.
|
||||
|
||||
|
||||
## Prior Art
|
||||
|
||||
The [NetworkTables 4.1 timestamp synchronization](https://github.com/wpilibsuite/allwpilib/blob/main/ntcore/doc/networktables4.adoc#timestamps) approach, an implementation of [Cristian's Algorithm](https://en.wikipedia.org/wiki/Cristian%27s_algorithm). We also implement Cristian’s Algorithm.
|
||||
|
||||
The [Precision Time Protocol](https://en.wikipedia.org/wiki/Precision_Time_Protocol#Synchronization) at it's core does something similar with Sync/Delay_Req/Delay_Resp. We do not have (guaranteed) access to hardware timestamping, but we utilize this PING/PONG pattern to estimate total round-trip time.
|
||||
|
||||
|
||||
## Roles
|
||||
|
||||
```{graphviz}
|
||||
digraph CristianAlgorithm {
|
||||
ratio=0.5;
|
||||
bgcolor="transparent";
|
||||
|
||||
node [
|
||||
fontcolor = "#e6e6e6",
|
||||
style = filled,
|
||||
color = "#e6e6e6",
|
||||
fillcolor = "#333333"
|
||||
fontsize=10;
|
||||
]
|
||||
|
||||
edge [
|
||||
color = "#e6e6e6",
|
||||
fontcolor = "#e6e6e6"
|
||||
fontsize=10;
|
||||
]
|
||||
|
||||
rankdir=LR;
|
||||
node [shape=box, style=filled, color=lightblue];
|
||||
|
||||
user_send [label="User Sends T1"];
|
||||
server_receive [label="Server Receives T1"];
|
||||
server_send [label="Server Sends T2"];
|
||||
user_receive [label="User Receives T2"];
|
||||
user_compute [label="User Computes Time"];
|
||||
|
||||
user_send -> server_receive [label="T1 (Request)"];
|
||||
server_receive -> server_send [label="T1 received by server"];
|
||||
server_send -> user_receive [label="T2 sent by server"];
|
||||
user_receive -> user_compute [label="T2 received by user"];
|
||||
user_compute -> user_send [label="Computed Time: T3 = T2 + (deltaT2 - deltaT1)/2"];
|
||||
}
|
||||
```
|
||||
|
||||
Time Synchronization Protocol (TSP) participants can assume either a server role or a client role. The server role is responsible for listening for incoming time synchronization requests from clients and replying appropriately. The client role is responsible for sending "Ping" messages to the server and listening for "Pong" replies to estimate the offset between the server and client time bases.
|
||||
|
||||
All time values shall use units of microseconds. The epoch of the time base this is measured against is unspecified.
|
||||
|
||||
Clients shall periodically (e.g. every few seconds) send, in a manner that minimizes transmission delays, a **TSP Ping Message** that contains the client's current local time.
|
||||
|
||||
When the server receives a **TSP Ping Message** from any client, it shall respond to the client, in a manner that minimizes transmission delays, with a **TSP Pong message** encoding a timestamp of its (the server's) current local time (in microseconds), and the client-provided data value.
|
||||
|
||||
When the client receives a **TSP Pong Message** from the server, it shall verify that the `Client Local Time` corresponds to the currently in-flight TSP Ping message; if not, it shall drop this packet. The round trip time (RTT) shall be computed from the delta between the message's data value and the current local time. If the RTT is less than that from previous measurements, the client shall use the timestamp in the message plus ½ the RTT as the server time equivalent to the current local time, and use this equivalence to compute server time base timestamps from local time for future messages.
|
||||
|
||||
## Transport
|
||||
|
||||
Communication between server and clients shall occur over the User Datagram Protocol (UDP) Port 5810.
|
||||
|
||||
## Message Format
|
||||
|
||||
The message format forgoes CRCs (as these are provided by the Ethernet physical layer) or packet delimination (as our packetsa are assumed be under the network MTU). **TSP Ping** and **TSP Pong** messages shall be encoded in a manor compatible with a WPILib packed struct with respect to byte alignment and endienness.
|
||||
|
||||
### TSP Ping
|
||||
|
||||
| Offset | Format | Data | Notes |
|
||||
| ------ | ------ | ---- | ----- |
|
||||
| 0 | uint8 | Protocol version | This field shall always set to 1 (0b1) for TSP Version 1. |
|
||||
| 1 | uint8 | Message ID | This field shall always be set to 1 (0b1). |
|
||||
| 2 | uint64 | Client Local Time | The client's local time value, at the time this Ping message was sent. |
|
||||
|
||||
### TSP Pong
|
||||
|
||||
| Offset | Format | Data | Notes |
|
||||
| ------ | ------ | ---- | ----- |
|
||||
| 0 | uint8 | Protocol version | This field shall always set to 1 (0b1) for TSP Version 1.
|
||||
| 1 | uint8 | Message ID | This field shall always be set to 2 (0b2).
|
||||
| 2 | uint64 | Client Local Time | The client's local time value from the Ping message that this Pong is generated in response to.
|
||||
| 10 | uint64 | Server Local Time | The current time at the server, at the time this Pong message was sent.
|
||||
|
||||
|
||||
## Optional Protocol Extensions
|
||||
|
||||
Clients may publish statistics to NetworkTables. If they do, they shall publish to a key that is globally unique per participant in the Time Synronization network. If a client implements this, it shall provide the following publishers:
|
||||
|
||||
| Key | Type | Notes |
|
||||
| ------ | ------ | ---- |
|
||||
| offset_us | Integer | The time offset that, when added to the client's local clock, provides server time |
|
||||
| ping_tx_count | Integer | The total number of TSP Ping packets transmitted |
|
||||
| ping_rx_count | Integer | The total number of TSP Ping packets received |
|
||||
| pong_rx_time_us | Integer | The time, in client local time, that the last pong was received |
|
||||
| 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.
|
||||
@@ -7,7 +7,7 @@ A Pre-Built Raspberry Pi image is available for ease of installation.
|
||||
Download the latest release of the PhotonVision Raspberry image (.xz file) from the [releases page](https://github.com/PhotonVision/photonvision/releases). You do not need to extract the downloaded ZIP file.
|
||||
|
||||
:::{note}
|
||||
Make sure you download the image that ends in '-RasberryPi.xz'.
|
||||
Make sure you download the image that ends in '-RaspberryPi.xz'.
|
||||
:::
|
||||
|
||||
## Flashing the Pi Image
|
||||
|
||||
@@ -14,7 +14,7 @@ You can control the vision LEDs of supported hardware via PhotonLib using the `s
|
||||
// Blink the LEDs.
|
||||
camera.SetLED(photonlib::VisionLEDMode::kBlink);
|
||||
|
||||
.. code-block:: Python
|
||||
.. code-block:: Python
|
||||
|
||||
# Coming Soon!
|
||||
```
|
||||
|
||||
@@ -62,7 +62,7 @@ You can also get the pipeline latency from a pipeline result using the `getLaten
|
||||
// Get the pipeline latency.
|
||||
units::second_t latency = result.GetLatency();
|
||||
|
||||
.. code-block:: Python
|
||||
.. code-block:: Python
|
||||
|
||||
# Coming Soon!
|
||||
```
|
||||
|
||||
@@ -24,7 +24,7 @@ The API documentation can be found in here: [Java](https://github.wpilib.org/all
|
||||
// The parameter for LoadAPrilTagLayoutField will be different depending on the game.
|
||||
frc::AprilTagFieldLayout aprilTagFieldLayout = frc::LoadAprilTagLayoutField(frc::AprilTagField::k2024Crescendo);
|
||||
|
||||
.. code-block:: Python
|
||||
.. code-block:: Python
|
||||
|
||||
# Coming Soon!
|
||||
|
||||
@@ -81,7 +81,7 @@ The PhotonPoseEstimator has a constructor that takes an `AprilTagFieldLayout` (s
|
||||
photonlib::RobotPoseEstimator estimator(
|
||||
aprilTags, photonlib::CLOSEST_TO_REFERENCE_POSE, cameras);
|
||||
|
||||
.. code-block:: Python
|
||||
.. code-block:: Python
|
||||
|
||||
kRobotToCam = wpimath.geometry.Transform3d(
|
||||
wpimath.geometry.Translation3d(0.5, 0.0, 0.5),
|
||||
@@ -123,7 +123,9 @@ Calling `update()` on your `PhotonPoseEstimator` will return an `EstimatedRobotP
|
||||
}
|
||||
}
|
||||
|
||||
.. code-block:: Python
|
||||
.. code-block:: Python
|
||||
|
||||
# Coming Soon!
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -17,10 +17,10 @@ const resetTempSettingsStruct = () => {
|
||||
const settingsValid = ref(true);
|
||||
|
||||
const isValidNetworkTablesIP = (v: string | undefined): boolean => {
|
||||
// Check if it is a valid team number between 1-9999
|
||||
const teamNumberRegex = /^[1-9][0-9]{0,3}$/;
|
||||
// Check if it is a valid team number between 1-99999 (5 digits)
|
||||
const teamNumberRegex = /^[1-9][0-9]{0,4}$/;
|
||||
// Check if it is a team number longer than 5 digits
|
||||
const badTeamNumberRegex = /^[0-9]{5,}$/;
|
||||
const badTeamNumberRegex = /^[0-9]{6,}$/;
|
||||
|
||||
if (v === undefined) return false;
|
||||
if (teamNumberRegex.test(v)) return true;
|
||||
|
||||
@@ -142,7 +142,7 @@ export interface CameraCalibrationResult {
|
||||
distCoeffs: JsonMatOfDouble;
|
||||
observations: BoardObservation[];
|
||||
calobjectWarp?: number[];
|
||||
// We might have to omit observations for bandwith, so backend will send us this
|
||||
// We might have to omit observations for bandwidth, so backend will send us this
|
||||
numSnapshots: number;
|
||||
meanErrors: number[];
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@ def nativeTasks = wpilibTools.createExtractionTasks {
|
||||
|
||||
nativeTasks.addToSourceSetResources(sourceSets.main)
|
||||
|
||||
nativeConfig.dependencies.add wpilibTools.deps.wpilib("wpilibc")
|
||||
nativeConfig.dependencies.add wpilibTools.deps.wpilib("wpimath")
|
||||
nativeConfig.dependencies.add wpilibTools.deps.wpilib("wpinet")
|
||||
nativeConfig.dependencies.add wpilibTools.deps.wpilib("wpiutil")
|
||||
@@ -24,7 +25,7 @@ nativeConfig.dependencies.add wpilibTools.deps.wpilib("ntcore")
|
||||
nativeConfig.dependencies.add wpilibTools.deps.wpilib("cscore")
|
||||
nativeConfig.dependencies.add wpilibTools.deps.wpilib("apriltag")
|
||||
nativeConfig.dependencies.add wpilibTools.deps.wpilib("hal")
|
||||
nativeConfig.dependencies.add wpilibTools.deps.wpilibOpenCv("frc" + wpi.frcYear.get(), wpi.versions.opencvVersion.get())
|
||||
nativeConfig.dependencies.add wpilibTools.deps.wpilibOpenCv("frc" + openCVYear, wpi.versions.opencvVersion.get())
|
||||
|
||||
dependencies {
|
||||
// JOGL stuff (currently we only distribute for aarch64, which is Pi 4)
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
rootProject.name = 'photon-core'
|
||||
@@ -84,15 +84,7 @@ public class CameraConfiguration {
|
||||
this.calibrations = new ArrayList<>();
|
||||
this.otherPaths = alternates;
|
||||
|
||||
logger.debug(
|
||||
"Creating USB camera configuration for "
|
||||
+ cameraType
|
||||
+ " "
|
||||
+ baseName
|
||||
+ " (AKA "
|
||||
+ nickname
|
||||
+ ") at "
|
||||
+ path);
|
||||
logger.debug("Creating USB camera configuration for " + this.toShortString());
|
||||
}
|
||||
|
||||
@JsonCreator
|
||||
@@ -120,15 +112,7 @@ public class CameraConfiguration {
|
||||
this.usbPID = usbPID;
|
||||
this.usbVID = usbVID;
|
||||
|
||||
logger.debug(
|
||||
"Creating camera configuration for "
|
||||
+ cameraType
|
||||
+ " "
|
||||
+ baseName
|
||||
+ " (AKA "
|
||||
+ nickname
|
||||
+ ") at "
|
||||
+ path);
|
||||
logger.debug("Loaded camera configuration for " + toShortString());
|
||||
}
|
||||
|
||||
public void addPipelineSettings(List<CVPipelineSettings> settings) {
|
||||
@@ -189,6 +173,30 @@ public class CameraConfiguration {
|
||||
return Arrays.stream(otherPaths).filter(path -> path.contains("/by-path/")).findFirst();
|
||||
}
|
||||
|
||||
public String toShortString() {
|
||||
return "CameraConfiguration [baseName="
|
||||
+ baseName
|
||||
+ ", uniqueName="
|
||||
+ uniqueName
|
||||
+ ", nickname="
|
||||
+ nickname
|
||||
+ ", path="
|
||||
+ path
|
||||
+ ", otherPaths="
|
||||
+ Arrays.toString(otherPaths)
|
||||
+ ", cameraType="
|
||||
+ cameraType
|
||||
+ ", cameraQuirks="
|
||||
+ cameraQuirks
|
||||
+ ", FOV="
|
||||
+ FOV
|
||||
+ "]"
|
||||
+ ", PID="
|
||||
+ usbPID
|
||||
+ ", VID="
|
||||
+ usbVID;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "CameraConfiguration [baseName="
|
||||
|
||||
@@ -340,7 +340,8 @@ public class ConfigManager {
|
||||
|
||||
/**
|
||||
* Disable flushing settings to disk as part of our JVM exit hook. Used to prevent uploading all
|
||||
* settings from getting its new configs overwritten at program exit and before theyre all loaded.
|
||||
* settings from getting its new configs overwritten at program exit and before they're all
|
||||
* loaded.
|
||||
*/
|
||||
public void disableFlushOnShutdown() {
|
||||
this.flushOnShutdown = false;
|
||||
|
||||
@@ -265,7 +265,7 @@ public class SqlConfigProvider extends ConfigProvider {
|
||||
JacksonUtils.deserialize(
|
||||
getOneConfigFile(conn, GlobalKeys.HARDWARE_CONFIG), HardwareConfig.class);
|
||||
} catch (IOException e) {
|
||||
logger.error("Could not deserialize hardware config! Loading defaults");
|
||||
logger.error("Could not deserialize hardware config! Loading defaults", e);
|
||||
hardwareConfig = new HardwareConfig();
|
||||
}
|
||||
|
||||
@@ -274,7 +274,7 @@ public class SqlConfigProvider extends ConfigProvider {
|
||||
JacksonUtils.deserialize(
|
||||
getOneConfigFile(conn, GlobalKeys.HARDWARE_SETTINGS), HardwareSettings.class);
|
||||
} catch (IOException e) {
|
||||
logger.error("Could not deserialize hardware settings! Loading defaults");
|
||||
logger.error("Could not deserialize hardware settings! Loading defaults", e);
|
||||
hardwareSettings = new HardwareSettings();
|
||||
}
|
||||
|
||||
@@ -283,7 +283,7 @@ public class SqlConfigProvider extends ConfigProvider {
|
||||
JacksonUtils.deserialize(
|
||||
getOneConfigFile(conn, GlobalKeys.NETWORK_CONFIG), NetworkConfig.class);
|
||||
} catch (IOException e) {
|
||||
logger.error("Could not deserialize network config! Loading defaults");
|
||||
logger.error("Could not deserialize network config! Loading defaults", e);
|
||||
networkConfig = new NetworkConfig();
|
||||
}
|
||||
|
||||
@@ -292,7 +292,7 @@ public class SqlConfigProvider extends ConfigProvider {
|
||||
JacksonUtils.deserialize(
|
||||
getOneConfigFile(conn, GlobalKeys.ATFL_CONFIG_FILE), AprilTagFieldLayout.class);
|
||||
} catch (IOException e) {
|
||||
logger.error("Could not deserialize apriltag layout! Loading defaults");
|
||||
logger.error("Could not deserialize apriltag layout! Loading defaults", e);
|
||||
try {
|
||||
atfl = AprilTagFieldLayout.loadField(AprilTagFields.kDefaultField);
|
||||
} catch (UncheckedIOException e2) {
|
||||
|
||||
@@ -20,7 +20,7 @@ package org.photonvision.common.dataflow.networktables;
|
||||
import edu.wpi.first.math.geometry.Transform3d;
|
||||
import edu.wpi.first.networktables.NetworkTable;
|
||||
import edu.wpi.first.networktables.NetworkTableEvent;
|
||||
import edu.wpi.first.util.WPIUtilJNI;
|
||||
import edu.wpi.first.networktables.NetworkTablesJNI;
|
||||
import java.util.List;
|
||||
import java.util.function.BooleanSupplier;
|
||||
import java.util.function.Consumer;
|
||||
@@ -146,13 +146,19 @@ public class NTDataPublisher implements CVPipelineResultConsumer {
|
||||
List.of(),
|
||||
result.inputAndOutputFrame);
|
||||
else acceptedResult = result;
|
||||
var now = WPIUtilJNI.now();
|
||||
var captureMicros = MathUtils.nanosToMicros(acceptedResult.getImageCaptureTimestampNanos());
|
||||
var now = NetworkTablesJNI.now();
|
||||
var captureMicros = MathUtils.nanosToMicros(result.getImageCaptureTimestampNanos());
|
||||
|
||||
var offset = NetworkTablesManager.getInstance().getOffset();
|
||||
|
||||
// Transform the metadata timestamps from the local nt::Now timebase to the Time Sync Server's
|
||||
// timebase
|
||||
var simplified =
|
||||
new PhotonPipelineResult(
|
||||
acceptedResult.sequenceID,
|
||||
captureMicros,
|
||||
now,
|
||||
captureMicros + offset,
|
||||
now + offset,
|
||||
NetworkTablesManager.getInstance().getTimeSinceLastPong(),
|
||||
TrackedTarget.simpleFromTrackedTargets(acceptedResult.targets),
|
||||
acceptedResult.multiTagResult);
|
||||
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
package org.photonvision.common.dataflow.networktables;
|
||||
|
||||
import edu.wpi.first.apriltag.AprilTagFieldLayout;
|
||||
import edu.wpi.first.networktables.LogMessage;
|
||||
import edu.wpi.first.networktables.NetworkTable;
|
||||
import edu.wpi.first.networktables.NetworkTableEvent;
|
||||
import edu.wpi.first.networktables.NetworkTableEvent.Kind;
|
||||
@@ -26,7 +27,6 @@ import edu.wpi.first.networktables.StringSubscriber;
|
||||
import java.io.IOException;
|
||||
import java.util.EnumSet;
|
||||
import java.util.HashMap;
|
||||
import java.util.function.Consumer;
|
||||
import org.photonvision.PhotonVersion;
|
||||
import org.photonvision.common.configuration.ConfigManager;
|
||||
import org.photonvision.common.configuration.NetworkConfig;
|
||||
@@ -34,6 +34,7 @@ import org.photonvision.common.dataflow.DataChangeService;
|
||||
import org.photonvision.common.dataflow.events.OutgoingUIEvent;
|
||||
import org.photonvision.common.hardware.HardwareManager;
|
||||
import org.photonvision.common.logging.LogGroup;
|
||||
import org.photonvision.common.logging.LogLevel;
|
||||
import org.photonvision.common.logging.Logger;
|
||||
import org.photonvision.common.scripting.ScriptEventType;
|
||||
import org.photonvision.common.scripting.ScriptManager;
|
||||
@@ -41,32 +42,39 @@ import org.photonvision.common.util.TimedTaskManager;
|
||||
import org.photonvision.common.util.file.JacksonUtils;
|
||||
|
||||
public class NetworkTablesManager {
|
||||
private static final Logger logger =
|
||||
new Logger(NetworkTablesManager.class, LogGroup.NetworkTables);
|
||||
|
||||
private final NetworkTableInstance ntInstance = NetworkTableInstance.getDefault();
|
||||
private final String kRootTableName = "/photonvision";
|
||||
private final String kFieldLayoutName = "apriltag_field_layout";
|
||||
public final NetworkTable kRootTable = ntInstance.getTable(kRootTableName);
|
||||
|
||||
private final NTLogger m_ntLogger = new NTLogger();
|
||||
|
||||
private boolean m_isRetryingConnection = false;
|
||||
|
||||
private StringSubscriber m_fieldLayoutSubscriber =
|
||||
kRootTable.getStringTopic(kFieldLayoutName).subscribe("");
|
||||
|
||||
private final TimeSyncManager m_timeSync = new TimeSyncManager(kRootTable);
|
||||
|
||||
private NetworkTablesManager() {
|
||||
ntInstance.addLogger(255, 255, (event) -> {}); // to hide error messages
|
||||
ntInstance.addConnectionListener(true, m_ntLogger); // to hide error messages
|
||||
ntInstance.addLogger(
|
||||
LogMessage.kInfo, LogMessage.kCritical, this::logNtMessage); // to hide error messages
|
||||
ntInstance.addConnectionListener(true, this::checkNtConnectState); // to hide error messages
|
||||
|
||||
ntInstance.addListener(
|
||||
m_fieldLayoutSubscriber, EnumSet.of(Kind.kValueAll), this::onFieldLayoutChanged);
|
||||
|
||||
TimedTaskManager.getInstance().addTask("NTManager", this::ntTick, 5000);
|
||||
|
||||
// Get the UI state in sync with the backend. NT should fire a callback when it first connects
|
||||
// to the robot
|
||||
broadcastConnectedStatus();
|
||||
}
|
||||
|
||||
public void registerTimedTasks() {
|
||||
m_timeSync.start();
|
||||
TimedTaskManager.getInstance().addTask("NTManager", this::ntTick, 5000);
|
||||
}
|
||||
|
||||
private static NetworkTablesManager INSTANCE;
|
||||
|
||||
public static NetworkTablesManager getInstance() {
|
||||
@@ -74,43 +82,72 @@ public class NetworkTablesManager {
|
||||
return INSTANCE;
|
||||
}
|
||||
|
||||
private static final Logger logger = new Logger(NetworkTablesManager.class, LogGroup.General);
|
||||
private void logNtMessage(NetworkTableEvent event) {
|
||||
String levelmsg = "DEBUG";
|
||||
LogLevel pvlevel = LogLevel.DEBUG;
|
||||
if (event.logMessage.level >= LogMessage.kCritical) {
|
||||
pvlevel = LogLevel.ERROR;
|
||||
levelmsg = "CRITICAL";
|
||||
} else if (event.logMessage.level >= LogMessage.kError) {
|
||||
pvlevel = LogLevel.ERROR;
|
||||
levelmsg = "ERROR";
|
||||
} else if (event.logMessage.level >= LogMessage.kWarning) {
|
||||
pvlevel = LogLevel.WARN;
|
||||
levelmsg = "WARNING";
|
||||
} else if (event.logMessage.level >= LogMessage.kInfo) {
|
||||
pvlevel = LogLevel.INFO;
|
||||
levelmsg = "INFO";
|
||||
}
|
||||
|
||||
private static class NTLogger implements Consumer<NetworkTableEvent> {
|
||||
private boolean hasReportedConnectionFailure = false;
|
||||
logger.log(
|
||||
"NT: "
|
||||
+ levelmsg
|
||||
+ " "
|
||||
+ event.logMessage.level
|
||||
+ ": "
|
||||
+ event.logMessage.message
|
||||
+ " ("
|
||||
+ event.logMessage.filename
|
||||
+ ":"
|
||||
+ event.logMessage.line
|
||||
+ ")",
|
||||
pvlevel);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void accept(NetworkTableEvent event) {
|
||||
var isConnEvent = event.is(Kind.kConnected);
|
||||
var isDisconnEvent = event.is(Kind.kDisconnected);
|
||||
public void checkNtConnectState(NetworkTableEvent event) {
|
||||
var isConnEvent = event.is(Kind.kConnected);
|
||||
var isDisconnEvent = event.is(Kind.kDisconnected);
|
||||
|
||||
if (!hasReportedConnectionFailure && isDisconnEvent) {
|
||||
var msg =
|
||||
String.format(
|
||||
"NT lost connection to %s:%d! (NT version %d). Will retry in background.",
|
||||
event.connInfo.remote_ip,
|
||||
event.connInfo.remote_port,
|
||||
event.connInfo.protocol_version);
|
||||
logger.error(msg);
|
||||
HardwareManager.getInstance().setNTConnected(false);
|
||||
if (isDisconnEvent) {
|
||||
var msg =
|
||||
String.format(
|
||||
"NT lost connection to %s:%d! (NT version %d). Will retry in background.",
|
||||
event.connInfo.remote_ip,
|
||||
event.connInfo.remote_port,
|
||||
event.connInfo.protocol_version);
|
||||
logger.error(msg);
|
||||
HardwareManager.getInstance().setNTConnected(false);
|
||||
|
||||
hasReportedConnectionFailure = true;
|
||||
getInstance().broadcastConnectedStatus();
|
||||
} else if (isConnEvent && event.connInfo != null) {
|
||||
var msg =
|
||||
String.format(
|
||||
"NT connected to %s:%d! (NT version %d)",
|
||||
event.connInfo.remote_ip,
|
||||
event.connInfo.remote_port,
|
||||
event.connInfo.protocol_version);
|
||||
logger.info(msg);
|
||||
HardwareManager.getInstance().setNTConnected(true);
|
||||
getInstance().broadcastConnectedStatus();
|
||||
} else if (isConnEvent && event.connInfo != null) {
|
||||
var msg =
|
||||
String.format(
|
||||
"NT connected to %s:%d! (NT version %d)",
|
||||
event.connInfo.remote_ip,
|
||||
event.connInfo.remote_port,
|
||||
event.connInfo.protocol_version);
|
||||
logger.info(msg);
|
||||
HardwareManager.getInstance().setNTConnected(true);
|
||||
|
||||
hasReportedConnectionFailure = false;
|
||||
ScriptManager.queueEvent(ScriptEventType.kNTConnected);
|
||||
getInstance().broadcastVersion();
|
||||
getInstance().broadcastConnectedStatus();
|
||||
}
|
||||
ScriptManager.queueEvent(ScriptEventType.kNTConnected);
|
||||
getInstance().broadcastVersion();
|
||||
getInstance().broadcastConnectedStatus();
|
||||
|
||||
m_timeSync.reportNtConnected();
|
||||
} else if (isConnEvent) {
|
||||
logger.warn("Got connection event with no connection info??");
|
||||
} else {
|
||||
logger.warn("Got a non-sensical connection message that is neither connect nor disconnect?");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -168,9 +205,16 @@ public class NetworkTablesManager {
|
||||
} else {
|
||||
setClientMode(config.ntServerAddress);
|
||||
}
|
||||
|
||||
m_timeSync.setConfig(config);
|
||||
|
||||
broadcastVersion();
|
||||
}
|
||||
|
||||
public long getOffset() {
|
||||
return m_timeSync.getOffset();
|
||||
}
|
||||
|
||||
private void setClientMode(String ntServerAddress) {
|
||||
ntInstance.stopServer();
|
||||
ntInstance.startClient4("photonvision");
|
||||
@@ -211,4 +255,8 @@ public class NetworkTablesManager {
|
||||
"[NetworkTablesManager] Could not connect to the robot! Will retry in the background...");
|
||||
}
|
||||
}
|
||||
|
||||
public long getTimeSinceLastPong() {
|
||||
return m_timeSync.getTimeSinceLastPong();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,169 @@
|
||||
/*
|
||||
* 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.dataflow.networktables;
|
||||
|
||||
import edu.wpi.first.cscore.CameraServerJNI;
|
||||
import edu.wpi.first.networktables.IntegerPublisher;
|
||||
import edu.wpi.first.networktables.NetworkTable;
|
||||
import edu.wpi.first.networktables.NetworkTableInstance;
|
||||
import org.photonvision.common.configuration.NetworkConfig;
|
||||
import org.photonvision.common.logging.LogGroup;
|
||||
import org.photonvision.common.logging.Logger;
|
||||
import org.photonvision.common.util.TimedTaskManager;
|
||||
import org.photonvision.jni.PhotonTargetingJniLoader;
|
||||
import org.photonvision.jni.TimeSyncClient;
|
||||
import org.photonvision.jni.TimeSyncServer;
|
||||
|
||||
public class TimeSyncManager {
|
||||
private static final Logger logger = new Logger(TimeSyncManager.class, LogGroup.NetworkTables);
|
||||
|
||||
private TimeSyncClient m_client = null;
|
||||
private TimeSyncServer m_server = null;
|
||||
|
||||
private NetworkTableInstance ntInstance;
|
||||
IntegerPublisher m_offsetPub;
|
||||
IntegerPublisher m_rtt2Pub;
|
||||
IntegerPublisher m_pingsPub;
|
||||
IntegerPublisher m_pongsPub;
|
||||
IntegerPublisher m_lastPongTimePub;
|
||||
|
||||
public TimeSyncManager(NetworkTable kRootTable) {
|
||||
if (!PhotonTargetingJniLoader.isWorking) {
|
||||
logger.error("PhotonTargetingJNI was not loaded! Cannot do time-sync");
|
||||
}
|
||||
|
||||
this.ntInstance = kRootTable.getInstance();
|
||||
|
||||
// Need this subtable to be unique per coprocessor. TODO: consider using MAC address or
|
||||
// something similar for metrics?
|
||||
var timeTable = kRootTable.getSubTable(".timesync").getSubTable(CameraServerJNI.getHostname());
|
||||
m_offsetPub = timeTable.getIntegerTopic("offset_us").publish();
|
||||
m_rtt2Pub = timeTable.getIntegerTopic("rtt2_us").publish();
|
||||
m_pingsPub = timeTable.getIntegerTopic("ping_tx_count").publish();
|
||||
m_pongsPub = timeTable.getIntegerTopic("pong_rx_count").publish();
|
||||
m_lastPongTimePub = timeTable.getIntegerTopic("pong_rx_time_us").publish();
|
||||
|
||||
// default to being a client
|
||||
logger.debug("Starting TimeSyncClient on localhost (for now)");
|
||||
m_client = new TimeSyncClient("127.0.0.1", 5810, 1.0);
|
||||
}
|
||||
|
||||
// Since we're spinning off tasks in a new thread, be careful and start it seperately
|
||||
public void start() {
|
||||
if (!PhotonTargetingJniLoader.isWorking) {
|
||||
logger.error("PhotonTargetingJNI was not loaded! Cannot start");
|
||||
}
|
||||
|
||||
TimedTaskManager.getInstance().addTask("TimeSyncManager::tick", this::tick, 1000);
|
||||
}
|
||||
|
||||
public synchronized long getOffset() {
|
||||
if (!PhotonTargetingJniLoader.isWorking) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
// if we're a client, return the offset to server time
|
||||
if (m_client != null) return m_client.getOffset();
|
||||
// if we're a server, our time (nt::Now) is the same as network time
|
||||
if (m_server != null) return 0;
|
||||
|
||||
// ????? should never hit
|
||||
logger.error("Client and server and null?");
|
||||
return 0;
|
||||
}
|
||||
|
||||
synchronized void setConfig(NetworkConfig config) {
|
||||
if (!PhotonTargetingJniLoader.isWorking) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (m_client == null && m_server == null) {
|
||||
throw new RuntimeException("Neither client nor server are null?");
|
||||
}
|
||||
|
||||
// if not already running a server, set it up
|
||||
if (config.runNTServer && m_server == null) {
|
||||
// tear down anything old
|
||||
if (m_client != null) {
|
||||
logger.debug("Tearing down old client");
|
||||
m_client.stop();
|
||||
m_client = null;
|
||||
}
|
||||
|
||||
logger.debug("Starting TimeSyncServer");
|
||||
m_server = new TimeSyncServer(5810);
|
||||
m_server.start();
|
||||
} else
|
||||
// if not already running a client, set it up
|
||||
if (m_client == null) {
|
||||
// tear down anything old
|
||||
if (m_server != null) {
|
||||
logger.debug("Tearing down old server");
|
||||
m_server.stop();
|
||||
m_server = null;
|
||||
}
|
||||
|
||||
// Guess at IP -- tick will take care of changing this (may take up to 1 second)
|
||||
logger.debug("Starting TimeSyncClient on localhost (for now)");
|
||||
m_client = new TimeSyncClient("127.0.0.1", 5810, 1.0);
|
||||
}
|
||||
}
|
||||
|
||||
synchronized void tick() {
|
||||
if (m_client != null) {
|
||||
var conns = ntInstance.getConnections();
|
||||
|
||||
if (conns.length > 0) {
|
||||
logger.debug("Changing TimeSyncClient server to " + conns[0].remote_ip);
|
||||
m_client.setServer(conns[0].remote_ip);
|
||||
}
|
||||
|
||||
if (m_client != null) {
|
||||
var m = m_client.getPingMetadata();
|
||||
|
||||
m_offsetPub.set(m.offset);
|
||||
m_rtt2Pub.set(m.rtt2);
|
||||
m_pingsPub.set(m.pingsSent);
|
||||
m_pongsPub.set(m.pongsReceived);
|
||||
m_lastPongTimePub.set(m.lastPongTime);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public synchronized long getTimeSinceLastPong() {
|
||||
if (m_client != null) {
|
||||
return m_client.getPingMetadata().timeSinceLastPong();
|
||||
} else if (m_server != null) {
|
||||
return 0;
|
||||
} else {
|
||||
// ????
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
/** Restart our timesync client if NT just connected */
|
||||
public synchronized void reportNtConnected() {
|
||||
if (m_client != null) {
|
||||
// restart (in java code; we could just add a reset metrics function...)
|
||||
logger.debug(
|
||||
"NT (re)connected -- restarting Time Sync Client at " + m_client.getServer() + ":5810");
|
||||
m_client.stop();
|
||||
m_client = new TimeSyncClient(m_client.getServer(), 5810, 1.0);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -28,7 +28,7 @@ public enum PigpioCommand {
|
||||
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_cyle(unsigned user_gpio)
|
||||
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)
|
||||
|
||||
|
||||
@@ -27,7 +27,7 @@ public class RK3588Cmds extends LinuxCmds {
|
||||
// CPU Temperature
|
||||
/* The RK3588 chip has 7 thermal zones that can be accessed via:
|
||||
* /sys/class/thermal/thermal_zoneX/temp
|
||||
* where X is an interger from 0 to 6.
|
||||
* where X is an integer from 0 to 6.
|
||||
*
|
||||
* || Zone || Location || Comments ||
|
||||
* | 0 | soc | soc thermal (near the center of the chip) |
|
||||
|
||||
@@ -25,4 +25,5 @@ public enum LogGroup {
|
||||
General,
|
||||
Config,
|
||||
CSCore,
|
||||
NetworkTables,
|
||||
}
|
||||
|
||||
@@ -100,6 +100,7 @@ public class Logger {
|
||||
levelMap.put(LogGroup.VisionModule, LogLevel.INFO);
|
||||
levelMap.put(LogGroup.Config, LogLevel.INFO);
|
||||
levelMap.put(LogGroup.CSCore, LogLevel.TRACE);
|
||||
levelMap.put(LogGroup.NetworkTables, LogLevel.DEBUG);
|
||||
}
|
||||
|
||||
static {
|
||||
@@ -200,7 +201,7 @@ public class Logger {
|
||||
return logLevel.code <= levelMap.get(group).code;
|
||||
}
|
||||
|
||||
void log(String message, LogLevel level) {
|
||||
public void log(String message, LogLevel level) {
|
||||
if (shouldLog(level)) {
|
||||
log(message, level, group, className);
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ package org.photonvision.common.networking;
|
||||
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.NoSuchElementException;
|
||||
|
||||
import org.photonvision.common.configuration.ConfigManager;
|
||||
@@ -71,10 +72,7 @@ public class NetworkManager {
|
||||
// Start tasks to monitor the network interface(s)
|
||||
var ethernetDevices = NetworkUtils.getAllWiredInterfaces();
|
||||
for (NMDeviceInfo deviceInfo : ethernetDevices) {
|
||||
var task = "deviceStatus-" + deviceInfo.devName;
|
||||
if (!TimedTaskManager.getInstance().taskActive(task)) {
|
||||
TimedTaskManager.getInstance().addTask(task, deviceStatus(deviceInfo.devName), 5000);
|
||||
}
|
||||
monitorDevice(deviceInfo.devName, 5000);
|
||||
}
|
||||
|
||||
var physicalDevices = NetworkUtils.getAllActiveWiredInterfaces();
|
||||
@@ -258,14 +256,22 @@ public class NetworkManager {
|
||||
}
|
||||
|
||||
// Detects changes in the carrier and reinitializes after re-connect
|
||||
private Runnable deviceStatus(String devName) {
|
||||
Path file = Path.of("/sys/class/net/{device}/carrier".replace("{device}", devName));
|
||||
logger.debug("Watching network interface at path: " + file.toString());
|
||||
var last = new Object() {boolean carrier = true;};
|
||||
return () ->
|
||||
{
|
||||
private void monitorDevice(String devName, int millisInterval) {
|
||||
String taskName = "deviceStatus-" + devName;
|
||||
if (TimedTaskManager.getInstance().taskActive(taskName)) {
|
||||
// task is already running
|
||||
return;
|
||||
}
|
||||
Path path = Paths.get("/sys/class/net/{device}/carrier".replace("{device}", devName));
|
||||
if (Files.notExists(path)) {
|
||||
logger.error("Can't find " + path + ", so can't monitor " + devName);
|
||||
return;
|
||||
}
|
||||
logger.debug("Watching network interface at path: " + path);
|
||||
var last = new Object() {boolean carrier = true; boolean exceptionLogged = false;};
|
||||
Runnable task = () -> {
|
||||
try {
|
||||
boolean carrier = Files.readString(file).trim().equals("1");
|
||||
boolean carrier = Files.readString(path).trim().equals("1");
|
||||
if (carrier != last.carrier) {
|
||||
if (carrier) {
|
||||
// carrier came back
|
||||
@@ -276,9 +282,16 @@ public class NetworkManager {
|
||||
}
|
||||
}
|
||||
last.carrier = carrier;
|
||||
} catch (Exception e) {
|
||||
logger.error("Could not check network status", e);
|
||||
}
|
||||
};
|
||||
last.exceptionLogged = false;
|
||||
} catch (Exception e) {
|
||||
if (!last.exceptionLogged) {
|
||||
// Log the exception only once, but keep trying
|
||||
logger.error("Could not check network status for " + devName, e);
|
||||
last.exceptionLogged = true;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
TimedTaskManager.getInstance().addTask(taskName, task, millisInterval);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,71 +18,20 @@
|
||||
package org.photonvision.common.util;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import edu.wpi.first.apriltag.jni.AprilTagJNI;
|
||||
import edu.wpi.first.cscore.CameraServerCvJNI;
|
||||
import edu.wpi.first.cscore.CameraServerJNI;
|
||||
import edu.wpi.first.hal.JNIWrapper;
|
||||
import edu.wpi.first.math.WPIMathJNI;
|
||||
import edu.wpi.first.math.geometry.Translation2d;
|
||||
import edu.wpi.first.math.util.Units;
|
||||
import edu.wpi.first.net.WPINetJNI;
|
||||
import edu.wpi.first.networktables.NetworkTablesJNI;
|
||||
import edu.wpi.first.util.CombinedRuntimeLoader;
|
||||
import edu.wpi.first.util.WPIUtilJNI;
|
||||
import java.awt.HeadlessException;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Path;
|
||||
import org.opencv.core.Core;
|
||||
import org.opencv.core.Mat;
|
||||
import org.opencv.highgui.HighGui;
|
||||
import org.photonvision.jni.WpilibLoader;
|
||||
import org.photonvision.vision.calibration.CameraCalibrationCoefficients;
|
||||
|
||||
public class TestUtils {
|
||||
private static boolean has_loaded = false;
|
||||
|
||||
public static boolean loadLibraries() {
|
||||
if (has_loaded) return true;
|
||||
|
||||
NetworkTablesJNI.Helper.setExtractOnStaticLoad(false);
|
||||
WPIUtilJNI.Helper.setExtractOnStaticLoad(false);
|
||||
WPIMathJNI.Helper.setExtractOnStaticLoad(false);
|
||||
CameraServerJNI.Helper.setExtractOnStaticLoad(false);
|
||||
CameraServerCvJNI.Helper.setExtractOnStaticLoad(false);
|
||||
// OpenCvLoader.Helper.setExtractOnStaticLoad(false);
|
||||
JNIWrapper.Helper.setExtractOnStaticLoad(false);
|
||||
WPINetJNI.Helper.setExtractOnStaticLoad(false);
|
||||
AprilTagJNI.Helper.setExtractOnStaticLoad(false);
|
||||
|
||||
// wpimathjni is a bit odd, it's all in the wpimathjni shared lib, but the java side stuff has
|
||||
// been split.
|
||||
// ArmFeedforwardJNI.Helper.setExtractOnStaticLoad(false);
|
||||
// DAREJNI.Helper.setExtractOnStaticLoad(false);
|
||||
// EigenJNI.Helper.setExtractOnStaticLoad(false);
|
||||
// Ellipse2dJNI.Helper.setExtractOnStaticLoad(false);
|
||||
// Pose3dJNI.Helper.setExtractOnStaticLoad(false);
|
||||
// StateSpaceUtilJNI.Helper.setExtractOnStaticLoad(false);
|
||||
// TrajectoryUtilJNI.Helper.setExtractOnStaticLoad(false);
|
||||
|
||||
try {
|
||||
CombinedRuntimeLoader.loadLibraries(
|
||||
TestUtils.class,
|
||||
"wpiutiljni",
|
||||
"wpimathjni",
|
||||
"ntcorejni",
|
||||
"wpinetjni",
|
||||
"wpiHaljni",
|
||||
Core.NATIVE_LIBRARY_NAME,
|
||||
"cscorejni",
|
||||
"apriltagjni");
|
||||
|
||||
has_loaded = true;
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
has_loaded = false;
|
||||
}
|
||||
|
||||
return has_loaded;
|
||||
return WpilibLoader.loadLibraries();
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
|
||||
@@ -33,6 +33,7 @@ import java.io.IOException;
|
||||
import java.nio.file.Path;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import org.eclipse.jetty.io.EofException;
|
||||
|
||||
public class JacksonUtils {
|
||||
public static class UIMap extends HashMap<String, Object> {}
|
||||
@@ -76,6 +77,10 @@ public class JacksonUtils {
|
||||
}
|
||||
|
||||
public static <T> T deserialize(String s, Class<T> ref) throws IOException {
|
||||
if (s.length() == 0) {
|
||||
throw new EofException("Provided empty string for class " + ref.getName());
|
||||
}
|
||||
|
||||
PolymorphicTypeValidator ptv =
|
||||
BasicPolymorphicTypeValidator.builder().allowIfBaseType(ref).build();
|
||||
ObjectMapper objectMapper =
|
||||
|
||||
@@ -25,7 +25,7 @@ import edu.wpi.first.math.geometry.Rotation3d;
|
||||
import edu.wpi.first.math.geometry.Transform3d;
|
||||
import edu.wpi.first.math.geometry.Translation3d;
|
||||
import edu.wpi.first.math.util.Units;
|
||||
import edu.wpi.first.util.WPIUtilJNI;
|
||||
import edu.wpi.first.networktables.NetworkTablesJNI;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import org.opencv.core.Core;
|
||||
@@ -98,7 +98,7 @@ public class MathUtils {
|
||||
}
|
||||
|
||||
public static long wpiNanoTime() {
|
||||
return microsToNanos(WPIUtilJNI.now());
|
||||
return microsToNanos(NetworkTablesJNI.now());
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -143,7 +143,7 @@ public class QuirkyCamera {
|
||||
* @param usbVid USB VID of camera
|
||||
* @param usbPid USB PID of camera
|
||||
* @param baseName CSCore name of camera
|
||||
* @param displayName Human-friendly quicky camera name
|
||||
* @param displayName Human-friendly quirky camera name
|
||||
* @param quirks Camera quirks
|
||||
*/
|
||||
private QuirkyCamera(
|
||||
@@ -160,7 +160,7 @@ public class QuirkyCamera {
|
||||
this.quirks.put(q, true);
|
||||
}
|
||||
|
||||
// (2) for all other quirks in CameraQuirks (in this version of Photon), defalut to false
|
||||
// (2) for all other quirks in CameraQuirks (in this version of Photon), default to false
|
||||
for (var q : CameraQuirk.values()) {
|
||||
this.quirks.putIfAbsent(q, false);
|
||||
}
|
||||
|
||||
@@ -240,7 +240,8 @@ public class GenericUSBCameraSettables extends VisionSourceSettables {
|
||||
|
||||
@Override
|
||||
public VideoMode getCurrentVideoMode() {
|
||||
return camera.isConnected() ? camera.getVideoMode() : null;
|
||||
return camera
|
||||
.getVideoMode(); // This returns the current video mode even if the camera is disconnected
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -250,7 +251,7 @@ public class GenericUSBCameraSettables extends VisionSourceSettables {
|
||||
logger.error("Got a null video mode! Doing nothing...");
|
||||
return;
|
||||
}
|
||||
camera.setVideoMode(videoMode);
|
||||
if (camera.setVideoMode(videoMode)) logger.debug("Failed to set video mode!");
|
||||
} catch (Exception e) {
|
||||
logger.error("Failed to set video mode!", e);
|
||||
}
|
||||
|
||||
@@ -152,17 +152,17 @@ public class FileSaveFrameConsumer implements Consumer<CVMat> {
|
||||
var matchType = ntMatchType.getAtomic();
|
||||
if (matchType.timestamp == 0) {
|
||||
// no NT info yet
|
||||
logger.warn("Did not recieve match type, defaulting to 0");
|
||||
logger.warn("Did not receive match type, defaulting to 0");
|
||||
}
|
||||
|
||||
var matchNum = ntMatchNum.getAtomic();
|
||||
if (matchNum.timestamp == 0) {
|
||||
logger.warn("Did not recieve match num, defaulting to -1");
|
||||
logger.warn("Did not receive match num, defaulting to -1");
|
||||
}
|
||||
|
||||
var eventName = ntEventName.getAtomic();
|
||||
if (eventName.timestamp == 0) {
|
||||
logger.warn("Did not recieve event name, defaulting to 'UNKNOWN'");
|
||||
logger.warn("Did not receive event name, defaulting to 'UNKNOWN'");
|
||||
}
|
||||
|
||||
String matchTypeStr =
|
||||
|
||||
@@ -42,18 +42,18 @@ public class USBFrameProvider extends CpuImageProcessor {
|
||||
|
||||
@Override
|
||||
public CapturedFrame getInputMat() {
|
||||
var mat = new CVMat(); // We do this so that we don't fill a Mat in use by another thread
|
||||
// This is from wpi::Now, or WPIUtilJNI.now()
|
||||
long time =
|
||||
cvSink.grabFrame(mat.getMat())
|
||||
* 1000; // Units are microseconds, epoch is the same as the Unix epoch
|
||||
// We allocate memory so we don't fill a Mat in use by another thread (memory model is easier)
|
||||
var mat = new CVMat();
|
||||
// This is from wpi::Now, or WPIUtilJNI.now(). The epoch from grabFrame is uS since
|
||||
// Hal::initialize was called
|
||||
long captureTimeNs = cvSink.grabFrame(mat.getMat()) * 1000;
|
||||
|
||||
if (time == 0) {
|
||||
if (captureTimeNs == 0) {
|
||||
var error = cvSink.getError();
|
||||
logger.error("Error grabbing image: " + error);
|
||||
}
|
||||
|
||||
return new CapturedFrame(mat, settables.getFrameStaticProperties(), time);
|
||||
return new CapturedFrame(mat, settables.getFrameStaticProperties(), captureTimeNs);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -59,6 +59,7 @@ public class AprilTagDetectionPipe
|
||||
public void setParams(AprilTagDetectionPipeParams newParams) {
|
||||
if (this.params == null || !this.params.equals(newParams)) {
|
||||
m_detector.setConfig(newParams.detectorParams);
|
||||
m_detector.setQuadThresholdParameters(newParams.quadParams);
|
||||
|
||||
m_detector.clearFamilies();
|
||||
m_detector.addFamily(newParams.family.getNativeName());
|
||||
|
||||
@@ -23,10 +23,15 @@ import org.photonvision.vision.apriltag.AprilTagFamily;
|
||||
public class AprilTagDetectionPipeParams {
|
||||
public final AprilTagFamily family;
|
||||
public final AprilTagDetector.Config detectorParams;
|
||||
public final AprilTagDetector.QuadThresholdParameters quadParams;
|
||||
|
||||
public AprilTagDetectionPipeParams(AprilTagFamily tagFamily, AprilTagDetector.Config config) {
|
||||
public AprilTagDetectionPipeParams(
|
||||
AprilTagFamily tagFamily,
|
||||
AprilTagDetector.Config config,
|
||||
AprilTagDetector.QuadThresholdParameters quadParams) {
|
||||
this.family = tagFamily;
|
||||
this.detectorParams = config;
|
||||
this.quadParams = quadParams;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -35,6 +40,7 @@ public class AprilTagDetectionPipeParams {
|
||||
int result = 1;
|
||||
result = prime * result + ((family == null) ? 0 : family.hashCode());
|
||||
result = prime * result + ((detectorParams == null) ? 0 : detectorParams.hashCode());
|
||||
result = prime * result + ((quadParams == null) ? 0 : quadParams.hashCode());
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -46,7 +52,11 @@ public class AprilTagDetectionPipeParams {
|
||||
AprilTagDetectionPipeParams other = (AprilTagDetectionPipeParams) obj;
|
||||
if (family != other.family) return false;
|
||||
if (detectorParams == null) {
|
||||
return other.detectorParams == null;
|
||||
} else return detectorParams.equals(other.detectorParams);
|
||||
if (other.detectorParams != null) return false;
|
||||
} else if (!detectorParams.equals(other.detectorParams)) return false;
|
||||
if (quadParams == null) {
|
||||
if (other.quadParams != null) return false;
|
||||
} else if (!quadParams.equals(other.quadParams)) return false;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -140,7 +140,7 @@ public class CornerDetectionPipe
|
||||
var compareDistToTr =
|
||||
Comparator.comparingDouble((Point p) -> distanceBetween(p, boundingBoxCorners.get(2)));
|
||||
|
||||
// top left and top right are the poly corners closest to the bouding box tl and tr
|
||||
// top left and top right are the poly corners closest to the bounding box tl and tr
|
||||
pointList.sort(compareDistToTl);
|
||||
var tl = pointList.get(0);
|
||||
pointList.remove(tl);
|
||||
|
||||
@@ -87,7 +87,21 @@ public class AprilTagPipeline extends CVPipeline<CVPipelineResult, AprilTagPipel
|
||||
config.refineEdges = settings.refineEdges;
|
||||
config.quadSigma = (float) settings.blur;
|
||||
config.quadDecimate = settings.decimate;
|
||||
aprilTagDetectionPipe.setParams(new AprilTagDetectionPipeParams(settings.tagFamily, config));
|
||||
|
||||
var quadParams = new AprilTagDetector.QuadThresholdParameters();
|
||||
// 5 was the default minClusterPixels in WPILib prior to 2025
|
||||
// increasing it causes detection problems when decimate > 1
|
||||
quadParams.minClusterPixels = 5;
|
||||
// these are the same as the values in WPILib 2025
|
||||
// setting them here to prevent upstream changes from changing behavior of the detector
|
||||
quadParams.maxNumMaxima = 10;
|
||||
quadParams.criticalAngle = 45 * Math.PI / 180.0;
|
||||
quadParams.maxLineFitMSE = 10.0f;
|
||||
quadParams.minWhiteBlackDiff = 5;
|
||||
quadParams.deglitch = false;
|
||||
|
||||
aprilTagDetectionPipe.setParams(
|
||||
new AprilTagDetectionPipeParams(settings.tagFamily, config, quadParams));
|
||||
|
||||
if (frameStaticProperties.cameraCalibration != null) {
|
||||
var cameraMatrix = frameStaticProperties.cameraCalibration.getCameraIntrinsicsMat();
|
||||
|
||||
@@ -512,7 +512,7 @@ public class PipelineManager {
|
||||
var oldSettings = userPipelineSettings.get(idx);
|
||||
|
||||
var name = getCurrentPipelineSettings().pipelineNickname;
|
||||
// Dummy settings to copy common fileds over
|
||||
// Dummy settings to copy common fields over
|
||||
var newSettings = createSettingsForType(type, name);
|
||||
|
||||
// Copy all fields from AdvancedPipelineSettings/its superclasses from old to new
|
||||
|
||||
@@ -30,6 +30,7 @@ import org.photonvision.common.configuration.ConfigManager;
|
||||
import org.photonvision.common.dataflow.DataChangeService;
|
||||
import org.photonvision.common.dataflow.events.OutgoingUIEvent;
|
||||
import org.photonvision.common.hardware.Platform;
|
||||
import org.photonvision.common.hardware.Platform.OSType;
|
||||
import org.photonvision.common.logging.LogGroup;
|
||||
import org.photonvision.common.logging.Logger;
|
||||
import org.photonvision.common.util.TimedTaskManager;
|
||||
@@ -128,20 +129,26 @@ public class VisionSourceManager {
|
||||
return tryMatchCamImpl(null);
|
||||
}
|
||||
|
||||
protected List<VisionSource> tryMatchCamImpl(ArrayList<CameraInfo> cameraInfos) {
|
||||
return tryMatchCamImpl(cameraInfos, Platform.getCurrentPlatform());
|
||||
}
|
||||
|
||||
/**
|
||||
* @param cameraInfos Used to feed camera info for unit tests.
|
||||
* @return New VisionSources.
|
||||
*/
|
||||
protected List<VisionSource> tryMatchCamImpl(ArrayList<CameraInfo> cameraInfos) {
|
||||
protected List<VisionSource> tryMatchCamImpl(
|
||||
ArrayList<CameraInfo> cameraInfos, Platform platform) {
|
||||
boolean createSources = true;
|
||||
List<CameraInfo> connectedCameras;
|
||||
if (cameraInfos == null) {
|
||||
// Detect USB cameras using CSCore
|
||||
connectedCameras = new ArrayList<>(filterAllowedDevices(getConnectedUSBCameras()));
|
||||
connectedCameras = new ArrayList<>(filterAllowedDevices(getConnectedUSBCameras(), platform));
|
||||
// Detect CSI cameras using libcamera
|
||||
connectedCameras.addAll(new ArrayList<>(filterAllowedDevices(getConnectedCSICameras())));
|
||||
connectedCameras.addAll(
|
||||
new ArrayList<>(filterAllowedDevices(getConnectedCSICameras(), platform)));
|
||||
} else {
|
||||
connectedCameras = new ArrayList<>(filterAllowedDevices(cameraInfos));
|
||||
connectedCameras = new ArrayList<>(filterAllowedDevices(cameraInfos, platform));
|
||||
createSources =
|
||||
false; // Dont create sources if we are using supplied camerainfo for unit tests.
|
||||
}
|
||||
@@ -162,7 +169,7 @@ public class VisionSourceManager {
|
||||
// All cameras are already loaded return no new sources.
|
||||
if (connectedCameras.isEmpty()) return null;
|
||||
|
||||
logger.debug("Matching " + connectedCameras.size() + " new cameras!");
|
||||
logger.debug("Matching " + connectedCameras.size() + " new camera(s)!");
|
||||
|
||||
// Debug prints
|
||||
for (var info : connectedCameras) {
|
||||
@@ -170,7 +177,7 @@ public class VisionSourceManager {
|
||||
}
|
||||
|
||||
if (!unmatchedLoadedConfigs.isEmpty())
|
||||
logger.debug("Trying to match " + unmatchedLoadedConfigs.size() + " unmatched configs...");
|
||||
logger.debug("Trying to match " + unmatchedLoadedConfigs.size() + " unmatched config(s)...");
|
||||
|
||||
// Match camera configs to physical cameras
|
||||
List<CameraConfiguration> matchedCameras =
|
||||
@@ -182,7 +189,7 @@ public class VisionSourceManager {
|
||||
() ->
|
||||
"After matching, "
|
||||
+ unmatchedLoadedConfigs.size()
|
||||
+ " configs remained unmatched. Is your camera disconnected?");
|
||||
+ " config(s) remained unmatched. Is your camera disconnected?");
|
||||
logger.warn(
|
||||
"Unloaded configs: "
|
||||
+ unmatchedLoadedConfigs.stream()
|
||||
@@ -233,7 +240,7 @@ public class VisionSourceManager {
|
||||
if (checkUSBPath && savedConfig.getUSBPath().isEmpty()) {
|
||||
logger.debug(
|
||||
"WARN: Camera has empty USB path, but asked to match by name: "
|
||||
+ camCfgToString(savedConfig));
|
||||
+ savedConfig.toShortString());
|
||||
}
|
||||
|
||||
return (CameraInfo physicalCamera) -> {
|
||||
@@ -277,22 +284,6 @@ public class VisionSourceManager {
|
||||
ConfigManager.getInstance().getConfig().getNetworkConfig().matchCamerasOnlyByPath);
|
||||
}
|
||||
|
||||
private static final String camCfgToString(CameraConfiguration c) {
|
||||
return new StringBuilder()
|
||||
.append("[baseName=")
|
||||
.append(c.baseName)
|
||||
.append(", uniqueName=")
|
||||
.append(c.uniqueName)
|
||||
.append(", otherPaths=")
|
||||
.append(Arrays.toString(c.otherPaths))
|
||||
.append(", vid=")
|
||||
.append(c.usbVID)
|
||||
.append(", pid=")
|
||||
.append(c.usbPID)
|
||||
.append("]")
|
||||
.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Create {@link CameraConfiguration}s based on a list of detected USB cameras and the configs on
|
||||
* disk.
|
||||
@@ -423,7 +414,7 @@ public class VisionSourceManager {
|
||||
logger.debug(
|
||||
String.format(
|
||||
"Trying to find a match for loaded camera %s (%s) with camera config: %s",
|
||||
config.baseName, config.uniqueName, camCfgToString(config)));
|
||||
config.baseName, config.uniqueName, config.toShortString()));
|
||||
|
||||
// Get matcher and filter against it, picking out the first match
|
||||
Predicate<CameraInfo> matches =
|
||||
@@ -463,7 +454,7 @@ public class VisionSourceManager {
|
||||
List<CameraConfiguration> loadedConfigs) {
|
||||
List<CameraConfiguration> ret = new ArrayList<CameraConfiguration>();
|
||||
logger.debug(
|
||||
"After matching loaded configs, these configs remained unmatched: "
|
||||
"After matching loaded configs, these cameras remained unmatched: "
|
||||
+ detectedCameraList.stream()
|
||||
.map(n -> String.valueOf(n))
|
||||
.collect(Collectors.joining("-", "{", "}")));
|
||||
@@ -535,9 +526,9 @@ public class VisionSourceManager {
|
||||
* Filter out any blacklisted or ignored devices.
|
||||
*
|
||||
* @param allDevices
|
||||
* @return list of devices with blacklisted or ingore devices removed.
|
||||
* @return list of devices with blacklisted or ignore devices removed.
|
||||
*/
|
||||
private List<CameraInfo> filterAllowedDevices(List<CameraInfo> allDevices) {
|
||||
private List<CameraInfo> filterAllowedDevices(List<CameraInfo> allDevices, Platform platform) {
|
||||
List<CameraInfo> filteredDevices = new ArrayList<>();
|
||||
for (var device : allDevices) {
|
||||
if (deviceBlacklist.contains(device.name)) {
|
||||
@@ -546,6 +537,13 @@ public class VisionSourceManager {
|
||||
} else if (device.name.matches(ignoredCamerasRegex)) {
|
||||
logger.trace("Skipping ignored device: \"" + device.name + "\" at \"" + device.path);
|
||||
} else if (device.getIsV4lCsiCamera()) {
|
||||
} else if (device.otherPaths.length == 0
|
||||
&& platform.osType == OSType.LINUX
|
||||
&& device.cameraType == CameraType.UsbCamera) {
|
||||
logger.trace(
|
||||
"Skipping device with no other paths: \"" + device.name + "\" at \"" + device.path);
|
||||
// If cscore hasnt passed this other paths aka a path by id or a path as in usb port then we
|
||||
// cant guarantee it is a valid camera.
|
||||
} else {
|
||||
filteredDevices.add(device);
|
||||
logger.trace(
|
||||
@@ -559,8 +557,6 @@ public class VisionSourceManager {
|
||||
List<CameraConfiguration> camConfigs, boolean createSources) {
|
||||
var cameraSources = new ArrayList<VisionSource>();
|
||||
for (var configuration : camConfigs) {
|
||||
logger.debug("Creating VisionSource for " + camCfgToString(configuration));
|
||||
|
||||
// In unit tests, create dummy
|
||||
if (!createSources) {
|
||||
cameraSources.add(new TestSource(configuration));
|
||||
@@ -580,6 +576,7 @@ public class VisionSourceManager {
|
||||
cameraSources.add(newCam);
|
||||
}
|
||||
}
|
||||
logger.debug("Creating VisionSource for " + configuration.toShortString());
|
||||
}
|
||||
return cameraSources;
|
||||
}
|
||||
|
||||
@@ -59,7 +59,7 @@ public abstract class VisionSourceSettables {
|
||||
public abstract void setGain(int gain);
|
||||
|
||||
// Pretty uncommon so instead of abstract this is just a no-op by default
|
||||
// Overriden by cameras with AWB gain support
|
||||
// Overriddenn by cameras with AWB gain support
|
||||
public void setRedGain(int red) {}
|
||||
|
||||
public void setBlueGain(int blue) {}
|
||||
|
||||
@@ -287,7 +287,7 @@ public class Calibrate3dPipeTest {
|
||||
}
|
||||
|
||||
/**
|
||||
* Uses a given camera coefficents matrix set to "undistort" every image file found in a given
|
||||
* Uses a given camera coefficientss matrix set to "undistort" every image file found in a given
|
||||
* directory and display them. Provides an easy way to visually debug the results of the
|
||||
* calibration routine. Seems to play havoc with CI and takes a chunk of time, so shouldn't
|
||||
* usually be left active in tests.
|
||||
|
||||
@@ -139,7 +139,7 @@ public class CalibrationRotationPipeTest {
|
||||
|
||||
Point[] originalPoints = {new Point(100, 100), new Point(200, 200), new Point(300, 100)};
|
||||
|
||||
// Distort the origional points
|
||||
// Distort the origonal points
|
||||
var distortedOriginalPoints =
|
||||
OpenCVHelp.distortPoints(
|
||||
List.of(originalPoints),
|
||||
@@ -153,14 +153,14 @@ public class CalibrationRotationPipeTest {
|
||||
.collect(Collectors.toList());
|
||||
|
||||
// Now let's instead rotate then distort
|
||||
var rotatedOrigionalPoints =
|
||||
var rotatedOriginalPoints =
|
||||
Arrays.stream(originalPoints)
|
||||
.map(it -> rot.rotatePoint(it, frameProps.imageWidth, frameProps.imageHeight))
|
||||
.collect(Collectors.toList());
|
||||
|
||||
var distortedRotatedPoints =
|
||||
OpenCVHelp.distortPoints(
|
||||
rotatedOrigionalPoints,
|
||||
rotatedOriginalPoints,
|
||||
rotatedFrameProps.cameraCalibration.getCameraIntrinsicsMat(),
|
||||
rotatedFrameProps.cameraCalibration.getDistCoeffsMat());
|
||||
|
||||
|
||||
@@ -18,8 +18,10 @@
|
||||
package org.photonvision.vision.processes;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import static org.junit.jupiter.api.Assertions.fail;
|
||||
|
||||
import edu.wpi.first.cscore.VideoMode;
|
||||
import java.io.IOException;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
@@ -31,6 +33,7 @@ import org.photonvision.common.configuration.CameraConfiguration;
|
||||
import org.photonvision.common.configuration.ConfigManager;
|
||||
import org.photonvision.common.dataflow.CVPipelineResultConsumer;
|
||||
import org.photonvision.common.util.TestUtils;
|
||||
import org.photonvision.jni.PhotonTargetingJniLoader;
|
||||
import org.photonvision.vision.camera.QuirkyCamera;
|
||||
import org.photonvision.vision.camera.USBCameras.USBCameraSource;
|
||||
import org.photonvision.vision.frame.FrameProvider;
|
||||
@@ -41,7 +44,16 @@ import org.photonvision.vision.pipeline.result.CVPipelineResult;
|
||||
public class VisionModuleManagerTest {
|
||||
@BeforeAll
|
||||
public static void init() {
|
||||
String classpathStr = System.getProperty("java.class.path");
|
||||
System.out.print(classpathStr);
|
||||
|
||||
TestUtils.loadLibraries();
|
||||
try {
|
||||
if (!PhotonTargetingJniLoader.load()) fail();
|
||||
} catch (UnsatisfiedLinkError | IOException e) {
|
||||
e.printStackTrace();
|
||||
fail(e);
|
||||
}
|
||||
}
|
||||
|
||||
private static class TestSource extends VisionSource {
|
||||
|
||||
@@ -17,14 +17,14 @@
|
||||
|
||||
package org.photonvision.vision.processes;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.photonvision.common.configuration.CameraConfiguration;
|
||||
import org.photonvision.common.configuration.ConfigManager;
|
||||
import org.photonvision.common.hardware.Platform;
|
||||
import org.photonvision.common.logging.LogGroup;
|
||||
import org.photonvision.common.logging.LogLevel;
|
||||
import org.photonvision.common.logging.Logger;
|
||||
@@ -62,7 +62,8 @@ public class VisionSourceManagerTest {
|
||||
config4.usbVID = 5;
|
||||
config4.usbPID = 6;
|
||||
|
||||
CameraInfo info1 = new CameraInfo(0, "dev/video0", "testVideo", new String[0], 1, 2);
|
||||
CameraInfo info1 =
|
||||
new CameraInfo(0, "dev/video0", "testVideo", new String[] {"/usb/path/0"}, 1, 2);
|
||||
|
||||
cameraInfos.add(info1);
|
||||
|
||||
@@ -73,7 +74,8 @@ public class VisionSourceManagerTest {
|
||||
assertTrue(inst.knownCameras.contains(info1));
|
||||
assertEquals(2, inst.unmatchedLoadedConfigs.size());
|
||||
|
||||
CameraInfo info2 = new CameraInfo(0, "dev/video1", "secondTestVideo", new String[0], 2, 3);
|
||||
CameraInfo info2 =
|
||||
new CameraInfo(0, "dev/video1", "secondTestVideo", new String[] {"/usb/path/1"}, 2, 3);
|
||||
|
||||
cameraInfos.add(info2);
|
||||
|
||||
@@ -297,7 +299,7 @@ public class VisionSourceManagerTest {
|
||||
new CameraConfiguration(
|
||||
"Arducam OV2311 USB Camera",
|
||||
"Arducam OV2311 USB Camera",
|
||||
"fromt-left",
|
||||
"front-left",
|
||||
"/dev/video0",
|
||||
CAM1_OLD_PATHS);
|
||||
camera1_saved_config.usbVID = 3141;
|
||||
@@ -306,7 +308,7 @@ public class VisionSourceManagerTest {
|
||||
new CameraConfiguration(
|
||||
"Arducam OV2311 USB Camera",
|
||||
"Arducam OV2311 USB Camera (1)",
|
||||
"fromt-left",
|
||||
"front-left",
|
||||
"/dev/video2",
|
||||
CAM2_OLD_PATH);
|
||||
camera2_saved_config.usbVID = 3141;
|
||||
@@ -362,7 +364,7 @@ public class VisionSourceManagerTest {
|
||||
new CameraConfiguration(
|
||||
"Arducam OV2311 USB Camera",
|
||||
"Arducam OV2311 USB Camera (1)",
|
||||
"fromt-left",
|
||||
"front-left",
|
||||
"/dev/video0",
|
||||
CAM1_OLD_PATHS);
|
||||
camera1_saved_config.usbVID = 3141;
|
||||
@@ -371,7 +373,7 @@ public class VisionSourceManagerTest {
|
||||
new CameraConfiguration(
|
||||
"Arducam OV2311 USB Camera",
|
||||
"Arducam OV2311 USB Camera (1)",
|
||||
"fromt-left",
|
||||
"front-left",
|
||||
"/dev/video2",
|
||||
CAM2_OLD_PATH);
|
||||
camera2_saved_config.usbVID = 3141;
|
||||
@@ -500,6 +502,43 @@ public class VisionSourceManagerTest {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testNoOtherPaths() {
|
||||
Logger.setLevel(LogGroup.Camera, LogLevel.DEBUG);
|
||||
|
||||
// List of known cameras
|
||||
var cameraInfos = new ArrayList<CameraInfo>();
|
||||
|
||||
var inst = new VisionSourceManager();
|
||||
ConfigManager.getInstance().clearConfig();
|
||||
ConfigManager.getInstance().load();
|
||||
ConfigManager.getInstance().getConfig().getNetworkConfig().matchCamerasOnlyByPath = false;
|
||||
|
||||
// Match empty camera infos
|
||||
inst.tryMatchCamImpl(cameraInfos);
|
||||
|
||||
CameraInfo info1 =
|
||||
new CameraInfo(0, "/dev/video0", "Arducam OV2311 USB Camera", new String[] {}, 3141, 25446);
|
||||
|
||||
cameraInfos.add(info1);
|
||||
|
||||
// Match two "new" cameras
|
||||
var ret1 = inst.tryMatchCamImpl(cameraInfos, Platform.LINUX_64);
|
||||
|
||||
// Our cameras should be "known"
|
||||
assertFalse(inst.knownCameras.contains(info1));
|
||||
assertEquals(0, inst.knownCameras.size());
|
||||
assertEquals(null, ret1);
|
||||
|
||||
// Match two "new" cameras
|
||||
var ret2 = inst.tryMatchCamImpl(cameraInfos, Platform.WINDOWS_64);
|
||||
|
||||
// Our cameras should be "known"
|
||||
assertTrue(inst.knownCameras.contains(info1));
|
||||
assertEquals(1, inst.knownCameras.size());
|
||||
assertEquals(1, ret2.size());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testIdenticalCameras() {
|
||||
Logger.setLevel(LogGroup.Camera, LogLevel.DEBUG);
|
||||
@@ -571,7 +610,7 @@ public class VisionSourceManagerTest {
|
||||
ret1.stream().filter(it -> thisName.equals(it.cameraConfiguration.uniqueName)).count());
|
||||
}
|
||||
|
||||
// duplciate cameras, same info, new ref
|
||||
// duplicate cameras, same info, new ref
|
||||
var duplicateCameraInfos = new ArrayList<CameraInfo>();
|
||||
CameraInfo info1_dup =
|
||||
new CameraInfo(
|
||||
@@ -609,7 +648,7 @@ public class VisionSourceManagerTest {
|
||||
assertTrue(inst.knownCameras.contains(info2_dup));
|
||||
assertEquals(2, inst.knownCameras.size());
|
||||
|
||||
// duplciate cameras this simulates unplugging one and plugging the other in where v4l assigns
|
||||
// duplicate cameras this simulates unplugging one and plugging the other in where v4l assigns
|
||||
// the same by-id path to the other camera
|
||||
var duplicateCameraInfos1 = new ArrayList<CameraInfo>();
|
||||
CameraInfo info3_dup =
|
||||
|
||||
@@ -217,7 +217,7 @@ task generateJavaDocs(type: Javadoc) {
|
||||
classpath = files(exportedProjects.collect { project(it).sourceSets.main.compileClasspath })
|
||||
dependsOn project(':photon-core').writeCurrentVersion
|
||||
|
||||
options.links("https://docs.oracle.com/en/java/javase/17/docs/api/")
|
||||
options.links "https://docs.oracle.com/en/java/javase/17/docs/api/", "https://github.wpilib.org/allwpilib/docs/release/java/"
|
||||
options.addStringOption("tag", "pre:a:Pre-Condition")
|
||||
options.addBooleanOption("Xdoclint:html,missing,reference,syntax", true)
|
||||
options.addBooleanOption('html5', true)
|
||||
|
||||
@@ -177,6 +177,7 @@ task generateVendorJson() {
|
||||
def read = photonlibFileInput.text
|
||||
.replace('${photon_version}', pubVersion)
|
||||
.replace('${frc_year}', frcYear)
|
||||
.replace('${wpilib_version}', wpilibVersion)
|
||||
photonlibFileOutput.text = read
|
||||
|
||||
outputs.upToDateWhen { false }
|
||||
@@ -331,6 +332,7 @@ def nativeTasks = wpilibTools.createExtractionTasks {
|
||||
|
||||
nativeTasks.addToSourceSetResources(sourceSets.test)
|
||||
|
||||
nativeConfig.dependencies.add wpilibTools.deps.wpilib("wpilibc")
|
||||
nativeConfig.dependencies.add wpilibTools.deps.wpilib("wpimath")
|
||||
nativeConfig.dependencies.add wpilibTools.deps.wpilib("wpinet")
|
||||
nativeConfig.dependencies.add wpilibTools.deps.wpilib("wpiutil")
|
||||
@@ -338,4 +340,4 @@ nativeConfig.dependencies.add wpilibTools.deps.wpilib("ntcore")
|
||||
nativeConfig.dependencies.add wpilibTools.deps.wpilib("cscore")
|
||||
nativeConfig.dependencies.add wpilibTools.deps.wpilib("apriltag")
|
||||
nativeConfig.dependencies.add wpilibTools.deps.wpilib("hal")
|
||||
nativeConfig.dependencies.add wpilibTools.deps.wpilibOpenCv("frc" + wpi.frcYear.get(), wpi.versions.opencvVersion.get())
|
||||
nativeConfig.dependencies.add wpilibTools.deps.wpilibOpenCv("frc" + openCVYear, wpi.versions.opencvVersion.get())
|
||||
|
||||
@@ -24,6 +24,7 @@ from ..targeting import *
|
||||
|
||||
|
||||
class MultiTargetPNPResultSerde:
|
||||
|
||||
# Message definition md5sum. See photon_packet.adoc for details
|
||||
MESSAGE_VERSION = "541096947e9f3ca2d3f425ff7b04aa7b"
|
||||
MESSAGE_FORMAT = "PnpResult:ae4d655c0a3104d88df4f5db144c1e86 estimatedPose;int16 fiducialIDsUsed[?];"
|
||||
|
||||
@@ -24,11 +24,10 @@ from ..targeting import *
|
||||
|
||||
|
||||
class PhotonPipelineMetadataSerde:
|
||||
|
||||
# Message definition md5sum. See photon_packet.adoc for details
|
||||
MESSAGE_VERSION = "626e70461cbdb274fb43ead09c255f4e"
|
||||
MESSAGE_FORMAT = (
|
||||
"int64 sequenceID;int64 captureTimestampMicros;int64 publishTimestampMicros;"
|
||||
)
|
||||
MESSAGE_VERSION = "ac0a45f686457856fb30af77699ea356"
|
||||
MESSAGE_FORMAT = "int64 sequenceID;int64 captureTimestampMicros;int64 publishTimestampMicros;int64 timeSinceLastPong;"
|
||||
|
||||
@staticmethod
|
||||
def unpack(packet: "Packet") -> "PhotonPipelineMetadata":
|
||||
@@ -43,6 +42,9 @@ class PhotonPipelineMetadataSerde:
|
||||
# publishTimestampMicros is of intrinsic type int64
|
||||
ret.publishTimestampMicros = packet.decodeLong()
|
||||
|
||||
# timeSinceLastPong is of intrinsic type int64
|
||||
ret.timeSinceLastPong = packet.decodeLong()
|
||||
|
||||
return ret
|
||||
|
||||
|
||||
|
||||
@@ -24,9 +24,10 @@ from ..targeting import *
|
||||
|
||||
|
||||
class PhotonPipelineResultSerde:
|
||||
|
||||
# Message definition md5sum. See photon_packet.adoc for details
|
||||
MESSAGE_VERSION = "5eeaa293d0c69aea90eaddea786a2b3b"
|
||||
MESSAGE_FORMAT = "PhotonPipelineMetadata:626e70461cbdb274fb43ead09c255f4e metadata;PhotonTrackedTarget:cc6dbb5c5c1e0fa808108019b20863f1 targets[?];optional MultiTargetPNPResult:541096947e9f3ca2d3f425ff7b04aa7b multitagResult;"
|
||||
MESSAGE_VERSION = "4b2ff16a964b5e2bf04be0c1454d91c4"
|
||||
MESSAGE_FORMAT = "PhotonPipelineMetadata:ac0a45f686457856fb30af77699ea356 metadata;PhotonTrackedTarget:cc6dbb5c5c1e0fa808108019b20863f1 targets[?];optional MultiTargetPNPResult:541096947e9f3ca2d3f425ff7b04aa7b multitagResult;"
|
||||
|
||||
@staticmethod
|
||||
def unpack(packet: "Packet") -> "PhotonPipelineResult":
|
||||
|
||||
@@ -24,6 +24,7 @@ from ..targeting import *
|
||||
|
||||
|
||||
class PhotonTrackedTargetSerde:
|
||||
|
||||
# Message definition md5sum. See photon_packet.adoc for details
|
||||
MESSAGE_VERSION = "cc6dbb5c5c1e0fa808108019b20863f1"
|
||||
MESSAGE_FORMAT = "float64 yaw;float64 pitch;float64 area;float64 skew;int32 fiducialId;int32 objDetectId;float32 objDetectConf;Transform3d bestCameraToTarget;Transform3d altCameraToTarget;float64 poseAmbiguity;TargetCorner:16f6ac0dedc8eaccb951f4895d9e18b6 minAreaRectCorners[?];TargetCorner:16f6ac0dedc8eaccb951f4895d9e18b6 detectedCorners[?];"
|
||||
|
||||
@@ -24,6 +24,7 @@ from ..targeting import *
|
||||
|
||||
|
||||
class PnpResultSerde:
|
||||
|
||||
# Message definition md5sum. See photon_packet.adoc for details
|
||||
MESSAGE_VERSION = "ae4d655c0a3104d88df4f5db144c1e86"
|
||||
MESSAGE_FORMAT = "Transform3d best;Transform3d alt;float64 bestReprojErr;float64 altReprojErr;float64 ambiguity;"
|
||||
|
||||
@@ -24,6 +24,7 @@ from ..targeting import *
|
||||
|
||||
|
||||
class TargetCornerSerde:
|
||||
|
||||
# Message definition md5sum. See photon_packet.adoc for details
|
||||
MESSAGE_VERSION = "16f6ac0dedc8eaccb951f4895d9e18b6"
|
||||
MESSAGE_FORMAT = "float64 x;float64 y;"
|
||||
|
||||
@@ -124,7 +124,7 @@ class PhotonCamera:
|
||||
pkt = Packet(byteList)
|
||||
newResult = PhotonPipelineResult.photonStruct.unpack(pkt)
|
||||
# NT4 allows us to correct the timestamp based on when the message was sent
|
||||
newResult.ntReceiveTimestampMicros = timestamp / 1e6
|
||||
newResult.ntReceiveTimestampMicros = timestamp
|
||||
ret.append(newResult)
|
||||
|
||||
return ret
|
||||
|
||||
@@ -24,6 +24,8 @@ class PhotonPipelineResult:
|
||||
ntReceiveTimestampMicros: int = -1
|
||||
|
||||
targets: list[PhotonTrackedTarget] = field(default_factory=list)
|
||||
# Python users beware! We don't currently run a Time Sync Server, so these timestamps are in
|
||||
# an arbitrary timebase. This is not true in C++ or Java.
|
||||
metadata: PhotonPipelineMetadata = field(default_factory=PhotonPipelineMetadata)
|
||||
multiTagResult: Optional[MultiTargetPNPResult] = None
|
||||
|
||||
|
||||
@@ -9,7 +9,47 @@
|
||||
"https://maven.photonvision.org/repository/snapshots"
|
||||
],
|
||||
"jsonUrl": "https://maven.photonvision.org/repository/internal/org/photonvision/photonlib-json/1.0/photonlib-json-1.0.json",
|
||||
"jniDependencies": [],
|
||||
"jniDependencies": [
|
||||
{
|
||||
"groupId": "edu.wpi.first.wpilibc",
|
||||
"artifactId": "wpilibc-cpp",
|
||||
"version": "${wpilib_version}",
|
||||
"skipInvalidPlatforms": true,
|
||||
"isJar": false,
|
||||
"validPlatforms": [
|
||||
"windowsx86-64",
|
||||
"linuxathena",
|
||||
"linuxx86-64",
|
||||
"osxuniversal"
|
||||
]
|
||||
},
|
||||
{
|
||||
"groupId": "org.photonvision",
|
||||
"artifactId": "photontargeting-cpp",
|
||||
"version": "${photon_version}",
|
||||
"skipInvalidPlatforms": true,
|
||||
"isJar": false,
|
||||
"validPlatforms": [
|
||||
"windowsx86-64",
|
||||
"linuxathena",
|
||||
"linuxx86-64",
|
||||
"osxuniversal"
|
||||
]
|
||||
},
|
||||
{
|
||||
"groupId": "org.photonvision",
|
||||
"artifactId": "photontargeting-jni",
|
||||
"version": "${photon_version}",
|
||||
"skipInvalidPlatforms": true,
|
||||
"isJar": true,
|
||||
"validPlatforms": [
|
||||
"windowsx86-64",
|
||||
"linuxathena",
|
||||
"linuxx86-64",
|
||||
"osxuniversal"
|
||||
]
|
||||
}
|
||||
],
|
||||
"cppDependencies": [
|
||||
{
|
||||
"groupId": "org.photonvision",
|
||||
|
||||
@@ -32,9 +32,7 @@ import edu.wpi.first.math.Nat;
|
||||
import edu.wpi.first.math.numbers.*;
|
||||
import edu.wpi.first.networktables.BooleanPublisher;
|
||||
import edu.wpi.first.networktables.BooleanSubscriber;
|
||||
import edu.wpi.first.networktables.DoubleArrayPublisher;
|
||||
import edu.wpi.first.networktables.DoubleArraySubscriber;
|
||||
import edu.wpi.first.networktables.DoublePublisher;
|
||||
import edu.wpi.first.networktables.IntegerEntry;
|
||||
import edu.wpi.first.networktables.IntegerPublisher;
|
||||
import edu.wpi.first.networktables.IntegerSubscriber;
|
||||
@@ -53,6 +51,7 @@ import java.util.stream.Collectors;
|
||||
import org.photonvision.common.hardware.VisionLEDMode;
|
||||
import org.photonvision.common.networktables.PacketSubscriber;
|
||||
import org.photonvision.targeting.PhotonPipelineResult;
|
||||
import org.photonvision.timesync.TimeSyncSingleton;
|
||||
|
||||
/** Represents a camera that is connected to PhotonVision. */
|
||||
public class PhotonCamera implements AutoCloseable {
|
||||
@@ -63,13 +62,6 @@ public class PhotonCamera implements AutoCloseable {
|
||||
PacketSubscriber<PhotonPipelineResult> resultSubscriber;
|
||||
BooleanPublisher driverModePublisher;
|
||||
BooleanSubscriber driverModeSubscriber;
|
||||
DoublePublisher latencyMillisEntry;
|
||||
BooleanPublisher hasTargetEntry;
|
||||
DoublePublisher targetPitchEntry;
|
||||
DoublePublisher targetYawEntry;
|
||||
DoublePublisher targetAreaEntry;
|
||||
DoubleArrayPublisher targetPoseEntry;
|
||||
DoublePublisher targetSkewEntry;
|
||||
StringSubscriber versionEntry;
|
||||
IntegerEntry inputSaveImgEntry, outputSaveImgEntry;
|
||||
IntegerPublisher pipelineIndexRequest, ledModeRequest;
|
||||
@@ -85,13 +77,6 @@ public class PhotonCamera implements AutoCloseable {
|
||||
resultSubscriber.close();
|
||||
driverModePublisher.close();
|
||||
driverModeSubscriber.close();
|
||||
latencyMillisEntry.close();
|
||||
hasTargetEntry.close();
|
||||
targetPitchEntry.close();
|
||||
targetYawEntry.close();
|
||||
targetAreaEntry.close();
|
||||
targetPoseEntry.close();
|
||||
targetSkewEntry.close();
|
||||
versionEntry.close();
|
||||
inputSaveImgEntry.close();
|
||||
outputSaveImgEntry.close();
|
||||
@@ -110,12 +95,15 @@ public class PhotonCamera implements AutoCloseable {
|
||||
|
||||
private static boolean VERSION_CHECK_ENABLED = true;
|
||||
private static long VERSION_CHECK_INTERVAL = 5;
|
||||
private double lastVersionCheckTime = 0;
|
||||
double lastVersionCheckTime = 0;
|
||||
|
||||
private long prevHeartbeatValue = -1;
|
||||
private double prevHeartbeatChangeTime = 0;
|
||||
private static final double HEARTBEAT_DEBOUNCE_SEC = 0.5;
|
||||
|
||||
double prevTimeSyncWarnTime = 0;
|
||||
private static final double WARN_DEBOUNCE_SEC = 5;
|
||||
|
||||
public static void setVersionCheckEnabled(boolean enabled) {
|
||||
VERSION_CHECK_ENABLED = enabled;
|
||||
}
|
||||
@@ -166,6 +154,9 @@ public class PhotonCamera implements AutoCloseable {
|
||||
|
||||
HAL.report(tResourceType.kResourceType_PhotonCamera, InstanceCount);
|
||||
InstanceCount++;
|
||||
|
||||
// HACK - start a TimeSyncServer, if we haven't yet.
|
||||
TimeSyncSingleton.load();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -189,13 +180,12 @@ public class PhotonCamera implements AutoCloseable {
|
||||
|
||||
List<PhotonPipelineResult> ret = new ArrayList<>();
|
||||
|
||||
// Grab the latest results. We don't care about the timestamps from NT - the metadata header has
|
||||
// this, latency compensated by the Time Sync Client
|
||||
var changes = resultSubscriber.getAllChanges();
|
||||
|
||||
// TODO: NT4 timestamps are still not to be trusted. But it's the best we can do until we can
|
||||
// make time sync more reliable.
|
||||
for (var c : changes) {
|
||||
var result = c.value;
|
||||
result.setReceiveTimestampMicros(c.timestamp);
|
||||
checkTimeSyncOrWarn(result);
|
||||
ret.add(result);
|
||||
}
|
||||
|
||||
@@ -213,21 +203,38 @@ public class PhotonCamera implements AutoCloseable {
|
||||
public PhotonPipelineResult getLatestResult() {
|
||||
verifyVersion();
|
||||
|
||||
// Grab the latest result. We don't care about the timestamp from NT - the metadata header has
|
||||
// this, latency compensated by the Time Sync Client
|
||||
var ret = resultSubscriber.get();
|
||||
|
||||
if (ret.timestamp == 0) return new PhotonPipelineResult();
|
||||
|
||||
var result = ret.value;
|
||||
|
||||
// Set the timestamp of the result. Since PacketSubscriber doesn't realize that the result
|
||||
// contains a thing with time knowledge, set it here.
|
||||
// getLatestChange returns in microseconds, so we divide by 1e6 to convert to seconds.
|
||||
// TODO: NT4 time sync is Not To Be Trusted, we should do something else?
|
||||
result.setReceiveTimestampMicros(ret.timestamp);
|
||||
checkTimeSyncOrWarn(result);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private void checkTimeSyncOrWarn(PhotonPipelineResult result) {
|
||||
if (result.metadata.timeSinceLastPong > 5L * 1000000L) {
|
||||
if (Timer.getFPGATimestamp() > (prevTimeSyncWarnTime + WARN_DEBOUNCE_SEC)) {
|
||||
prevTimeSyncWarnTime = Timer.getFPGATimestamp();
|
||||
|
||||
DriverStation.reportWarning(
|
||||
"PhotonVision coprocessor at path "
|
||||
+ path
|
||||
+ " is not connected to the TimeSyncServer? It's been "
|
||||
+ String.format("%.2f", result.metadata.timeSinceLastPong / 1e6)
|
||||
+ "s since the coprocessor last heard a pong.\n\nCheck /photonvision/.timesync/{COPROCESSOR_HOSTNAME} for more information.",
|
||||
false);
|
||||
}
|
||||
} else {
|
||||
// Got a valid packet, reset the last time
|
||||
prevTimeSyncWarnTime = 0;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether the camera is in driver mode.
|
||||
*
|
||||
@@ -373,7 +380,7 @@ public class PhotonCamera implements AutoCloseable {
|
||||
return cameraTable;
|
||||
}
|
||||
|
||||
private void verifyVersion() {
|
||||
void verifyVersion() {
|
||||
if (!VERSION_CHECK_ENABLED) return;
|
||||
|
||||
if ((Timer.getFPGATimestamp() - lastVersionCheckTime) < VERSION_CHECK_INTERVAL) return;
|
||||
@@ -410,7 +417,7 @@ public class PhotonCamera implements AutoCloseable {
|
||||
// Check for connection status. Warn if disconnected.
|
||||
else if (!isConnected()) {
|
||||
DriverStation.reportWarning(
|
||||
"PhotonVision coprocessor at path " + path + " is not sending new data.", true);
|
||||
"PhotonVision coprocessor at path " + path + " is not sending new data.", false);
|
||||
}
|
||||
|
||||
String versionString = versionEntry.get("");
|
||||
@@ -425,7 +432,7 @@ public class PhotonCamera implements AutoCloseable {
|
||||
"PhotonVision coprocessor at path "
|
||||
+ path
|
||||
+ " has not reported a message interface UUID - is your coprocessor's camera started?",
|
||||
true);
|
||||
false);
|
||||
} else if (!local_uuid.equals(remote_uuid)) {
|
||||
// Error on a verified version mismatch
|
||||
// But stay silent otherwise
|
||||
|
||||
@@ -408,7 +408,7 @@ public class PhotonPoseEstimator {
|
||||
result.getTargets(),
|
||||
PoseStrategy.MULTI_TAG_PNP_ON_COPROCESSOR));
|
||||
} else {
|
||||
// We can nver fall back on another multitag strategy
|
||||
// We can never fall back on another multitag strategy
|
||||
return update(result, Optional.empty(), Optional.empty(), this.multiTagFallbackStrategy);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -68,8 +68,8 @@ import org.photonvision.targeting.PnpResult;
|
||||
public class PhotonCameraSim implements AutoCloseable {
|
||||
private final PhotonCamera cam;
|
||||
|
||||
NTTopicSet ts = new NTTopicSet();
|
||||
private long heartbeatCounter = 0;
|
||||
protected NTTopicSet ts = new NTTopicSet();
|
||||
private long heartbeatCounter = 1;
|
||||
|
||||
/** This simulated camera's {@link SimCameraProperties} */
|
||||
public final SimCameraProperties prop;
|
||||
@@ -553,9 +553,10 @@ public class PhotonCameraSim implements AutoCloseable {
|
||||
heartbeatCounter,
|
||||
now - (long) (latencyMillis * 1000),
|
||||
now,
|
||||
// Pretend like we heard a pong recently
|
||||
1000L + (long) ((Math.random() - 0.5) * 50),
|
||||
detectableTgts,
|
||||
multitagResult);
|
||||
ret.setReceiveTimestampMicros(now);
|
||||
return ret;
|
||||
}
|
||||
|
||||
@@ -605,6 +606,8 @@ public class PhotonCameraSim implements AutoCloseable {
|
||||
|
||||
ts.cameraIntrinsicsPublisher.set(prop.getIntrinsics().getData(), receiveTimestamp);
|
||||
ts.cameraDistortionPublisher.set(prop.getDistCoeffs().getData(), receiveTimestamp);
|
||||
ts.heartbeatPublisher.set(heartbeatCounter++, receiveTimestamp);
|
||||
|
||||
ts.heartbeatPublisher.set(heartbeatCounter, receiveTimestamp);
|
||||
heartbeatCounter += 1;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,6 +38,7 @@ import java.util.Map;
|
||||
import org.opencv.core.Core;
|
||||
import org.opencv.core.CvType;
|
||||
import org.opencv.core.Mat;
|
||||
import org.opencv.core.MatOfByte;
|
||||
import org.opencv.core.MatOfPoint;
|
||||
import org.opencv.core.MatOfPoint2f;
|
||||
import org.opencv.core.Point;
|
||||
@@ -99,11 +100,18 @@ public class VideoSimUtil {
|
||||
*
|
||||
* @param id The fiducial id of the desired tag
|
||||
*/
|
||||
public static Mat get36h11TagImage(int id) {
|
||||
private static Mat get36h11TagImage(int id) {
|
||||
RawFrame frame = AprilTag.generate36h11AprilTagImage(id);
|
||||
Mat result = new Mat(10, 10, CvType.CV_8UC1, frame.getData(), frame.getStride()).clone();
|
||||
frame.close();
|
||||
return result;
|
||||
|
||||
var buf = frame.getData();
|
||||
byte[] arr = new byte[buf.remaining()];
|
||||
buf.get(arr);
|
||||
// frame.close();
|
||||
|
||||
var mat = new MatOfByte(arr).reshape(1, 10).submat(new Rect(0, 0, 10, 10));
|
||||
mat.dump();
|
||||
|
||||
return mat;
|
||||
}
|
||||
|
||||
/** Gets the points representing the marker(black square) corners. */
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
/*
|
||||
* MIT License
|
||||
*
|
||||
* Copyright (c) PhotonVision
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
|
||||
package org.photonvision.timesync;
|
||||
|
||||
import java.io.IOException;
|
||||
import org.photonvision.jni.PhotonTargetingJniLoader;
|
||||
import org.photonvision.jni.TimeSyncServer;
|
||||
|
||||
/** Helper to hold a single TimeSyncServer instance with some default config */
|
||||
public class TimeSyncSingleton {
|
||||
private static TimeSyncServer INSTANCE = null;
|
||||
|
||||
public static boolean load() {
|
||||
if (INSTANCE == null) {
|
||||
try {
|
||||
if (!PhotonTargetingJniLoader.load()) {
|
||||
return false;
|
||||
}
|
||||
} catch (UnsatisfiedLinkError | IOException e) {
|
||||
e.printStackTrace();
|
||||
return false;
|
||||
}
|
||||
|
||||
INSTANCE = new TimeSyncServer(5810);
|
||||
INSTANCE.start();
|
||||
}
|
||||
|
||||
return INSTANCE != null;
|
||||
}
|
||||
}
|
||||
@@ -25,6 +25,7 @@
|
||||
#include "photon/PhotonCamera.h"
|
||||
|
||||
#include <hal/FRCUsageReporting.h>
|
||||
#include <net/TimeSyncServer.h>
|
||||
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
@@ -59,6 +60,11 @@ inline constexpr std::string_view bfw =
|
||||
">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>\n"
|
||||
"\n\n";
|
||||
|
||||
// bit of a hack -- start a TimeSync server on port 5810 (hard-coded)
|
||||
static std::mutex g_timeSyncServerMutex;
|
||||
static bool g_timeSyncServerStarted;
|
||||
static wpi::tsp::TimeSyncServer timesyncServer{5810};
|
||||
|
||||
namespace photon {
|
||||
|
||||
constexpr const units::second_t VERSION_CHECK_INTERVAL = 5_s;
|
||||
@@ -110,6 +116,14 @@ PhotonCamera::PhotonCamera(nt::NetworkTableInstance instance,
|
||||
cameraName(cameraName) {
|
||||
HAL_Report(HALUsageReporting::kResourceType_PhotonCamera, InstanceCount);
|
||||
InstanceCount++;
|
||||
|
||||
{
|
||||
std::lock_guard lock{g_timeSyncServerMutex};
|
||||
if (!g_timeSyncServerStarted) {
|
||||
timesyncServer.Start();
|
||||
g_timeSyncServerStarted = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
PhotonCamera::PhotonCamera(const std::string_view cameraName)
|
||||
|
||||
@@ -330,7 +330,8 @@ PhotonPipelineResult PhotonCameraSim::Process(
|
||||
heartbeatCounter++;
|
||||
return PhotonPipelineResult{
|
||||
PhotonPipelineMetadata{heartbeatCounter, 0,
|
||||
units::microsecond_t{latency}.to<int64_t>()},
|
||||
units::microsecond_t{latency}.to<int64_t>(),
|
||||
1000000},
|
||||
detectableTgts, multiTagResults};
|
||||
}
|
||||
void PhotonCameraSim::SubmitProcessedFrame(const PhotonPipelineResult& result) {
|
||||
|
||||
@@ -26,7 +26,6 @@ package org.photonvision;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
import edu.wpi.first.cscore.CameraServerCvJNI;
|
||||
import edu.wpi.first.math.MathUtil;
|
||||
import edu.wpi.first.math.geometry.Pose3d;
|
||||
import edu.wpi.first.math.geometry.Rotation2d;
|
||||
@@ -34,10 +33,12 @@ import edu.wpi.first.math.geometry.Rotation3d;
|
||||
import edu.wpi.first.math.geometry.Transform3d;
|
||||
import edu.wpi.first.math.geometry.Translation3d;
|
||||
import edu.wpi.first.networktables.NetworkTableInstance;
|
||||
import edu.wpi.first.util.CombinedRuntimeLoader;
|
||||
import java.io.IOException;
|
||||
import java.util.List;
|
||||
import org.junit.jupiter.api.BeforeAll;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.opencv.core.Core;
|
||||
import org.photonvision.estimation.CameraTargetRelation;
|
||||
import org.photonvision.estimation.OpenCVHelp;
|
||||
import org.photonvision.estimation.RotTrlTransform3d;
|
||||
@@ -77,7 +78,7 @@ public class OpenCVTest {
|
||||
|
||||
@BeforeAll
|
||||
public static void setUp() throws IOException {
|
||||
CameraServerCvJNI.forceLoad();
|
||||
CombinedRuntimeLoader.loadLibraries(OpenCVTest.class, Core.NATIVE_LIBRARY_NAME);
|
||||
|
||||
// NT live for debug purposes
|
||||
NetworkTableInstance.getDefault().startServer();
|
||||
@@ -150,7 +151,7 @@ public class OpenCVTest {
|
||||
assertEquals(
|
||||
actualRelation.camToTargPitch.getDegrees(),
|
||||
pitchDiff.getDegrees()
|
||||
* Math.cos(yaw2d.getRadians()), // adjust for unaccounted perpsective distortion
|
||||
* Math.cos(yaw2d.getRadians()), // adjust for unaccounted perspective distortion
|
||||
kRotDeltaDeg,
|
||||
"2d pitch doesn't match 3d");
|
||||
assertEquals(
|
||||
|
||||
@@ -24,12 +24,48 @@
|
||||
|
||||
package org.photonvision;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
|
||||
import static org.photonvision.UnitTestUtils.waitForCondition;
|
||||
import static org.photonvision.UnitTestUtils.waitForSequenceNumber;
|
||||
|
||||
import edu.wpi.first.hal.HAL;
|
||||
import edu.wpi.first.math.geometry.Rotation2d;
|
||||
import edu.wpi.first.networktables.NetworkTableInstance;
|
||||
import edu.wpi.first.networktables.NetworkTablesJNI;
|
||||
import edu.wpi.first.wpilibj.Timer;
|
||||
import java.io.IOException;
|
||||
import java.util.stream.Stream;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.Assertions;
|
||||
import org.junit.jupiter.api.BeforeAll;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.Arguments;
|
||||
import org.junit.jupiter.params.provider.MethodSource;
|
||||
import org.photonvision.common.dataflow.structures.Packet;
|
||||
import org.photonvision.jni.PhotonTargetingJniLoader;
|
||||
import org.photonvision.jni.TimeSyncClient;
|
||||
import org.photonvision.jni.WpilibLoader;
|
||||
import org.photonvision.simulation.PhotonCameraSim;
|
||||
import org.photonvision.targeting.PhotonPipelineResult;
|
||||
|
||||
class PhotonCameraTest {
|
||||
@BeforeAll
|
||||
public static void load_wpilib() {
|
||||
WpilibLoader.loadLibraries();
|
||||
}
|
||||
|
||||
@BeforeEach
|
||||
public void setup() {
|
||||
HAL.initialize(500, 0);
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
public void teardown() {
|
||||
HAL.shutdown();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testEmpty() {
|
||||
Assertions.assertDoesNotThrow(
|
||||
@@ -40,4 +76,158 @@ class PhotonCameraTest {
|
||||
PhotonPipelineResult.photonStruct.pack(packet, ret);
|
||||
});
|
||||
}
|
||||
|
||||
// Just a smoketest for dev use -- don't run by default
|
||||
@Test
|
||||
public void testTimeSyncServerWithPhotonCamera() throws InterruptedException, IOException {
|
||||
load_wpilib();
|
||||
PhotonTargetingJniLoader.load();
|
||||
|
||||
HAL.initialize(500, 0);
|
||||
|
||||
NetworkTableInstance.getDefault().stopClient();
|
||||
NetworkTableInstance.getDefault().startServer();
|
||||
|
||||
var camera = new PhotonCamera("Arducam_OV2311_USB_Camera");
|
||||
PhotonCamera.setVersionCheckEnabled(false);
|
||||
|
||||
for (int i = 0; i < 5; i++) {
|
||||
Thread.sleep(500);
|
||||
|
||||
var res = camera.getLatestResult();
|
||||
var captureTime = res.getTimestampSeconds();
|
||||
var now = Timer.getFPGATimestamp();
|
||||
|
||||
// expectTrue(captureTime < now);
|
||||
|
||||
System.out.println(
|
||||
"sequence "
|
||||
+ res.metadata.sequenceID
|
||||
+ " image capture "
|
||||
+ captureTime
|
||||
+ " received at "
|
||||
+ res.getTimestampSeconds()
|
||||
+ " now: "
|
||||
+ NetworkTablesJNI.now() / 1e6
|
||||
+ " time since last pong: "
|
||||
+ res.metadata.timeSinceLastPong / 1e6);
|
||||
}
|
||||
|
||||
HAL.shutdown();
|
||||
}
|
||||
|
||||
private static Stream<Arguments> testNtOffsets() {
|
||||
return Stream.of(
|
||||
// various initializaiton orders
|
||||
Arguments.of(1, 10, 30, 30),
|
||||
Arguments.of(10, 2, 30, 30),
|
||||
Arguments.of(10, 10, 30, 30),
|
||||
// Reboot just the robot
|
||||
Arguments.of(1, 1, 10, 30),
|
||||
// Reboot just the coproc
|
||||
Arguments.of(1, 1, 30, 10));
|
||||
}
|
||||
|
||||
/**
|
||||
* Try starting client before server and vice-versa, making sure that we never fail the version
|
||||
* check
|
||||
*/
|
||||
@ParameterizedTest
|
||||
@MethodSource("testNtOffsets")
|
||||
public void testRestartingRobotAndCoproc(
|
||||
int robotStart, int coprocStart, int robotRestart, int coprocRestart) throws Throwable {
|
||||
var robotNt = NetworkTableInstance.create();
|
||||
var coprocNt = NetworkTableInstance.create();
|
||||
|
||||
robotNt.addLogger(10, 255, (it) -> System.out.println("ROBOT: " + it.logMessage.message));
|
||||
coprocNt.addLogger(10, 255, (it) -> System.out.println("CLIENT: " + it.logMessage.message));
|
||||
|
||||
TimeSyncClient tspClient = null;
|
||||
|
||||
var robotCamera = new PhotonCamera(robotNt, "MY_CAMERA");
|
||||
|
||||
// apparently need a PhotonCamera to hand down
|
||||
var fakePhotonCoprocCam = new PhotonCamera(coprocNt, "MY_CAMERA");
|
||||
var coprocSim = new PhotonCameraSim(fakePhotonCoprocCam);
|
||||
coprocSim.prop.setCalibration(640, 480, Rotation2d.fromDegrees(90));
|
||||
coprocSim.prop.setFPS(30);
|
||||
coprocSim.setMinTargetAreaPixels(20.0);
|
||||
|
||||
for (int i = 0; i < 20; i++) {
|
||||
int seq = i + 1;
|
||||
|
||||
if (i == coprocRestart) {
|
||||
System.out.println("Restarting coprocessor NT client");
|
||||
|
||||
fakePhotonCoprocCam.close();
|
||||
coprocNt.close();
|
||||
coprocNt = NetworkTableInstance.create();
|
||||
|
||||
coprocNt.addLogger(10, 255, (it) -> System.out.println("CLIENT: " + it.logMessage.message));
|
||||
|
||||
fakePhotonCoprocCam = new PhotonCamera(coprocNt, "MY_CAMERA");
|
||||
coprocSim = new PhotonCameraSim(fakePhotonCoprocCam);
|
||||
coprocSim.prop.setCalibration(640, 480, Rotation2d.fromDegrees(90));
|
||||
coprocSim.prop.setFPS(30);
|
||||
coprocSim.setMinTargetAreaPixels(20.0);
|
||||
}
|
||||
if (i == robotRestart) {
|
||||
System.out.println("Restarting robot NT server");
|
||||
|
||||
robotNt.close();
|
||||
robotNt = NetworkTableInstance.create();
|
||||
robotNt.addLogger(10, 255, (it) -> System.out.println("ROBOT: " + it.logMessage.message));
|
||||
robotCamera = new PhotonCamera(robotNt, "MY_CAMERA");
|
||||
}
|
||||
|
||||
if (i == coprocStart || i == coprocRestart) {
|
||||
coprocNt.setServer("127.0.0.1", 5940);
|
||||
coprocNt.startClient4("testClient");
|
||||
|
||||
// PhotonCamera makes a server by default - connect to it
|
||||
tspClient = new TimeSyncClient("127.0.0.1", 5810, 0.5);
|
||||
}
|
||||
|
||||
if (i == robotStart || i == robotRestart) {
|
||||
robotNt.startServer("networktables_random.json", "", 5941, 5940);
|
||||
}
|
||||
|
||||
Thread.sleep(100);
|
||||
|
||||
if (i == Math.max(coprocStart, robotStart)) {
|
||||
final var c = coprocNt;
|
||||
final var r = robotNt;
|
||||
waitForCondition("Coproc connection", () -> c.getConnections().length == 1);
|
||||
waitForCondition("Rio connection", () -> r.getConnections().length == 1);
|
||||
}
|
||||
|
||||
var result1 = new PhotonPipelineResult();
|
||||
result1.metadata.captureTimestampMicros = seq * 100;
|
||||
result1.metadata.publishTimestampMicros = seq * 150;
|
||||
result1.metadata.sequenceID = seq;
|
||||
if (tspClient != null) {
|
||||
result1.metadata.timeSinceLastPong = tspClient.getPingMetadata().timeSinceLastPong();
|
||||
} else {
|
||||
result1.metadata.timeSinceLastPong = Long.MAX_VALUE;
|
||||
}
|
||||
|
||||
coprocSim.submitProcessedFrame(result1, NetworkTablesJNI.now());
|
||||
coprocNt.flush();
|
||||
|
||||
if (i > robotStart && i > coprocStart) {
|
||||
var ret = waitForSequenceNumber(robotCamera, seq);
|
||||
System.out.println(ret);
|
||||
}
|
||||
|
||||
// force verifyVersion to do checks
|
||||
robotCamera.lastVersionCheckTime = -100;
|
||||
robotCamera.prevTimeSyncWarnTime = -100;
|
||||
assertDoesNotThrow(robotCamera::verifyVersion);
|
||||
}
|
||||
|
||||
coprocSim.close();
|
||||
coprocNt.close();
|
||||
robotNt.close();
|
||||
tspClient.stop();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -64,8 +64,9 @@ class PhotonPoseEstimatorTest {
|
||||
cameraOne.result =
|
||||
new PhotonPipelineResult(
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
11 * 1000000,
|
||||
1100000,
|
||||
1024,
|
||||
List.of(
|
||||
new PhotonTrackedTarget(
|
||||
3.0,
|
||||
@@ -130,7 +131,6 @@ class PhotonPoseEstimatorTest {
|
||||
new TargetCorner(3, 4),
|
||||
new TargetCorner(5, 6),
|
||||
new TargetCorner(7, 8)))));
|
||||
cameraOne.result.setReceiveTimestampMicros((long) (11 * 1e6));
|
||||
|
||||
PhotonPoseEstimator estimator =
|
||||
new PhotonPoseEstimator(aprilTags, PoseStrategy.LOWEST_AMBIGUITY, new Transform3d());
|
||||
@@ -150,8 +150,9 @@ class PhotonPoseEstimatorTest {
|
||||
cameraOne.result =
|
||||
new PhotonPipelineResult(
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
4000000,
|
||||
1100000,
|
||||
1024,
|
||||
List.of(
|
||||
new PhotonTrackedTarget(
|
||||
3.0,
|
||||
@@ -217,8 +218,6 @@ class PhotonPoseEstimatorTest {
|
||||
new TargetCorner(5, 6),
|
||||
new TargetCorner(7, 8)))));
|
||||
|
||||
cameraOne.result.setReceiveTimestampMicros((long) (4 * 1e6));
|
||||
|
||||
PhotonPoseEstimator estimator =
|
||||
new PhotonPoseEstimator(
|
||||
aprilTags,
|
||||
@@ -240,8 +239,9 @@ class PhotonPoseEstimatorTest {
|
||||
cameraOne.result =
|
||||
new PhotonPipelineResult(
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
17000000,
|
||||
1100000,
|
||||
1024,
|
||||
List.of(
|
||||
new PhotonTrackedTarget(
|
||||
3.0,
|
||||
@@ -306,7 +306,6 @@ class PhotonPoseEstimatorTest {
|
||||
new TargetCorner(3, 4),
|
||||
new TargetCorner(5, 6),
|
||||
new TargetCorner(7, 8)))));
|
||||
cameraOne.result.setReceiveTimestampMicros((long) (17 * 1e6));
|
||||
|
||||
PhotonPoseEstimator estimator =
|
||||
new PhotonPoseEstimator(
|
||||
@@ -330,8 +329,9 @@ class PhotonPoseEstimatorTest {
|
||||
cameraOne.result =
|
||||
new PhotonPipelineResult(
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
1000000,
|
||||
1100000,
|
||||
1024,
|
||||
List.of(
|
||||
new PhotonTrackedTarget(
|
||||
3.0,
|
||||
@@ -396,7 +396,6 @@ class PhotonPoseEstimatorTest {
|
||||
new TargetCorner(3, 4),
|
||||
new TargetCorner(5, 6),
|
||||
new TargetCorner(7, 8)))));
|
||||
cameraOne.result.setReceiveTimestampMicros((long) (1 * 1e6));
|
||||
|
||||
PhotonPoseEstimator estimator =
|
||||
new PhotonPoseEstimator(
|
||||
@@ -412,8 +411,9 @@ class PhotonPoseEstimatorTest {
|
||||
cameraOne.result =
|
||||
new PhotonPipelineResult(
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
7000000,
|
||||
1100000,
|
||||
1024,
|
||||
List.of(
|
||||
new PhotonTrackedTarget(
|
||||
3.0,
|
||||
@@ -478,7 +478,6 @@ class PhotonPoseEstimatorTest {
|
||||
new TargetCorner(3, 4),
|
||||
new TargetCorner(5, 6),
|
||||
new TargetCorner(7, 8)))));
|
||||
cameraOne.result.setReceiveTimestampMicros((long) (7 * 1e6));
|
||||
|
||||
estimatedPose = estimator.update(cameraOne.result);
|
||||
pose = estimatedPose.get().estimatedPose;
|
||||
@@ -495,8 +494,9 @@ class PhotonPoseEstimatorTest {
|
||||
var result =
|
||||
new PhotonPipelineResult(
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
20000000,
|
||||
1100000,
|
||||
1024,
|
||||
List.of(
|
||||
new PhotonTrackedTarget(
|
||||
3.0,
|
||||
@@ -519,7 +519,6 @@ class PhotonPoseEstimatorTest {
|
||||
new TargetCorner(3, 4),
|
||||
new TargetCorner(5, 6),
|
||||
new TargetCorner(7, 8)))));
|
||||
result.setReceiveTimestampMicros((long) (20 * 1e6));
|
||||
|
||||
PhotonPoseEstimator estimator =
|
||||
new PhotonPoseEstimator(
|
||||
@@ -529,7 +528,7 @@ class PhotonPoseEstimatorTest {
|
||||
|
||||
// Empty result, expect empty result
|
||||
cameraOne.result = new PhotonPipelineResult();
|
||||
cameraOne.result.setReceiveTimestampMicros((long) (1 * 1e6));
|
||||
cameraOne.result.metadata.captureTimestampMicros = (long) (1 * 1e6);
|
||||
Optional<EstimatedRobotPose> estimatedPose = estimator.update(cameraOne.result);
|
||||
assertFalse(estimatedPose.isPresent());
|
||||
|
||||
@@ -563,8 +562,9 @@ class PhotonPoseEstimatorTest {
|
||||
cameraOne.result =
|
||||
new PhotonPipelineResult(
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
20 * 1000000,
|
||||
1100000,
|
||||
1024,
|
||||
List.of(
|
||||
new PhotonTrackedTarget(
|
||||
3.0,
|
||||
@@ -629,7 +629,6 @@ class PhotonPoseEstimatorTest {
|
||||
new TargetCorner(3, 4),
|
||||
new TargetCorner(5, 6),
|
||||
new TargetCorner(7, 8))))); // 3 3 3 ambig .4
|
||||
cameraOne.result.setReceiveTimestampMicros(20 * 1000000);
|
||||
|
||||
PhotonPoseEstimator estimator =
|
||||
new PhotonPoseEstimator(
|
||||
|
||||
74
photon-lib/src/test/java/org/photonvision/UnitTestUtils.java
Normal file
74
photon-lib/src/test/java/org/photonvision/UnitTestUtils.java
Normal file
@@ -0,0 +1,74 @@
|
||||
/*
|
||||
* MIT License
|
||||
*
|
||||
* Copyright (c) PhotonVision
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
|
||||
package org.photonvision;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import static org.junit.jupiter.api.Assertions.fail;
|
||||
|
||||
import java.util.function.BooleanSupplier;
|
||||
import org.photonvision.targeting.PhotonPipelineResult;
|
||||
|
||||
public class UnitTestUtils {
|
||||
static void waitForCondition(String name, BooleanSupplier condition) {
|
||||
// wait up to 1 second for satisfaction
|
||||
for (int i = 0; i < 100; i++) {
|
||||
if (condition.getAsBoolean()) {
|
||||
System.out.println(name + " satisfied on iteration " + i);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
Thread.sleep(60);
|
||||
} catch (InterruptedException e) {
|
||||
e.printStackTrace();
|
||||
fail(e);
|
||||
}
|
||||
}
|
||||
throw new RuntimeException(name + " was never satisfied");
|
||||
}
|
||||
|
||||
static PhotonPipelineResult waitForSequenceNumber(PhotonCamera camera, int seq) {
|
||||
assertTrue(camera.heartbeatEntry.getTopic().getHandle() != 0);
|
||||
|
||||
System.out.println(
|
||||
"Waiting for seq=" + seq + " on " + camera.heartbeatEntry.getTopic().getName());
|
||||
// wait up to 1 second for a new result
|
||||
for (int i = 0; i < 100; i++) {
|
||||
var res = camera.getLatestResult();
|
||||
System.out.println(res);
|
||||
if (res.metadata.sequenceID == seq) {
|
||||
return res;
|
||||
}
|
||||
|
||||
try {
|
||||
Thread.sleep(60);
|
||||
} catch (InterruptedException e) {
|
||||
e.printStackTrace();
|
||||
fail(e);
|
||||
}
|
||||
}
|
||||
throw new RuntimeException("Never saw sequence number " + seq);
|
||||
}
|
||||
}
|
||||
@@ -27,9 +27,12 @@ package org.photonvision;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import static org.junit.jupiter.api.Assertions.fail;
|
||||
import static org.photonvision.UnitTestUtils.waitForSequenceNumber;
|
||||
|
||||
import edu.wpi.first.apriltag.AprilTag;
|
||||
import edu.wpi.first.apriltag.AprilTagFieldLayout;
|
||||
import edu.wpi.first.hal.HAL;
|
||||
import edu.wpi.first.math.geometry.Pose2d;
|
||||
import edu.wpi.first.math.geometry.Pose3d;
|
||||
import edu.wpi.first.math.geometry.Rotation2d;
|
||||
@@ -39,19 +42,25 @@ import edu.wpi.first.math.geometry.Translation2d;
|
||||
import edu.wpi.first.math.geometry.Translation3d;
|
||||
import edu.wpi.first.math.util.Units;
|
||||
import edu.wpi.first.networktables.NetworkTableInstance;
|
||||
import edu.wpi.first.wpilibj.smartdashboard.SmartDashboard;
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.stream.Stream;
|
||||
import org.junit.jupiter.api.AfterAll;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.Assertions;
|
||||
import org.junit.jupiter.api.BeforeAll;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.Arguments;
|
||||
import org.junit.jupiter.params.provider.MethodSource;
|
||||
import org.junit.jupiter.params.provider.ValueSource;
|
||||
import org.photonvision.estimation.OpenCVHelp;
|
||||
import org.photonvision.estimation.TargetModel;
|
||||
import org.photonvision.estimation.VisionEstimation;
|
||||
import org.photonvision.jni.PhotonTargetingJniLoader;
|
||||
import org.photonvision.jni.WpilibLoader;
|
||||
import org.photonvision.simulation.PhotonCameraSim;
|
||||
import org.photonvision.simulation.VisionSystemSim;
|
||||
import org.photonvision.simulation.VisionTargetSim;
|
||||
@@ -61,6 +70,46 @@ class VisionSystemSimTest {
|
||||
private static final double kTrlDelta = 0.005;
|
||||
private static final double kRotDeltaDeg = 0.25;
|
||||
|
||||
NetworkTableInstance inst;
|
||||
|
||||
@BeforeAll
|
||||
public static void setUp() {
|
||||
WpilibLoader.loadLibraries();
|
||||
try {
|
||||
if (!PhotonTargetingJniLoader.load()) fail();
|
||||
} catch (UnsatisfiedLinkError | IOException e) {
|
||||
e.printStackTrace();
|
||||
fail(e);
|
||||
}
|
||||
|
||||
OpenCVHelp.forceLoadOpenCV();
|
||||
}
|
||||
|
||||
@BeforeEach
|
||||
public void init() {
|
||||
// // No version check for testing
|
||||
// PhotonCamera.setVersionCheckEnabled(false);
|
||||
}
|
||||
|
||||
@BeforeEach
|
||||
public void setup() {
|
||||
HAL.initialize(500, 0);
|
||||
|
||||
inst = NetworkTableInstance.create();
|
||||
inst.stopClient();
|
||||
inst.stopServer();
|
||||
inst.startLocal();
|
||||
SmartDashboard.setNetworkTableInstance(inst);
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
public void teardown() {
|
||||
inst.close();
|
||||
inst = null;
|
||||
|
||||
HAL.shutdown();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testEmpty() {
|
||||
Assertions.assertDoesNotThrow(
|
||||
@@ -74,41 +123,12 @@ class VisionSystemSimTest {
|
||||
});
|
||||
}
|
||||
|
||||
@BeforeAll
|
||||
public static void setUp() {
|
||||
// NT live for debug purposes
|
||||
NetworkTableInstance.getDefault().startServer();
|
||||
|
||||
// No version check for testing
|
||||
PhotonCamera.setVersionCheckEnabled(false);
|
||||
}
|
||||
|
||||
@AfterAll
|
||||
public static void shutDown() {}
|
||||
|
||||
// @ParameterizedTest
|
||||
// @ValueSource(doubles = {5, 10, 15, 20, 25, 30})
|
||||
// public void testDistanceAligned(double dist) {
|
||||
// final var targetPose = new Pose2d(new Translation2d(15.98, 0), new Rotation2d());
|
||||
// var sysUnderTest =
|
||||
// new SimVisionSystem("Test", 80.0, 0.0, new Transform2d(), 1, 99999, 320, 240, 0);
|
||||
// sysUnderTest.addSimVisionTarget(new SimVisionTarget(targetPose, 0.0, 1.0, 1.0));
|
||||
|
||||
// final var robotPose = new Pose2d(new Translation2d(35 - dist, 0), new Rotation2d());
|
||||
// sysUnderTest.processFrame(robotPose);
|
||||
|
||||
// var result = sysUnderTest.cam.getLatestResult();
|
||||
|
||||
// assertTrue(result.hasTargets());
|
||||
// assertEquals(result.getBestTarget().getCameraToTarget().getTranslation().getNorm(), dist);
|
||||
// }
|
||||
|
||||
@Test
|
||||
public void testVisibilityCupidShuffle() {
|
||||
final var targetPose =
|
||||
new Pose3d(new Translation3d(15.98, 0, 2), new Rotation3d(0, 0, Math.PI));
|
||||
var visionSysSim = new VisionSystemSim("Test");
|
||||
var camera = new PhotonCamera("camera");
|
||||
var camera = new PhotonCamera(inst, "camera");
|
||||
var cameraSim = new PhotonCameraSim(camera);
|
||||
visionSysSim.addCamera(cameraSim, new Transform3d());
|
||||
cameraSim.prop.setCalibration(640, 480, Rotation2d.fromDegrees(80));
|
||||
@@ -117,42 +137,51 @@ class VisionSystemSimTest {
|
||||
// To the right, to the right
|
||||
var robotPose = new Pose2d(new Translation2d(5, 0), Rotation2d.fromDegrees(-70));
|
||||
visionSysSim.update(robotPose);
|
||||
|
||||
assertFalse(camera.getLatestResult().hasTargets());
|
||||
|
||||
// To the right, to the right
|
||||
robotPose = new Pose2d(new Translation2d(5, 0), Rotation2d.fromDegrees(-95));
|
||||
visionSysSim.update(robotPose);
|
||||
|
||||
assertFalse(camera.getLatestResult().hasTargets());
|
||||
|
||||
// To the left, to the left
|
||||
robotPose = new Pose2d(new Translation2d(5, 0), Rotation2d.fromDegrees(90));
|
||||
visionSysSim.update(robotPose);
|
||||
|
||||
assertFalse(camera.getLatestResult().hasTargets());
|
||||
|
||||
// To the left, to the left
|
||||
robotPose = new Pose2d(new Translation2d(5, 0), Rotation2d.fromDegrees(65));
|
||||
visionSysSim.update(robotPose);
|
||||
|
||||
assertFalse(camera.getLatestResult().hasTargets());
|
||||
|
||||
// now kick, now kick
|
||||
robotPose = new Pose2d(new Translation2d(2, 0), Rotation2d.fromDegrees(5));
|
||||
visionSysSim.update(robotPose);
|
||||
|
||||
waitForSequenceNumber(camera, 5);
|
||||
assertTrue(camera.getLatestResult().hasTargets());
|
||||
|
||||
// now kick, now kick
|
||||
robotPose = new Pose2d(new Translation2d(2, 0), Rotation2d.fromDegrees(-5));
|
||||
visionSysSim.update(robotPose);
|
||||
|
||||
assertTrue(camera.getLatestResult().hasTargets());
|
||||
|
||||
// now walk it by yourself
|
||||
robotPose = new Pose2d(new Translation2d(2, 0), Rotation2d.fromDegrees(-179));
|
||||
visionSysSim.update(robotPose);
|
||||
|
||||
assertFalse(camera.getLatestResult().hasTargets());
|
||||
|
||||
// now walk it by yourself
|
||||
visionSysSim.adjustCamera(
|
||||
cameraSim, new Transform3d(new Translation3d(), new Rotation3d(0, 0, Math.PI)));
|
||||
visionSysSim.update(robotPose);
|
||||
|
||||
assertTrue(camera.getLatestResult().hasTargets());
|
||||
}
|
||||
|
||||
@@ -161,7 +190,7 @@ class VisionSystemSimTest {
|
||||
final var targetPose =
|
||||
new Pose3d(new Translation3d(15.98, 0, 1), new Rotation3d(0, 0, Math.PI));
|
||||
var visionSysSim = new VisionSystemSim("Test");
|
||||
var camera = new PhotonCamera("camera");
|
||||
var camera = new PhotonCamera(inst, "camera");
|
||||
var cameraSim = new PhotonCameraSim(camera);
|
||||
visionSysSim.addCamera(cameraSim, new Transform3d());
|
||||
cameraSim.prop.setCalibration(640, 480, Rotation2d.fromDegrees(80));
|
||||
@@ -169,12 +198,14 @@ class VisionSystemSimTest {
|
||||
|
||||
var robotPose = new Pose2d(new Translation2d(5, 0), Rotation2d.fromDegrees(5));
|
||||
visionSysSim.update(robotPose);
|
||||
assertTrue(camera.getLatestResult().hasTargets());
|
||||
|
||||
assertTrue(waitForSequenceNumber(camera, 1).hasTargets());
|
||||
|
||||
visionSysSim.adjustCamera( // vooop selfie stick
|
||||
cameraSim, new Transform3d(new Translation3d(0, 0, 5000), new Rotation3d(0, 0, Math.PI)));
|
||||
visionSysSim.update(robotPose);
|
||||
assertFalse(camera.getLatestResult().hasTargets());
|
||||
|
||||
assertFalse(waitForSequenceNumber(camera, 2).hasTargets());
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -184,7 +215,7 @@ class VisionSystemSimTest {
|
||||
var robotToCamera =
|
||||
new Transform3d(new Translation3d(0, 0, 1), new Rotation3d(0, -Math.PI / 4, 0));
|
||||
var visionSysSim = new VisionSystemSim("Test");
|
||||
var camera = new PhotonCamera("camera");
|
||||
var camera = new PhotonCamera(inst, "camera");
|
||||
var cameraSim = new PhotonCameraSim(camera);
|
||||
visionSysSim.addCamera(cameraSim, robotToCamera);
|
||||
cameraSim.prop.setCalibration(1234, 1234, Rotation2d.fromDegrees(80));
|
||||
@@ -192,12 +223,15 @@ class VisionSystemSimTest {
|
||||
|
||||
var robotPose = new Pose2d(new Translation2d(13.98, 0), Rotation2d.fromDegrees(5));
|
||||
visionSysSim.update(robotPose);
|
||||
assertTrue(camera.getLatestResult().hasTargets());
|
||||
|
||||
// Pitched back camera should mean target goes out of view below the robot as distance increases
|
||||
assertTrue(waitForSequenceNumber(camera, 1).hasTargets());
|
||||
|
||||
// Pitched back camera should mean target goes out of view below the robot as
|
||||
// distance increases
|
||||
robotPose = new Pose2d(new Translation2d(0, 0), Rotation2d.fromDegrees(5));
|
||||
visionSysSim.update(robotPose);
|
||||
assertFalse(camera.getLatestResult().hasTargets());
|
||||
|
||||
assertFalse(waitForSequenceNumber(camera, 2).hasTargets());
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -205,7 +239,7 @@ class VisionSystemSimTest {
|
||||
final var targetPose =
|
||||
new Pose3d(new Translation3d(15.98, 0, 1), new Rotation3d(0, 0, Math.PI));
|
||||
var visionSysSim = new VisionSystemSim("Test");
|
||||
var camera = new PhotonCamera("camera");
|
||||
var camera = new PhotonCamera(inst, "camera");
|
||||
var cameraSim = new PhotonCameraSim(camera);
|
||||
visionSysSim.addCamera(cameraSim, new Transform3d());
|
||||
cameraSim.prop.setCalibration(640, 480, Rotation2d.fromDegrees(80));
|
||||
@@ -214,11 +248,13 @@ class VisionSystemSimTest {
|
||||
|
||||
var robotPose = new Pose2d(new Translation2d(12, 0), Rotation2d.fromDegrees(5));
|
||||
visionSysSim.update(robotPose);
|
||||
assertTrue(camera.getLatestResult().hasTargets());
|
||||
|
||||
assertTrue(waitForSequenceNumber(camera, 1).hasTargets());
|
||||
|
||||
robotPose = new Pose2d(new Translation2d(0, 0), Rotation2d.fromDegrees(5));
|
||||
visionSysSim.update(robotPose);
|
||||
assertFalse(camera.getLatestResult().hasTargets());
|
||||
|
||||
assertFalse(waitForSequenceNumber(camera, 2).hasTargets());
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -226,7 +262,7 @@ class VisionSystemSimTest {
|
||||
final var targetPose =
|
||||
new Pose3d(new Translation3d(15.98, 0, 1), new Rotation3d(0, 0, Math.PI));
|
||||
var visionSysSim = new VisionSystemSim("Test");
|
||||
var camera = new PhotonCamera("camera");
|
||||
var camera = new PhotonCamera(inst, "camera");
|
||||
var cameraSim = new PhotonCameraSim(camera);
|
||||
visionSysSim.addCamera(cameraSim, new Transform3d());
|
||||
cameraSim.prop.setCalibration(640, 480, Rotation2d.fromDegrees(80));
|
||||
@@ -236,20 +272,22 @@ class VisionSystemSimTest {
|
||||
|
||||
var robotPose = new Pose2d(new Translation2d(10, 0), Rotation2d.fromDegrees(5));
|
||||
visionSysSim.update(robotPose);
|
||||
assertTrue(camera.getLatestResult().hasTargets());
|
||||
|
||||
assertTrue(waitForSequenceNumber(camera, 1).hasTargets());
|
||||
|
||||
robotPose = new Pose2d(new Translation2d(0, 0), Rotation2d.fromDegrees(5));
|
||||
visionSysSim.update(robotPose);
|
||||
assertFalse(camera.getLatestResult().hasTargets());
|
||||
|
||||
assertFalse(waitForSequenceNumber(camera, 2).hasTargets());
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@ValueSource(doubles = {-10, -5, -0, -1, -2, 5, 7, 10.23})
|
||||
public void testYawAngles(double testYaw) {
|
||||
public void testYawAngles(double testYaw) throws InterruptedException {
|
||||
final var targetPose =
|
||||
new Pose3d(new Translation3d(15.98, 0, 1), new Rotation3d(0, 0, 3 * Math.PI / 4));
|
||||
var visionSysSim = new VisionSystemSim("Test");
|
||||
var camera = new PhotonCamera("camera");
|
||||
var camera = new PhotonCamera(inst, "camera");
|
||||
var cameraSim = new PhotonCameraSim(camera);
|
||||
visionSysSim.addCamera(cameraSim, new Transform3d());
|
||||
cameraSim.prop.setCalibration(640, 480, Rotation2d.fromDegrees(80));
|
||||
@@ -259,7 +297,8 @@ class VisionSystemSimTest {
|
||||
// If the robot is rotated x deg (CCW+), the target yaw should be x deg (CW+)
|
||||
var robotPose = new Pose2d(new Translation2d(10, 0), Rotation2d.fromDegrees(testYaw));
|
||||
visionSysSim.update(robotPose);
|
||||
var res = camera.getLatestResult();
|
||||
|
||||
var res = waitForSequenceNumber(camera, 1);
|
||||
assertTrue(res.hasTargets());
|
||||
var tgt = res.getBestTarget();
|
||||
assertEquals(testYaw, tgt.getYaw(), kRotDeltaDeg);
|
||||
@@ -267,12 +306,12 @@ class VisionSystemSimTest {
|
||||
|
||||
@ParameterizedTest
|
||||
@ValueSource(doubles = {-10, -5, -0, -1, -2, 5, 7, 10.23, 20.21, -19.999})
|
||||
public void testPitchAngles(double testPitch) {
|
||||
public void testPitchAngles(double testPitch) throws InterruptedException {
|
||||
final var targetPose =
|
||||
new Pose3d(new Translation3d(15.98, 0, 0), new Rotation3d(0, 0, 3 * Math.PI / 4));
|
||||
final var robotPose = new Pose2d(new Translation2d(10, 0), new Rotation2d(0));
|
||||
var visionSysSim = new VisionSystemSim("Test");
|
||||
var camera = new PhotonCamera("camera");
|
||||
var camera = new PhotonCamera(inst, "camera");
|
||||
var cameraSim = new PhotonCameraSim(camera);
|
||||
visionSysSim.addCamera(cameraSim, new Transform3d());
|
||||
cameraSim.prop.setCalibration(640, 480, Rotation2d.fromDegrees(120));
|
||||
@@ -284,12 +323,15 @@ class VisionSystemSimTest {
|
||||
cameraSim,
|
||||
new Transform3d(
|
||||
new Translation3d(), new Rotation3d(0, Units.degreesToRadians(testPitch), 0)));
|
||||
|
||||
visionSysSim.update(robotPose);
|
||||
var res = camera.getLatestResult();
|
||||
var res = waitForSequenceNumber(camera, 1);
|
||||
System.out.println("Got result: " + res);
|
||||
assertTrue(res.hasTargets());
|
||||
var tgt = res.getBestTarget();
|
||||
|
||||
// Since the camera is level with the target, a positive-upward point will mean the target is in
|
||||
// Since the camera is level with the target, a positive-upward point will mean
|
||||
// the target is in
|
||||
// the
|
||||
// lower half of the image
|
||||
// which should produce negative pitch.
|
||||
@@ -297,7 +339,8 @@ class VisionSystemSimTest {
|
||||
}
|
||||
|
||||
private static Stream<Arguments> testDistanceCalcArgs() {
|
||||
// Arbitrary and fairly random assortment of distances, camera pitches, and heights
|
||||
// Arbitrary and fairly random assortment of distances, camera pitches, and
|
||||
// heights
|
||||
return Stream.of(
|
||||
Arguments.of(5, -15.98, 0),
|
||||
Arguments.of(6, -15.98, 1),
|
||||
@@ -321,7 +364,8 @@ class VisionSystemSimTest {
|
||||
@ParameterizedTest
|
||||
@MethodSource("testDistanceCalcArgs")
|
||||
public void testDistanceCalc(double testDist, double testPitch, double testHeight) {
|
||||
// Assume dist along ground and tgt height the same. Iterate over other parameters.
|
||||
// Assume dist along ground and tgt height the same. Iterate over other
|
||||
// parameters.
|
||||
|
||||
final var targetPose =
|
||||
new Pose3d(new Translation3d(15.98, 0, 1), new Rotation3d(0, 0, Math.PI * 0.98));
|
||||
@@ -335,7 +379,7 @@ class VisionSystemSimTest {
|
||||
var visionSysSim =
|
||||
new VisionSystemSim(
|
||||
"absurdlylongnamewhichshouldneveractuallyhappenbuteehwelltestitanywaysohowsyourdaygoingihopegoodhaveagreatrestofyourlife!");
|
||||
var camera = new PhotonCamera("camera");
|
||||
var camera = new PhotonCamera(inst, "camera");
|
||||
var cameraSim = new PhotonCameraSim(camera);
|
||||
visionSysSim.addCamera(cameraSim, new Transform3d());
|
||||
cameraSim.prop.setCalibration(640, 480, Rotation2d.fromDegrees(160));
|
||||
@@ -345,11 +389,15 @@ class VisionSystemSimTest {
|
||||
|
||||
visionSysSim.update(robotPose);
|
||||
|
||||
// Note that target 2d yaw/pitch accuracy is hindered by two factors in photonvision:
|
||||
// 1. These are calculated with the average of the minimum area rectangle, which does not
|
||||
// Note that target 2d yaw/pitch accuracy is hindered by two factors in
|
||||
// photonvision:
|
||||
// 1. These are calculated with the average of the minimum area rectangle, which
|
||||
// does not
|
||||
// actually find the target center because of perspective distortion.
|
||||
// 2. Yaw and pitch are calculated separately which gives incorrect pitch values.
|
||||
var res = camera.getLatestResult();
|
||||
// 2. Yaw and pitch are calculated separately which gives incorrect pitch
|
||||
// values.
|
||||
|
||||
var res = waitForSequenceNumber(camera, 1);
|
||||
assertTrue(res.hasTargets());
|
||||
var tgt = res.getBestTarget();
|
||||
assertEquals(0.0, tgt.getYaw(), 0.5);
|
||||
@@ -375,7 +423,7 @@ class VisionSystemSimTest {
|
||||
new Pose3d(new Translation3d(15.98, -2, 0), new Rotation3d(0, 0, Math.PI));
|
||||
|
||||
var visionSysSim = new VisionSystemSim("Test");
|
||||
var camera = new PhotonCamera("camera");
|
||||
var camera = new PhotonCamera(inst, "camera");
|
||||
var cameraSim = new PhotonCameraSim(camera);
|
||||
visionSysSim.addCamera(cameraSim, new Transform3d());
|
||||
cameraSim.prop.setCalibration(640, 480, Rotation2d.fromDegrees(80));
|
||||
@@ -450,7 +498,9 @@ class VisionSystemSimTest {
|
||||
|
||||
var robotPose = new Pose2d(new Translation2d(6.0, 0), Rotation2d.fromDegrees(0.25));
|
||||
visionSysSim.update(robotPose);
|
||||
var res = camera.getLatestResult();
|
||||
|
||||
var res = waitForSequenceNumber(camera, 1);
|
||||
|
||||
assertTrue(res.hasTargets());
|
||||
List<PhotonTrackedTarget> tgtList;
|
||||
tgtList = res.getTargets();
|
||||
@@ -460,7 +510,7 @@ class VisionSystemSimTest {
|
||||
@Test
|
||||
public void testPoseEstimation() {
|
||||
var visionSysSim = new VisionSystemSim("Test");
|
||||
var camera = new PhotonCamera("camera");
|
||||
var camera = new PhotonCamera(inst, "camera");
|
||||
var cameraSim = new PhotonCameraSim(camera);
|
||||
visionSysSim.addCamera(cameraSim, new Transform3d());
|
||||
cameraSim.prop.setCalibration(640, 480, Rotation2d.fromDegrees(90));
|
||||
@@ -479,11 +529,12 @@ class VisionSystemSimTest {
|
||||
new VisionTargetSim(tagList.get(0).pose, TargetModel.kAprilTag16h5, 0));
|
||||
|
||||
visionSysSim.update(robotPose);
|
||||
|
||||
var results =
|
||||
VisionEstimation.estimateCamPosePNP(
|
||||
camera.getCameraMatrix().get(),
|
||||
camera.getDistCoeffs().get(),
|
||||
camera.getLatestResult().getTargets(),
|
||||
waitForSequenceNumber(camera, 1).getTargets(),
|
||||
layout,
|
||||
TargetModel.kAprilTag16h5)
|
||||
.get();
|
||||
@@ -499,6 +550,7 @@ class VisionSystemSimTest {
|
||||
new VisionTargetSim(tagList.get(2).pose, TargetModel.kAprilTag16h5, 2));
|
||||
|
||||
visionSysSim.update(robotPose);
|
||||
|
||||
results =
|
||||
VisionEstimation.estimateCamPosePNP(
|
||||
camera.getCameraMatrix().get(),
|
||||
|
||||
@@ -1,79 +0,0 @@
|
||||
/*
|
||||
* MIT License
|
||||
*
|
||||
* Copyright (c) PhotonVision
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
|
||||
package org.photonvision.estimation;
|
||||
|
||||
import edu.wpi.first.apriltag.AprilTagFieldLayout;
|
||||
import edu.wpi.first.apriltag.AprilTagFields;
|
||||
import edu.wpi.first.math.geometry.Transform3d;
|
||||
import edu.wpi.first.networktables.NetworkTableInstance;
|
||||
import edu.wpi.first.wpilibj.smartdashboard.Field2d;
|
||||
import edu.wpi.first.wpilibj.smartdashboard.SmartDashboard;
|
||||
import java.io.IOException;
|
||||
import org.junit.jupiter.api.BeforeAll;
|
||||
import org.photonvision.PhotonCamera;
|
||||
import org.photonvision.PhotonPoseEstimator;
|
||||
|
||||
public class ApriltagWorkbenchTest {
|
||||
@BeforeAll
|
||||
public static void setUp() {
|
||||
// No version check for testing
|
||||
PhotonCamera.setVersionCheckEnabled(false);
|
||||
}
|
||||
|
||||
// @Test
|
||||
public void testMeme() throws IOException, InterruptedException {
|
||||
NetworkTableInstance instance = NetworkTableInstance.getDefault();
|
||||
instance.stopServer();
|
||||
// set the NT server if simulating this code.
|
||||
// "localhost" for photon on desktop, or "photonvision.local" / "[ip-address]"
|
||||
// for coprocessor
|
||||
instance.setServer("localhost");
|
||||
instance.startClient4("myRobot");
|
||||
|
||||
var robotToCamera = new Transform3d();
|
||||
var cam = new PhotonCamera("WPI2023");
|
||||
var tagLayout =
|
||||
AprilTagFieldLayout.loadFromResource(AprilTagFields.k2023ChargedUp.m_resourceFile);
|
||||
|
||||
var pe =
|
||||
new PhotonPoseEstimator(
|
||||
tagLayout,
|
||||
PhotonPoseEstimator.PoseStrategy.MULTI_TAG_PNP_ON_COPROCESSOR,
|
||||
robotToCamera);
|
||||
|
||||
var field = new Field2d();
|
||||
SmartDashboard.putData(field);
|
||||
|
||||
while (!Thread.interrupted()) {
|
||||
Thread.sleep(500);
|
||||
|
||||
for (var change : cam.getAllUnreadResults()) {
|
||||
var ret = pe.update(change);
|
||||
System.out.println(ret);
|
||||
field.setRobotPose(ret.get().estimatedPose.toPose2d());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
54
photon-lib/src/test/native/cpp/PhotonCameraTest.cpp
Normal file
54
photon-lib/src/test/native/cpp/PhotonCameraTest.cpp
Normal file
@@ -0,0 +1,54 @@
|
||||
/*
|
||||
* MIT License
|
||||
*
|
||||
* Copyright (c) PhotonVision
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in
|
||||
* all copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
#include <hal/HAL.h>
|
||||
#include <net/TimeSyncClient.h>
|
||||
#include <net/TimeSyncServer.h>
|
||||
|
||||
#include "photon/PhotonCamera.h"
|
||||
|
||||
TEST(TimeSyncProtocolTest, Smoketest) {
|
||||
using namespace wpi::tsp;
|
||||
using namespace std::chrono_literals;
|
||||
|
||||
// start a server implicitly
|
||||
photon::PhotonCamera camera{"camera"};
|
||||
|
||||
TimeSyncClient client{"127.0.0.1", 5810, 100ms};
|
||||
client.Start();
|
||||
|
||||
for (int i = 0; i < 10; i++) {
|
||||
std::this_thread::sleep_for(100ms);
|
||||
TimeSyncClient::Metadata m = client.GetMetadata();
|
||||
|
||||
// give us time to warm up
|
||||
if (i > 5) {
|
||||
EXPECT_TRUE(m.rtt2 > 0);
|
||||
EXPECT_TRUE(m.pongsReceived > 0);
|
||||
}
|
||||
}
|
||||
|
||||
client.Stop();
|
||||
}
|
||||
@@ -86,7 +86,7 @@ TEST(PhotonPoseEstimatorTest, LowestAmbiguityStrategy) {
|
||||
|
||||
cameraOne.test = true;
|
||||
cameraOne.testResult = {photon::PhotonPipelineResult{
|
||||
photon::PhotonPipelineMetadata{0, 0, 2000}, targets, std::nullopt}};
|
||||
photon::PhotonPipelineMetadata{0, 0, 2000, 1000}, targets, std::nullopt}};
|
||||
cameraOne.testResult[0].SetReceiveTimestamp(units::second_t(11));
|
||||
|
||||
photon::PhotonPoseEstimator estimator(aprilTags, photon::LOWEST_AMBIGUITY,
|
||||
@@ -147,7 +147,7 @@ TEST(PhotonPoseEstimatorTest, ClosestToCameraHeightStrategy) {
|
||||
|
||||
cameraOne.test = true;
|
||||
cameraOne.testResult = {photon::PhotonPipelineResult{
|
||||
photon::PhotonPipelineMetadata{0, 0, 2000}, targets, std::nullopt}};
|
||||
photon::PhotonPipelineMetadata{0, 0, 2000, 1000}, targets, std::nullopt}};
|
||||
cameraOne.testResult[0].SetReceiveTimestamp(17_s);
|
||||
|
||||
photon::PhotonPoseEstimator estimator(
|
||||
@@ -196,7 +196,7 @@ TEST(PhotonPoseEstimatorTest, ClosestToReferencePoseStrategy) {
|
||||
|
||||
cameraOne.test = true;
|
||||
cameraOne.testResult = {photon::PhotonPipelineResult{
|
||||
photon::PhotonPipelineMetadata{0, 0, 2000}, targets, std::nullopt}};
|
||||
photon::PhotonPipelineMetadata{0, 0, 2000, 1000}, targets, std::nullopt}};
|
||||
cameraOne.testResult[0].SetReceiveTimestamp(units::second_t(17));
|
||||
|
||||
photon::PhotonPoseEstimator estimator(aprilTags,
|
||||
@@ -247,7 +247,7 @@ TEST(PhotonPoseEstimatorTest, ClosestToLastPose) {
|
||||
|
||||
cameraOne.test = true;
|
||||
cameraOne.testResult = {photon::PhotonPipelineResult{
|
||||
photon::PhotonPipelineMetadata{0, 0, 2000}, targets, std::nullopt}};
|
||||
photon::PhotonPipelineMetadata{0, 0, 2000, 1000}, targets, std::nullopt}};
|
||||
cameraOne.testResult[0].SetReceiveTimestamp(units::second_t(17));
|
||||
|
||||
photon::PhotonPoseEstimator estimator(aprilTags, photon::CLOSEST_TO_LAST_POSE,
|
||||
@@ -287,7 +287,8 @@ TEST(PhotonPoseEstimatorTest, ClosestToLastPose) {
|
||||
0.4, corners, detectedCorners}};
|
||||
|
||||
cameraOne.testResult = {photon::PhotonPipelineResult{
|
||||
photon::PhotonPipelineMetadata{0, 0, 2000}, targetsThree, std::nullopt}};
|
||||
photon::PhotonPipelineMetadata{0, 0, 2000, 1000}, targetsThree,
|
||||
std::nullopt}};
|
||||
cameraOne.testResult[0].SetReceiveTimestamp(units::second_t(21));
|
||||
|
||||
// std::optional<photon::EstimatedRobotPose> estimatedPose;
|
||||
@@ -333,7 +334,7 @@ TEST(PhotonPoseEstimatorTest, AverageBestPoses) {
|
||||
|
||||
cameraOne.test = true;
|
||||
cameraOne.testResult = {photon::PhotonPipelineResult{
|
||||
photon::PhotonPipelineMetadata{0, 0, 2000}, targets, std::nullopt}};
|
||||
photon::PhotonPipelineMetadata{0, 0, 2000, 1000}, targets, std::nullopt}};
|
||||
cameraOne.testResult[0].SetReceiveTimestamp(units::second_t(15));
|
||||
|
||||
photon::PhotonPoseEstimator estimator(aprilTags, photon::AVERAGE_BEST_TARGETS,
|
||||
@@ -387,7 +388,7 @@ TEST(PhotonPoseEstimatorTest, PoseCache) {
|
||||
|
||||
// empty input, expect empty out
|
||||
cameraOne.testResult = {photon::PhotonPipelineResult{
|
||||
photon::PhotonPipelineMetadata{0, 0, 2000},
|
||||
photon::PhotonPipelineMetadata{0, 0, 2000, 1000},
|
||||
std::vector<photon::PhotonTrackedTarget>{}, std::nullopt}};
|
||||
cameraOne.testResult[0].SetReceiveTimestamp(units::second_t(1));
|
||||
|
||||
@@ -400,7 +401,7 @@ TEST(PhotonPoseEstimatorTest, PoseCache) {
|
||||
|
||||
// Set result, and update -- expect present and timestamp to be 15
|
||||
cameraOne.testResult = {photon::PhotonPipelineResult{
|
||||
photon::PhotonPipelineMetadata{0, 0, 3000}, targets, std::nullopt}};
|
||||
photon::PhotonPipelineMetadata{0, 0, 3000, 1000}, targets, std::nullopt}};
|
||||
cameraOne.testResult[0].SetReceiveTimestamp(units::second_t(15));
|
||||
|
||||
for (const auto& result : cameraOne.GetAllUnreadResults()) {
|
||||
@@ -422,7 +423,7 @@ TEST(PhotonPoseEstimatorTest, CopyResult) {
|
||||
std::vector<photon::PhotonTrackedTarget> targets{};
|
||||
|
||||
auto testResult = photon::PhotonPipelineResult{
|
||||
photon::PhotonPipelineMetadata{0, 0, 2000}, targets, std::nullopt};
|
||||
photon::PhotonPipelineMetadata{0, 0, 2000, 1000}, targets, std::nullopt};
|
||||
testResult.SetReceiveTimestamp(units::second_t(11));
|
||||
|
||||
auto test2 = testResult;
|
||||
|
||||
@@ -22,10 +22,14 @@
|
||||
* SOFTWARE.
|
||||
*/
|
||||
|
||||
#include <hal/HAL.h>
|
||||
|
||||
#include "gtest/gtest.h"
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
HAL_Initialize(500, 0);
|
||||
::testing::InitGoogleTest(&argc, argv);
|
||||
int ret = RUN_ALL_TESTS();
|
||||
HAL_Shutdown();
|
||||
return ret;
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ Like Rosmsg. But worse.
|
||||
The code for a single type is split across 3 files. Let's look at PnpResult:
|
||||
- [The struct definition](src/struct/pnpresult_struct.h): This is the data the object holds. Auto-generated. The data this object holds can be primitives or other, fully-deserialized types (like Vec2)
|
||||
- [The user class](src/targeting/pnpresult_struct.h): This is the fully-deserialized PnpResult type. This contains extra functions users might need to expose like `Amgiguity`, or other computed helper things.
|
||||
- [The serde interface](src/serde/pnpresult_struct.h): This is a template specilization for converting the user class to/from bytes
|
||||
- [The serde interface](src/serde/pnpresult_struct.h): This is a template specialization for converting the user class to/from bytes
|
||||
|
||||
## Prior art
|
||||
|
||||
|
||||
@@ -7,6 +7,8 @@
|
||||
type: int64
|
||||
- name: publishTimestampMicros
|
||||
type: int64
|
||||
- name: timeSinceLastPong
|
||||
type: int64
|
||||
|
||||
- name: Transform3d
|
||||
shimmed: True
|
||||
|
||||
@@ -71,7 +71,7 @@ remotes {
|
||||
task findDeployTarget {
|
||||
doLast {
|
||||
if(project.hasProperty('tgtIP')){
|
||||
//If user specificed IP, default to using the PI profile
|
||||
//If user specified IP, default to using the PI profile
|
||||
// but adjust hostname to match the provided IP address
|
||||
findDeployTarget.ext.rmt = remotes.pi
|
||||
findDeployTarget.ext.rmt.host=tgtIP
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
rootProject.name = 'photon-server'
|
||||
@@ -17,6 +17,7 @@
|
||||
|
||||
package org.photonvision;
|
||||
|
||||
import edu.wpi.first.hal.HAL;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
@@ -39,6 +40,7 @@ import org.photonvision.common.logging.PvCSCoreLogger;
|
||||
import org.photonvision.common.networking.NetworkManager;
|
||||
import org.photonvision.common.util.TestUtils;
|
||||
import org.photonvision.common.util.numbers.IntegerCouple;
|
||||
import org.photonvision.jni.PhotonTargetingJniLoader;
|
||||
import org.photonvision.jni.RknnDetectorJNI;
|
||||
import org.photonvision.mrcal.MrCalJNILoader;
|
||||
import org.photonvision.raspi.LibCameraJNILoader;
|
||||
@@ -379,7 +381,25 @@ public class Main {
|
||||
logger.error("Failed to load native libraries!", e);
|
||||
System.exit(1);
|
||||
}
|
||||
logger.info("Native libraries loaded.");
|
||||
logger.info("WPI JNI libraries loaded.");
|
||||
|
||||
try {
|
||||
boolean success = PhotonTargetingJniLoader.load();
|
||||
|
||||
if (!success) {
|
||||
logger.error("Failed to load native libraries! Giving up :(");
|
||||
System.exit(1);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
logger.error("Failed to load photon-targeting JNI!", e);
|
||||
System.exit(1);
|
||||
}
|
||||
logger.info("photon-targeting JNI libraries loaded.");
|
||||
|
||||
if (!HAL.initialize(500, 0)) {
|
||||
logger.error("Failed to initialize the HAL! Giving up :(");
|
||||
System.exit(1);
|
||||
}
|
||||
|
||||
try {
|
||||
if (Platform.isRaspberryPi()) {
|
||||
@@ -438,6 +458,7 @@ public class Main {
|
||||
logger.debug("Loading NetworkTablesManager...");
|
||||
NetworkTablesManager.getInstance()
|
||||
.setConfig(ConfigManager.getInstance().getConfig().getNetworkConfig());
|
||||
NetworkTablesManager.getInstance().registerTimedTasks();
|
||||
|
||||
if (isSmoketest) {
|
||||
logger.info("PhotonVision base functionality loaded -- smoketest complete");
|
||||
|
||||
@@ -1,7 +1,3 @@
|
||||
plugins {
|
||||
id 'edu.wpi.first.WpilibTools' version '1.3.0'
|
||||
}
|
||||
|
||||
ext {
|
||||
nativeName = "photontargeting"
|
||||
}
|
||||
@@ -9,6 +5,7 @@ ext {
|
||||
apply plugin: 'cpp'
|
||||
apply plugin: 'google-test-test-suite'
|
||||
apply plugin: 'edu.wpi.first.NativeUtils'
|
||||
apply plugin: 'edu.wpi.first.WpilibTools'
|
||||
apply plugin: 'edu.wpi.first.GradleJni'
|
||||
|
||||
apply from: "${rootDir}/shared/config.gradle"
|
||||
@@ -93,12 +90,23 @@ model {
|
||||
nativeUtils.useRequiredLibrary(it, "wpiutil_shared")
|
||||
nativeUtils.useRequiredLibrary(it, "wpinet_shared")
|
||||
nativeUtils.useRequiredLibrary(it, "ntcore_shared")
|
||||
nativeUtils.useRequiredLibrary(it, "wpimath_shared")
|
||||
}
|
||||
|
||||
all {
|
||||
binaries.withType(SharedLibraryBinarySpec) { binary ->
|
||||
// check that we're building for the platform (per PArchOverride/wpilib plat detection)
|
||||
if (binary.targetPlatform.name == jniPlatform) {
|
||||
def platName = jniPlatform
|
||||
def realWpilibName = wpilibNativeName;
|
||||
|
||||
if (jniPlatform.equals("osxarm64") || jniPlatform.equals("osxx86-64")) {
|
||||
// native-utils calls this osxuniversal
|
||||
platName = "osxuniversal";
|
||||
realWpilibName = "osxuniversal";
|
||||
}
|
||||
|
||||
if (binary.targetPlatform.name == platName) {
|
||||
|
||||
|
||||
// only include release binaries (hard coded for now)
|
||||
def isDebug = binary.buildType.name.contains('debug')
|
||||
@@ -106,7 +114,7 @@ model {
|
||||
syncOutputsFolder {
|
||||
// Just shove the shared library into the root of the jar output by photon-targeting:jar
|
||||
from(binary.sharedLibraryFile) {
|
||||
into "nativelibraries/${wpilibNativeName}/"
|
||||
into "nativelibraries/${realWpilibName}/"
|
||||
}
|
||||
// And (not sure if this is a hack) make the jar task depend on the build task
|
||||
dependsOn binary.identifier.projectScopedName
|
||||
@@ -206,6 +214,7 @@ def nativeTasks = wpilibTools.createExtractionTasks {
|
||||
|
||||
nativeTasks.addToSourceSetResources(sourceSets.test)
|
||||
|
||||
nativeConfig.dependencies.add wpilibTools.deps.wpilib("wpilibc")
|
||||
nativeConfig.dependencies.add wpilibTools.deps.wpilib("wpiutil")
|
||||
nativeConfig.dependencies.add wpilibTools.deps.wpilib("wpimath")
|
||||
nativeConfig.dependencies.add wpilibTools.deps.wpilib("wpinet")
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
pluginManagement {
|
||||
repositories {
|
||||
mavenLocal()
|
||||
gradlePluginPortal()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
/*
|
||||
* 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.jni;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import org.opencv.core.Core;
|
||||
|
||||
import edu.wpi.first.apriltag.jni.AprilTagJNI;
|
||||
import edu.wpi.first.cscore.CameraServerJNI;
|
||||
import edu.wpi.first.cscore.OpenCvLoader;
|
||||
import edu.wpi.first.hal.JNIWrapper;
|
||||
import edu.wpi.first.math.jni.ArmFeedforwardJNI;
|
||||
import edu.wpi.first.math.jni.DAREJNI;
|
||||
import edu.wpi.first.math.jni.EigenJNI;
|
||||
import edu.wpi.first.math.jni.Ellipse2dJNI;
|
||||
import edu.wpi.first.math.jni.Pose3dJNI;
|
||||
import edu.wpi.first.math.jni.StateSpaceUtilJNI;
|
||||
import edu.wpi.first.math.jni.TrajectoryUtilJNI;
|
||||
import edu.wpi.first.net.WPINetJNI;
|
||||
import edu.wpi.first.networktables.NetworkTablesJNI;
|
||||
import edu.wpi.first.util.CombinedRuntimeLoader;
|
||||
import edu.wpi.first.util.WPIUtilJNI;
|
||||
|
||||
public class WpilibLoader {
|
||||
private static boolean has_loaded = false;
|
||||
public static boolean loadLibraries() {
|
||||
if (has_loaded) return true;
|
||||
|
||||
NetworkTablesJNI.Helper.setExtractOnStaticLoad(false);
|
||||
WPIUtilJNI.Helper.setExtractOnStaticLoad(false);
|
||||
CameraServerJNI.Helper.setExtractOnStaticLoad(false);
|
||||
OpenCvLoader.Helper.setExtractOnStaticLoad(false);
|
||||
JNIWrapper.Helper.setExtractOnStaticLoad(false);
|
||||
WPINetJNI.Helper.setExtractOnStaticLoad(false);
|
||||
AprilTagJNI.Helper.setExtractOnStaticLoad(false);
|
||||
|
||||
// wpimathjni is a bit odd, it's all in the wpimathjni shared lib, but the java side stuff has
|
||||
// been split.
|
||||
ArmFeedforwardJNI.Helper.setExtractOnStaticLoad(false);
|
||||
DAREJNI.Helper.setExtractOnStaticLoad(false);
|
||||
EigenJNI.Helper.setExtractOnStaticLoad(false);
|
||||
Ellipse2dJNI.Helper.setExtractOnStaticLoad(false);
|
||||
Pose3dJNI.Helper.setExtractOnStaticLoad(false);
|
||||
StateSpaceUtilJNI.Helper.setExtractOnStaticLoad(false);
|
||||
TrajectoryUtilJNI.Helper.setExtractOnStaticLoad(false);
|
||||
|
||||
try {
|
||||
CombinedRuntimeLoader.loadLibraries(
|
||||
WpilibLoader.class,
|
||||
"wpiutiljni",
|
||||
"wpilibc",
|
||||
"wpimathjni",
|
||||
"ntcorejni",
|
||||
"wpinetjni",
|
||||
"wpiHaljni",
|
||||
"wpi",
|
||||
"cscorejni",
|
||||
"apriltagjni");
|
||||
|
||||
CombinedRuntimeLoader.loadLibraries(WpilibLoader.class, Core.NATIVE_LIBRARY_NAME);
|
||||
|
||||
has_loaded = true;
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
has_loaded = false;
|
||||
}
|
||||
|
||||
return has_loaded;
|
||||
}
|
||||
}
|
||||
@@ -35,9 +35,9 @@ import edu.wpi.first.util.struct.Struct;
|
||||
*/
|
||||
public class PhotonPipelineMetadataSerde implements PacketSerde<PhotonPipelineMetadata> {
|
||||
@Override
|
||||
public final String getInterfaceUUID() { return "626e70461cbdb274fb43ead09c255f4e"; }
|
||||
public final String getInterfaceUUID() { return "ac0a45f686457856fb30af77699ea356"; }
|
||||
@Override
|
||||
public final String getSchema() { return "int64 sequenceID;int64 captureTimestampMicros;int64 publishTimestampMicros;"; }
|
||||
public final String getSchema() { return "int64 sequenceID;int64 captureTimestampMicros;int64 publishTimestampMicros;int64 timeSinceLastPong;"; }
|
||||
@Override
|
||||
public final String getTypeName() { return "PhotonPipelineMetadata"; }
|
||||
|
||||
@@ -57,6 +57,9 @@ public class PhotonPipelineMetadataSerde implements PacketSerde<PhotonPipelineMe
|
||||
|
||||
// field publishTimestampMicros is of intrinsic type int64
|
||||
packet.encode((long) value.publishTimestampMicros);
|
||||
|
||||
// field timeSinceLastPong is of intrinsic type int64
|
||||
packet.encode((long) value.timeSinceLastPong);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -72,6 +75,9 @@ public class PhotonPipelineMetadataSerde implements PacketSerde<PhotonPipelineMe
|
||||
// publishTimestampMicros is of intrinsic type int64
|
||||
ret.publishTimestampMicros = packet.decodeLong();
|
||||
|
||||
// timeSinceLastPong is of intrinsic type int64
|
||||
ret.timeSinceLastPong = packet.decodeLong();
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
|
||||
@@ -35,9 +35,9 @@ import edu.wpi.first.util.struct.Struct;
|
||||
*/
|
||||
public class PhotonPipelineResultSerde implements PacketSerde<PhotonPipelineResult> {
|
||||
@Override
|
||||
public final String getInterfaceUUID() { return "5eeaa293d0c69aea90eaddea786a2b3b"; }
|
||||
public final String getInterfaceUUID() { return "4b2ff16a964b5e2bf04be0c1454d91c4"; }
|
||||
@Override
|
||||
public final String getSchema() { return "PhotonPipelineMetadata:626e70461cbdb274fb43ead09c255f4e metadata;PhotonTrackedTarget:cc6dbb5c5c1e0fa808108019b20863f1 targets[?];optional MultiTargetPNPResult:541096947e9f3ca2d3f425ff7b04aa7b multitagResult;"; }
|
||||
public final String getSchema() { return "PhotonPipelineMetadata:ac0a45f686457856fb30af77699ea356 metadata;PhotonTrackedTarget:cc6dbb5c5c1e0fa808108019b20863f1 targets[?];optional MultiTargetPNPResult:541096947e9f3ca2d3f425ff7b04aa7b multitagResult;"; }
|
||||
@Override
|
||||
public final String getTypeName() { return "PhotonPipelineResult"; }
|
||||
|
||||
@@ -78,7 +78,7 @@ public class PhotonPipelineResultSerde implements PacketSerde<PhotonPipelineResu
|
||||
@Override
|
||||
public PacketSerde<?>[] getNestedPhotonMessages() {
|
||||
return new PacketSerde<?>[] {
|
||||
PhotonPipelineMetadata.photonStruct,MultiTargetPNPResult.photonStruct,PhotonTrackedTarget.photonStruct
|
||||
MultiTargetPNPResult.photonStruct,PhotonTrackedTarget.photonStruct,PhotonPipelineMetadata.photonStruct
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -28,6 +28,7 @@ void StructType::Pack(Packet& packet, const PhotonPipelineMetadata& value) {
|
||||
packet.Pack<int64_t>(value.sequenceID);
|
||||
packet.Pack<int64_t>(value.captureTimestampMicros);
|
||||
packet.Pack<int64_t>(value.publishTimestampMicros);
|
||||
packet.Pack<int64_t>(value.timeSinceLastPong);
|
||||
}
|
||||
|
||||
PhotonPipelineMetadata StructType::Unpack(Packet& packet) {
|
||||
@@ -35,6 +36,7 @@ PhotonPipelineMetadata StructType::Unpack(Packet& packet) {
|
||||
.sequenceID = packet.Unpack<int64_t>(),
|
||||
.captureTimestampMicros = packet.Unpack<int64_t>(),
|
||||
.publishTimestampMicros = packet.Unpack<int64_t>(),
|
||||
.timeSinceLastPong = packet.Unpack<int64_t>(),
|
||||
}};
|
||||
}
|
||||
|
||||
|
||||
@@ -34,12 +34,12 @@ namespace photon {
|
||||
template <>
|
||||
struct WPILIB_DLLEXPORT SerdeType<PhotonPipelineMetadata> {
|
||||
static constexpr std::string_view GetSchemaHash() {
|
||||
return "626e70461cbdb274fb43ead09c255f4e";
|
||||
return "ac0a45f686457856fb30af77699ea356";
|
||||
}
|
||||
|
||||
static constexpr std::string_view GetSchema() {
|
||||
return "int64 sequenceID;int64 captureTimestampMicros;int64 "
|
||||
"publishTimestampMicros;";
|
||||
"publishTimestampMicros;int64 timeSinceLastPong;";
|
||||
}
|
||||
|
||||
static photon::PhotonPipelineMetadata Unpack(photon::Packet& packet);
|
||||
|
||||
@@ -39,11 +39,11 @@ namespace photon {
|
||||
template <>
|
||||
struct WPILIB_DLLEXPORT SerdeType<PhotonPipelineResult> {
|
||||
static constexpr std::string_view GetSchemaHash() {
|
||||
return "5eeaa293d0c69aea90eaddea786a2b3b";
|
||||
return "4b2ff16a964b5e2bf04be0c1454d91c4";
|
||||
}
|
||||
|
||||
static constexpr std::string_view GetSchema() {
|
||||
return "PhotonPipelineMetadata:626e70461cbdb274fb43ead09c255f4e "
|
||||
return "PhotonPipelineMetadata:ac0a45f686457856fb30af77699ea356 "
|
||||
"metadata;PhotonTrackedTarget:cc6dbb5c5c1e0fa808108019b20863f1 "
|
||||
"targets[?];optional "
|
||||
"MultiTargetPNPResult:541096947e9f3ca2d3f425ff7b04aa7b "
|
||||
|
||||
@@ -29,6 +29,7 @@ struct PhotonPipelineMetadata_PhotonStruct {
|
||||
int64_t sequenceID;
|
||||
int64_t captureTimestampMicros;
|
||||
int64_t publishTimestampMicros;
|
||||
int64_t timeSinceLastPong;
|
||||
|
||||
friend bool operator==(PhotonPipelineMetadata_PhotonStruct const&,
|
||||
PhotonPipelineMetadata_PhotonStruct const&) = default;
|
||||
|
||||
@@ -54,7 +54,7 @@ public enum Platform {
|
||||
LINUX_ARM32("Linux ARM32", "linuxarm32", false, OSType.LINUX, false), // ODROID XU4, C1+
|
||||
UNKNOWN("Unsupported Platform", "", false, OSType.UNKNOWN, false);
|
||||
|
||||
private enum OSType {
|
||||
public enum OSType {
|
||||
WINDOWS,
|
||||
LINUX,
|
||||
MACOS,
|
||||
@@ -123,7 +123,7 @@ public enum Platform {
|
||||
private static final String UnknownPlatformString =
|
||||
String.format("Unknown Platform. OS: %s, Architecture: %s", OS_NAME, OS_ARCH);
|
||||
|
||||
private static Platform getCurrentPlatform() {
|
||||
public static Platform getCurrentPlatform() {
|
||||
if (RuntimeDetector.isWindows()) {
|
||||
if (RuntimeDetector.is32BitIntel()) {
|
||||
return WINDOWS_32;
|
||||
|
||||
@@ -80,7 +80,8 @@ public class NTTopicSet {
|
||||
.publish(
|
||||
PhotonPipelineResult.photonStruct.getTypeString(),
|
||||
PubSubOption.periodic(0.01),
|
||||
PubSubOption.sendAll(true));
|
||||
PubSubOption.sendAll(true),
|
||||
PubSubOption.keepDuplicates(true));
|
||||
|
||||
resultPublisher =
|
||||
new PacketPublisher<PhotonPipelineResult>(rawBytesEntry, PhotonPipelineResult.photonStruct);
|
||||
|
||||
@@ -87,11 +87,9 @@ public class PacketPublisher<T> implements AutoCloseable {
|
||||
instance.addSchema(typeString, "photonstructschema", struct.getSchema());
|
||||
|
||||
for (var inner : struct.getNestedPhotonMessages()) {
|
||||
System.out.println(inner.getTypeString());
|
||||
addSchemaImpl(inner, seen);
|
||||
}
|
||||
for (var inner : struct.getNestedWpilibMessages()) {
|
||||
System.out.println(inner.getTypeString());
|
||||
instance.addSchema(inner);
|
||||
}
|
||||
seen.remove(typeString);
|
||||
|
||||
@@ -90,6 +90,13 @@ public class PacketSubscriber<T> implements AutoCloseable {
|
||||
public String getInterfaceUUID() {
|
||||
// ntcore hands us a JSON string with leading/trailing quotes - remove those
|
||||
var uuidStr = subscriber.getTopic().getProperty("message_uuid");
|
||||
|
||||
// "null" can be returned if the property does not exist. From system knowledge, uuid can never
|
||||
// be the string literal "null".
|
||||
if (uuidStr.equals("null")) {
|
||||
return "";
|
||||
}
|
||||
|
||||
return uuidStr.replace("\"", "");
|
||||
}
|
||||
|
||||
|
||||
@@ -203,7 +203,7 @@ public final class OpenCVHelp {
|
||||
* @param <T> Element type
|
||||
* @param elements list elements
|
||||
* @param backwards If indexing should happen in reverse (0, size-1, size-2, ...)
|
||||
* @param shiftStart How much the inital index should be shifted (instead of starting at index 0,
|
||||
* @param shiftStart How much the initial index should be shifted (instead of starting at index 0,
|
||||
* start at shiftStart, negated if backwards)
|
||||
* @return Reordered list
|
||||
*/
|
||||
@@ -257,7 +257,7 @@ public final class OpenCVHelp {
|
||||
*
|
||||
* @param pointsList the undistorted points
|
||||
* @param cameraMatrix standard OpenCV camera mat
|
||||
* @param distCoeffs standard OpenCV distortion coefficeints. Must OPENCV5 or OPENCV8
|
||||
* @param distCoeffs standard OpenCV distortion coefficients. Must OPENCV5 or OPENCV8
|
||||
*/
|
||||
public static List<Point> distortPoints(
|
||||
List<Point> pointsList, Mat cameraMatrix, Mat distCoeffs) {
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
/*
|
||||
* 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.jni;
|
||||
|
||||
import edu.wpi.first.util.RuntimeDetector;
|
||||
import edu.wpi.first.util.RuntimeLoader;
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.util.List;
|
||||
import org.photonvision.common.hardware.Platform;
|
||||
|
||||
public class PhotonTargetingJniLoader {
|
||||
public static boolean isWorking = false;
|
||||
|
||||
public static boolean load() throws IOException, UnsatisfiedLinkError {
|
||||
if (isWorking) return true;
|
||||
isWorking = load_();
|
||||
return isWorking;
|
||||
}
|
||||
|
||||
public static boolean load_() throws IOException, UnsatisfiedLinkError {
|
||||
// We always extract the shared object (we could hash each so, but that's a lot
|
||||
// of work)
|
||||
String arch_name = Platform.getNativeLibraryFolderName();
|
||||
var clazz = PhotonTargetingJniLoader.class;
|
||||
|
||||
for (var libraryName : List.of("photontargeting", "photontargetingJNI")) {
|
||||
if (RuntimeDetector.isAthena()) {
|
||||
System.out.println("Detected rio - loading directly");
|
||||
RuntimeLoader.loadLibrary(libraryName);
|
||||
continue;
|
||||
}
|
||||
|
||||
var nativeLibName = System.mapLibraryName(libraryName);
|
||||
var path = "/nativelibraries/" + arch_name + "/" + nativeLibName;
|
||||
var in = clazz.getResourceAsStream(path);
|
||||
|
||||
if (in == null) {
|
||||
System.err.println("Could not get resource at path " + path);
|
||||
return false;
|
||||
}
|
||||
|
||||
// It's important that we don't mangle the names of these files on Windows at
|
||||
// least
|
||||
var tempfolder = Files.createTempDirectory("nativeextract");
|
||||
File temp = new File(tempfolder.toAbsolutePath().toString(), nativeLibName);
|
||||
System.out.println(temp.getAbsolutePath().toString());
|
||||
FileOutputStream fos = new FileOutputStream(temp);
|
||||
|
||||
int read = -1;
|
||||
byte[] buffer = new byte[1024];
|
||||
while ((read = in.read(buffer)) != -1) {
|
||||
fos.write(buffer, 0, read);
|
||||
}
|
||||
fos.close();
|
||||
in.close();
|
||||
|
||||
try {
|
||||
System.load(temp.getAbsolutePath());
|
||||
} catch (Throwable t) {
|
||||
System.err.println("Unable to System.load " + temp.getName() + " : " + t.getMessage());
|
||||
t.printStackTrace();
|
||||
return false;
|
||||
}
|
||||
|
||||
System.out.println("Successfully loaded shared object " + temp.getName());
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,164 @@
|
||||
/*
|
||||
* 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.jni;
|
||||
|
||||
import edu.wpi.first.networktables.NetworkTablesJNI;
|
||||
|
||||
/**
|
||||
* Send ping-pongs to estimate server time, relative to nt::Now. The underlying implementation does
|
||||
* technically allow us to provide a different source, but all photon code assumes nt::Now is used
|
||||
*/
|
||||
public class TimeSyncClient {
|
||||
public static class PingMetadata {
|
||||
// offset, us
|
||||
public long offset;
|
||||
// outgoing count
|
||||
public long pingsSent;
|
||||
// incoming count
|
||||
public long pongsReceived;
|
||||
// when we last heard back from the server, uS, in local time base
|
||||
public long lastPongTime;
|
||||
// RTT2, time from ping send to pong receive at the client, uS
|
||||
public long rtt2;
|
||||
|
||||
public PingMetadata(
|
||||
long offset, long pingsSent, long pongsReceived, long lastPongTime, long rtt2) {
|
||||
this.offset = offset;
|
||||
this.pingsSent = pingsSent;
|
||||
this.pongsReceived = pongsReceived;
|
||||
this.lastPongTime = lastPongTime;
|
||||
this.rtt2 = rtt2;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "PingMetadata [offset="
|
||||
+ offset
|
||||
+ ", pingsSent="
|
||||
+ pingsSent
|
||||
+ ", pongsReceived="
|
||||
+ pongsReceived
|
||||
+ ", lastPongTime="
|
||||
+ lastPongTime
|
||||
+ ", rtt2="
|
||||
+ rtt2
|
||||
+ "]";
|
||||
}
|
||||
|
||||
/**
|
||||
* How long, in us, since we last heard back from the server
|
||||
*
|
||||
* @return Time between last pong RX and now, or Long.MAX_VALUE if we have heard zero pongs
|
||||
*/
|
||||
public long timeSinceLastPong() {
|
||||
// If no pongs, it's been forever
|
||||
if (pongsReceived < 1) {
|
||||
return Long.MAX_VALUE;
|
||||
}
|
||||
|
||||
return NetworkTablesJNI.now() - lastPongTime;
|
||||
}
|
||||
}
|
||||
|
||||
private final Object mutex = new Object();
|
||||
|
||||
private long handle;
|
||||
private String server;
|
||||
private int port;
|
||||
private double interval;
|
||||
|
||||
public TimeSyncClient(String server, int port, double interval) {
|
||||
this.server = server;
|
||||
this.port = port;
|
||||
this.interval = interval;
|
||||
|
||||
this.handle = TimeSyncClient.create(server, port, interval);
|
||||
TimeSyncClient.start(handle);
|
||||
}
|
||||
|
||||
public void setServer(String newServer) {
|
||||
if (!server.equals(newServer)) {
|
||||
synchronized (mutex) {
|
||||
stop();
|
||||
this.handle = TimeSyncClient.create(newServer, port, interval);
|
||||
TimeSyncClient.start(handle);
|
||||
this.server = newServer;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void stop() {
|
||||
synchronized (mutex) {
|
||||
if (handle > 0) {
|
||||
TimeSyncClient.stop(handle);
|
||||
handle = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This offset, when added to the current value of nt::now(), yields the timestamp in the timebase
|
||||
* of the TSP Server
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
public long getOffset() {
|
||||
synchronized (mutex) {
|
||||
if (handle > 0) {
|
||||
return TimeSyncClient.getOffset(handle);
|
||||
}
|
||||
|
||||
System.err.println("TimeSyncClient: use after free?");
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Best estimate of the current timestamp at the TSP server
|
||||
*
|
||||
* @return The current time estimate, in microseconds, at the TSP server
|
||||
*/
|
||||
public long currentServerTimestamp() {
|
||||
return NetworkTablesJNI.now() + getOffset();
|
||||
}
|
||||
|
||||
public PingMetadata getPingMetadata() {
|
||||
synchronized (mutex) {
|
||||
if (handle > 0) {
|
||||
return TimeSyncClient.getLatestMetadata(handle);
|
||||
}
|
||||
|
||||
System.err.println("TimeSyncClient: use after free?");
|
||||
return new PingMetadata(0, 0, 0, 0, 0);
|
||||
}
|
||||
}
|
||||
|
||||
public String getServer() {
|
||||
return server;
|
||||
}
|
||||
|
||||
private static native long create(String serverIP, int serverPort, double pingIntervalSeconds);
|
||||
|
||||
private static native void start(long handle);
|
||||
|
||||
private static native void stop(long handle);
|
||||
|
||||
private static native long getOffset(long handle);
|
||||
|
||||
private static native PingMetadata getLatestMetadata(long handle);
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
/*
|
||||
* 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.jni;
|
||||
|
||||
public class TimeSyncServer {
|
||||
private final Object mutex = new Object();
|
||||
private long handle;
|
||||
|
||||
public TimeSyncServer(int port) {
|
||||
this.handle = TimeSyncServer.create(port);
|
||||
}
|
||||
|
||||
public void start() {
|
||||
synchronized (mutex) {
|
||||
if (handle > 0) {
|
||||
TimeSyncServer.start(handle);
|
||||
} else {
|
||||
System.err.println("TimeSyncServer: use after free?");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void stop() {
|
||||
if (handle > 0) {
|
||||
TimeSyncServer.stop(handle);
|
||||
handle = 0;
|
||||
}
|
||||
}
|
||||
|
||||
private static native long create(int port);
|
||||
|
||||
private static native void start(long handle);
|
||||
|
||||
private static native void stop(long handle);
|
||||
}
|
||||
@@ -22,24 +22,30 @@ import org.photonvision.struct.PhotonPipelineMetadataSerde;
|
||||
import org.photonvision.targeting.serde.PhotonStructSerializable;
|
||||
|
||||
public class PhotonPipelineMetadata implements PhotonStructSerializable<PhotonPipelineMetadata> {
|
||||
// Mirror of the heartbeat entry -- monotonically increasing
|
||||
public long sequenceID;
|
||||
|
||||
// Image capture and NT publish timestamp, in microseconds and in the
|
||||
// coprocessor timebase. As
|
||||
// reported by WPIUtilJNI::now.
|
||||
// Image capture and NT publish timestamp, in microseconds
|
||||
// The timebase is nt::Now on the time sync server
|
||||
public long captureTimestampMicros;
|
||||
public long publishTimestampMicros;
|
||||
|
||||
// Mirror of the heartbeat entry -- monotonically increasing
|
||||
public long sequenceID;
|
||||
|
||||
// Time from last Time Sync Pong received and the construction of this metadata, in uS
|
||||
public long timeSinceLastPong;
|
||||
|
||||
public PhotonPipelineMetadata(
|
||||
long captureTimestampMicros, long publishTimestampMicros, long sequenceID) {
|
||||
long captureTimestampMicros,
|
||||
long publishTimestampMicros,
|
||||
long sequenceID,
|
||||
long timeSinceLastPong) {
|
||||
this.captureTimestampMicros = captureTimestampMicros;
|
||||
this.publishTimestampMicros = publishTimestampMicros;
|
||||
this.sequenceID = sequenceID;
|
||||
this.timeSinceLastPong = timeSinceLastPong;
|
||||
}
|
||||
|
||||
public PhotonPipelineMetadata() {
|
||||
this(-1, -1, -1);
|
||||
this(-1, -1, -1, Long.MAX_VALUE);
|
||||
}
|
||||
|
||||
/** Returns the time between image capture and publish to NT */
|
||||
@@ -67,12 +73,14 @@ public class PhotonPipelineMetadata implements PhotonStructSerializable<PhotonPi
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "PhotonPipelineMetadata [sequenceID="
|
||||
+ sequenceID
|
||||
+ ", captureTimestampMicros="
|
||||
return "PhotonPipelineMetadata [captureTimestampMicros="
|
||||
+ captureTimestampMicros
|
||||
+ ", publishTimestampMicros="
|
||||
+ publishTimestampMicros
|
||||
+ ", sequenceID="
|
||||
+ sequenceID
|
||||
+ ", timeSinceLastPong="
|
||||
+ timeSinceLastPong
|
||||
+ "]";
|
||||
}
|
||||
|
||||
@@ -80,9 +88,10 @@ public class PhotonPipelineMetadata implements PhotonStructSerializable<PhotonPi
|
||||
public int hashCode() {
|
||||
final int prime = 31;
|
||||
int result = 1;
|
||||
result = prime * result + (int) (sequenceID ^ (sequenceID >>> 32));
|
||||
result = prime * result + (int) (captureTimestampMicros ^ (captureTimestampMicros >>> 32));
|
||||
result = prime * result + (int) (publishTimestampMicros ^ (publishTimestampMicros >>> 32));
|
||||
result = prime * result + (int) (sequenceID ^ (sequenceID >>> 32));
|
||||
result = prime * result + (int) (timeSinceLastPong ^ (timeSinceLastPong >>> 32));
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -92,9 +101,10 @@ public class PhotonPipelineMetadata implements PhotonStructSerializable<PhotonPi
|
||||
if (obj == null) return false;
|
||||
if (getClass() != obj.getClass()) return false;
|
||||
PhotonPipelineMetadata other = (PhotonPipelineMetadata) obj;
|
||||
if (sequenceID != other.sequenceID) return false;
|
||||
if (captureTimestampMicros != other.captureTimestampMicros) return false;
|
||||
if (publishTimestampMicros != other.publishTimestampMicros) return false;
|
||||
if (sequenceID != other.sequenceID) return false;
|
||||
if (timeSinceLastPong != other.timeSinceLastPong) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user