Compare commits

...

25 Commits

Author SHA1 Message Date
Matt M
d9ada4c26c dont download 2024-11-07 11:31:50 -08:00
Matt M
535e5d02f9 oops 2024-11-07 11:20:10 -08:00
Matt M
38f40bf03d oops 2024-11-07 11:19:22 -08:00
Matt M
7cc491536b oops 2024-11-07 11:15:31 -08:00
Matt M
66ccc35840 kill all other workflows 2024-11-07 11:14:43 -08:00
Matt M
812dc61b33 asdf 2024-11-07 11:12:39 -08:00
Matt M
dbbdc14c7c Fix TSP table 2024-11-06 13:34:33 -08:00
Matt
cf73f981b7 Publish vendor JSON in releases 2024-11-06 13:34:33 -08:00
Kouyang07
a99a8e750b Fixed Python code block being in C++ block (#1527) 2024-11-06 12:41:13 -05:00
William Toth
a0b22cd8a3 Update docs to specify that WPILib JDK is required on Windows (#1522) 2024-11-04 23:27:49 -05:00
Cameron (3539)
5d55d215ec Another config matching bug (#1518)
This is quite an odd issue/fix. 

So this is what happened... Photonvision booted with the camera
connected and the camera was working...
After a short time the camera stopped working (for some reason maybe
static, maybe temp, maybe wiring, idk).
During this time pv showed

Jul 04 06:25:18 BackLeft java[643]: [2024-07-04 06:25:18] [CSCore -
PvCSCoreLogger] [ERROR] CS: ERROR 40: ioctl VIDIOC_QBUF failed at
UsbCameraImpl.cpp:723: Invalid argument (UsbUtil.cpp:156)
Jul 04 06:25:18 BackLeft java[643]: [2024-07-04 06:25:18] [CSCore -
PvCSCoreLogger] [WARN] CS: WARNING 30: BackLeft: could not queue buffer
0 (UsbCameraImpl.cpp:724)

I went over and played with the wire. The camera fully disconnected but
it ended up "reconnecting"
When the camera was "reconnected" photonvision detected a "new camera"
except this time with no otherpaths (aka no usb path, or by id path).
That resulted in pv creating a new camera configuration for a camera
with no otherpaths
Cscore then started to report errors that look like it attempted to
connect to the same camera twice

This fixes it by filtering out USB cameras that have no otherpath on
linux.
2024-11-04 21:50:18 -05:00
Craig Schardt
625dacb020 Add QuadThresholdParameters to AprilTag config (#1519)
This works around a change made to the default QuadThresholdParameters in the WPILib AprilTagDetector for 2025.
https://github.com/wpilibsuite/allwpilib/pull/6847
2024-11-03 21:53:53 -06:00
Matt
fc8ecac376 Create TSP Server in C++ photonlib (#1516)
Automatically starts a TCP server in C++. Also adds warnings to Python.
2024-11-01 23:32:38 -07:00
Jade
75e2498f53 Fix typos (#1508)
Signed-off-by: Jade Turner <spacey-sooty@proton.me>
2024-11-01 23:51:16 -04:00
Matt
7a4ea3dd56 Assert that version checking won't throw on startup (#1512)
# Overview

Previously if the coproc came up later, getProperty would return the
string literal "null", which made us print the BFW. Add tests to make
sure that we don't do that anymore by rebooting a sim coproc +
robot in a combination of different orders.
2024-11-01 23:50:21 -04:00
Jade
5e1a93950e Fix photon-targetting being a seperate project (#1504) 2024-10-31 22:23:52 -07:00
Jade
380546cee0 Remove nonsensical settings.gradles (#1506) 2024-10-31 22:23:12 -07:00
Cameron (3539)
d7a7610917 Fix videomode is null (#1513)
There is a weird edge case at least with arducam/broken arducams/used
arducams where cscore will see it when pv starts but not be able to
connect to it. If we always read out the "current" video mode instead of
null when it is disconnected things will work. If the camera is
disconnected while we try to change the video mode when we get the
current video mode it will tell us what we wanted to set it to. Then
when the camera reconnects it will be in that video mode.
2024-10-31 23:13:36 -04:00
Matt
37aaa49b32 Create timesync JNI for testing client (#1433) 2024-10-31 08:27:19 -07:00
Cameron (3539)
937bafa8e2 Bump to WPILib 2025 Beta 1 & remove C++ protobuf (#1484)
Remove C++ protobuf support until
https://github.com/wpilibsuite/allwpilib/issues/7250 is addressed.
Developers should upgrade to wpilib vscode 2025 beta 1.

---------

Co-authored-by: Matt <matthew.morley.ca@gmail.com>
2024-10-31 02:59:39 -04:00
Matt
3d18ded3f6 Link to wpilib javadocs in ours (#1509)
![image](https://github.com/user-attachments/assets/d197b637-bf52-4a03-bf55-32a45fff8b06)
2024-10-29 17:11:53 -07:00
Jade
daa5842fb5 Remove explicit NativeUtils specification (#1495) 2024-10-28 09:18:12 -07:00
Emmy Chow
6f52267c26 Install script improvements (#1456) 2024-10-27 15:07:28 -07:00
Craig Schardt
acbae88d34 Reduce log spam if network monitor fails (#1494)
This prevents spamming of the logs by the network interface device
monitor by:

- checking to make sure the device file exists before starting the
monitoring task
- only logging once if it throws an exception, but keep trying in case
the exception is transient
2024-10-27 16:33:14 -05:00
42
986c7020c3 docs: update link to PhotonVision running examples (#1493) 2024-10-26 15:15:34 -07:00
182 changed files with 3189 additions and 2216 deletions

View File

@@ -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 }}"}'

View File

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

View File

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

View File

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

View File

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

View File

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

View 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

View File

@@ -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";

View File

@@ -37,6 +37,7 @@ extensions = [
"sphinx_design",
"myst_parser",
"sphinx.ext.mathjax",
"sphinx.ext.graphviz",
]
# Configure OpenGraph support

View File

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

View File

@@ -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:**

View File

@@ -3,4 +3,5 @@
```{toctree}
:maxdepth: 1
image-rotation
time-sync
```

View 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 Cristians 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.

View File

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

View File

@@ -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!
```

View File

@@ -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!
```

View File

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

View File

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

View File

@@ -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[];
}

View File

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

View File

@@ -1 +0,0 @@
rootProject.name = 'photon-core'

View File

@@ -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="

View File

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

View File

@@ -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) {

View File

@@ -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);

View File

@@ -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();
}
}

View File

@@ -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);
}
}
}

View File

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

View File

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

View File

@@ -25,4 +25,5 @@ public enum LogGroup {
General,
Config,
CSCore,
NetworkTables,
}

View File

@@ -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);
}

View File

@@ -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);
}
}

View File

@@ -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")

View File

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

View File

@@ -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());
}
/**

View File

@@ -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);
}

View File

@@ -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);
}

View File

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

View File

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

View File

@@ -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());

View File

@@ -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;
}
}

View File

@@ -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);

View File

@@ -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();

View File

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

View File

@@ -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;
}

View File

@@ -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) {}

View File

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

View File

@@ -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());

View File

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

View File

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

View File

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

View File

@@ -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())

View File

@@ -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[?];"

View File

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

View File

@@ -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":

View File

@@ -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[?];"

View File

@@ -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;"

View File

@@ -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;"

View File

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

View File

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

View File

@@ -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",

View File

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

View File

@@ -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);
}
}

View File

@@ -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;
}
}

View File

@@ -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. */

View File

@@ -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;
}
}

View File

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

View File

@@ -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) {

View File

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

View File

@@ -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();
}
}

View File

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

View 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);
}
}

View File

@@ -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(),

View File

@@ -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());
}
}
}
}

View 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();
}

View File

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

View File

@@ -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;
}

View File

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

View File

@@ -7,6 +7,8 @@
type: int64
- name: publishTimestampMicros
type: int64
- name: timeSinceLastPong
type: int64
- name: Transform3d
shimmed: True

View File

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

View File

@@ -1 +0,0 @@
rootProject.name = 'photon-server'

View File

@@ -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");

View File

@@ -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")

View File

@@ -1,6 +0,0 @@
pluginManagement {
repositories {
mavenLocal()
gradlePluginPortal()
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}

View File

@@ -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
};
}

View File

@@ -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>(),
}};
}

View File

@@ -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);

View File

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

View File

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

View File

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

View File

@@ -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);

View File

@@ -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);

View File

@@ -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("\"", "");
}

View File

@@ -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) {

View File

@@ -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;
}
}

View File

@@ -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);
}

View File

@@ -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);
}

View File

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