Compare commits

...

23 Commits

Author SHA1 Message Date
Gold856
a585a1d339 Fix incorrect hostname on non-managed devices (#2203)
I have all sorts of weird adapters from npcap and Hyper-V that don't
have MAC addresses, so I'm simplifying the logic down so that it always
tries to find _any_ adapter with a MAC address, but attempts to see if
it can find what adapter is in use right now and use the MAC address
from that. This also unpublishes old MAC address topics, which wasn't
done before.
2025-12-12 00:40:34 -06:00
Sam Freund
9490c2f2cd Bump wpilib to 2026 beta (#2192)
## Description

<!-- What changed? Why? (the code + comments should speak for itself on
the "how") -->

<!-- Fun screenshots or a cool video or something are super helpful as
well. If this touches platform-specific behavior, this is where test
evidence should be collected. -->

<!-- Any issues this pull request closes or pull requests this
supersedes should be linked with `Closes #issuenumber`. -->

Bump to wpilib 2026 beta. This does not bump our pythonlib, as robotpy
hasn't come out yet.

## Meta

Merge checklist:
- [ ] Pull Request title is [short, imperative
summary](https://cbea.ms/git-commit/) of proposed changes
- [ ] The description documents the _what_ and _why_
- [ ] This PR has been
[linted](https://docs.photonvision.org/en/latest/docs/contributing/linting.html).
- [ ] If this PR changes behavior or adds a feature, user documentation
is updated
- [ ] If this PR touches photon-serde, all messages have been
regenerated and hashes have not changed unexpectedly
- [ ] If this PR touches configuration, this is backwards compatible
with settings back to v2025.3.2
- [ ] If this PR touches pipeline settings or anything related to data
exchange, the frontend typing is updated
- [ ] If this PR addresses a bug, a regression test for it is added

---------

Co-authored-by: Gold856 <117957790+Gold856@users.noreply.github.com>
2025-12-11 23:53:54 -06:00
Alan Everett
467f22bfdc Use diozero for GPIO (#2171)
The old GPIO abstraction was replaced with
[`diozero`](https://www.diozero.com), which supports most hardware
running Linux due to its use of GPIO character devices provided by the
Linux kernel. `diozero` also supports [alternate
providers](https://www.diozero.com/concepts/providers.html#providers) if
for some reason the character device API is insufficient. Certain
capabilities outside of the character device API is also implemented for
common hardware.

Custom GPIO commands are implemented via a custom `diozero` provider.
The configuration for custom GPIO will need manually updated according
to the Hardware Config documentation page.

The status LED class was also reworked to support additional statuses or
LED indication types, although none have been added yet.

This was tested on a RPi 5 with LL3 illumination LEDs and an RGB status
led attached. All capabilities worked as expected. All 8 status LED
colors were tested and functional via modifying the code. Basic
functionality of custom GPIO was tested with dummy commands.
2025-12-11 22:28:18 -06:00
Gold856
c2433e0332 Fix Jackson being unable to deserialize neural network config (#2232)
## Description

#2224 removed the custom deserializers for `Path`, but we still need one
to be able to deserialize the `Path` key in
`NeuralNetworkPropertyManager`. Additionally, Jackson seems to
auto-convert the `Path` key to a `String` using `toString` instead of
its own serializers, so a custom key serializer is also needed to
consistently use the same format for paths. This also removes unused
serde methods in `JacksonUtils` to minimize potential future churn, and
tacks an `@JsonIgnore` on `getModels` to prevent Jackson from
serializing a `ModelProperty` array into the database.

## Meta

Merge checklist:
- [x] Pull Request title is [short, imperative
summary](https://cbea.ms/git-commit/) of proposed changes
- [x] The description documents the _what_ and _why_
- [ ] If this PR changes behavior or adds a feature, user documentation
is updated
- [ ] If this PR touches photon-serde, all messages have been
regenerated and hashes have not changed unexpectedly
- [ ] If this PR touches configuration, this is backwards compatible
with settings back to v2025.3.2
- [ ] If this PR touches pipeline settings or anything related to data
exchange, the frontend typing is updated
- [x] If this PR addresses a bug, a regression test for it is added
2025-12-09 18:09:49 -05:00
Sam Freund
1bb05a0e3e Remove PhotonJNICommon in favor of CombinedRuntimeLoader (#2223)
## Description

PhotonJNICommon is just our implementation of combined runtime loader,
which we don't really need. This removes it and just uses
CombinedRuntimeLoader directly. This also fixes the issues introduced in
#2219, which lead to some of our JNIs not loading.

## Meta

Merge checklist:
- [x] Pull Request title is [short, imperative
summary](https://cbea.ms/git-commit/) of proposed changes
- [x] The description documents the _what_ and _why_
- [ ] If this PR changes behavior or adds a feature, user documentation
is updated
- [ ] If this PR touches photon-serde, all messages have been
regenerated and hashes have not changed unexpectedly
- [ ] If this PR touches configuration, this is backwards compatible
with settings back to v2025.3.2
- [ ] If this PR touches pipeline settings or anything related to data
exchange, the frontend typing is updated
- [x] If this PR addresses a bug, a regression test for it is added

---------

Co-authored-by: Matt M <matthew.morley.ca@gmail.com>
2025-12-09 08:39:41 +00:00
Sam Freund
0c4c310c66 Fix pipeline type when creating a new pipeline being off by one (#2229)
## Description

<!-- What changed? Why? (the code + comments should speak for itself on
the "how") -->

<!-- Fun screenshots or a cool video or something are super helpful as
well. If this touches platform-specific behavior, this is where test
evidence should be collected. -->

<!-- Any issues this pull request closes or pull requests this
supersedes should be linked with `Closes #issuenumber`. -->

This cherry-picks the bug fix from #2225, but not the tests as we're
having issues with those. #2225 will remain open for the tests.

https://github.com/PhotonVision/photonvision/pull/2204 fixed the
off-by-one error on the frontend, but again, because enums are
serialized with ordinal(), DataSocketHandler needed to be updated to
account for the indices shifting by one.

## Meta

Merge checklist:
- [x] Pull Request title is [short, imperative
summary](https://cbea.ms/git-commit/) of proposed changes
- [x] The description documents the _what_ and _why_
- [ ] If this PR changes behavior or adds a feature, user documentation
is updated
- [ ] If this PR touches photon-serde, all messages have been
regenerated and hashes have not changed unexpectedly
- [ ] If this PR touches configuration, this is backwards compatible
with settings back to v2025.3.2
- [ ] If this PR touches pipeline settings or anything related to data
exchange, the frontend typing is updated
- [ ] ~If this PR addresses a bug, a regression test for it is added~
Deferred

Co-authored-by: Gold856 <117957790+Gold856@users.noreply.github.com>
2025-12-08 20:49:18 -08:00
Matt Morley
98f4a8642f Add WireShark dissector (#2140)
## Description

<img width="1918" height="1030" alt="image"
src="https://github.com/user-attachments/assets/5af1a5ee-012d-461d-9162-2d4de6ad0c62"
/>

A wireshark dissector can be handy for quickly visualizing time sync
messages. See the docs for how to use this!

Full disclosure -- this dissector was generated by Claude 4.5, and I
spot-checked all the numbers for correctness. This seems like idiomatic
Lua to me, but I don't know Lua at all. I don't see a nice QOL thing
nobody else will use as being a tech debt concern.

## Meta

Merge checklist:
- [x] Pull Request title is [short, imperative
summary](https://cbea.ms/git-commit/) of proposed changes
- [x] The description documents the _what_ and _why_
- [ ] If this PR changes behavior or adds a feature, user documentation
is updated
- [ ] If this PR touches photon-serde, all messages have been
regenerated and hashes have not changed unexpectedly
- [ ] If this PR touches configuration, this is backwards compatible
with settings back to v2025.3.2
- [ ] If this PR touches pipeline settings or anything related to data
exchange, the frontend typing is updated
- [ ] If this PR addresses a bug, a regression test for it is added
2025-12-08 05:14:24 +00:00
Sam Freund
88d0f16e80 Bump image version to v2026.0.6 (#2228) 2025-12-07 22:14:18 -05:00
Gold856
6285f1ee24 Consistently serialize neural network data (#2224) 2025-12-07 17:45:14 -06:00
Craig Schardt
192e2a115c Use photon-image-runner to build images (#2210)
## Description

This PR switches image building from `pguyot/arm-runner-action` to
`PhotonVision/photon-image-runner`. The new runner uses native arm
runners on GitHub and is much faster than the emulation used by the
pguyot version.

Images from
a95914ca36
tested and working on:

- [x] RaspberryPi4b
- [x] OrangePi5
- [x] OrangePi5pro
- [x] RubikPi3

Closes #2191

<!-- Any issues this pull request closes or pull requests this
supersedes should be linked with `Closes #issuenumber`. -->

## Meta

Merge checklist:
- [x] Pull Request title is [short, imperative
summary](https://cbea.ms/git-commit/) of proposed changes
- [x] The description documents the _what_ and _why_
- [ ] If this PR changes behavior or adds a feature, user documentation
is updated
- [ ] If this PR touches photon-serde, all messages have been
regenerated and hashes have not changed unexpectedly
- [ ] If this PR touches configuration, this is backwards compatible
with settings back to v2024.3.1
- [ ] If this PR touches pipeline settings or anything related to data
exchange, the frontend typing is updated
- [ ] If this PR addresses a bug, a regression test for it is added
2025-12-07 22:46:23 +00:00
Gold856
1c802f5eca Restore original dashboard view behavior (#2227) 2025-12-07 15:59:07 -06:00
Gold856
d44d9fbbeb Prevent slider from going past bounds (#2222)
## Description

#1900 updated how the value was handled in pv-slider, and
unintentionally removed bounds protection. This restores bounds
protection.

Unfortunately, there is an edge case that might be rather difficult to
solve. If the slider is already at the min/max, you can enter a number
through the text field, and while the value won't actually update, the
text field keeps the entered value, likely because the model value
didn't change, and therefore, a rerender isn't triggered. However, this
is an edge case that I doubt many people will actually encounter, so we
should still ship this.

Fixes #2221.

## Meta

Merge checklist:
- [x] Pull Request title is [short, imperative
summary](https://cbea.ms/git-commit/) of proposed changes
- [x] The description documents the _what_ and _why_
- [ ] If this PR changes behavior or adds a feature, user documentation
is updated
- [ ] If this PR touches photon-serde, all messages have been
regenerated and hashes have not changed unexpectedly
- [ ] If this PR touches configuration, this is backwards compatible
with settings back to v2025.3.2
- [ ] If this PR touches pipeline settings or anything related to data
exchange, the frontend typing is updated
- [x] If this PR addresses a bug, a regression test for it is added
2025-12-07 09:49:14 -08:00
Gold856
9e45e8b80a Remove redundant photon-targeting JNI library load (#2220)
## Description

#2137 made it so `TestUtils.loadLibraries()` loads both WPILib and
photon-targeting, which is used in Main.java to load libraries, but it
left the separate photon-targeting load in Main. This removes the extra
library load, since it's covered by `TestUtils.loadLibraries()`.

## Meta

Merge checklist:
- [x] Pull Request title is [short, imperative
summary](https://cbea.ms/git-commit/) of proposed changes
- [x] The description documents the _what_ and _why_
- [ ] If this PR changes behavior or adds a feature, user documentation
is updated
- [ ] If this PR touches photon-serde, all messages have been
regenerated and hashes have not changed unexpectedly
- [ ] If this PR touches configuration, this is backwards compatible
with settings back to v2025.3.2
- [ ] If this PR touches pipeline settings or anything related to data
exchange, the frontend typing is updated
- [ ] If this PR addresses a bug, a regression test for it is added
2025-12-07 09:48:59 -08:00
Gold856
9d7222a19e Optimize UI CPU usage (#2168) 2025-12-06 22:35:49 -08:00
Matt Morley
674f6e2361 [NFC] Kill stupid while loop copy in PhotonJNICommon (#2219)
## Description

We can avoid copying files by chunks just using `Files.copy`. This
should be NFC, just makes the code cleaner

<!-- What changed? Why? (the code + comments should speak for itself on
the "how") -->

<!-- Fun screenshots or a cool video or something are super helpful as
well. If this touches platform-specific behavior, this is where test
evidence should be collected. -->

<!-- Any issues this pull request closes or pull requests this
supersedes should be linked with `Closes #issuenumber`. -->

## Meta

Merge checklist:
- [x] Pull Request title is [short, imperative
summary](https://cbea.ms/git-commit/) of proposed changes
- [ ] The description documents the _what_ and _why_
- [ ] If this PR changes behavior or adds a feature, user documentation
is updated
- [ ] If this PR touches photon-serde, all messages have been
regenerated and hashes have not changed unexpectedly
- [ ] If this PR touches configuration, this is backwards compatible
with settings back to v2025.3.2
- [ ] If this PR touches pipeline settings or anything related to data
exchange, the frontend typing is updated
- [ ] If this PR addresses a bug, a regression test for it is added
2025-12-06 16:51:14 -05:00
Gold856
5e830fae57 Update mrcal-java (#2214)
## Description

Updating to take advantage of the now independent mrcal-java (no longer
need to install SuiteSparse and friends!)

## Meta

Merge checklist:
- [x] Pull Request title is [short, imperative
summary](https://cbea.ms/git-commit/) of proposed changes
- [x] The description documents the _what_ and _why_
- [ ] If this PR changes behavior or adds a feature, user documentation
is updated
- [ ] If this PR touches photon-serde, all messages have been
regenerated and hashes have not changed unexpectedly
- [ ] If this PR touches configuration, this is backwards compatible
with settings back to v2025.3.2
- [ ] If this PR touches pipeline settings or anything related to data
exchange, the frontend typing is updated
- [ ] If this PR addresses a bug, a regression test for it is added

---------

Co-authored-by: Matt Morley <matthew.morley.ca@gmail.com>
2025-12-06 18:10:47 +00:00
Vasista Vovveti
16751e9ecd revert demo domain url (#2218) 2025-12-05 21:30:50 -06:00
Sam Freund
b7054f2a6f Update PR linting instructions (#2213)
## Description

Removes the checkbox for linting because it's easily invalidated by
later pushes. Also prints a link to the linting docs on CI failure.

## Meta

Merge checklist:
- [x] Pull Request title is [short, imperative
summary](https://cbea.ms/git-commit/) of proposed changes
- [x] The description documents the _what_ and _why_
- [ ] This PR has been
[linted](https://docs.photonvision.org/en/latest/docs/contributing/linting.html).
- [ ] If this PR changes behavior or adds a feature, user documentation
is updated
- [ ] If this PR touches photon-serde, all messages have been
regenerated and hashes have not changed unexpectedly
- [ ] If this PR touches configuration, this is backwards compatible
with settings back to v2025.3.2
- [ ] If this PR touches pipeline settings or anything related to data
exchange, the frontend typing is updated
- [ ] If this PR addresses a bug, a regression test for it is added
2025-12-05 16:50:05 -05:00
Gold856
3ab8bc0e5d Fix import for GlobalSettingsCard (#2217)
Co-authored-by: samfreund <samf.236@proton.me>
2025-12-05 19:36:45 +00:00
Sam Freund
ca0923e27f Rename NetworkingCard.vue to GlobalSettingsCard.vue (#2215)
## Description

In the UI, the title for this card is `Global Settings`. This more
closely follows the UI.

## Meta

Merge checklist:
- [x] Pull Request title is [short, imperative
summary](https://cbea.ms/git-commit/) of proposed changes
- [x] The description documents the _what_ and _why_
- [ ] This PR has been
[linted](https://docs.photonvision.org/en/latest/docs/contributing/linting.html).
- [ ] If this PR changes behavior or adds a feature, user documentation
is updated
- [ ] If this PR touches photon-serde, all messages have been
regenerated and hashes have not changed unexpectedly
- [ ] If this PR touches configuration, this is backwards compatible
with settings back to v2025.3.2
- [ ] If this PR touches pipeline settings or anything related to data
exchange, the frontend typing is updated
- [ ] If this PR addresses a bug, a regression test for it is added
2025-12-05 04:35:13 -05:00
Devon Doyle
369d10eb91 Fix button theming for custom themes (#2216) 2025-12-05 03:02:46 -06:00
Sam Freund
017b074eae Add playwright E2E tests (#2174) 2025-12-04 22:25:48 -06:00
Sam Freund
f821657d2b Use updated image metadata (#2209) 2025-11-29 18:33:21 -06:00
137 changed files with 2255 additions and 2389 deletions

View File

@@ -11,7 +11,6 @@
Merge checklist:
- [ ] Pull Request title is [short, imperative summary](https://cbea.ms/git-commit/) of proposed changes
- [ ] The description documents the _what_ and _why_
- [ ] This PR has been [linted](https://docs.photonvision.org/en/latest/docs/contributing/linting.html).
- [ ] If this PR changes behavior or adds a feature, user documentation is updated
- [ ] If this PR touches photon-serde, all messages have been regenerated and hashes have not changed unexpectedly
- [ ] If this PR touches configuration, this is backwards compatible with settings back to v2025.3.2

View File

@@ -10,7 +10,7 @@ concurrency:
cancel-in-progress: true
env:
IMAGE_VERSION: v2026.0.4
IMAGE_VERSION: v2026.0.6
jobs:
@@ -50,15 +50,65 @@ jobs:
distribution: temurin
- name: Install RoboRIO Toolchain
run: ./gradlew installRoboRioToolchain
- name: Delete duplicate toolchains
run: |
find ~/.gradle/cache/ -name *roborio-academic* -exec rm -rf {} +
du -h . | sort -h
if: matrix.os == 'ubuntu-22.04'
# Need to publish to maven local first, so that C++ sim can pick it up
- name: Publish photonlib to maven local
run: ./gradlew photon-targeting:publishtomavenlocal photon-lib:publishtomavenlocal -x check
- name: Build Java examples
working-directory: photonlib-java-examples
run: ./gradlew build
run: |
./gradlew build
./gradlew clean
- name: Build C++ examples
working-directory: photonlib-cpp-examples
run: ./gradlew build
run: |
./gradlew build
./gradlew clean
playwright-tests:
name: "Playwright E2E tests"
runs-on: ubuntu-22.04
needs: [validation]
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@v4
with:
java-version: 17
distribution: temurin
- name: Install pnpm
uses: pnpm/action-setup@v4
with:
version: 10
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 22
- name: Setup tests
working-directory: photon-client
run: |
pnpm install
pnpm test-setup
- name: Prebuild Gradle
run: ./gradlew photon-targeting:build photon-core:build photon-server:build -x check
- name: Run Playwright tests
working-directory: photon-client
run: pnpm test
- uses: actions/upload-artifact@v4
if: ${{ !cancelled() }}
with:
name: playwright-report
path: photon-client/playwright-report/
retention-days: 30
build-gradle:
name: "Gradle Build"
runs-on: ubuntu-22.04
@@ -84,8 +134,6 @@ jobs:
uses: actions/setup-node@v4
with:
node-version: 22
- name: Install mrcal deps
run: sudo apt-get update && sudo apt-get install -y libcholmod3 liblapack3 libsuitesparseconfig5
- name: Gradle Build
run: ./gradlew photon-targeting:build photon-core:build photon-server:build -x check
- name: Gradle Tests and Coverage
@@ -356,12 +404,6 @@ jobs:
- uses: actions/download-artifact@v4
with:
name: ${{ matrix.artifact-name }}
# On linux, install mrcal packages
- run: |
sudo apt-get update
sudo apt-get install --yes libcholmod3 liblapack3 libsuitesparseconfig5
if: ${{ (matrix.os) == 'ubuntu-24.04' }}
# and actually run the jar
- run: java -jar ${{ matrix.extraOpts }} *.jar --smoketest
if: ${{ (matrix.os) != 'windows-latest' }}
- run: ls *.jar | %{ Write-Host "Running $($_.Name)"; Start-Process "java" -ArgumentList "-jar `"$($_.FullName)`" --smoketest" -NoNewWindow -Wait; break }
@@ -374,35 +416,31 @@ jobs:
fail-fast: false
matrix:
include:
- os: ubuntu-24.04
artifact-name: LinuxArm64
image_suffix: RaspberryPi
- image_suffix: RaspberryPi
image_url: https://github.com/PhotonVision/photon-image-modifier/releases/download/$IMAGE_VERSION/photonvision_raspi.img.xz
cpu: cortex-a7
image_additional_mb: 0
extraOpts: -Djdk.lang.Process.launchMechanism=vfork
- image_suffix: rubikpi3
image_url: https://github.com/PhotonVision/photon-image-modifier/releases/download/$IMAGE_VERSION/photonvision_rubikpi3.tar.xz
root_location: 'offset=569376768'
- image_suffix: orangepi5
image_url: https://github.com/PhotonVision/photon-image-modifier/releases/download/$IMAGE_VERSION/photonvision_opi5.img.xz
runs-on: ${{ matrix.os }}
runs-on: ubuntu-24.04-arm
name: smoketest-${{ matrix.image_suffix }}
steps:
- uses: actions/download-artifact@v4
with:
name: jar-${{ matrix.artifact-name }}
name: jar-LinuxArm64
- uses: pguyot/arm-runner-action@v2
- uses: photonvision/photon-image-runner@HEAD
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
image_url: ${{ matrix.image_url }}
root_location: ${{ matrix.root_location || 'partition=2' }}
# our image better have java installed already
commands: |
java -jar ${{ matrix.extraOpts }} *.jar --smoketest
java -jar *.jar --smoketest
build-image:
needs: [build-package]
@@ -413,78 +451,73 @@ jobs:
fail-fast: false
matrix:
include:
- os: ubuntu-24.04
- os: ubuntu-24.04-arm
artifact-name: LinuxArm64
image_suffix: RaspberryPi
image_url: https://github.com/PhotonVision/photon-image-modifier/releases/download/$IMAGE_VERSION/photonvision_raspi.img.xz
cpu: cortex-a7
image_additional_mb: 0
- os: ubuntu-24.04
minimum_free_mb: 100
- os: ubuntu-24.04-arm
artifact-name: LinuxArm64
image_suffix: limelight2
image_url: https://github.com/PhotonVision/photon-image-modifier/releases/download/$IMAGE_VERSION/photonvision_limelight.img.xz
cpu: cortex-a7
image_additional_mb: 0
- os: ubuntu-24.04
minimum_free_mb: 100
- os: ubuntu-24.04-arm
artifact-name: LinuxArm64
image_suffix: limelight3
image_url: https://github.com/PhotonVision/photon-image-modifier/releases/download/$IMAGE_VERSION/photonvision_limelight3.img.xz
cpu: cortex-a7
image_additional_mb: 0
- os: ubuntu-24.04
minimum_free_mb: 100
- os: ubuntu-24.04-arm
artifact-name: LinuxArm64
image_suffix: limelight3G
image_url: https://github.com/PhotonVision/photon-image-modifier/releases/download/$IMAGE_VERSION/photonvision_limelight3g.img.xz
cpu: cortex-a7
image_additional_mb: 0
- os: ubuntu-24.04
minimum_free_mb: 100
- os: ubuntu-24.04-arm
artifact-name: LinuxArm64
image_suffix: limelight4
image_url: https://github.com/PhotonVision/photon-image-modifier/releases/download/$IMAGE_VERSION/photonvision_limelight4.img.xz
cpu: cortex-a76
image_additional_mb: 0
- os: ubuntu-24.04
minimum_free_mb: 100
- os: ubuntu-24.04-arm
artifact-name: LinuxArm64
image_suffix: luma_p1
image_url: https://github.com/PhotonVision/photon-image-modifier/releases/download/$IMAGE_VERSION/photonvision_luma_p1.img.xz
cpu: cortex-a76
image_additional_mb: 0
- os: ubuntu-24.04
minimum_free_mb: 100
- os: ubuntu-24.04-arm
artifact-name: LinuxArm64
image_suffix: orangepi5
image_url: https://github.com/PhotonVision/photon-image-modifier/releases/download/$IMAGE_VERSION/photonvision_opi5.img.xz
cpu: cortex-a8
image_additional_mb: 1024
- os: ubuntu-24.04
minimum_free_mb: 1024
- os: ubuntu-24.04-arm
artifact-name: LinuxArm64
image_suffix: orangepi5b
image_url: https://github.com/PhotonVision/photon-image-modifier/releases/download/$IMAGE_VERSION/photonvision_opi5b.img.xz
cpu: cortex-a8
image_additional_mb: 1024
- os: ubuntu-24.04
minimum_free_mb: 1024
- os: ubuntu-24.04-arm
artifact-name: LinuxArm64
image_suffix: orangepi5plus
image_url: https://github.com/PhotonVision/photon-image-modifier/releases/download/$IMAGE_VERSION/photonvision_opi5plus.img.xz
cpu: cortex-a8
image_additional_mb: 1024
- os: ubuntu-24.04
minimum_free_mb: 1024
- os: ubuntu-24.04-arm
artifact-name: LinuxArm64
image_suffix: orangepi5pro
image_url: https://github.com/PhotonVision/photon-image-modifier/releases/download/$IMAGE_VERSION/photonvision_opi5pro.img.xz
cpu: cortex-a8
image_additional_mb: 1024
- os: ubuntu-24.04
minimum_free_mb: 1024
- os: ubuntu-24.04-arm
artifact-name: LinuxArm64
image_suffix: orangepi5max
image_url: https://github.com/PhotonVision/photon-image-modifier/releases/download/$IMAGE_VERSION/photonvision_opi5max.img.xz
cpu: cortex-a8
image_additional_mb: 1024
- os: ubuntu-24.04
minimum_free_mb: 1024
- os: ubuntu-24.04-arm
artifact-name: LinuxArm64
image_suffix: rock5c
image_url: https://github.com/PhotonVision/photon-image-modifier/releases/download/$IMAGE_VERSION/photonvision_rock5c.img.xz
cpu: cortex-a8
image_additional_mb: 1024
minimum_free_mb: 1024
- os: ubuntu-24.04-arm
artifact-name: LinuxArm64
image_suffix: rubikpi3
image_url: https://github.com/PhotonVision/photon-image-modifier/releases/download/$IMAGE_VERSION/photonvision_rubikpi3.tar.xz
minimum_free_mb: 1024
root_location: 'offset=569376768'
shrink_image: 'no'
runs-on: ${{ matrix.os }}
name: "Build image - ${{ matrix.image_suffix }}"
@@ -497,64 +530,48 @@ jobs:
- uses: actions/download-artifact@v4
with:
name: jar-${{ matrix.artifact-name }}
- uses: pguyot/arm-runner-action@HEAD
- uses: photonvision/photon-image-runner@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
image_url: ${{ matrix.image_url }}
minimum_free_mb: ${{ matrix.minimum_free_mb }}
root_location: ${{ matrix.root_location || 'partition=2' }}
shrink_image: ${{ matrix.shrink_image || 'yes' }}
commands: |
chmod +x scripts/armrunner.sh
./scripts/armrunner.sh
- name: Compress image
# Compress the standard images
if: ${{ ! startsWith(matrix.image_suffix, 'rubik') }}
run: |
new_jar=$(realpath $(find . -name photonvision\*-linuxarm64.jar))
new_image_name=$(basename "${new_jar/.jar/_${{ matrix.image_suffix }}.img}")
mv ${{ steps.generate_image.outputs.image }} $new_image_name
sudo mv ${{ steps.generate_image.outputs.image }} $new_image_name
sudo xz -T 0 -v $new_image_name
- name: Tar built image
# Build the RubikPi3-specific tar file
if: ${{ startsWith(matrix.image_suffix, 'rubik') }}
run: |
new_jar=$(realpath $(find . -name photonvision\*-linuxarm64.jar))
new_image_name=$(basename "${new_jar/.jar/_${{ matrix.image_suffix }}.img}")
imagedir=$(dirname ${{ steps.generate_image.outputs.image }})
tardir=${new_image_name}
sudo mkdir --parents ${tardir}
sudo mv ${imagedir}/* ${tardir}/
sudo tar -I 'xz -T0' -cf ${new_image_name}.tar.xz ${tardir} --checkpoint=10000 --checkpoint-action=echo='%T'
- uses: actions/upload-artifact@v4
name: Upload image
with:
name: image-${{ matrix.image_suffix }}
path: photonvision*.xz
build-rubik-image:
needs: [build-package]
if: ${{ github.event_name != 'pull_request' }}
runs-on: ubuntu-24.04
name: "Build image - Rubik Pi 3"
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: actions/download-artifact@v4
with:
name: jar-LinuxArm64
- name: Generate image
run: |
wget https://raw.githubusercontent.com/PhotonVision/photon-image-modifier/refs/tags/$IMAGE_VERSION/mount_rubikpi3.sh
chmod +x mount_rubikpi3.sh
./mount_rubikpi3.sh https://github.com/PhotonVision/photon-image-modifier/releases/download/$IMAGE_VERSION/photonvision_rubikpi3.tar.xz /tmp/build/scripts/armrunner.sh
- name: Compress image
run: |
new_jar=$(realpath $(find . -name photonvision\*-linuxarm64.jar))
new_image_name=$(basename "${new_jar/.jar/_rubikpi3.img}")
mv photonvision_rubikpi3 $new_image_name
tar -I 'xz -T0' -cf ${new_image_name}.tar.xz $new_image_name --checkpoint=10000 --checkpoint-action=echo='%T'
- uses: actions/upload-artifact@v4
name: Upload image
with:
name: image-rubikpi3
path: photonvision*.xz
release:
needs: [build-photonlib-vendorjson, build-package, build-image, build-rubik-image, combine]
needs: [build-photonlib-vendorjson, build-package, build-image, combine]
runs-on: ubuntu-22.04
steps:
# Download all fat JARs

View File

@@ -35,7 +35,14 @@ jobs:
- name: Run
run: wpiformat
- name: Check output
run: git --no-pager diff --exit-code HEAD
run: |
set +e
git --no-pager diff --exit-code HEAD
exit_code=$?
if test "$exit_code" -ne "0"; then
echo "::error ::Linting failed. See https://docs.photonvision.org/en/latest/docs/contributing/linting.html"
exit $exit_code
fi
- name: Generate diff
run: git diff HEAD > wpiformat-fixes.patch
if: ${{ failure() }}
@@ -56,9 +63,15 @@ jobs:
with:
java-version: 17
distribution: temurin
- run: ./gradlew spotlessCheck
- run: |
set +e
./gradlew spotlessCheck
exit_code=$?
if test "$exit_code" -ne "0"; then
echo "::error ::Linting failed. See https://docs.photonvision.org/en/latest/docs/contributing/linting.html"
exit $exit_code
fi
name: Run spotless
client-lint-format:
name: "PhotonClient Lint and Formatting"
defaults:
@@ -80,6 +93,20 @@ jobs:
- name: Install Dependencies
run: pnpm i --frozen-lockfile
- name: Check Linting
run: pnpm run lint-ci
run: |
set +e
pnpm run lint-ci
exit_code=$?
if test "$exit_code" -ne "0"; then
echo "::error ::Linting failed. See https://docs.photonvision.org/en/latest/docs/contributing/linting.html"
exit $exit_code
fi
- name: Check Formatting
run: pnpm run format-ci
run: |
set +e
pnpm run format-ci
exit_code=$?
if test "$exit_code" -ne "0"; then
echo "::error ::Linting failed. See https://docs.photonvision.org/en/latest/docs/contributing/linting.html"
exit $exit_code
fi

8
.gitignore vendored
View File

@@ -146,5 +146,13 @@ networktables.json
photon-server/src/main/resources/web/*
node_modules
dist
dist-ssr
components.d.ts
photon-server/src/main/resources/web/index.html
# Playwright
photon-client/test-results/
photon-client/playwright-report/
photon-client/blob-report/
photon-client/playwright/.cache/
photon-client/playwright/.auth/

View File

@@ -17,7 +17,7 @@ If you are interested in contributing code or documentation to the project, plea
## Documentation
- Our main documentation page: [docs.photonvision.org](https://docs.photonvision.org)
- Photon UI demo: [http://photonvision.global/](http://photonvision.global/)
- Photon UI demo: [demo.photonvision.org](https://demo.photonvision.org)
- Javadocs: [javadocs.photonvision.org](https://javadocs.photonvision.org)
- C++ Doxygen [cppdocs.photonvision.org](https://cppdocs.photonvision.org)
@@ -58,14 +58,6 @@ PhotonVision uses the following additional out-of-source repositories for buildi
- Custom build of OpenCV with GStreamer/Protobuf/other custom flags: https://github.com/PhotonVision/thirdparty-opencv
- JNI code for aruco-nano: https://github.com/PhotonVision/aruconano-jni
## Additional packages
For now, using mrcal requires installing these additional packages on Linux systems:
```
sudo apt install libcholmod3 liblapack3 libsuitesparseconfig5
```
## Acknowledgments
PhotonVision was forked from [Chameleon Vision](https://github.com/Chameleon-Vision/chameleon-vision/). Thank you to everyone who worked on the original project.

View File

@@ -4,7 +4,7 @@ plugins {
id "cpp"
id "com.diffplug.spotless" version "6.24.0"
id "edu.wpi.first.wpilib.repositories.WPILibRepositoriesPlugin" version "2020.2"
id "edu.wpi.first.GradleRIO" version "2025.3.2"
id "edu.wpi.first.GradleRIO" version "2026.1.1-beta-1"
id 'edu.wpi.first.WpilibTools' version '1.3.0'
id 'com.google.protobuf' version '0.9.3' apply false
id 'edu.wpi.first.GradleJni' version '1.1.0'
@@ -32,16 +32,16 @@ ext.allOutputsFolder = file("$project.buildDir/outputs")
apply from: "versioningHelper.gradle"
ext {
wpilibVersion = "2025.3.2"
wpilibVersion = "2026.1.1-beta-1"
wpimathVersion = wpilibVersion
openCVYear = "2025"
openCVversion = "4.10.0-3"
javalinVersion = "6.7.0"
libcameraDriverVersion = "v2025.0.4"
rknnVersion = "dev-v2025.0.0-5-g666c0c6"
rubikVersion = "dev-v2025.1.0-6-g4a5e508"
frcYear = "2025"
mrcalVersion = "v2025.0.0";
libcameraDriverVersion = "dev-v2025.0.4-2-gc91d4b7"
rknnVersion = "dev-v2025.0.0-7-g83c1bf3"
rubikVersion = "dev-v2025.1.0-7-g39588a8"
frcYear = "2026beta"
mrcalVersion = "dev-v2025.0.0-2-g2adb187";
pubVersion = versionString

165
devTools/photon.lua Normal file
View File

@@ -0,0 +1,165 @@
-- PhotonVision Time Synchronization Protocol Dissector
-- Protocol runs on UDP port 5810
-- Reference: https://docs.photonvision.org/en/v2026.0.0-alpha-1/docs/contributing/design-descriptions/time-sync.html
photon_timesync_proto = Proto("photon_timesync", "PhotonVision Time Sync Protocol")
-- Protocol fields
local pf_version = ProtoField.uint8("photon_timesync.version", "Version", base.DEC)
local pf_message_id = ProtoField.uint8("photon_timesync.message_id", "Message ID", base.DEC, {
[0] = "Ping",
[1] = "Pong"
})
local pf_client_time = ProtoField.uint64("photon_timesync.client_time", "Client Time (μs)", base.DEC)
local pf_server_time = ProtoField.uint64("photon_timesync.server_time", "Server Time (μs)", base.DEC)
local pf_response_in = ProtoField.framenum("photon_timesync.response_in", "Response In Frame", base.NONE,
frametype.RESPONSE)
local pf_response_to = ProtoField.framenum("photon_timesync.response_to", "Response To Frame", base.NONE,
frametype.REQUEST)
local pf_response_time = ProtoField.relative_time("photon_timesync.response_time", "Response Time")
-- Register fields
photon_timesync_proto.fields = {
pf_version,
pf_message_id,
pf_client_time,
pf_server_time,
pf_response_in,
pf_response_to,
pf_response_time
}
-- Table to track ping/pong relationships
-- Key: client_time as string, Value: frame number of ping
local ping_table = {}
-- Table to store pong responses for pings
-- Key: ping frame number, Value: pong frame number
local pong_table = {}
-- Dissector function
function photon_timesync_proto.dissector(buffer, pinfo, tree)
-- Check if buffer has minimum length (TspPing = 10 bytes)
local length = buffer:len()
if length < 10 then
return 0
end
-- Set protocol column
pinfo.cols.protocol = photon_timesync_proto.name
-- Create protocol tree
local subtree = tree:add(photon_timesync_proto, buffer(), "PhotonVision Time Sync Protocol Data")
-- Parse version (1 byte)
local version = buffer(0, 1):uint()
subtree:add(pf_version, buffer(0, 1))
-- Parse message_id (1 byte)
local msg_id = buffer(1, 1):uint()
subtree:add(pf_message_id, buffer(1, 1))
-- Parse client_time (8 bytes, little-endian uint64)
local client_time = buffer(2, 8):le_uint64()
subtree:add_le(pf_client_time, buffer(2, 8))
-- Convert client_time to string for use as key
local client_time_key = tostring(client_time)
local frame_num = pinfo.number
-- Track relationships between ping and pong
if not pinfo.visited then
-- First pass: build the relationship tables
if msg_id == 1 then
-- This is a Ping - store it
ping_table[client_time_key] = frame_num
elseif msg_id == 2 then
-- This is a Pong - find matching Ping
local ping_frame = ping_table[client_time_key]
if ping_frame then
pong_table[ping_frame] = frame_num
end
end
end
-- Update info column and parse based on message type
if msg_id == 1 then
-- TspPing: version(1) + message_id(1) + client_time(8) = 10 bytes
pinfo.cols.info = string.format("Time Sync Ping (client_time: %s μs)", tostring(client_time))
-- Check if we have a response for this ping
local pong_frame = pong_table[frame_num]
if pong_frame then
local response_item = subtree:add(pf_response_in, pong_frame)
response_item:set_generated()
end
elseif msg_id == 2 then
-- TspPong: TspPing + server_time(8) = 18 bytes
pinfo.cols.info = "Time Sync Pong"
if length >= 18 then
local server_time = buffer(10, 8):le_uint64()
subtree:add_le(pf_server_time, buffer(10, 8))
pinfo.cols.info = string.format("Time Sync Pong (client: %s, server: %s μs)",
tostring(client_time), tostring(server_time))
-- Find the matching ping frame
local ping_frame = ping_table[client_time_key]
if ping_frame then
local request_item = subtree:add(pf_response_to, ping_frame)
request_item:set_generated()
-- Calculate response time if we can get the ping packet
local ping_time = pinfo.abs_ts - pinfo.rel_ts
-- Note: This is an approximation. For accurate timing, we'd need to
-- store the timestamp of the ping packet
end
end
else
pinfo.cols.info = string.format("Time Sync Unknown (ID: %d)", msg_id)
end
return length
end
-- Register dissector on UDP port 5810
local udp_port = DissectorTable.get("udp.port")
udp_port:add(5810, photon_timesync_proto)
-- Heuristic dissector function
local function heuristic_checker(buffer, pinfo, tree)
local length = buffer:len()
-- Check minimum length (TspPing = 10 bytes)
if length < 10 then
return false
end
local version = buffer(0, 1):uint()
local msg_id = buffer(1, 1):uint()
-- Check if this looks like our protocol
-- Version should be reasonable (0-10), message_id should be 1 or 2
if version <= 10 and (msg_id == 1 or msg_id == 2) then
-- Validate packet structure
if msg_id == 1 and length == 10 then
-- TspPing is exactly 10 bytes
photon_timesync_proto.dissector(buffer, pinfo, tree)
return true
elseif msg_id == 2 and length == 18 then
-- TspPong is exactly 18 bytes
photon_timesync_proto.dissector(buffer, pinfo, tree)
return true
end
end
return false
end
-- Register heuristic dissector
photon_timesync_proto:register_heuristic("udp", heuristic_checker)
-- Initialize function to reset tables on new capture
function photon_timesync_proto.init()
ping_table = {}
pong_table = {}
end

View File

@@ -3,7 +3,6 @@
"supportURL" : "https://limelightvision.io",
"ledPins" : [ 13, 18 ],
"ledsCanDim" : true,
"ledPWMRange" : [ 0, 100 ],
"ledPWMFrequency" : 30000,
"vendorFOV" : 75.76079874010732
}

View File

@@ -12,7 +12,7 @@ VERY Limited macOS support is available.
## Installing Java
PhotonVision requires a JDK installed and on the system path. JDK 17 is needed (different versions will not work). You may already have this if you have installed WPILib 2025+. If not, [download and install it from here](https://adoptium.net/temurin/releases?version=17).
PhotonVision requires a JDK installed and on the system path. JDK 17 is needed (different versions will not work). You may already have this if you have installed WPILib 2026+. If not, [download and install it from here](https://adoptium.net/temurin/releases?version=17).
:::{warning}
Using a JDK other than JDK17 will cause issues when running PhotonVision and is not supported.

View File

@@ -12,15 +12,7 @@ Bonjour provides more stable networking when using Windows PCs. Install [Bonjour
## Installing Java
PhotonVision requires a JDK installed and on the system path. **JDK 17 is needed. Windows Users must use the JDK that ships with WPILib.** [Download and install it from here.](https://github.com/wpilibsuite/allwpilib/releases/tag/v2025.3.2) Either ensure the only Java on your PATH is the WPILIB Java or specify it to gradle with `-Dorg.gradle.java.home=C:\Users\Public\wpilib\2025\jdk`:
```
> ./gradlew run "-Dorg.gradle.java.home=C:\Users\Public\wpilib\2025\jdk"
```
:::{warning}
Using a JDK other than WPILIB's JDK17 will cause issues when running PhotonVision and is not supported.
:::
PhotonVision requires a JDK installed and on the system path. **JDK 17 is needed.** You may already have it if you installed WPILib, but ensure that running `java -version` shows JDK 17. You will likely have to add WPILib's JDK to JAVA_HOME and the JDK's `bin` directory to PATH. If you do not have a JDK 17 install, [download and install it from here.](https://adoptium.net/temurin/releases?version=17)
## Downloading the Latest Stable Release of PhotonVision

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+. **Windows Users must use the JDK that ships with WPILib.** For other platforms, you can follow the instructions to install JDK 17 for your platform [here](https://bell-sw.com/pages/downloads/#jdk-17-lts).
This project requires Java Development Kit (JDK) 17 to be compiled. This is the same Java version that comes with WPILib for 2026+. **Windows Users must use the JDK that ships with WPILib.** For other platforms, you can follow the instructions to install JDK 17 for your platform [here](https://bell-sw.com/pages/downloads/#jdk-17-lts).
**Node JS:**

Binary file not shown.

After

Width:  |  Height:  |  Size: 512 KiB

View File

@@ -109,3 +109,13 @@ Clients may publish statistics to NetworkTables. If they do, they shall publish
| rtt2_us | Integer | The time in us from last complete (ping transmission to pong reception) |
PhotonVision has chosen to publish to the sub-table `/photonvision/.timesync/{DEVICE_HOSTNAME}`. Future implementations of this protocol may decide to implement this as a structured data type.
## Wireshark Dissector
![](images/wireshark.jpg)
A [WireShark dissector](https://raw.githubusercontent.com/PhotonVision/photonvision/refs/heads/main/devTools/photon.lua) created for Wireshark ~=4.6 can be used to inspect Time Syncronization messages. Copy the dissector to your Wireshark plugin directory (for me, that's `C:\Users\Me\AppData\Roaming\Wireshark\plugins`), and open the capture. Because TSP uses UDP Unicast, data must be collected on the coprocessor or robot processor using a command similar to:
```
sudo tcpdump -i any port 5810 -w tsp_capture.pcap
```

View File

@@ -8,7 +8,7 @@ By default, PhotonVision attempts to make minimal assumptions of the hardware it
## LED Support
For Raspberry-Pi based hardware, PhotonVision can use [PiGPIO](https://abyz.me.uk/rpi/pigpio/) to control IO pins. The mapping of which pins control which LED's is part of the hardware config. The pins are active-high: set high when LED's are commanded on, and set low when commanded off.
When running on Linux, PhotonVision can use [diozero](https://www.diozero.com) to control IO pins. The mapping of which pins control which LED's is part of the hardware config. The illumination LED pins are active-high: set high when LED's are commanded on, and set low when commanded off.
```{eval-rst}
.. tab-set-code::
@@ -16,14 +16,11 @@ For Raspberry-Pi based hardware, PhotonVision can use [PiGPIO](https://abyz.me.u
{
"ledPins" : [ 13 ],
"ledSetCommand" : "",
"ledsCanDim" : true,
"ledPWMRange" : [ 0, 100 ],
"ledPWMSetRange" : "",
"ledBrightnessRange" : [ 0, 100 ],
"ledPWMFrequency" : 0,
"ledDimCommand" : "",
"ledBlinkCommand" : "",
"statusRGBPins" : [ ],
"statusRGBActiveHigh" : false,
}
```
@@ -31,6 +28,49 @@ For Raspberry-Pi based hardware, PhotonVision can use [PiGPIO](https://abyz.me.u
No hardware boards with status RGB LED pins or non-dimming LED's have been tested yet. Please reach out to the development team if these features are desired, they can assist with configuration and testing.
:::
### GPIO Pinout
::::{tab-set}
:::{tab-item} Raspberry Pi
The following diagram shows the GPIO pin numbering of the 40-pin header on Raspberry Pi hardware, courtesy of [pinout.xyz](https://pinout.xyz). Compute modules use the pin numbering from their respective datasheet.
```{image} https://raw.githubusercontent.com/pinout-xyz/Pinout.xyz/master/resources/raspberry-pi-pinout.png
:alt: Raspberry Pi GPIO Pinout
```
:::
::::
### Custom GPIO
If your hardware does not support diozero's default provider, custom commands can be provided to interact with the GPIO lines. The examples below show what parameters are provided to each command, which can be used in any order or multiple times as needed.
```{eval-rst}
.. tab-set-code::
.. code-block:: json
{
"getGPIOCommand" : "getGPIO {p}",
"setGPIOCommand" : "setGPIO {p} {s}",
"setPWMCommand" : "setPWM {p} {v}",
"setPWMFrequencyCommand" : "setPWMFrequency {p} {f}",
"releaseGPIOCommand" : "releseGPIO {p}",
}
```
The following template strings are used to input parameters to the commands:
| Template | Parameter | Values |
| -------- | ---------- | ---------- |
| `{p}` | pin number | integers |
| `{s}` | state | true/false |
| `{v}` | value | 0.0-1.0 |
| `{f}` | frequency | integers |
If you were using custom LED commands from 2025 or earlier and still need custom GPIO commands, they can likely be copied over. `ledSetCommand` can be reused as `setGPIOCommand`. `ledDimCommand` can be reused with edits as `setPWMCommand`, replacing any occurrences of `{v}` with `$(awk 'BEGIN{ print int({v}*100) }')` if your command requires integer percentages.
## Hardware Interaction Commands
For Non-Raspberry-Pi hardware, users must provide valid hardware-specific commands for some parts of the UI interaction (including performance metrics, and executing system restarts).
@@ -101,14 +141,16 @@ Here is a complete example `hardwareConfig.json`:
"deviceLogoPath" : "",
"supportURL" : "https://www.youtube.com/watch?v=b-CvLWbfZhU",
"ledPins" : [2, 13],
"ledSetCommand" : "",
"ledsCanDim" : true,
"ledPWMRange" : [ 0, 100 ],
"ledPWMSetRange" : "",
"ledBrightnessRange" : [ 0, 100 ],
"ledPWMFrequency" : 0,
"ledDimCommand" : "",
"ledBlinkCommand" : "",
"statusRGBPins" : [ ],
"statusRGBActiveHigh" : false,
"getGPIOCommand" : "getGPIO {p}",
"setGPIOCommand" : "setGPIO {p} {s}",
"setPWMCommand" : "setPWM {p} {v}",
"setPWMFrequencyCommand" : "setPWMFrequency {p} {f}",
"releaseGPIOCommand" : "releseGPIO {p}",
"cpuTempCommand" : "",
"cpuMemoryCommand" : "",
"cpuUtilCommand" : "",

View File

@@ -8,7 +8,7 @@ If youre not using cameras in 3D mode, calibration is optional, but it can st
## Print the Calibration Target
- Downloaded from our [demo site](http://photonvision.global/#/cameras), or directly from your coprocessors cameras tab.
- Downloaded from our [demo site](https://demo.photonvision.org/#/cameras), or directly from your coprocessors cameras tab.
- Use the ChArUco calibration board:
- Board Type: ChAruCo
- Tag Family: 4x4

View File

@@ -11,8 +11,8 @@ A few issues make up the majority of support requests. Run through this checklis
- Even if there's a switch between your laptop and coprocessor, you'll still want a radio or router in the loop somehow.
- The FRC radio is the _only_ router we will officially support due to the innumerable variations between routers.
- (Raspberry Pi, Orange Pi & Limelight only) have you flashed the correct image, and is it [up to date](https://github.com/PhotonVision/photonvision/releases/latest)?
- Is your robot code using a **2025** version of WPILib, and is your coprocessor using the most up to date **2025** release?
- 2022, 2023, 2024, and 2025 versions of either cannot be mix-and-matched!
- Is your robot code using a **2026** version of WPILib, and is your coprocessor using the most up to date **2026** release?
- 2022, 2023, 2024, 2025, and 2026 versions of either cannot be mix-and-matched!
- Your PhotonVision version can be checked on the settings tab.
- Is your team number correctly set on the settings tab?
@@ -30,7 +30,7 @@ Please check that:
1\. You don't have the NetworkTables Server on (toggleable in the settings tab). Turn this off when doing work on a robot.
2\. You have your team number set properly in the settings tab.
3\. Your camera name in the `PhotonCamera` constructor matches the name in the UI.
4\. You are using the 2025 version of WPILib and RoboRIO image.
4\. You are using the 2026 version of WPILib and RoboRIO image.
5\. Your robot is on.
If all of the above are met and you still have issues, feel free to {ref}`contact us <index:contact us>` and provide the following information:

View File

@@ -1,8 +0,0 @@
node_modules
.DS_Store
dist
dist-ssr
# Editor directories and files
.idea
components.d.ts

View File

@@ -8,7 +8,7 @@ export default defineConfigWithVueTs(
vueTsConfigs.recommended,
skipFormattingConfig,
{
ignores: ["**/dist/**"]
ignores: ["**/dist/**", "playwright-report"]
},
{
//extends: ["js/recommended"],

View File

@@ -9,9 +9,13 @@
"build": "vite build",
"build-demo": "vite build --mode demo",
"lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix",
"format": "prettier --write src/",
"format": "prettier --write src/ tests/",
"lint-ci": "eslint . --max-warnings 0 --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts",
"format-ci": "prettier --check src/"
"format-ci": "prettier --check src/",
"test": "playwright test",
"test-ui": "playwright test --ui",
"test-setup": "playwright install --with-deps"
},
"dependencies": {
"@fontsource/prompt": "^5.2.6",
@@ -28,6 +32,7 @@
},
"devDependencies": {
"@eslint/js": "^9.31.0",
"@playwright/test": "^1.56.1",
"@types/node": "^22.15.14",
"@types/three": "^0.178.0",
"@vitejs/plugin-vue": "^6.0.0",

View File

@@ -0,0 +1,83 @@
import { defineConfig, devices } from "@playwright/test";
import path from "path";
/**
* Read environment variables from file.
* https://github.com/motdotla/dotenv
*/
// import dotenv from 'dotenv';
// import path from 'path';
// dotenv.config({ path: path.resolve(__dirname, '.env') });
/**
* See https://playwright.dev/docs/test-configuration.
*/
export default defineConfig({
globalSetup: "./tests/global-setup",
testDir: "./tests",
/* Run tests in files in parallel */
fullyParallel: false,
/* Fail the build on CI if you accidentally left test.only in the source code. */
forbidOnly: !!process.env.CI,
/* Retry on CI only */
retries: process.env.CI ? 2 : 0,
/* Opt out of parallel tests on CI. */
workers: 1,
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
reporter: "html",
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
use: {
/* Base URL to use in actions like `await page.goto('')`. */
baseURL: "http://localhost:5800",
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
trace: "on-first-retry"
},
/* Configure projects for major browsers */
projects: [
{
name: "chromium",
use: { ...devices["Desktop Chrome"] }
},
{
name: "firefox",
use: { ...devices["Desktop Firefox"] }
},
{
name: "webkit",
use: { ...devices["Desktop Safari"] }
},
/* Test against mobile viewports. */
// {
// name: 'Mobile Chrome',
// use: { ...devices['Pixel 5'] },
// },
// {
// name: 'Mobile Safari',
// use: { ...devices['iPhone 12'] },
// },
/* Test against branded browsers. */
{
name: "Microsoft Edge",
use: { ...devices["Desktop Edge"], channel: "msedge" }
}
// {
// name: 'Google Chrome',
// use: { ...devices['Desktop Chrome'], channel: 'chrome' },
// },
],
/* Run your local dev server before starting the tests */
webServer: {
command: process.platform == "win32" ? "" : "./" + "gradlew run",
url: "http://localhost:5800",
timeout: 300 * 1000,
reuseExistingServer: !process.env.CI,
cwd: path.normalize("../")
}
});

View File

@@ -45,6 +45,9 @@ importers:
'@eslint/js':
specifier: ^9.31.0
version: 9.31.0
'@playwright/test':
specifier: ^1.56.1
version: 1.56.1
'@types/node':
specifier: ^22.15.14
version: 22.15.14
@@ -430,6 +433,11 @@ packages:
resolution: {integrity: sha512-ROFF39F6ZrnzSUEmQQZUar0Jt4xVoP9WnDRdWwF4NNcXs3xBTLgBUDoOwW141y1jP+S8nahIbdxbFC7IShw9Iw==}
engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0}
'@playwright/test@1.56.1':
resolution: {integrity: sha512-vSMYtL/zOcFpvJCW71Q/OEGQb7KYBPAdKh35WNSkaZA75JlAO8ED8UN6GUNTm3drWomcbcqRPFqQbLae8yBTdg==}
engines: {node: '>=18'}
hasBin: true
'@rolldown/pluginutils@1.0.0-beta.19':
resolution: {integrity: sha512-3FL3mnMbPu0muGOCaKAhhFEYmqv9eTfPSJRJmANrCwtgK8VuxpsZDGK+m0LYAGoyO8+0j5uRe4PeyPDK1yA/hA==}
@@ -1019,6 +1027,11 @@ packages:
resolution: {integrity: sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==}
engines: {node: '>= 6'}
fsevents@2.3.2:
resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==}
engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
os: [darwin]
fsevents@2.3.3:
resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==}
engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
@@ -1247,6 +1260,16 @@ packages:
typescript:
optional: true
playwright-core@1.56.1:
resolution: {integrity: sha512-hutraynyn31F+Bifme+Ps9Vq59hKuUCz7H1kDOcBs+2oGguKkWTU50bBWrtz34OUWmIwpBTWDxaRPXrIXkgvmQ==}
engines: {node: '>=18'}
hasBin: true
playwright@1.56.1:
resolution: {integrity: sha512-aFi5B0WovBHTEvpM3DzXTUaeN6eN0qWnTkKx4NQaH4Wvcmc153PdaY2UBdSYKaGYw+UyWXSVyxDUg5DoPEttjw==}
engines: {node: '>=18'}
hasBin: true
postcss-selector-parser@6.1.2:
resolution: {integrity: sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==}
engines: {node: '>=4'}
@@ -1759,6 +1782,10 @@ snapshots:
'@pkgr/core@0.2.4': {}
'@playwright/test@1.56.1':
dependencies:
playwright: 1.56.1
'@rolldown/pluginutils@1.0.0-beta.19': {}
'@rollup/rollup-android-arm-eabi@4.40.2':
@@ -2399,6 +2426,9 @@ snapshots:
hasown: 2.0.2
mime-types: 2.1.35
fsevents@2.3.2:
optional: true
fsevents@2.3.3:
optional: true
@@ -2605,6 +2635,14 @@ snapshots:
optionalDependencies:
typescript: 5.8.3
playwright-core@1.56.1: {}
playwright@1.56.1:
dependencies:
playwright-core: 1.56.1
optionalDependencies:
fsevents: 2.3.2
postcss-selector-parser@6.1.2:
dependencies:
cssesc: 3.0.0

View File

@@ -25,8 +25,7 @@ const renderCompact = computed<boolean>(() => compact.value || !mdAndUp.value);
<template>
<v-navigation-drawer permanent :rail="renderCompact" color="sidebar">
<v-list nav color="primary">
<!-- List item for the heading; note that there are some tricks in setting padding and image width make things look right -->
<v-list-item :class="renderCompact ? 'pr-0 pl-0' : ''" style="display: flex; justify-content: center">
<v-list-item class="pr-0 pl-0" style="display: flex; justify-content: center">
<template #prepend>
<img v-if="!renderCompact" class="logo" src="@/assets/images/logoLarge.svg" alt="large logo" />
<img v-else class="logo" src="@/assets/images/logoSmallTransparent.svg" alt="small logo" />

View File

@@ -177,7 +177,7 @@ const wrappedCameras = computed<SelectItem[]>(() =>
<v-btn
block
size="small"
color="primary"
color="buttonActive"
:disabled="!settingsHaveChanged()"
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
@click="saveCameraSettings"

View File

@@ -27,7 +27,13 @@ function debounce(func: (...args: any[]) => void, wait: number) {
}
const debouncedEmit = debounce((v: number) => {
emit("update:modelValue", v);
if (v < props.min) {
emit("update:modelValue", props.min);
} else if (v > props.max) {
emit("update:modelValue", props.max);
} else {
emit("update:modelValue", v);
}
}, 20);
const localValue = computed({

View File

@@ -86,7 +86,7 @@ const renameModel = async (model: ObjectDetectionModelProperties, newName: strin
});
axiosPost("/objectdetection/rename", "rename an object detection model", {
modelPath: model.modelPath.replace("file:", ""),
modelPath: model.modelPath,
newName: newName
});
showRenameDialog.value.show = false;
@@ -224,6 +224,7 @@ const handleBulkImport = () => {
v-model="importVersion"
variant="underlined"
label="Model Version"
data-testid="import-version-select"
:items="
useSettingsStore().general.supportedBackends?.includes('RKNN')
? ['YOLOv5', 'YOLOv8', 'YOLO11']
@@ -324,7 +325,7 @@ const handleBulkImport = () => {
<th>Info</th>
</tr>
</thead>
<tbody>
<tbody data-testid="model-table">
<tr v-for="model in supportedModels" :key="model.modelPath">
<td>{{ model.nickname }}</td>
<td>{{ model.labels.join(", ") }}</td>
@@ -417,7 +418,7 @@ const handleBulkImport = () => {
<a
ref="exportIndividualModel"
style="color: black; text-decoration: none; display: none"
:href="`http://${address}/api/objectdetection/exportIndividual?modelPath=${showInfo.model.modelPath.replace('file:', '')}`"
:href="`http://${address}/api/objectdetection/exportIndividual?modelPath=${showInfo.model.modelPath}`"
:download="`${showInfo.model.nickname}_${showInfo.model.family}_${showInfo.model.version}_${showInfo.model.resolutionWidth}x${showInfo.model.resolutionHeight}_${showInfo.model.labels.join('_')}.${showInfo.model.family.toLowerCase()}`"
target="_blank"
/>

View File

@@ -52,11 +52,12 @@ export const restoreThemeConfig = (theme: ThemeInstance) => {
: (customSurface ?? defaultTheme.colors!.sidebar!);
theme.themes.value[theme.global.name.value].colors.primary = customPrimary ?? defaultTheme.colors!.primary!;
theme.themes.value[theme.global.name.value].colors.buttonActive = customPrimary ?? defaultTheme.colors!.buttonActive!;
theme.themes.value[theme.global.name.value].colors.buttonActive =
(themeType === "light" ? customPrimary : customSecondary) ?? defaultTheme.colors!.buttonActive!;
theme.themes.value[theme.global.name.value].colors.secondary = customSecondary ?? defaultTheme.colors!.secondary!;
theme.themes.value[theme.global.name.value].colors.buttonPassive =
customSecondary ?? defaultTheme.colors!.buttonPassive!;
(themeType === "light" ? customSecondary : customPrimary) ?? defaultTheme.colors!.buttonPassive!;
theme.themes.value[theme.global.name.value].colors.accent = customSecondary ?? defaultTheme.colors!.accent!;
theme.themes.value[theme.global.name.value].colors.toggle = customSecondary ?? defaultTheme.colors!.toggle!;

View File

@@ -27,10 +27,7 @@ export const useCameraSettingsStore = defineStore("cameraSettings", {
}),
getters: {
needsCameraConfiguration(): boolean {
return (
JSON.stringify(useCameraSettingsStore().cameras[PlaceholderCameraSettings.uniqueName]) ===
JSON.stringify(PlaceholderCameraSettings)
);
return useCameraSettingsStore().cameras["Placeholder Name"] === PlaceholderCameraSettings;
},
// TODO update types to update this value being undefined. This would be a decently large change.
currentCameraSettings(): UiCameraConfiguration {

View File

@@ -1,6 +1,7 @@
import { type ActivePipelineSettings, DefaultAprilTagPipelineSettings } from "@/types/PipelineTypes";
import type { Pose3d } from "@/types/PhotonTrackingTypes";
import type { WebsocketCameraSettingsUpdate } from "./WebsocketDataTypes";
import { reactive } from "vue";
export interface GeneralSettings {
version?: string;
@@ -274,7 +275,7 @@ export interface CameraSettingsChangeRequest {
quirksToChange: Record<ValidQuirks, boolean>;
}
export const PlaceholderCameraSettings: UiCameraConfiguration = {
export const PlaceholderCameraSettings: UiCameraConfiguration = reactive({
cameraPath: "/dev/null",
nickname: "Placeholder Camera",
@@ -391,7 +392,7 @@ export const PlaceholderCameraSettings: UiCameraConfiguration = {
isConnected: true,
hasConnected: true,
mismatch: false
};
});
export enum CalibrationBoardTypes {
Chessboard = 0,

View File

@@ -87,7 +87,7 @@ const unmatchedCameras = computed(() => {
const activeVisionModules = computed(() =>
Object.values(useCameraSettingsStore().cameras)
// Ignore placeholder camera
.filter((camera) => JSON.stringify(camera) !== JSON.stringify(PlaceholderCameraSettings))
.filter((camera) => camera !== PlaceholderCameraSettings)
// Display connected cameras first
.sort(
(first, second) =>

View File

@@ -64,10 +64,8 @@ const cameraMismatchWarningShown = computed<boolean>(() => {
return (
Object.values(useCameraSettingsStore().cameras)
// Ignore placeholder camera
.filter((camera) => JSON.stringify(camera) !== JSON.stringify(PlaceholderCameraSettings))
.some((camera) => {
return camera.mismatch;
})
.filter((camera) => camera !== PlaceholderCameraSettings)
.some((camera) => camera.mismatch)
);
});
@@ -148,6 +146,7 @@ const showCameraSetupDialog = ref(useCameraSettingsStore().needsCameraConfigurat
<PipelineConfigCard />
<!-- TODO - not sure this belongs here -->
<!-- Need v-model to allow the dialog to be dismissed and v-if to only display when cameras need configuration -->
<v-dialog
v-if="useCameraSettingsStore().needsCameraConfiguration"
v-model="showCameraSetupDialog"

View File

@@ -2,7 +2,7 @@
import MetricsCard from "@/components/settings/MetricsCard.vue";
import DeviceControlCard from "@/components/settings/DeviceControlCard.vue";
import ObjectDetectionCard from "@/components/settings/ObjectDetectionCard.vue";
import NetworkingCard from "@/components/settings/NetworkingCard.vue";
import GlobalSettingsCard from "@/components/settings/GlobalSettingsCard.vue";
import LightingControlCard from "@/components/settings/LEDControlCard.vue";
import { useSettingsStore } from "@/stores/settings/GeneralSettingsStore";
import ApriltagControlCard from "@/components/settings/ApriltagControlCard.vue";
@@ -12,7 +12,7 @@ import ApriltagControlCard from "@/components/settings/ApriltagControlCard.vue";
<div class="pa-3">
<MetricsCard />
<DeviceControlCard />
<NetworkingCard />
<GlobalSettingsCard />
<ObjectDetectionCard v-if="useSettingsStore().general.supportedBackends.length > 0" />
<LightingControlCard v-if="useSettingsStore().lighting.supported" />
<Suspense>

View File

@@ -0,0 +1,16 @@
import { test as base } from "@playwright/test";
import axios from "axios";
export const test = base.extend({
page: async ({ page }, use) => {
// Use the page in the test (no per-test backend reset here)
axios.defaults.baseURL = "http://localhost:5800/api/test";
await use(page);
}
});
test.beforeAll(async () => {
console.log("Running before all tests: Resetting backend state...");
await axios.post("http://localhost:5800/api/test/resetBackend");
await axios.post("http://localhost:5800/api/test/activateTestMode");
});

View File

@@ -0,0 +1,7 @@
async function globalSetup() {
// You can perform global setup tasks here, such as starting a server or setting environment variables
const path = await import("path");
process.env.TESTS_DIR = path.resolve(process.cwd());
}
export default globalSetup;

View File

@@ -0,0 +1,39 @@
import { expect } from "@playwright/test";
import { test } from "./fixtures.ts";
test("Camera Gain Slider won't go past max or min", async ({ page }) => {
await page.goto("http://localhost:5800/#/dashboard");
await page.locator("div").filter({ hasText: "Set up some cameras to get started!" }).nth(2).press("Escape");
// Fill in Camera Gain text field with 1000
await page.locator("#input-v-44").fill("1000");
await page.locator("#input-v-44").press("Enter");
await expect(page.locator("#input-v-44")).toHaveValue("100");
// Try using buttons to go past the max
await page.getByRole("button", { name: "appended action" }).nth(2).click();
await expect(page.locator("#input-v-44")).toHaveValue("100");
// Make sure the value is actually properly limited, not just visually
await page.getByRole("button", { name: "prepended action" }).nth(2).click();
await expect(page.locator("#input-v-44")).toHaveValue("99");
await page.locator("#input-v-44").fill("-10");
await page.locator("#input-v-44").press("Enter");
await expect(page.locator("#input-v-44")).toHaveValue("0");
await page.getByRole("button", { name: "prepended action" }).nth(2).click();
await expect(page.locator("#input-v-44")).toHaveValue("0");
// Make sure the value is actually properly limited, not just visually
await page.getByRole("button", { name: "appended action" }).nth(2).click();
await expect(page.locator("#input-v-44")).toHaveValue("1");
// Make sure that the guard actually prevents value setting, instead of just reverting the value
// This can be ensured by making sure the Camera Gain field doesn't disappear (disappears when the value is -1)
await page.getByRole("button", { name: "prepended action" }).nth(2).click();
await page.getByRole("button", { name: "prepended action" }).nth(2).click();
await expect(page.locator("#input-v-44")).toHaveValue("0");
await expect(page.getByText("Camera Gain", { exact: true })).toBeVisible();
});

View File

@@ -0,0 +1,79 @@
import { expect } from "@playwright/test";
import { test } from "../fixtures";
import axios from "axios";
import path from "path";
const fakeModelName = "FAKE-MODEL";
const fakeLabels = "test, 1, woof";
const newModelName = "foo-bar";
const platforms = ["LINUX_RK3588_64", "LINUX_QCS6490"];
for (const platform of platforms) {
test.describe(`Platform: ${platform}`, () => {
test.beforeEach(async ({ page }) => {
await page.goto("/#/settings");
await axios.post("/override/platform", { platform: platform });
await page.reload();
});
test("testSettingsPage", async ({ page }) => {
if (platform.endsWith("RK3588_64")) {
await expect(page.getByRole("main")).toContainText("Linux AARCH 64-bit with RK3588");
} else if (platform.endsWith("QCS6490")) {
await expect(page.getByRole("main")).toContainText("Linux AARCH 64-bit with QCS6490");
}
await expect(page.getByText("Object Detection")).toBeVisible();
});
test("Upload model", async ({ page }) => {
const testsDir = process.env.TESTS_DIR;
if (!testsDir) {
throw new Error("TESTS_DIR is not set");
}
await page.getByRole("button", { name: "Import Model" }).click();
await page.getByRole("textbox", { name: "Labels" }).fill(fakeLabels);
await page.getByRole("spinbutton", { name: "Width" }).fill("640");
await page.getByRole("spinbutton", { name: "Height" }).fill("640");
await page.getByTestId("import-version-select").click();
await page.getByRole("option", { name: "YOLOv8" }).click();
const modelFile = platform.endsWith("RK3588_64") ? `${fakeModelName}.rknn` : `${fakeModelName}.tflite`;
await page
.getByRole("button", { name: "Model File Model File" })
.setInputFiles(path.join(testsDir, "tests/resources", modelFile));
await page.getByRole("button", { name: "Import Object Detection Model" }).click();
await page.goto("/#/settings");
const tableRow = page.getByTestId("model-table").locator("tr", { hasText: fakeModelName });
await expect(tableRow).toBeVisible();
await expect(tableRow).toContainText(fakeLabels);
});
test("Rename model", async ({ page }) => {
const tableRow = page.getByTestId("model-table").locator("tr", { hasText: fakeModelName });
await tableRow.getByRole("button", { name: "Rename Model" }).click();
await page.getByRole("textbox", { name: "New Name New Name" }).fill(newModelName);
await page.getByRole("button", { name: "Rename", exact: true }).click();
await page.reload();
const renamedRow = page.getByTestId("model-table").locator("tr", { hasText: newModelName });
await expect(renamedRow).toContainText(fakeLabels);
});
test("Delete model", async ({ page }) => {
const tableRow = page.getByTestId("model-table").locator("tr", { hasText: newModelName });
await tableRow.getByRole("button", { name: "Delete Model" }).click();
await page.getByRole("button", { name: "Delete model", exact: true }).click();
await page.reload();
const deletedRow = page.getByTestId("model-table").locator("tr", { hasText: newModelName });
await expect(deletedRow).toHaveCount(0);
});
});
}

View File

@@ -28,25 +28,32 @@ dependencies {
wpilibNatives wpilibTools.deps.wpilib("hal")
wpilibNatives wpilibTools.deps.wpilibOpenCv("frc" + openCVYear, wpi.versions.opencvVersion.get())
// Zip
// These stay as implementation dependencies since they don't have native code that gets packaged
implementation 'org.zeroturnaround:zt-zip:1.14'
implementation "org.xerial:sqlite-jdbc:3.41.0.0"
implementation("org.photonvision:rknn_jni-jni:$rknnVersion:linuxarm64") {
transitive = false
implementation 'com.diozero:diozero-core:1.4.1'
// The JNI libraries use wpilibNatives, the java libraries use implementation
if (jniPlatform == "linuxarm64") {
wpilibNatives("org.photonvision:rknn_jni-jni:$rknnVersion:$wpilibNativeName") {
transitive = false
}
wpilibNatives("org.photonvision:rubik_jni-jni:$rubikVersion:$wpilibNativeName") {
transitive = false
}
wpilibNatives("org.photonvision:photon-libcamera-gl-driver-jni:$libcameraDriverVersion:$wpilibNativeName") {
transitive = false
}
}
implementation("org.photonvision:rknn_jni-java:$rknnVersion") {
transitive = false
}
implementation("org.photonvision:rubik_jni-jni:$rubikVersion:linuxarm64") {
transitive = false
}
implementation("org.photonvision:rubik_jni-java:$rubikVersion") {
transitive = false
}
implementation("org.photonvision:photon-libcamera-gl-driver-jni:$libcameraDriverVersion:linuxarm64") {
transitive = false
}
implementation "org.photonvision:photon-libcamera-gl-driver-java:$libcameraDriverVersion"
implementation "org.photonvision:photon-mrcal-java:$mrcalVersion"
@@ -56,7 +63,7 @@ dependencies {
"osxx86-64",
"osxarm64"
])) {
implementation("org.photonvision:photon-mrcal-jni:$mrcalVersion:$wpilibNativeName") {
wpilibNatives("org.photonvision:photon-mrcal-jni:$mrcalVersion:$wpilibNativeName") {
transitive = false
}
}

View File

@@ -0,0 +1,59 @@
/*
* Copyright (C) Photon Vision.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.photonvision.common;
import edu.wpi.first.util.CombinedRuntimeLoader;
import java.io.IOException;
import java.util.HashMap;
import org.photonvision.jni.LibraryLoader;
public class LoadJNI {
private static HashMap<JNITypes, Boolean> loadedMap = new HashMap<>();
public enum JNITypes {
RUBIK_DETECTOR("tensorflowlite", "tensorflowlite_c", "external_delegate", "rubik_jni"),
RKNN_DETECTOR("rga", "rknnrt", "rknn_jni"),
MRCAL("mrcal_jni"),
LIBCAMERA("photonlibcamera");
public final String[] libraries;
JNITypes(String... libraries) {
this.libraries = libraries;
}
}
public static synchronized void forceLoad(JNITypes type) throws IOException {
loadLibraries();
if (loadedMap.getOrDefault(type, false)) {
return;
}
CombinedRuntimeLoader.loadLibraries(LoadJNI.class, type.libraries);
loadedMap.put(type, true);
}
public static boolean loadLibraries() {
return LibraryLoader.loadWpiLibraries() && LibraryLoader.loadTargeting();
}
public static boolean hasLoaded(JNITypes t) {
return loadedMap.getOrDefault(t, false);
}
}

View File

@@ -28,12 +28,18 @@ public class HardwareConfig {
// LED control
public final ArrayList<Integer> ledPins;
public final String ledSetCommand;
public final boolean ledsCanDim;
public final ArrayList<Integer> ledBrightnessRange;
public final String ledDimCommand;
public final String ledBlinkCommand;
public final int ledPWMFrequency;
public final ArrayList<Integer> statusRGBPins;
public final boolean statusRGBActiveHigh;
// Custom GPIO
public final String getGPIOCommand;
public final String setGPIOCommand;
public final String setPWMCommand;
public final String setPWMFrequencyCommand;
public final String releaseGPIOCommand;
// Metrics
public final String cpuTempCommand;
@@ -55,12 +61,16 @@ public class HardwareConfig {
String deviceLogoPath,
String supportURL,
ArrayList<Integer> ledPins,
String ledSetCommand,
boolean ledsCanDim,
ArrayList<Integer> ledBrightnessRange,
String ledDimCommand,
String ledBlinkCommand,
int ledPwmFrequency,
ArrayList<Integer> statusRGBPins,
boolean statusRGBActiveHigh,
String getGPIOCommand,
String setGPIOCommand,
String setPWMCommand,
String setPWMFrequencyCommand,
String releaseGPIOCommand,
String cpuTempCommand,
String cpuMemoryCommand,
String cpuUtilCommand,
@@ -76,12 +86,16 @@ public class HardwareConfig {
this.deviceLogoPath = deviceLogoPath;
this.supportURL = supportURL;
this.ledPins = ledPins;
this.ledSetCommand = ledSetCommand;
this.ledsCanDim = ledsCanDim;
this.ledBrightnessRange = ledBrightnessRange;
this.ledDimCommand = ledDimCommand;
this.ledBlinkCommand = ledBlinkCommand;
this.ledPWMFrequency = ledPwmFrequency;
this.statusRGBPins = statusRGBPins;
this.statusRGBActiveHigh = statusRGBActiveHigh;
this.getGPIOCommand = getGPIOCommand;
this.setGPIOCommand = setGPIOCommand;
this.setPWMCommand = setPWMCommand;
this.setPWMFrequencyCommand = setPWMFrequencyCommand;
this.releaseGPIOCommand = releaseGPIOCommand;
this.cpuTempCommand = cpuTempCommand;
this.cpuMemoryCommand = cpuMemoryCommand;
this.cpuUtilCommand = cpuUtilCommand;
@@ -100,12 +114,16 @@ public class HardwareConfig {
deviceLogoPath = "";
supportURL = "";
ledPins = new ArrayList<>();
ledSetCommand = "";
ledsCanDim = false;
ledBrightnessRange = new ArrayList<>();
ledDimCommand = "";
ledBlinkCommand = "";
ledPWMFrequency = 0;
statusRGBPins = new ArrayList<>();
statusRGBActiveHigh = false;
getGPIOCommand = "";
setGPIOCommand = "";
setPWMCommand = "";
setPWMFrequencyCommand = "";
releaseGPIOCommand = "";
cpuTempCommand = "";
cpuMemoryCommand = "";
cpuUtilCommand = "";
@@ -127,7 +145,7 @@ public class HardwareConfig {
}
/**
* @return True if any command has been configured to a non-default empty, false otherwise
* @return True if any info command has been configured to be non-empty, false otherwise
*/
public final boolean hasCommandsConfigured() {
return cpuTempCommand != ""
@@ -137,11 +155,21 @@ public class HardwareConfig {
|| cpuUptimeCommand != ""
|| gpuMemoryCommand != ""
|| ramUtilCommand != ""
|| ledBlinkCommand != ""
|| gpuMemUsageCommand != ""
|| diskUsageCommand != "";
}
/**
* @return True if any gpio command has been configured to be non-empty, false otherwise
*/
public final boolean hasGPIOCommandsConfigured() {
return getGPIOCommand != ""
|| setGPIOCommand != ""
|| setPWMCommand != ""
|| setPWMFrequencyCommand != ""
|| releaseGPIOCommand != "";
}
@Override
public String toString() {
return "HardwareConfig[deviceName="
@@ -152,18 +180,26 @@ public class HardwareConfig {
+ supportURL
+ ", ledPins="
+ ledPins
+ ", ledSetCommand="
+ ledSetCommand
+ ", ledsCanDim="
+ ledsCanDim
+ ", ledBrightnessRange="
+ ledBrightnessRange
+ ", ledDimCommand="
+ ledDimCommand
+ ", ledBlinkCommand="
+ ledBlinkCommand
+ ", ledPWMFrequency="
+ ledPWMFrequency
+ ", statusRGBPins="
+ statusRGBPins
+ ", statusRGBActiveHigh"
+ statusRGBActiveHigh
+ ", getGPIOCommand="
+ getGPIOCommand
+ ", setGPIOCommand="
+ setGPIOCommand
+ ", setPWMCommand="
+ setPWMCommand
+ ", setPWMFrequencyCommand="
+ setPWMFrequencyCommand
+ ", releaseGPIOCommand="
+ releaseGPIOCommand
+ ", cpuTempCommand="
+ cpuTempCommand
+ ", cpuMemoryCommand="

View File

@@ -212,12 +212,23 @@ public class NeuralNetworkModelManager {
}
/**
* Returns the singleton instance of the NeuralNetworkModelManager
* Returns the singleton instance of the NeuralNetworkModelManager. Call getInstance() to use the
* default (no reset), or getInstance(true) to reset.
*
* @return The singleton instance
*/
public static NeuralNetworkModelManager getInstance() {
if (INSTANCE == null) {
return getInstance(false);
}
/**
* Returns the singleton instance of the NeuralNetworkModelManager, optionally resetting it.
*
* @param reset If true, resets the instance
* @return The singleton instance
*/
public static NeuralNetworkModelManager getInstance(boolean reset) {
if (INSTANCE == null || reset) {
INSTANCE = new NeuralNetworkModelManager();
}
return INSTANCE;

View File

@@ -18,6 +18,7 @@
package org.photonvision.common.configuration;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonProperty;
import java.nio.file.Path;
import java.util.HashMap;
@@ -125,6 +126,7 @@ public class NeuralNetworkPropertyManager {
*
* @return A list of all models
*/
@JsonIgnore
public ModelProperties[] getModels() {
return modelPathToProperties.values().toArray(new ModelProperties[0]);
}

View File

@@ -253,6 +253,8 @@ public class NetworkTablesManager {
String mac = NetworkUtils.getMacAddress();
if (!mac.equals(currentMacAddress)) {
logger.debug("MAC address changed! New MAC address is " + mac + ", was " + currentMacAddress);
kCoprocTable.getSubTable(currentMacAddress).getEntry("hostname").unpublish();
kCoprocTable.getSubTable(currentMacAddress).getEntry("cameraNames").unpublish();
currentMacAddress = mac;
}
if (mac.isEmpty()) {
@@ -260,7 +262,13 @@ public class NetworkTablesManager {
return;
}
String hostname = ConfigManager.getInstance().getConfig().getNetworkConfig().hostname;
var config = ConfigManager.getInstance().getConfig();
String hostname;
if (config.getNetworkConfig().shouldManage) {
hostname = config.getNetworkConfig().hostname;
} else {
hostname = CameraServerJNI.getHostname();
}
if (hostname == null || hostname.isEmpty()) {
logger.error("Cannot check hostname and camera names, hostname is not set!");
return;

View File

@@ -19,14 +19,14 @@ package org.photonvision.common.dataflow.websocket;
import java.util.List;
import org.photonvision.PhotonVersion;
import org.photonvision.common.LoadJNI;
import org.photonvision.common.LoadJNI.JNITypes;
import org.photonvision.common.configuration.NeuralNetworkModelManager;
import org.photonvision.common.configuration.PhotonConfiguration;
import org.photonvision.common.dataflow.networktables.NetworkTablesManager;
import org.photonvision.common.hardware.Platform;
import org.photonvision.common.networking.NetworkManager;
import org.photonvision.common.networking.NetworkUtils;
import org.photonvision.mrcal.MrCalJNILoader;
import org.photonvision.raspi.LibCameraJNILoader;
import org.photonvision.vision.processes.VisionModule;
import org.photonvision.vision.processes.VisionSourceManager;
@@ -53,8 +53,8 @@ public class UIPhotonConfiguration {
new UIGeneralSettings(
PhotonVersion.versionString,
// TODO add support for other types of GPU accel
LibCameraJNILoader.getInstance().isSupported() ? "Zerocopy Libcamera Working" : "",
MrCalJNILoader.getInstance().isLoaded(),
LoadJNI.hasLoaded(JNITypes.LIBCAMERA) ? "Zerocopy Libcamera Working" : "",
LoadJNI.hasLoaded(JNITypes.MRCAL),
c.neuralNetworkPropertyManager().getModels(),
NeuralNetworkModelManager.getInstance().getSupportedBackends(),
c.getHardwareConfig().deviceName.isEmpty()

View File

@@ -1,99 +0,0 @@
/*
* Copyright (C) Photon Vision.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.photonvision.common.hardware.GPIO;
import org.photonvision.common.configuration.HardwareConfig;
import org.photonvision.common.hardware.Platform;
public class CustomGPIO extends GPIOBase {
private boolean currentState;
private final int port;
public CustomGPIO(int port) {
this.port = port;
}
@Override
public void togglePin() {
if (this.port != -1) {
execute(
commands
.get("setState")
.replace("{s}", String.valueOf(!currentState))
.replace("{p}", String.valueOf(this.port)));
currentState = !currentState;
}
}
@Override
public int getPinNumber() {
return port;
}
@Override
public void setStateImpl(boolean state) {
if (this.port != -1) {
execute(
commands
.get("setState")
.replace("{s}", String.valueOf(state))
.replace("{p}", String.valueOf(port)));
currentState = state;
}
}
@Override
public boolean shutdown() {
if (this.port != -1) {
execute(commands.get("shutdown"));
return true;
}
return false;
}
@Override
public boolean getStateImpl() {
return currentState;
}
@Override
public void blinkImpl(int pulseTimeMillis, int blinks) {
execute(
commands
.get("blink")
.replace("{pulseTime}", String.valueOf(pulseTimeMillis))
.replace("{blinks}", String.valueOf(blinks))
.replace("{p}", String.valueOf(this.port)));
}
@Override
public void setBrightnessImpl(int brightness) {
execute(
commands
.get("dim")
.replace("{p}", String.valueOf(port))
.replace("{v}", String.valueOf(brightness)));
}
public static void setConfig(HardwareConfig config) {
if (Platform.isRaspberryPi()) return;
commands.replace("setState", config.ledSetCommand);
commands.replace("dim", config.ledDimCommand);
commands.replace("blink", config.ledBlinkCommand);
}
}

View File

@@ -1,99 +0,0 @@
/*
* Copyright (C) Photon Vision.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.photonvision.common.hardware.GPIO;
import java.util.Arrays;
import java.util.HashMap;
import org.photonvision.common.logging.LogGroup;
import org.photonvision.common.logging.Logger;
import org.photonvision.common.util.ShellExec;
public abstract class GPIOBase {
private static final Logger logger = new Logger(GPIOBase.class, LogGroup.General);
private static final ShellExec runCommand = new ShellExec(true, true);
protected static HashMap<String, String> commands =
new HashMap<>() {
{
put("setState", "");
put("shutdown", "");
put("dim", "");
put("blink", "");
}
};
protected static String execute(String command) {
try {
runCommand.executeBashCommand(command);
} catch (Exception e) {
logger.error(Arrays.toString(e.getStackTrace()));
return "";
}
return runCommand.getOutput();
}
public abstract int getPinNumber();
public void setState(boolean state) {
if (getPinNumber() != -1) {
setStateImpl(state);
}
}
protected abstract void setStateImpl(boolean state);
public final void setOff() {
setState(false);
}
public final void setOn() {
setState(true);
}
public void togglePin() {
setState(!getStateImpl());
}
public abstract boolean shutdown();
public final boolean getState() {
if (getPinNumber() != -1) {
return getStateImpl();
} else return false;
}
public abstract boolean getStateImpl();
public final void blink(int pulseTimeMillis, int blinks) {
if (getPinNumber() != -1) {
blinkImpl(pulseTimeMillis, blinks);
}
}
protected abstract void blinkImpl(int pulseTimeMillis, int blinks);
public final void setBrightness(int brightness) {
if (getPinNumber() != -1) {
if (brightness > 100) brightness = 100;
if (brightness < 0) brightness = 0;
setBrightnessImpl(brightness);
}
}
protected abstract void setBrightnessImpl(int brightness);
}

View File

@@ -1,40 +0,0 @@
/*
* Copyright (C) Photon Vision.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.photonvision.common.hardware.GPIO.pi;
@SuppressWarnings("SpellCheckingInspection")
public enum PigpioCommand {
PCMD_READ(3), // int gpio_read(unsigned gpio)
PCMD_WRITE(4), // int gpio_write(unsigned gpio, unsigned level)
PCMD_WVCLR(27), // int wave_clear(void)
PCMD_WVAG(28), // int wave_add_generic(unsigned numPulses, gpioPulse_t *pulses)
PCMD_WVHLT(33), // int wave_tx_stop(void)
PCMD_WVCRE(49), // int wave_create(void)
PCMD_WVDEL(50), // int wave_delete(unsigned wave_id)
PCMD_WVTX(51), // int wave_tx_send(unsigned wave_id) (once)
PCMD_WVTXR(52), // int wave_tx_send(unsigned wave_id) (repeat)
PCMD_GDC(83), // int get_duty_cycle(unsigned user_gpio)
PCMD_HP(86), // int hardware_pwm(unsigned gpio, unsigned PWMfreq, unsigned PWMduty)
PCMD_WVTXM(100); // int wave_tx_send(unsigned wave_id, unsigned wave_mode)
public final int value;
PigpioCommand(int value) {
this.value = value;
}
}

View File

@@ -1,343 +0,0 @@
/*
* Copyright (C) Photon Vision.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.photonvision.common.hardware.GPIO.pi;
import java.util.HashMap;
/**
* A class that defines the exceptions that can be thrown by Pigpio.
*
* <p>Credit to nkolban
* https://github.com/nkolban/jpigpio/blob/master/JPigpio/src/jpigpio/PigpioException.java
*/
@SuppressWarnings({"SpellCheckingInspection", "unused", "RedundantSuppression"})
public class PigpioException extends Exception {
private int rc = -99999999;
private static final long serialVersionUID = 443595760654129068L;
public PigpioException() {
super();
}
public PigpioException(int rc) {
super();
this.rc = rc;
}
public PigpioException(int rc, String msg) {
super(msg);
this.rc = rc;
}
public PigpioException(String arg0, Throwable arg1, boolean arg2, boolean arg3) {
super(arg0, arg1, arg2, arg3);
}
public PigpioException(String arg0, Throwable arg1) {
super(arg0, arg1);
}
public PigpioException(String arg0) {
super(arg0);
}
public PigpioException(Throwable arg0) {
super(arg0);
}
@Override
public String getMessage() {
return "(" + rc + ") " + getMessageForError(rc);
}
/**
* Retrieve the error code that was returned by the underlying Pigpio call.
*
* @return The error code that was returned by the underlying Pigpio call.
*/
public int getErrorCode() {
return rc;
} // End of getErrorCode
// Public constants for the error codes that can be thrown by Pigpio
public static final int PI_INIT_FAILED = -1; // gpioInitialise failed
public static final int PI_BAD_USER_GPIO = -2; // gpio not 0-31
public static final int PI_BAD_GPIO = -3; // gpio not 0-53
public static final int PI_BAD_MODE = -4; // mode not 0-7
public static final int PI_BAD_LEVEL = -5; // level not 0-1
public static final int PI_BAD_PUD = -6; // pud not 0-2
public static final int PI_BAD_PULSEWIDTH = -7; // pulsewidth not 0 or 500-2500
public static final int PI_BAD_DUTYCYCLE = -8; // dutycycle outside set range
public static final int PI_BAD_TIMER = -9; // timer not 0-9
public static final int PI_BAD_MS = -10; // ms not 10-60000
public static final int PI_BAD_TIMETYPE = -11; // timetype not 0-1
public static final int PI_BAD_SECONDS = -12; // seconds < 0
public static final int PI_BAD_MICROS = -13; // micros not 0-999999
public static final int PI_TIMER_FAILED = -14; // gpioSetTimerFunc failed
public static final int PI_BAD_WDOG_TIMEOUT = -15; // timeout not 0-60000
public static final int PI_NO_ALERT_FUNC = -16; // DEPRECATED
public static final int PI_BAD_CLK_PERIPH = -17; // clock peripheral not 0-1
public static final int PI_BAD_CLK_SOURCE = -18; // DEPRECATED
public static final int PI_BAD_CLK_MICROS = -19; // clock micros not 1, 2, 4, 5, 8, or 10
public static final int PI_BAD_BUF_MILLIS = -20; // buf millis not 100-10000
public static final int PI_BAD_DUTYRANGE = -21; // dutycycle range not 25-40000
public static final int PI_BAD_DUTY_RANGE = -21; // DEPRECATED (use PI_BAD_DUTYRANGE)
public static final int PI_BAD_SIGNUM = -22; // signum not 0-63
public static final int PI_BAD_PATHNAME = -23; // can't open pathname
public static final int PI_NO_HANDLE = -24; // no handle available
public static final int PI_BAD_HANDLE = -25; // unknown handle
public static final int PI_BAD_IF_FLAGS = -26; // ifFlags > 3
public static final int PI_BAD_CHANNEL = -27; // DMA channel not 0-14
public static final int PI_BAD_PRIM_CHANNEL = -27; // DMA primary channel not 0-14
public static final int PI_BAD_SOCKET_PORT = -28; // socket port not 1024-32000
public static final int PI_BAD_FIFO_COMMAND = -29; // unrecognized fifo command
public static final int PI_BAD_SECO_CHANNEL = -30; // DMA secondary channel not 0-6
public static final int PI_NOT_INITIALISED = -31; // function called before gpioInitialise
public static final int PI_INITIALISED = -32; // function called after gpioInitialise
public static final int PI_BAD_WAVE_MODE = -33; // waveform mode not 0-1
public static final int PI_BAD_CFG_INTERNAL = -34; // bad parameter in gpioCfgInternals call
public static final int PI_BAD_WAVE_BAUD = -35; // baud rate not 50-250K(RX)/50-1M(TX)
public static final int PI_TOO_MANY_PULSES = -36; // waveform has too many pulses
public static final int PI_TOO_MANY_CHARS = -37; // waveform has too many chars
public static final int PI_NOT_SERIAL_GPIO = -38; // no serial read in progress on gpio
public static final int PI_BAD_SERIAL_STRUC = -39; // bad (null) serial structure parameter
public static final int PI_BAD_SERIAL_BUF = -40; // bad (null) serial buf parameter
public static final int PI_NOT_PERMITTED = -41; // gpio operation not permitted
public static final int PI_SOME_PERMITTED = -42; // one or more gpios not permitted
public static final int PI_BAD_WVSC_COMMND = -43; // bad WVSC subcommand
public static final int PI_BAD_WVSM_COMMND = -44; // bad WVSM subcommand
public static final int PI_BAD_WVSP_COMMND = -45; // bad WVSP subcommand
public static final int PI_BAD_PULSELEN = -46; // trigger pulse length not 1-100
public static final int PI_BAD_SCRIPT = -47; // invalid script
public static final int PI_BAD_SCRIPT_ID = -48; // unknown script id
public static final int PI_BAD_SER_OFFSET = -49; // add serial data offset > 30 minutes
public static final int PI_GPIO_IN_USE = -50; // gpio already in use
public static final int PI_BAD_SERIAL_COUNT = -51; // must read at least a byte at a time
public static final int PI_BAD_PARAM_NUM = -52; // script parameter id not 0-9
public static final int PI_DUP_TAG = -53; // script has duplicate tag
public static final int PI_TOO_MANY_TAGS = -54; // script has too many tags
public static final int PI_BAD_SCRIPT_CMD = -55; // illegal script command
public static final int PI_BAD_VAR_NUM = -56; // script variable id not 0-149
public static final int PI_NO_SCRIPT_ROOM = -57; // no more room for scripts
public static final int PI_NO_MEMORY = -58; // can't allocate temporary memory
public static final int PI_SOCK_READ_FAILED = -59; // socket read failed
public static final int PI_SOCK_WRIT_FAILED = -60; // socket write failed
public static final int PI_TOO_MANY_PARAM = -61; // too many script parameters (> 10)
public static final int PI_NOT_HALTED = -62; // script already running or failed
public static final int PI_BAD_TAG = -63; // script has unresolved tag
public static final int PI_BAD_MICS_DELAY = -64; // bad MICS delay (too large)
public static final int PI_BAD_MILS_DELAY = -65; // bad MILS delay (too large)
public static final int PI_BAD_WAVE_ID = -66; // non existent wave id
public static final int PI_TOO_MANY_CBS = -67; // No more CBs for waveform
public static final int PI_TOO_MANY_OOL = -68; // No more OOL for waveform
public static final int PI_EMPTY_WAVEFORM = -69; // attempt to create an empty waveform
public static final int PI_NO_WAVEFORM_ID = -70; // no more waveforms
public static final int PI_I2C_OPEN_FAILED = -71; // can't open I2C device
public static final int PI_SER_OPEN_FAILED = -72; // can't open serial device
public static final int PI_SPI_OPEN_FAILED = -73; // can't open SPI device
public static final int PI_BAD_I2C_BUS = -74; // bad I2C bus
public static final int PI_BAD_I2C_ADDR = -75; // bad I2C address
public static final int PI_BAD_SPI_CHANNEL = -76; // bad SPI channel
public static final int PI_BAD_FLAGS = -77; // bad i2c/spi/ser open flags
public static final int PI_BAD_SPI_SPEED = -78; // bad SPI speed
public static final int PI_BAD_SER_DEVICE = -79; // bad serial device name
public static final int PI_BAD_SER_SPEED = -80; // bad serial baud rate
public static final int PI_BAD_PARAM = -81; // bad i2c/spi/ser parameter
public static final int PI_I2C_WRITE_FAILED = -82; // i2c write failed
public static final int PI_I2C_READ_FAILED = -83; // i2c read failed
public static final int PI_BAD_SPI_COUNT = -84; // bad SPI count
public static final int PI_SER_WRITE_FAILED = -85; // ser write failed
public static final int PI_SER_READ_FAILED = -86; // ser read failed
public static final int PI_SER_READ_NO_DATA = -87; // ser read no data available
public static final int PI_UNKNOWN_COMMAND = -88; // unknown command
public static final int PI_SPI_XFER_FAILED = -89; // spi xfer/read/write failed
public static final int PI_BAD_POINTER = -90; // bad (NULL) pointer
public static final int PI_NO_AUX_SPI = -91; // need a A+/B+/Pi2 for auxiliary SPI
public static final int PI_NOT_PWM_GPIO = -92; // gpio is not in use for PWM
public static final int PI_NOT_SERVO_GPIO = -93; // gpio is not in use for servo pulses
public static final int PI_NOT_HCLK_GPIO = -94; // gpio has no hardware clock
public static final int PI_NOT_HPWM_GPIO = -95; // gpio has no hardware PWM
public static final int PI_BAD_HPWM_FREQ = -96; // hardware PWM frequency not 1-125M
public static final int PI_BAD_HPWM_DUTY = -97; // hardware PWM dutycycle not 0-1M
public static final int PI_BAD_HCLK_FREQ = -98; // hardware clock frequency not 4689-250M
public static final int PI_BAD_HCLK_PASS = -99; // need password to use hardware clock 1
public static final int PI_HPWM_ILLEGAL = -100; // illegal, PWM in use for main clock
public static final int PI_BAD_DATABITS = -101; // serial data bits not 1-32
public static final int PI_BAD_STOPBITS = -102; // serial (half) stop bits not 2-8
public static final int PI_MSG_TOOBIG = -103; // socket/pipe message too big
public static final int PI_BAD_MALLOC_MODE = -104; // bad memory allocation mode
public static final int PI_TOO_MANY_SEGS = -105; // too many I2C transaction parts
public static final int PI_BAD_I2C_SEG = -106; // a combined I2C transaction failed
public static final int PI_BAD_SMBUS_CMD = -107;
public static final int PI_NOT_I2C_GPIO = -108;
public static final int PI_BAD_I2C_WLEN = -109;
public static final int PI_BAD_I2C_RLEN = -110;
public static final int PI_BAD_I2C_CMD = -111;
public static final int PI_BAD_I2C_BAUD = -112;
public static final int PI_CHAIN_LOOP_CNT = -113;
public static final int PI_BAD_CHAIN_LOOP = -114;
public static final int PI_CHAIN_COUNTER = -115;
public static final int PI_BAD_CHAIN_CMD = -116;
public static final int PI_BAD_CHAIN_DELAY = -117;
public static final int PI_CHAIN_NESTING = -118;
public static final int PI_CHAIN_TOO_BIG = -119;
public static final int PI_DEPRECATED = -120;
public static final int PI_BAD_SER_INVERT = -121;
public static final int PI_BAD_EDGE = -122;
public static final int PI_BAD_ISR_INIT = -123;
public static final int PI_BAD_FOREVER = -124;
public static final int PI_BAD_FILTER = -125;
public static final int PI_PIGIF_ERR_0 = -2000;
public static final int PI_PIGIF_ERR_99 = -2099;
public static final int PI_CUSTOM_ERR_0 = -3000;
public static final int PI_CUSTOM_ERR_999 = -3999;
private static final HashMap<Integer, String> errorMessages = new HashMap<>();
static {
errorMessages.put(PI_INIT_FAILED, "pigpio initialisation failed");
errorMessages.put(PI_BAD_USER_GPIO, "GPIO not 0-31");
errorMessages.put(PI_BAD_GPIO, "GPIO not 0-53");
errorMessages.put(PI_BAD_MODE, "mode not 0-7");
errorMessages.put(PI_BAD_LEVEL, "level not 0-1");
errorMessages.put(PI_BAD_PUD, "pud not 0-2");
errorMessages.put(PI_BAD_PULSEWIDTH, "pulsewidth not 0 or 500-2500");
errorMessages.put(PI_BAD_DUTYCYCLE, "dutycycle not 0-range (default 255)");
errorMessages.put(PI_BAD_TIMER, "timer not 0-9");
errorMessages.put(PI_BAD_MS, "ms not 10-60000");
errorMessages.put(PI_BAD_TIMETYPE, "timetype not 0-1");
errorMessages.put(PI_BAD_SECONDS, "seconds < 0");
errorMessages.put(PI_BAD_MICROS, "micros not 0-999999");
errorMessages.put(PI_TIMER_FAILED, "gpioSetTimerFunc failed");
errorMessages.put(PI_BAD_WDOG_TIMEOUT, "timeout not 0-60000");
errorMessages.put(PI_NO_ALERT_FUNC, "DEPRECATED");
errorMessages.put(PI_BAD_CLK_PERIPH, "clock peripheral not 0-1");
errorMessages.put(PI_BAD_CLK_SOURCE, "DEPRECATED");
errorMessages.put(PI_BAD_CLK_MICROS, "clock micros not 1, 2, 4, 5, 8, or 10");
errorMessages.put(PI_BAD_BUF_MILLIS, "buf millis not 100-10000");
errorMessages.put(PI_BAD_DUTYRANGE, "dutycycle range not 25-40000");
errorMessages.put(PI_BAD_SIGNUM, "signum not 0-63");
errorMessages.put(PI_BAD_PATHNAME, "can't open pathname");
errorMessages.put(PI_NO_HANDLE, "no handle available");
errorMessages.put(PI_BAD_HANDLE, "unknown handle");
errorMessages.put(PI_BAD_IF_FLAGS, "ifFlags > 3");
errorMessages.put(PI_BAD_CHANNEL, "DMA channel not 0-14");
errorMessages.put(PI_BAD_SOCKET_PORT, "socket port not 1024-30000");
errorMessages.put(PI_BAD_FIFO_COMMAND, "unknown fifo command");
errorMessages.put(PI_BAD_SECO_CHANNEL, "DMA secondary channel not 0-14");
errorMessages.put(PI_NOT_INITIALISED, "function called before gpioInitialise");
errorMessages.put(PI_INITIALISED, "function called after gpioInitialise");
errorMessages.put(PI_BAD_WAVE_MODE, "waveform mode not 0-1");
errorMessages.put(PI_BAD_CFG_INTERNAL, "bad parameter in gpioCfgInternals call");
errorMessages.put(PI_BAD_WAVE_BAUD, "baud rate not 50-250000(RX)/1000000(TX)");
errorMessages.put(PI_TOO_MANY_PULSES, "waveform has too many pulses");
errorMessages.put(PI_TOO_MANY_CHARS, "waveform has too many chars");
errorMessages.put(PI_NOT_SERIAL_GPIO, "no bit bang serial read in progress on GPIO");
errorMessages.put(PI_NOT_PERMITTED, "no permission to update GPIO");
errorMessages.put(PI_SOME_PERMITTED, "no permission to update one or more GPIO");
errorMessages.put(PI_BAD_WVSC_COMMND, "bad WVSC subcommand");
errorMessages.put(PI_BAD_WVSM_COMMND, "bad WVSM subcommand");
errorMessages.put(PI_BAD_WVSP_COMMND, "bad WVSP subcommand");
errorMessages.put(PI_BAD_PULSELEN, "trigger pulse length not 1-100");
errorMessages.put(PI_BAD_SCRIPT, "invalid script");
errorMessages.put(PI_BAD_SCRIPT_ID, "unknown script id");
errorMessages.put(PI_BAD_SER_OFFSET, "add serial data offset > 30 minute");
errorMessages.put(PI_GPIO_IN_USE, "GPIO already in use");
errorMessages.put(PI_BAD_SERIAL_COUNT, "must read at least a byte at a time");
errorMessages.put(PI_BAD_PARAM_NUM, "script parameter id not 0-9");
errorMessages.put(PI_DUP_TAG, "script has duplicate tag");
errorMessages.put(PI_TOO_MANY_TAGS, "script has too many tags");
errorMessages.put(PI_BAD_SCRIPT_CMD, "illegal script command");
errorMessages.put(PI_BAD_VAR_NUM, "script variable id not 0-149");
errorMessages.put(PI_NO_SCRIPT_ROOM, "no more room for scripts");
errorMessages.put(PI_NO_MEMORY, "can't allocate temporary memory");
errorMessages.put(PI_SOCK_READ_FAILED, "socket read failed");
errorMessages.put(PI_SOCK_WRIT_FAILED, "socket write failed");
errorMessages.put(PI_TOO_MANY_PARAM, "too many script parameters (> 10)");
errorMessages.put(PI_NOT_HALTED, "script already running or failed");
errorMessages.put(PI_BAD_TAG, "script has unresolved tag");
errorMessages.put(PI_BAD_MICS_DELAY, "bad MICS delay (too large)");
errorMessages.put(PI_BAD_MILS_DELAY, "bad MILS delay (too large)");
errorMessages.put(PI_BAD_WAVE_ID, "non existent wave id");
errorMessages.put(PI_TOO_MANY_CBS, "No more CBs for waveform");
errorMessages.put(PI_TOO_MANY_OOL, "No more OOL for waveform");
errorMessages.put(PI_EMPTY_WAVEFORM, "attempt to create an empty waveform");
errorMessages.put(PI_NO_WAVEFORM_ID, "No more waveform ids");
errorMessages.put(PI_I2C_OPEN_FAILED, "can't open I2C device");
errorMessages.put(PI_SER_OPEN_FAILED, "can't open serial device");
errorMessages.put(PI_SPI_OPEN_FAILED, "can't open SPI device");
errorMessages.put(PI_BAD_I2C_BUS, "bad I2C bus");
errorMessages.put(PI_BAD_I2C_ADDR, "bad I2C address");
errorMessages.put(PI_BAD_SPI_CHANNEL, "bad SPI channel");
errorMessages.put(PI_BAD_FLAGS, "bad i2c/spi/ser open flags");
errorMessages.put(PI_BAD_SPI_SPEED, "bad SPI speed");
errorMessages.put(PI_BAD_SER_DEVICE, "bad serial device name");
errorMessages.put(PI_BAD_SER_SPEED, "bad serial baud rate");
errorMessages.put(PI_BAD_PARAM, "bad i2c/spi/ser parameter");
errorMessages.put(PI_I2C_WRITE_FAILED, "I2C write failed");
errorMessages.put(PI_I2C_READ_FAILED, "I2C read failed");
errorMessages.put(PI_BAD_SPI_COUNT, "bad SPI count");
errorMessages.put(PI_SER_WRITE_FAILED, "ser write failed");
errorMessages.put(PI_SER_READ_FAILED, "ser read failed");
errorMessages.put(PI_SER_READ_NO_DATA, "ser read no data available");
errorMessages.put(PI_UNKNOWN_COMMAND, "unknown command");
errorMessages.put(PI_SPI_XFER_FAILED, "SPI xfer/read/write failed");
errorMessages.put(PI_BAD_POINTER, "bad (NULL) pointer");
errorMessages.put(PI_NO_AUX_SPI, "no auxiliary SPI on Pi A or B");
errorMessages.put(PI_NOT_PWM_GPIO, "GPIO is not in use for PWM");
errorMessages.put(PI_NOT_SERVO_GPIO, "GPIO is not in use for servo pulses");
errorMessages.put(PI_NOT_HCLK_GPIO, "GPIO has no hardware clock");
errorMessages.put(PI_NOT_HPWM_GPIO, "GPIO has no hardware PWM");
errorMessages.put(PI_BAD_HPWM_FREQ, "hardware PWM frequency not 1-125M");
errorMessages.put(PI_BAD_HPWM_DUTY, "hardware PWM dutycycle not 0-1M");
errorMessages.put(PI_BAD_HCLK_FREQ, "hardware clock frequency not 4689-250M");
errorMessages.put(PI_BAD_HCLK_PASS, "need password to use hardware clock 1");
errorMessages.put(PI_HPWM_ILLEGAL, "illegal, PWM in use for main clock");
errorMessages.put(PI_BAD_DATABITS, "serial data bits not 1-32");
errorMessages.put(PI_BAD_STOPBITS, "serial (half) stop bits not 2-8");
errorMessages.put(PI_MSG_TOOBIG, "socket/pipe message too big");
errorMessages.put(PI_BAD_MALLOC_MODE, "bad memory allocation mode");
errorMessages.put(PI_TOO_MANY_SEGS, "too many I2C transaction segments");
errorMessages.put(PI_BAD_I2C_SEG, "an I2C transaction segment failed");
errorMessages.put(PI_BAD_SMBUS_CMD, "SMBus command not supported");
errorMessages.put(PI_NOT_I2C_GPIO, "no bit bang I2C in progress on GPIO");
errorMessages.put(PI_BAD_I2C_WLEN, "bad I2C write length");
errorMessages.put(PI_BAD_I2C_RLEN, "bad I2C read length");
errorMessages.put(PI_BAD_I2C_CMD, "bad I2C command");
errorMessages.put(PI_BAD_I2C_BAUD, "bad I2C baud rate, not 50-500k");
errorMessages.put(PI_CHAIN_LOOP_CNT, "bad chain loop count");
errorMessages.put(PI_BAD_CHAIN_LOOP, "empty chain loop");
errorMessages.put(PI_CHAIN_COUNTER, "too many chain counters");
errorMessages.put(PI_BAD_CHAIN_CMD, "bad chain command");
errorMessages.put(PI_BAD_CHAIN_DELAY, "bad chain delay micros");
errorMessages.put(PI_CHAIN_NESTING, "chain counters nested too deeply");
errorMessages.put(PI_CHAIN_TOO_BIG, "chain is too long");
errorMessages.put(PI_DEPRECATED, "deprecated function removed");
errorMessages.put(PI_BAD_SER_INVERT, "bit bang serial invert not 0 or 1");
errorMessages.put(PI_BAD_EDGE, "bad ISR edge value, not 0-2");
errorMessages.put(PI_BAD_ISR_INIT, "bad ISR initialisation");
errorMessages.put(PI_BAD_FOREVER, "loop forever must be last chain command");
errorMessages.put(PI_BAD_FILTER, "bad filter parameter");
}
public static String getMessageForError(int errorCode) {
return errorMessages.get(errorCode);
}
}

View File

@@ -1,93 +0,0 @@
/*
* Copyright (C) Photon Vision.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.photonvision.common.hardware.GPIO.pi;
import org.photonvision.common.hardware.GPIO.GPIOBase;
import org.photonvision.common.logging.LogGroup;
import org.photonvision.common.logging.Logger;
public class PigpioPin extends GPIOBase {
public static final Logger logger = new Logger(PigpioPin.class, LogGroup.General);
private static final PigpioSocket piSocket = new PigpioSocket();
private final boolean isHardwarePWMPin;
private final int pinNo;
private boolean hasFailedHardwarePWM;
public PigpioPin(int pinNo) {
isHardwarePWMPin = pinNo == 12 || pinNo == 13 || pinNo == 17 || pinNo == 18;
this.pinNo = pinNo;
}
@Override
public int getPinNumber() {
return pinNo;
}
@Override
protected void setStateImpl(boolean state) {
try {
piSocket.gpioWrite(pinNo, state);
} catch (PigpioException e) {
logger.error("gpioWrite FAIL - " + e.getMessage());
}
}
@Override
public boolean shutdown() {
setState(false);
return true;
}
@Override
public boolean getStateImpl() {
try {
return piSocket.gpioRead(pinNo);
} catch (PigpioException e) {
logger.error("gpioRead FAIL - " + e.getMessage());
return false;
}
}
@Override
protected void blinkImpl(int pulseTimeMillis, int blinks) {
try {
piSocket.generateAndSendWaveform(pulseTimeMillis, blinks, pinNo);
} catch (PigpioException e) {
logger.error("Could not set blink - " + e.getMessage());
}
}
@Override
protected void setBrightnessImpl(int brightness) {
if (isHardwarePWMPin) {
try {
piSocket.hardwarePWM(pinNo, 22000, (int) (1000000 * (brightness / 100.0)));
} catch (PigpioException e) {
logger.error("Failed to hardPWM - " + e.getMessage());
}
} else if (!hasFailedHardwarePWM) {
logger.warn(
"Specified pin ("
+ pinNo
+ ") is not capable of hardware PWM - no action will be taken.");
hasFailedHardwarePWM = true;
}
}
}

View File

@@ -1,39 +0,0 @@
/*
* Copyright (C) Photon Vision.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.photonvision.common.hardware.GPIO.pi;
public class PigpioPulse {
int gpioOn;
int gpioOff;
int delayMicros;
/**
* Initialises a pulse.
*
* @param gpioOn GPIO number to switch on at the start of the pulse. If zero, then no GPIO will be
* switched on.
* @param gpioOff GPIO number to switch off at the start of the pulse. If zero, then no GPIO will
* be switched off.
* @param delayMicros the delay in microseconds before the next pulse.
*/
public PigpioPulse(int gpioOn, int gpioOff, int delayMicros) {
this.gpioOn = gpioOn != 0 ? 1 << gpioOn : 0;
this.gpioOff = gpioOff != 0 ? 1 << gpioOff : 0;
this.delayMicros = delayMicros;
}
}

View File

@@ -1,357 +0,0 @@
/*
* Copyright (C) Photon Vision.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.photonvision.common.hardware.GPIO.pi;
import static org.photonvision.common.hardware.GPIO.pi.PigpioException.*;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.util.ArrayList;
import org.photonvision.common.logging.LogGroup;
import org.photonvision.common.logging.Logger;
@SuppressWarnings({"SpellCheckingInspection", "unused"})
public class PigpioSocket {
private static final Logger logger = new Logger(PigpioSocket.class, LogGroup.General);
private static final int PIGPIOD_MESSAGE_SIZE = 12;
private PigpioSocketLock commandSocket;
private int activeWaveformID = -1;
/** Creates and starts a socket connection to a pigpio daemon on localhost */
public PigpioSocket() {
this("127.0.0.1", 8888);
}
/**
* Creates and starts a socket connection to a pigpio daemon on a remote host with the specified
* address and port
*
* @param addr Address of remote pigpio daemon
* @param port Port of remote pigpio daemon
*/
public PigpioSocket(String addr, int port) {
try {
commandSocket = new PigpioSocketLock(addr, port);
} catch (IOException e) {
logger.error("Failed to create or connect to Pigpio Daemon socket", e);
}
}
/**
* Reconnects to the pigpio daemon
*
* @throws PigpioException on failure
*/
public void reconnect() throws PigpioException {
try {
commandSocket.reconnect();
} catch (IOException e) {
logger.error("Failed to reconnect to Pigpio Daemon socket", e);
throw new PigpioException("reconnect", e);
}
}
/**
* Terminates the connection to the pigpio daemon
*
* @throws PigpioException on failure
*/
public void gpioTerminate() throws PigpioException {
try {
commandSocket.terminate();
} catch (IOException e) {
logger.error("Failed to terminate connection to Pigpio Daemon socket", e);
throw new PigpioException("gpioTerminate", e);
}
}
/**
* Read the GPIO level
*
* @param pin Pin to read from
* @return Value of the pin
* @throws PigpioException on failure
*/
public boolean gpioRead(int pin) throws PigpioException {
try {
int retCode = commandSocket.sendCmd(PigpioCommand.PCMD_READ.value, pin);
if (retCode < 0) throw new PigpioException(retCode);
return retCode != 0;
} catch (IOException e) {
logger.error("Failed to read GPIO pin: " + pin, e);
throw new PigpioException("gpioRead", e);
}
}
/**
* Write the GPIO level
*
* @param pin Pin to write to
* @param value Value to write
* @throws PigpioException on failure
*/
public void gpioWrite(int pin, boolean value) throws PigpioException {
try {
int retCode = commandSocket.sendCmd(PigpioCommand.PCMD_WRITE.value, pin, value ? 1 : 0);
if (retCode < 0) throw new PigpioException(retCode);
} catch (IOException e) {
logger.error("Failed to write to GPIO pin: " + pin, e);
throw new PigpioException("gpioWrite", e);
}
}
/**
* Clears all waveforms and any data added by calls to {@link #waveAddGeneric(ArrayList)}
*
* @throws PigpioException on failure
*/
public void waveClear() throws PigpioException {
try {
int retCode = commandSocket.sendCmd(PigpioCommand.PCMD_WVCLR.value);
if (retCode < 0) throw new PigpioException(retCode);
} catch (IOException e) {
logger.error("Failed to clear waveforms", e);
throw new PigpioException("waveClear", e);
}
}
/**
* Adds a number of pulses to the current waveform
*
* @param pulses ArrayList of pulses to add
* @return the new total number of pulses in the current waveform
* @throws PigpioException on failure
*/
private int waveAddGeneric(ArrayList<PigpioPulse> pulses) throws PigpioException {
// pigpio wave message format
// I p1 0
// I p2 0
// I p3 pulses * 12
// ## extension ##
// III on/off/delay * pulses
if (pulses == null || pulses.isEmpty()) return 0;
try {
ByteBuffer bb = ByteBuffer.allocate(pulses.size() * 12);
bb.order(ByteOrder.LITTLE_ENDIAN);
for (var pulse : pulses) {
bb.putInt(pulse.gpioOn).putInt(pulse.gpioOff).putInt(pulse.delayMicros);
}
int retCode =
commandSocket.sendCmd(
PigpioCommand.PCMD_WVAG.value,
0,
0,
pulses.size() * PIGPIOD_MESSAGE_SIZE,
bb.array());
if (retCode < 0) throw new PigpioException(retCode);
return retCode;
} catch (IOException e) {
logger.error("Failed to add pulse(s) to waveform", e);
throw new PigpioException("waveAddGeneric", e);
}
}
/**
* Creates pulses and adds them to the current waveform
*
* @param pulseTimeMillis Pulse length in milliseconds
* @param blinks Number of times to pulse. -1 for repeat
* @param pinNo Pin to pulse
*/
private void addBlinkPulsesToWaveform(int pulseTimeMillis, int blinks, int pinNo) {
boolean repeat = blinks == -1;
if (blinks == 0) return;
if (repeat) {
blinks = 1;
}
try {
ArrayList<PigpioPulse> pulses = new ArrayList<>();
var startPulse = new PigpioPulse(pinNo, 0, pulseTimeMillis * 1000);
var endPulse = new PigpioPulse(0, pinNo, pulseTimeMillis * 1000);
for (int i = 0; i < blinks; i++) {
pulses.add(startPulse);
pulses.add(endPulse);
}
waveAddGeneric(pulses);
pulses.clear();
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* Generates and sends a waveform to the given pins with the specified parameters.
*
* @param pulseTimeMillis Pulse length in milliseconds
* @param blinks Number of times to pulse. -1 for repeat
* @param pins Pins to pulse
* @throws PigpioException on failure
*/
public void generateAndSendWaveform(int pulseTimeMillis, int blinks, int... pins)
throws PigpioException {
if (pins.length == 0) return;
boolean repeat = blinks == -1;
if (blinks == 0) return;
// stop any active waves
waveTxStop();
waveClear();
if (activeWaveformID != -1) {
waveDelete(activeWaveformID);
activeWaveformID = -1;
}
for (int pin : pins) {
addBlinkPulsesToWaveform(pulseTimeMillis, blinks, pin);
}
int waveformId = waveCreate();
if (waveformId >= 0) {
if (repeat) {
waveSendRepeat(waveformId);
} else {
waveSendOnce(waveformId);
}
} else {
logger.error("Failed to send wave: " + getMessageForError(waveformId));
}
}
/**
* Stops the transmission of the current waveform
*
* @return success
* @throws PigpioException on failure
*/
public boolean waveTxStop() throws PigpioException {
try {
int retCode = commandSocket.sendCmd(PigpioCommand.PCMD_WVHLT.value);
if (retCode < 0) throw new PigpioException(retCode);
return retCode == 0;
} catch (IOException e) {
logger.error("Failed to stop waveform", e);
throw new PigpioException("waveTxStop", e);
}
}
/**
* Creates a waveform from the data provided by the prior calls to {@link
* #waveAddGeneric(ArrayList)} Upon success a wave ID greater than or equal to 0 is returned
*
* @return ID of the created waveform
* @throws PigpioException on failure
*/
public int waveCreate() throws PigpioException {
try {
int retCode = commandSocket.sendCmd(PigpioCommand.PCMD_WVCRE.value);
if (retCode < 0) throw new PigpioException(retCode);
return retCode;
} catch (IOException e) {
logger.error("Failed to create new waveform", e);
throw new PigpioException("waveCreate", e);
}
}
/**
* Deletes the waveform with specified wave ID
*
* @param waveId ID of the waveform to delete
* @throws PigpioException on failure
*/
public void waveDelete(int waveId) throws PigpioException {
try {
int retCode = commandSocket.sendCmd(PigpioCommand.PCMD_WVDEL.value, waveId);
if (retCode < 0) throw new PigpioException(retCode);
} catch (IOException e) {
logger.error("Failed to delete wave: " + waveId, e);
throw new PigpioException("waveDelete", e);
}
}
/**
* Transmits the waveform with specified wave ID. The waveform is sent once
*
* @param waveId ID of the waveform to transmit
* @return The number of DMA control blocks in the waveform
* @throws PigpioException on failure
*/
public int waveSendOnce(int waveId) throws PigpioException {
try {
int retCode = commandSocket.sendCmd(PigpioCommand.PCMD_WVTX.value, waveId);
if (retCode < 0) throw new PigpioException(retCode);
return retCode;
} catch (IOException e) {
throw new PigpioException("waveSendOnce", e);
}
}
/**
* Transmits the waveform with specified wave ID. The waveform cycles until cancelled (either by
* the sending of a new waveform or {@link #waveTxStop()}
*
* @param waveId ID of the waveform to transmit
* @return The number of DMA control blocks in the waveform
* @throws PigpioException on failure
*/
public int waveSendRepeat(int waveId) throws PigpioException {
try {
int retCode = commandSocket.sendCmd(PigpioCommand.PCMD_WVTXR.value, waveId);
if (retCode < 0) throw new PigpioException(retCode);
return retCode;
} catch (IOException e) {
throw new PigpioException("waveSendRepeat", e);
}
}
/**
* Starts hardware PWM on a GPIO at the specified frequency and dutycycle
*
* @param pin GPIO pin to start PWM on
* @param pwmFrequency Frequency to run at (1Hz-125MHz). Frequencies above 30MHz are unlikely to
* work
* @param pwmDuty Duty cycle to run at (0-1,000,000)
* @throws PigpioException on failure
*/
public void hardwarePWM(int pin, int pwmFrequency, int pwmDuty) throws PigpioException {
try {
ByteBuffer bb = ByteBuffer.allocate(4);
bb.order(ByteOrder.LITTLE_ENDIAN);
bb.putInt(pwmDuty);
int retCode =
commandSocket.sendCmd(PigpioCommand.PCMD_HP.value, pin, pwmFrequency, 4, bb.array());
if (retCode < 0) throw new PigpioException(retCode);
} catch (IOException e) {
throw new PigpioException("hardwarePWM", e);
}
}
}

View File

@@ -1,147 +0,0 @@
/*
* Copyright (C) Photon Vision.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.photonvision.common.hardware.GPIO.pi;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.net.Socket;
import java.nio.ByteBuffer;
/**
* Credit to nkolban
* https://github.com/nkolban/jpigpio/blob/master/JPigpio/src/jpigpio/SocketLock.java
*/
final class PigpioSocketLock {
private static final int replyTimeoutMillis = 1000;
private final String addr;
private final int port;
private Socket socket;
private DataInputStream in;
private DataOutputStream out;
public PigpioSocketLock(String addr, int port) throws IOException {
this.addr = addr;
this.port = port;
reconnect();
}
public void reconnect() throws IOException {
socket = new Socket(addr, port);
out = new DataOutputStream(socket.getOutputStream());
in = new DataInputStream(socket.getInputStream());
}
public void terminate() throws IOException {
in.close();
in = null;
out.flush();
out.close();
out = null;
socket.close();
socket = null;
}
public synchronized int sendCmd(int cmd) throws IOException {
byte[] b = {};
return sendCmd(cmd, 0, 0, 0, b);
}
public synchronized int sendCmd(int cmd, int p1) throws IOException {
byte[] b = {};
return sendCmd(cmd, p1, 0, 0, b);
}
public synchronized int sendCmd(int cmd, int p1, int p2) throws IOException {
byte[] b = {};
return sendCmd(cmd, p1, p2, 0, b);
}
public synchronized int sendCmd(int cmd, int p1, int p2, int p3) throws IOException {
byte[] b = {};
return sendCmd(cmd, p1, p2, p3, b);
}
/**
* Send extended command to pigpiod and return result code
*
* @param cmd Command to send
* @param p1 Command parameter 1
* @param p2 Command parameter 2
* @param p3 Command parameter 3 (usually length of extended data - see paramater ext)
* @param ext Array of bytes containing extended data
* @return Command result code
* @throws IOException in case of network connection error
*/
@SuppressWarnings("UnusedAssignment")
public synchronized int sendCmd(int cmd, int p1, int p2, int p3, byte[] ext) throws IOException {
ByteBuffer bb = ByteBuffer.allocate(16 + ext.length);
bb.putInt(Integer.reverseBytes(cmd));
bb.putInt(Integer.reverseBytes(p1));
bb.putInt(Integer.reverseBytes(p2));
bb.putInt(Integer.reverseBytes(p3));
if (ext.length > 0) {
bb.put(ext);
}
out.write(bb.array());
out.flush();
int w = replyTimeoutMillis;
int a = in.available();
// if by any chance there is no response from pigpiod, then wait up to
// specified timeout
while (w > 0 && a < 16) {
w -= 10;
try {
Thread.sleep(10);
} catch (InterruptedException ignored) {
}
a = in.available();
}
// throw exception if response from pigpiod has not arrived yet
if (in.available() < 16) {
throw new IOException(
"Timeout: No response from pigpio daemon within " + replyTimeoutMillis + " ms.");
}
int resp = Integer.reverseBytes(in.readInt()); // ignore response
resp = Integer.reverseBytes(in.readInt()); // ignore response
resp = Integer.reverseBytes(in.readInt()); // ignore response
resp = Integer.reverseBytes(in.readInt()); // contains error or response
return resp;
}
/**
* Read all remaining bytes coming from pigpiod
*
* @param data Array to store read bytes.
* @throws IOException if unable to read from network
*/
public void readBytes(byte[] data) throws IOException {
in.readFully(data);
}
}

View File

@@ -17,18 +17,24 @@
package org.photonvision.common.hardware;
import com.diozero.api.DeviceMode;
import com.diozero.internal.spi.NativeDeviceFactoryInterface;
import com.diozero.sbc.BoardPinInfo;
import com.diozero.sbc.DeviceFactoryHelper;
import edu.wpi.first.networktables.IntegerPublisher;
import edu.wpi.first.networktables.IntegerSubscriber;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.function.Supplier;
import org.photonvision.common.configuration.ConfigManager;
import org.photonvision.common.configuration.HardwareConfig;
import org.photonvision.common.configuration.HardwareSettings;
import org.photonvision.common.dataflow.networktables.NTDataChangeListener;
import org.photonvision.common.dataflow.networktables.NetworkTablesManager;
import org.photonvision.common.hardware.GPIO.CustomGPIO;
import org.photonvision.common.hardware.GPIO.pi.PigpioSocket;
import org.photonvision.common.hardware.gpio.CustomAdapter;
import org.photonvision.common.hardware.gpio.CustomDeviceFactory;
import org.photonvision.common.hardware.metrics.MetricsManager;
import org.photonvision.common.logging.LogGroup;
import org.photonvision.common.logging.Logger;
@@ -59,8 +65,6 @@ public class HardwareManager {
public final VisionLED visionLED; // May be null if no LED is specified
private final PigpioSocket pigpioSocket; // will be null unless on Raspi
public static HardwareManager getInstance() {
if (instance == null) {
var conf = ConfigManager.getInstance().getConfig();
@@ -88,32 +92,44 @@ public class HardwareManager {
NetworkTablesManager.getInstance().kRootTable.getIntegerTopic("ledModeState").publish();
ledModeState.set(VisionLEDMode.kDefault.value);
CustomGPIO.setConfig(hardwareConfig);
// Device factory is lazy to prevent creating one if it will go unused.
Supplier<NativeDeviceFactoryInterface> lazyDeviceFactory =
new Supplier<NativeDeviceFactoryInterface>() {
NativeDeviceFactoryInterface deviceFactory = null;
if (Platform.isRaspberryPi()) {
pigpioSocket = new PigpioSocket();
} else {
pigpioSocket = null;
}
@Override
public NativeDeviceFactoryInterface get() {
if (deviceFactory == null) {
if (hardwareConfig.hasGPIOCommandsConfigured()) {
deviceFactory = HardwareManager.configureCustomGPIO(hardwareConfig);
} else {
deviceFactory = DeviceFactoryHelper.getNativeDeviceFactory();
}
}
return deviceFactory;
}
};
statusLED =
hardwareConfig.statusRGBPins.size() == 3
? new StatusLED(hardwareConfig.statusRGBPins)
? new StatusLED(
lazyDeviceFactory.get(),
hardwareConfig.statusRGBPins,
hardwareConfig.statusRGBActiveHigh)
: null;
if (statusLED != null) {
TimedTaskManager.getInstance().addTask("StatusLEDUpdate", this::statusLEDUpdate, 150);
}
var hasBrightnessRange = hardwareConfig.ledBrightnessRange.size() == 2;
visionLED =
hardwareConfig.ledPins.isEmpty()
? null
: new VisionLED(
lazyDeviceFactory.get(),
hardwareConfig.ledPins,
hardwareConfig.ledsCanDim,
hasBrightnessRange ? hardwareConfig.ledBrightnessRange.get(0) : 0,
hasBrightnessRange ? hardwareConfig.ledBrightnessRange.get(1) : 100,
pigpioSocket,
hardwareConfig.ledPWMFrequency,
ledModeState::set);
ledModeListener =
@@ -135,6 +151,33 @@ public class HardwareManager {
// if (Platform.isLinux()) MetricsPublisher.getInstance().startTask();
}
public static NativeDeviceFactoryInterface configureCustomGPIO(HardwareConfig hardwareConfig) {
// Create a new adapter and device factory using the commands from hardwareConfig
CustomAdapter adapter =
new CustomAdapter(
hardwareConfig.getGPIOCommand,
hardwareConfig.setGPIOCommand,
hardwareConfig.setPWMCommand,
hardwareConfig.setPWMFrequencyCommand,
hardwareConfig.setPWMFrequencyCommand);
CustomDeviceFactory deviceFactory = new CustomDeviceFactory(adapter);
BoardPinInfo pinInfo = deviceFactory.getBoardPinInfo();
// Populate pin info according to hardware config
for (int pin : hardwareConfig.ledPins) {
if (hardwareConfig.ledsCanDim) {
pinInfo.addGpioPinInfo(pin, pin, List.of(DeviceMode.PWM_OUTPUT, DeviceMode.DIGITAL_OUTPUT));
} else {
pinInfo.addGpioPinInfo(pin, pin, List.of(DeviceMode.DIGITAL_OUTPUT));
}
}
for (int pin : hardwareConfig.statusRGBPins) {
pinInfo.addGpioPinInfo(pin, pin, List.of(DeviceMode.DIGITAL_OUTPUT));
}
return deviceFactory;
}
public void setBrightnessPercent(int percent) {
if (percent != hardwareSettings.ledBrightnessPercentage) {
hardwareSettings.ledBrightnessPercentage = percent;
@@ -170,59 +213,51 @@ public class HardwareManager {
// API's supporting status LEDs
private Map<String, Boolean> pipelineTargets = new HashMap<String, Boolean>();
private Set<String> pipelineTargets = new HashSet<String>();
private boolean ntConnected = false;
private boolean systemRunning = false;
private int blinkCounter = 0;
public void setTargetsVisibleStatus(String uniqueName, boolean hasTargets) {
pipelineTargets.put(uniqueName, hasTargets);
if (hasTargets) {
pipelineTargets.add(uniqueName);
} else {
pipelineTargets.remove(uniqueName);
}
updateStatus();
}
public void setNTConnected(boolean isConnected) {
this.ntConnected = isConnected;
ntConnected = isConnected;
updateStatus();
}
public void setRunning(boolean isRunning) {
this.systemRunning = isRunning;
}
private void statusLEDUpdate() {
// make blinky
boolean blinky = ((blinkCounter % 3) > 0);
// check if any pipeline has a visible target
boolean anyTarget = false;
for (var t : this.pipelineTargets.values()) {
if (t) {
anyTarget = true;
}
public void setError(PhotonStatus status) {
if (status == null || !status.isError()) {
updateStatus();
} else if (statusLED != null) {
statusLED.setStatus(status);
}
}
if (this.systemRunning) {
if (!this.ntConnected) {
if (anyTarget) {
// Blue Flashing
statusLED.setRGB(false, false, blinky);
} else {
// Yellow flashing
statusLED.setRGB(blinky, blinky, false);
}
private void updateStatus() {
if (statusLED == null) {
return;
}
PhotonStatus status;
boolean anyTarget = !pipelineTargets.isEmpty();
if (ntConnected) {
if (anyTarget) {
status = PhotonStatus.NT_CONNECTED_TARGETS_VISIBLE;
} else {
if (anyTarget) {
// Blue
statusLED.setRGB(false, false, blinky);
} else {
// blinky green
statusLED.setRGB(false, blinky, false);
}
status = PhotonStatus.NT_CONNECTED_TARGETS_MISSING;
}
} else {
// Faulted, not running... blinky red
statusLED.setRGB(blinky, false, false);
if (anyTarget) {
status = PhotonStatus.NT_DISCONNECTED_TARGETS_VISIBLE;
} else {
status = PhotonStatus.NT_DISCONNECTED_TARGETS_MISSING;
}
}
blinkCounter++;
statusLED.setStatus(status);
}
public void publishMetrics() {

View File

@@ -0,0 +1,97 @@
/*
* Copyright (C) Photon Vision.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.photonvision.common.hardware;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.PropertyNamingStrategies;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Optional;
import org.photonvision.common.logging.LogGroup;
import org.photonvision.common.logging.Logger;
/**
* Our blessed images inject the current version via the build process in
* https://github.com/PhotonVision/photon-image-modifier
*
* <p>This class provides a convenient abstraction around this
*/
public class OsImageData {
private static final Logger logger = new Logger(OsImageData.class, LogGroup.General);
private static Path imageVersionFile = Path.of("/opt/photonvision/image-version");
private static Path imageMetadataFile = Path.of("/opt/photonvision/image-version.json");
/** The OS image version string, if available. This is legacy, use {@link ImageMetadata}. */
public static final Optional<String> IMAGE_VERSION = getImageVersion();
private static Optional<String> getImageVersion() {
if (!imageVersionFile.toFile().exists()) {
logger.warn("Photon cannot locate base OS image version at " + imageVersionFile.toString());
return Optional.empty();
}
try {
return Optional.of(Files.readString(imageVersionFile).strip());
} catch (IOException e) {
logger.error("Couldn't read image-version file", e);
}
return Optional.empty();
}
public static final Optional<ImageMetadata> IMAGE_METADATA = getImageMetadata();
public static record ImageMetadata(
String buildDate, String commitSha, String commitTag, String imageName, String imageSource) {}
private static Optional<ImageMetadata> getImageMetadata() {
if (!imageMetadataFile.toFile().exists()) {
logger.warn("Photon cannot locate OS image metadata at " + imageMetadataFile.toString());
return Optional.empty();
}
try {
String content = Files.readString(imageMetadataFile).strip();
ObjectMapper mapper =
new ObjectMapper().setPropertyNamingStrategy(PropertyNamingStrategies.SNAKE_CASE);
ImageMetadata md = mapper.readValue(content, ImageMetadata.class);
if (md.buildDate() == null
&& md.commitSha() == null
&& md.commitTag() == null
&& md.imageName() == null
&& md.imageSource() == null) {
logger.warn(
"OS image metadata JSON did not contain recognized fields; preserving legacy behavior");
return Optional.empty();
}
return Optional.of(md);
} catch (IOException e) {
logger.error("Couldn't read image metadata file", e);
} catch (Exception e) {
logger.error("Failed to parse image metadata", e);
}
return Optional.empty();
}
}

View File

@@ -1,55 +0,0 @@
/*
* Copyright (C) Photon Vision.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.photonvision.common.hardware;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Optional;
import org.photonvision.common.logging.LogGroup;
import org.photonvision.common.logging.Logger;
/**
* Our blessed images inject the current version via this build workflow:
* https://github.com/PhotonVision/photon-image-modifier/blob/2e5ddb6b599df0be921c12c8dbe7b939ecd7f615/.github/workflows/main.yml#L67
*
* <p>This class provides a convenient abstraction around this
*/
public class OsImageVersion {
private static final Logger logger = new Logger(OsImageVersion.class, LogGroup.General);
private static Path imageVersionFile = Path.of("/opt/photonvision/image-version");
public static final Optional<String> IMAGE_VERSION = getImageVersion();
private static Optional<String> getImageVersion() {
if (!imageVersionFile.toFile().exists()) {
logger.warn(
"Photon cannot locate base OS image version metadata at " + imageVersionFile.toString());
return Optional.empty();
}
try {
return Optional.of(Files.readString(imageVersionFile).strip());
} catch (IOException e) {
logger.error("Couldn't read image-version file", e);
}
return Optional.empty();
}
}

View File

@@ -0,0 +1,43 @@
/*
* Copyright (C) Photon Vision.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.photonvision.common.hardware;
public enum PhotonStatus {
// Nominal states
NT_CONNECTED_TARGETS_VISIBLE,
NT_CONNECTED_TARGETS_MISSING,
NT_DISCONNECTED_TARGETS_VISIBLE,
NT_DISCONNECTED_TARGETS_MISSING,
// Error states
GENERIC_ERROR(true);
final boolean error;
PhotonStatus() {
this(false);
}
PhotonStatus(boolean error) {
this.error = error;
}
boolean isError() {
return this.error;
}
}

View File

@@ -17,17 +17,21 @@
package org.photonvision.common.hardware;
import com.diozero.devices.LED;
import com.diozero.internal.spi.NativeDeviceFactoryInterface;
import java.util.List;
import org.photonvision.common.hardware.GPIO.CustomGPIO;
import org.photonvision.common.hardware.GPIO.GPIOBase;
import org.photonvision.common.hardware.GPIO.pi.PigpioPin;
import org.photonvision.common.util.TimedTaskManager;
public class StatusLED {
public final GPIOBase redLED;
public final GPIOBase greenLED;
public final GPIOBase blueLED;
public class StatusLED implements AutoCloseable {
public final LED redLED;
public final LED greenLED;
public final LED blueLED;
protected int blinkCounter;
public StatusLED(List<Integer> statusLedPins) {
protected PhotonStatus status = PhotonStatus.GENERIC_ERROR;
public StatusLED(
NativeDeviceFactoryInterface deviceFactory, List<Integer> statusLedPins, boolean activeHigh) {
// fill unassigned pins with -1 to disable
if (statusLedPins.size() != 3) {
for (int i = 0; i < 3 - statusLedPins.size(); i++) {
@@ -35,24 +39,53 @@ public class StatusLED {
}
}
if (Platform.isRaspberryPi()) {
redLED = new PigpioPin(statusLedPins.get(0));
greenLED = new PigpioPin(statusLedPins.get(1));
blueLED = new PigpioPin(statusLedPins.get(2));
} else {
redLED = new CustomGPIO(statusLedPins.get(0));
greenLED = new CustomGPIO(statusLedPins.get(1));
blueLED = new CustomGPIO(statusLedPins.get(2));
}
// Outputs are active-low for a common-anode RGB LED
redLED = new LED(deviceFactory, statusLedPins.get(0), activeHigh, false);
greenLED = new LED(deviceFactory, statusLedPins.get(1), activeHigh, false);
blueLED = new LED(deviceFactory, statusLedPins.get(2), activeHigh, false);
TimedTaskManager.getInstance().addTask("StatusLEDUpdate", this::updateLED, 150);
}
public void setRGB(boolean r, boolean g, boolean b) {
// Outputs are active-low, so invert the level applied
redLED.setState(!r);
redLED.setBrightness(r ? 0 : 100);
greenLED.setState(!g);
greenLED.setBrightness(g ? 0 : 100);
blueLED.setState(!b);
blueLED.setBrightness(b ? 0 : 100);
protected void setRGB(boolean r, boolean g, boolean b) {
redLED.setOn(r);
greenLED.setOn(g);
blueLED.setOn(b);
}
public void setStatus(PhotonStatus status) {
this.status = status;
}
protected void updateLED() {
boolean blink = blinkCounter > 0;
switch (status) {
case NT_CONNECTED_TARGETS_VISIBLE ->
// Blue
setRGB(false, false, true);
case NT_CONNECTED_TARGETS_MISSING ->
// Blinking Green
setRGB(false, blink, false);
case NT_DISCONNECTED_TARGETS_VISIBLE ->
// Blinking Blue
setRGB(false, false, blink);
case NT_DISCONNECTED_TARGETS_MISSING ->
// Blinking Yellow
setRGB(blink, blink, false);
case GENERIC_ERROR ->
// Blinking Red
setRGB(blink, false, false);
}
blinkCounter++;
blinkCounter %= 3;
}
@Override
public void close() {
redLED.close();
greenLED.close();
blueLED.close();
}
}

View File

@@ -17,54 +17,62 @@
package org.photonvision.common.hardware;
import com.diozero.devices.LED;
import com.diozero.devices.PwmLed;
import com.diozero.internal.spi.NativeDeviceFactoryInterface;
import com.diozero.sbc.BoardPinInfo;
import edu.wpi.first.networktables.NetworkTableEvent;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.BooleanSupplier;
import java.util.function.Consumer;
import org.photonvision.common.hardware.GPIO.CustomGPIO;
import org.photonvision.common.hardware.GPIO.GPIOBase;
import org.photonvision.common.hardware.GPIO.pi.PigpioException;
import org.photonvision.common.hardware.GPIO.pi.PigpioPin;
import org.photonvision.common.hardware.GPIO.pi.PigpioSocket;
import org.photonvision.common.logging.LogGroup;
import org.photonvision.common.logging.Logger;
import org.photonvision.common.util.TimedTaskManager;
import org.photonvision.common.util.math.MathUtils;
public class VisionLED {
public class VisionLED implements AutoCloseable {
private static final Logger logger = new Logger(VisionLED.class, LogGroup.VisionModule);
private static final String blinkTaskID = "VisionLEDBlink";
private final int[] ledPins;
private final List<GPIOBase> visionLEDs = new ArrayList<>();
private final List<LED> visionLEDs = new ArrayList<>();
private final List<PwmLed> dimmableVisionLEDs = new ArrayList<>();
private final int brightnessMin;
private final int brightnessMax;
private final PigpioSocket pigpioSocket;
private VisionLEDMode currentLedMode = VisionLEDMode.kDefault;
private BooleanSupplier pipelineModeSupplier;
private int mappedBrightnessPercentage;
private float mappedBrightness = 0.0f;
private final Consumer<Integer> modeConsumer;
public VisionLED(
NativeDeviceFactoryInterface deviceFactory,
List<Integer> ledPins,
boolean ledsCanDim,
int brightnessMin,
int brightnessMax,
PigpioSocket pigpioSocket,
int pwmFrequency,
Consumer<Integer> visionLEDmode) {
this.brightnessMin = brightnessMin;
this.brightnessMax = brightnessMax;
this.pigpioSocket = pigpioSocket;
this.modeConsumer = visionLEDmode;
this.ledPins = ledPins.stream().mapToInt(i -> i).toArray();
if (pwmFrequency > 0) {
deviceFactory.setBoardPwmFrequency(pwmFrequency);
}
BoardPinInfo boardPinInfo = deviceFactory.getBoardPinInfo();
ledPins.forEach(
pin -> {
if (Platform.isRaspberryPi()) {
visionLEDs.add(new PigpioPin(pin));
if (ledsCanDim && boardPinInfo.getByPwmOrGpioNumberOrThrow(pin).isPwmOutputSupported()) {
PwmLed led = new PwmLed(deviceFactory, pin);
if (pwmFrequency > 0) {
led.setPwmFrequency(pwmFrequency);
}
dimmableVisionLEDs.add(led);
} else {
visionLEDs.add(new CustomGPIO(pin));
visionLEDs.add(new LED(deviceFactory, pin));
}
});
pipelineModeSupplier = () -> false;
@@ -75,7 +83,8 @@ public class VisionLED {
}
public void setBrightness(int percentage) {
mappedBrightnessPercentage = MathUtils.map(percentage, 0, 100, brightnessMin, brightnessMax);
mappedBrightness =
(float) (MathUtils.map(percentage, 0.0, 100.0, brightnessMin, brightnessMax) / 100.0);
setInternal(currentLedMode, false);
}
@@ -87,42 +96,32 @@ public class VisionLED {
}
private void blinkImpl(int pulseLengthMillis, int blinkCount) {
if (Platform.isRaspberryPi()) {
try {
setStateImpl(false); // hack to ensure hardware PWM has stopped before trying to blink
pigpioSocket.generateAndSendWaveform(pulseLengthMillis, blinkCount, ledPins);
} catch (PigpioException e) {
logger.error("Failed to blink!", e);
} catch (NullPointerException e) {
logger.error("Failed to blink, pigpio internal issue!", e);
}
} else {
for (GPIOBase led : visionLEDs) {
led.blink(pulseLengthMillis, blinkCount);
}
}
TimedTaskManager.getInstance().cancelTask(blinkTaskID);
AtomicInteger blinks = new AtomicInteger();
TimedTaskManager.getInstance()
.addTask(
blinkTaskID,
() -> {
for (LED led : visionLEDs) {
led.toggle();
}
for (PwmLed led : dimmableVisionLEDs) {
led.setValue(mappedBrightness - led.getValue());
}
if (blinks.incrementAndGet() >= blinkCount * 2) {
TimedTaskManager.getInstance().cancelTask(blinkTaskID);
}
},
pulseLengthMillis);
}
private void setStateImpl(boolean state) {
if (Platform.isRaspberryPi()) {
try {
// stop any active blink
pigpioSocket.waveTxStop();
} catch (PigpioException e) {
logger.error("Failed to stop blink!", e);
} catch (NullPointerException e) {
logger.error("Failed to blink, pigpio internal issue!", e);
}
TimedTaskManager.getInstance().cancelTask(blinkTaskID);
for (LED led : visionLEDs) {
led.setOn(state);
}
try {
// if the user has set an LED brightness other than 100%, use that instead
if (mappedBrightnessPercentage == 100 || !state) {
visionLEDs.forEach((led) -> led.setState(state));
} else {
visionLEDs.forEach((led) -> led.setBrightness(mappedBrightnessPercentage));
}
} catch (NullPointerException e) {
logger.error("Failed to blink, pigpio internal issue!", e);
for (PwmLed led : dimmableVisionLEDs) {
led.setValue(mappedBrightness);
}
}
@@ -152,28 +151,42 @@ public class VisionLED {
}
private void setInternal(VisionLEDMode newLedMode, boolean fromNT) {
// LED state has three different sources:
// Pipeline, which supplies the default LED state
// NT, which supplies the user override LED state
// Internal methods, which supply special actions such as the startup blink
//
// LED state is set with this method when the pipeline changes, NT state changes,
// or an internal request is received.
var lastLedMode = currentLedMode;
if (fromNT) {
if (fromNT || currentLedMode == VisionLEDMode.kDefault) {
switch (newLedMode) {
case kDefault -> setStateImpl(pipelineModeSupplier.getAsBoolean());
case kOff -> setStateImpl(false);
case kOn -> setStateImpl(true);
case kBlink -> blinkImpl(85, -1);
}
}
if (fromNT) {
currentLedMode = newLedMode;
logger.info(
"Changing LED mode from \"" + lastLedMode.toString() + "\" to \"" + newLedMode + "\"");
} else {
if (currentLedMode == VisionLEDMode.kDefault) {
switch (newLedMode) {
case kDefault -> setStateImpl(pipelineModeSupplier.getAsBoolean());
case kOff -> setStateImpl(false);
case kOn -> setStateImpl(true);
case kBlink -> blinkImpl(85, -1);
}
}
logger.info("Changing LED internal state to " + newLedMode.toString());
}
}
@Override
public void close() {
TimedTaskManager.getInstance().cancelTask(blinkTaskID);
for (LED led : visionLEDs) {
led.close();
}
for (PwmLed led : dimmableVisionLEDs) {
led.close();
}
}
}

View File

@@ -0,0 +1,87 @@
/*
* Copyright (C) Photon Vision.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.photonvision.common.hardware.gpio;
import org.photonvision.common.logging.LogGroup;
import org.photonvision.common.logging.Logger;
import org.photonvision.common.util.ShellExec;
public class CustomAdapter {
private static final Logger logger = new Logger(CustomAdapter.class, LogGroup.General);
private static final ThreadLocal<ShellExec> runCommand =
ThreadLocal.withInitial(() -> new ShellExec(true, true));
protected final String getGPIOCommand;
protected final String setGPIOCommand;
protected final String setPWMCommand;
protected final String setPWMFrequencyCommand;
protected final String releaseGPIOCommand;
public CustomAdapter(
String getGPIOCommand,
String setGPIOCommand,
String setPWMCommand,
String setPWMFrequencyCommand,
String releaseGPIOCommand) {
this.getGPIOCommand = getGPIOCommand;
this.setGPIOCommand = setGPIOCommand;
this.setPWMCommand = setPWMCommand;
this.setPWMFrequencyCommand = setPWMFrequencyCommand;
this.releaseGPIOCommand = releaseGPIOCommand;
}
protected static String execute(String command) {
try {
runCommand.get().executeBashCommand(command);
} catch (Exception e) {
logger.error("Exception caught running GPIO command \"" + command + "\"", e);
return "";
}
return runCommand.get().getOutput();
}
public boolean getGPIO(int gpio) {
return Boolean.parseBoolean(
execute(getGPIOCommand.replace("{p}", Integer.toString(gpio))).trim());
}
public void setGPIO(int gpio, boolean state) {
execute(
setGPIOCommand
.replace("{p}", Integer.toString(gpio))
.replace("{s}", Boolean.toString(state)));
}
public void setPWM(int gpio, double value) {
execute(
setPWMCommand
.replace("{p}", Integer.toString(gpio))
.replace("{v}", Double.toString(value)));
}
public void setPwmFrequency(int gpio, int frequency) {
execute(
setPWMFrequencyCommand
.replace("{p}", Integer.toString(gpio))
.replace("{f}", Integer.toString(frequency)));
}
public void releaseGPIO(int gpio) {
execute(releaseGPIOCommand.replace("{p}", Integer.toString(gpio)));
}
}

View File

@@ -0,0 +1,201 @@
/*
* Copyright (C) Photon Vision.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.photonvision.common.hardware.gpio;
import com.diozero.api.DeviceMode;
import com.diozero.api.GpioEventTrigger;
import com.diozero.api.GpioPullUpDown;
import com.diozero.api.I2CConstants.AddressSize;
import com.diozero.api.PinInfo;
import com.diozero.api.RuntimeIOException;
import com.diozero.api.SerialConstants.DataBits;
import com.diozero.api.SerialConstants.Parity;
import com.diozero.api.SerialConstants.StopBits;
import com.diozero.api.SpiClockMode;
import com.diozero.internal.spi.AnalogInputDeviceInterface;
import com.diozero.internal.spi.AnalogOutputDeviceInterface;
import com.diozero.internal.spi.BaseNativeDeviceFactory;
import com.diozero.internal.spi.GpioDigitalInputDeviceInterface;
import com.diozero.internal.spi.GpioDigitalInputOutputDeviceInterface;
import com.diozero.internal.spi.GpioDigitalOutputDeviceInterface;
import com.diozero.internal.spi.InternalI2CDeviceInterface;
import com.diozero.internal.spi.InternalPwmOutputDeviceInterface;
import com.diozero.internal.spi.InternalSerialDeviceInterface;
import com.diozero.internal.spi.InternalServoDeviceInterface;
import com.diozero.internal.spi.InternalSpiDeviceInterface;
import org.photonvision.common.logging.LogGroup;
import org.photonvision.common.logging.Logger;
public class CustomDeviceFactory extends BaseNativeDeviceFactory {
public static final String DEVICE_NAME = "Custom";
public static final String GET_GPIO_PROP = "diozero.custom.getGPIO";
public static final String SET_GPIO_PROP = "diozero.custom.setGPIO";
public static final String SET_PWM_PROP = "diozero.custom.setPWM";
public static final String SET_PWM_FREQUENCY_PROP = "diozero.custom.setPWMFrequency";
public static final String RELEASE_GPIO_PROP = "diozero.custom.releaseGPIO";
private static final Logger logger = new Logger(CustomDeviceFactory.class, LogGroup.General);
public final CustomAdapter adapter;
public CustomDeviceFactory(CustomAdapter adapter) {
this.adapter = adapter;
}
public CustomDeviceFactory() {
String getGPIOCommand = System.getProperty(GET_GPIO_PROP, "");
String setGPIOCommand = System.getProperty(SET_GPIO_PROP, "");
String setPWMCommand = System.getProperty(SET_PWM_PROP, "");
String setPWMFrequencyCommand = System.getProperty(SET_PWM_FREQUENCY_PROP, "");
String releaseGPIOCommand = System.getProperty(RELEASE_GPIO_PROP, "");
this.adapter =
new CustomAdapter(
getGPIOCommand,
setGPIOCommand,
setPWMCommand,
setPWMFrequencyCommand,
releaseGPIOCommand);
}
@Override
public void start() {}
@Override
public void shutdown() {}
@Override
public String getName() {
return DEVICE_NAME;
}
@Override
public int getGpioValue(int gpio) {
return adapter.getGPIO(gpio) ? 1 : 0;
}
@Override
public DeviceMode getGpioMode(int gpio) {
throw new UnsupportedOperationException("GPIO mode can not be retrieved");
}
@Override
public int getBoardPwmFrequency() {
throw new UnsupportedOperationException("PWM frequency cannot be retrieved");
}
@Override
public void setBoardPwmFrequency(int pwmFrequency) {
logger.warn("Setting PWM frequency is not implemented");
}
@Override
public int getBoardServoFrequency() {
throw new UnsupportedOperationException("Servo frequency cannot be retrieved");
}
@Override
public void setBoardServoFrequency(int servoFrequency) {
logger.warn("Setting servo frequency is not implemented");
}
@Override
public GpioDigitalInputDeviceInterface createDigitalInputDevice(
String key, PinInfo pinInfo, GpioPullUpDown pud, GpioEventTrigger trigger) {
return new CustomDigitalInputDevice(this, key, pinInfo.getDeviceNumber(), pud, trigger);
}
@Override
public GpioDigitalOutputDeviceInterface createDigitalOutputDevice(
String key, PinInfo pinInfo, boolean initialValue) {
return new CustomDigitalOutputDevice(this, key, pinInfo.getDeviceNumber(), initialValue);
}
@Override
public GpioDigitalInputOutputDeviceInterface createDigitalInputOutputDevice(
String key, PinInfo pinInfo, DeviceMode mode) {
return new CustomDigitalInputOutputDevice(this, key, pinInfo.getDeviceNumber(), mode);
}
@Override
public InternalPwmOutputDeviceInterface createPwmOutputDevice(
String key, PinInfo pinInfo, int pwmFrequency, float initialValue) {
return new CustomPwmOutputDevice(
this, key, pinInfo.getDeviceNumber(), pwmFrequency, initialValue);
}
@Override
public InternalServoDeviceInterface createServoDevice(
String key,
PinInfo pinInfo,
int frequencyHz,
int minPulseWidthUs,
int maxPulseWidthUs,
int initialPulseWidthUs) {
throw new UnsupportedOperationException(
"Servo devices are not supported by this device factory");
}
@Override
public AnalogInputDeviceInterface createAnalogInputDevice(String key, PinInfo pinInfo) {
throw new UnsupportedOperationException(
"Analog inputs are not supported by this device factory");
}
@Override
public AnalogOutputDeviceInterface createAnalogOutputDevice(
String key, PinInfo pinInfo, float initialValue) {
throw new UnsupportedOperationException(
"Analog outputs are not supported by this device factory");
}
@Override
public InternalSpiDeviceInterface createSpiDevice(
String key,
int controller,
int chipSelect,
int frequency,
SpiClockMode spiClockMode,
boolean lsbFirst)
throws RuntimeIOException {
throw new UnsupportedOperationException("SPI devices are not supported by this device factory");
}
@Override
public InternalI2CDeviceInterface createI2CDevice(
String key, int controller, int address, AddressSize addressSize) throws RuntimeIOException {
throw new UnsupportedOperationException("I2C devices are not supported by this device factory");
}
@Override
public InternalSerialDeviceInterface createSerialDevice(
String key,
String deviceFilename,
int baud,
DataBits dataBits,
StopBits stopBits,
Parity parity,
boolean readBlocking,
int minReadChars,
int readTimeoutMillis)
throws RuntimeIOException {
throw new UnsupportedOperationException(
"Serial communication is not supported by this device factory");
}
}

View File

@@ -0,0 +1,63 @@
/*
* Copyright (C) Photon Vision.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.photonvision.common.hardware.gpio;
import com.diozero.api.DigitalInputEvent;
import com.diozero.api.GpioEventTrigger;
import com.diozero.api.GpioPullUpDown;
import com.diozero.api.RuntimeIOException;
import com.diozero.internal.spi.AbstractInputDevice;
import com.diozero.internal.spi.GpioDigitalInputDeviceInterface;
public class CustomDigitalInputDevice extends AbstractInputDevice<DigitalInputEvent>
implements GpioDigitalInputDeviceInterface {
protected final CustomAdapter adapter;
protected final int gpio;
public CustomDigitalInputDevice(
CustomDeviceFactory deviceFactory,
String key,
int gpio,
GpioPullUpDown pud,
GpioEventTrigger trigger) {
super(key, deviceFactory);
adapter = deviceFactory.adapter;
this.gpio = gpio;
}
@Override
public boolean getValue() throws RuntimeIOException {
return adapter.getGPIO(gpio);
}
@Override
public int getGpio() {
return gpio;
}
@Override
public void setDebounceTimeMillis(int debounceTime) {
throw new UnsupportedOperationException("Debounce is not supported");
}
@Override
protected void closeDevice() throws RuntimeIOException {
adapter.releaseGPIO(gpio);
}
}

View File

@@ -0,0 +1,71 @@
/*
* Copyright (C) Photon Vision.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.photonvision.common.hardware.gpio;
import com.diozero.api.DeviceMode;
import com.diozero.api.DigitalInputEvent;
import com.diozero.api.RuntimeIOException;
import com.diozero.internal.spi.AbstractInputDevice;
import com.diozero.internal.spi.GpioDigitalInputOutputDeviceInterface;
public class CustomDigitalInputOutputDevice extends AbstractInputDevice<DigitalInputEvent>
implements GpioDigitalInputOutputDeviceInterface {
protected final CustomAdapter adapter;
protected final int gpio;
private boolean outputValue = false;
public CustomDigitalInputOutputDevice(
CustomDeviceFactory deviceFactory, String key, int gpio, DeviceMode mode) {
super(key, deviceFactory);
this.adapter = deviceFactory.adapter;
this.gpio = gpio;
setMode(mode);
}
@Override
public void setValue(boolean value) throws RuntimeIOException {
outputValue = value;
setValue(value);
}
@Override
public boolean getValue() throws RuntimeIOException {
return adapter.getGPIO(gpio);
}
@Override
public int getGpio() {
return gpio;
}
@Override
public void setMode(DeviceMode mode) {
if (mode == DeviceMode.DIGITAL_INPUT) {
getValue(); // Ensure the pin direction is input
} else if (mode == DeviceMode.DIGITAL_OUTPUT) {
setValue(outputValue); // Restore the last output state
}
}
@Override
public void closeDevice() {
adapter.releaseGPIO(gpio);
}
}

View File

@@ -0,0 +1,61 @@
/*
* Copyright (C) Photon Vision.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.photonvision.common.hardware.gpio;
import com.diozero.api.RuntimeIOException;
import com.diozero.internal.spi.AbstractDevice;
import com.diozero.internal.spi.GpioDigitalOutputDeviceInterface;
public class CustomDigitalOutputDevice extends AbstractDevice
implements GpioDigitalOutputDeviceInterface {
protected final CustomAdapter adapter;
protected final int gpio;
private boolean state;
public CustomDigitalOutputDevice(
CustomDeviceFactory deviceFactory, String key, int gpio, boolean initialValue) {
super(key, deviceFactory);
this.adapter = deviceFactory.adapter;
this.gpio = gpio;
this.state = initialValue;
setValue(initialValue);
}
@Override
public boolean getValue() throws RuntimeIOException {
return state;
}
@Override
public int getGpio() {
return gpio;
}
@Override
public void setValue(boolean value) throws RuntimeIOException {
state = value;
adapter.setGPIO(gpio, value);
}
@Override
protected void closeDevice() throws RuntimeIOException {
adapter.releaseGPIO(gpio);
}
}

View File

@@ -0,0 +1,82 @@
/*
* Copyright (C) Photon Vision.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.photonvision.common.hardware.gpio;
import com.diozero.api.RuntimeIOException;
import com.diozero.internal.spi.AbstractDevice;
import com.diozero.internal.spi.InternalPwmOutputDeviceInterface;
public class CustomPwmOutputDevice extends AbstractDevice
implements InternalPwmOutputDeviceInterface {
protected final CustomAdapter adapter;
protected final int gpio;
private float state;
private int frequency;
public CustomPwmOutputDevice(
CustomDeviceFactory deviceFactory,
String key,
int gpio,
int pwmFrequency,
float initialValue) {
super(key, deviceFactory);
this.adapter = deviceFactory.adapter;
this.gpio = gpio;
this.frequency = pwmFrequency;
setValue(initialValue);
}
@Override
public int getGpio() {
return gpio;
}
@Override
public int getPwmNum() {
return gpio;
}
@Override
public float getValue() throws RuntimeIOException {
return state;
}
@Override
public void setValue(float value) throws RuntimeIOException {
state = value;
adapter.setPWM(gpio, value);
}
@Override
public int getPwmFrequency() throws RuntimeIOException {
return frequency;
}
@Override
public void setPwmFrequency(int frequencyHz) throws RuntimeIOException {
frequency = frequencyHz;
adapter.setPwmFrequency(gpio, frequencyHz);
}
@Override
protected void closeDevice() throws RuntimeIOException {
adapter.releaseGPIO(gpio);
}
}

View File

@@ -17,7 +17,9 @@
package org.photonvision.common.networking;
import edu.wpi.first.networktables.NetworkTableInstance;
import java.io.IOException;
import java.net.InetAddress;
import java.net.NetworkInterface;
import java.util.ArrayList;
import java.util.List;
@@ -218,40 +220,57 @@ public class NetworkUtils {
return String.join(", ", addresses);
}
/**
* Gets a MAC address of a network interface. On devices where networking is managed by
* PhotonVision, this will return the MAC address of the configured interface. Otherwise, this
* will attempt to search for the network interface in current use and use that interface's MAC
* address, and if that fails, it will return a MAC address from the first network interface with
* a MAC address, as sorted by {@link NetworkInterface#networkInterfaces()}.
*
* @return The MAC address.
*/
public static String getMacAddress() {
var config = ConfigManager.getInstance().getConfig().getNetworkConfig();
if (config.networkManagerIface == null || config.networkManagerIface.isBlank()) {
// This is a silly heuristic to find a network interface that PV might be using. It looks like
// it works pretty well, but Hyper-V adapters still show up in the list. But we're using MAC
// address as a semi-unique identifier, not as a source of truth, so this should be fine.
// Hyper-V adapters seem to show up near the end of the list anyways, so it's super likely
// we'll find the right adapter anyways
try {
for (var iface : NetworkInterface.networkInterfaces().toList()) {
if (iface.isUp() && !iface.isVirtual() && !iface.isLoopback()) {
byte[] mac = iface.getHardwareAddress();
if (mac == null) {
logger.error("No MAC address found for " + iface.getDisplayName());
}
try {
// Not managed? See if we're connected to a network. General assumption is one interface in
// use at a time
if (config.networkManagerIface == null || config.networkManagerIface.isBlank()) {
// Use NT client IP address to find the interface in use
if (!config.runNTServer) {
var conn = NetworkTableInstance.getDefault().getConnections();
if (conn.length > 0 && !conn[0].remote_ip.equals("127.0.0.1")) {
var addr = InetAddress.getByName(conn[0].remote_ip);
return formatMacAddress(NetworkInterface.getByInetAddress(addr).getHardwareAddress());
}
}
// Connected to a localhost server or we are the server? Try resolving ourselves. Only
// returns a localhost address when there's no other interface available on Windows, but
// like to return a localhost address on Linux
var localIface = NetworkInterface.getByInetAddress(InetAddress.getLocalHost());
if (localIface != null) {
byte[] mac = localIface.getHardwareAddress();
if (mac != null) {
return formatMacAddress(mac);
}
}
} catch (Exception e) {
logger.error("Error getting MAC address:", e);
// Fine. Just find something with a MAC address
for (var iface : NetworkInterface.networkInterfaces().toList()) {
if (iface.isUp() && iface.getHardwareAddress() != null) {
return formatMacAddress(iface.getHardwareAddress());
}
}
} else { // Managed? We should have a working interface available
byte[] mac = NetworkInterface.getByName(config.networkManagerIface).getHardwareAddress();
if (mac != null) {
return formatMacAddress(mac);
} else {
logger.error("No MAC address found for " + config.networkManagerIface);
}
}
return "";
}
try {
byte[] mac = NetworkInterface.getByName(config.networkManagerIface).getHardwareAddress();
if (mac == null) {
logger.error("No MAC address found for " + config.networkManagerIface);
return "";
}
return formatMacAddress(mac);
} catch (Exception e) {
logger.error("Error getting MAC address for " + config.networkManagerIface, e);
return "";
logger.error("Error getting MAC address", e);
}
return "";
}
private static String formatMacAddress(byte[] mac) {

View File

@@ -26,16 +26,11 @@ import java.io.IOException;
import java.nio.file.Path;
import org.opencv.core.Mat;
import org.opencv.highgui.HighGui;
import org.photonvision.jni.LibraryLoader;
import org.photonvision.vision.calibration.CameraCalibrationCoefficients;
import org.photonvision.vision.pipeline.result.CVPipelineResult;
import org.photonvision.vision.target.TrackedTarget;
public class TestUtils {
public static boolean loadLibraries() {
return LibraryLoader.loadWpiLibraries() && LibraryLoader.loadTargeting();
}
@SuppressWarnings("unused")
public enum WPI2019Image {
kCargoAngledDark48in(1.2192),

View File

@@ -18,24 +18,22 @@
package org.photonvision.common.util.file;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.json.JsonReadFeature;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.JsonDeserializer;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializerProvider;
import com.fasterxml.jackson.databind.deser.std.StdDeserializer;
import com.fasterxml.jackson.databind.ext.NioPathDeserializer;
import com.fasterxml.jackson.databind.ext.NioPathSerializer;
import com.fasterxml.jackson.databind.json.JsonMapper;
import com.fasterxml.jackson.databind.jsontype.BasicPolymorphicTypeValidator;
import com.fasterxml.jackson.databind.jsontype.PolymorphicTypeValidator;
import com.fasterxml.jackson.databind.module.SimpleModule;
import com.fasterxml.jackson.databind.ser.std.StdSerializer;
import java.io.File;
import java.io.FileDescriptor;
import java.io.FileOutputStream;
import java.io.IOException;
import java.net.URI;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.HashMap;
@@ -45,37 +43,20 @@ import org.eclipse.jetty.io.EofException;
public class JacksonUtils {
public static class UIMap extends HashMap<String, Object> {}
// Custom Path serializer that outputs just the path string without file:/ prefix
public static class PathSerializer extends JsonSerializer<Path> {
// Custom Path key deserializer for Maps with Path keys
public static class PathKeySerializer
extends com.fasterxml.jackson.databind.JsonSerializer<Path> {
@Override
public void serialize(Path value, JsonGenerator gen, SerializerProvider serializers)
throws IOException {
if (value == null) {
gen.writeNull();
} else {
gen.writeString(value.toString());
gen.writeFieldName(value.toUri().toString());
}
}
}
// Custom Path deserializer that reads path strings
public static class PathDeserializer extends JsonDeserializer<Path> {
@Override
public Path deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
String pathString = p.getValueAsString();
if (pathString == null || pathString.isEmpty()) {
return null;
}
// Handle case where old serialized data might still have file:/ prefix
if (pathString.startsWith("file:/")) {
pathString = pathString.substring(6); // Remove "file:/" prefix
}
return Paths.get(pathString);
}
}
// Custom Path key deserializer for Maps with Path keys
public static class PathKeyDeserializer extends com.fasterxml.jackson.databind.KeyDeserializer {
@Override
@@ -83,13 +64,7 @@ public class JacksonUtils {
if (key == null || key.isEmpty()) {
return null;
}
// Handle case where old serialized data might still have file:/ prefix
if (key.startsWith("file:/")) {
key = key.substring(6); // Remove "file:/" prefix
}
return Paths.get(key);
return Paths.get(URI.create(key));
}
}
@@ -99,8 +74,9 @@ public class JacksonUtils {
BasicPolymorphicTypeValidator.builder().allowIfBaseType(baseType).build();
SimpleModule pathModule = new SimpleModule();
pathModule.addSerializer(Path.class, new PathSerializer());
pathModule.addDeserializer(Path.class, new PathDeserializer());
pathModule.addSerializer(Path.class, new NioPathSerializer());
pathModule.addKeySerializer(Path.class, new PathKeySerializer());
pathModule.addDeserializer(Path.class, new NioPathDeserializer());
pathModule.addKeyDeserializer(Path.class, new PathKeyDeserializer());
return JsonMapper.builder()
@@ -151,48 +127,6 @@ public class JacksonUtils {
return null;
}
public static <T> T deserialize(Path path, Class<T> ref, StdDeserializer<T> deserializer)
throws IOException {
ObjectMapper objectMapper = new ObjectMapper();
SimpleModule module = new SimpleModule();
module.addDeserializer(ref, deserializer);
// Add Path support to custom deserializer case as well
module.addSerializer(Path.class, new PathSerializer());
module.addDeserializer(Path.class, new PathDeserializer());
module.addKeyDeserializer(Path.class, new PathKeyDeserializer());
objectMapper.registerModule(module);
File jsonFile = new File(path.toString());
if (jsonFile.exists() && jsonFile.length() > 0) {
return objectMapper.readValue(jsonFile, ref);
}
return null;
}
public static <T> void serialize(Path path, T object, Class<T> ref, StdSerializer<T> serializer)
throws IOException {
serialize(path, object, ref, serializer, true);
}
public static <T> void serialize(
Path path, T object, Class<T> ref, StdSerializer<T> serializer, boolean forceSync)
throws IOException {
ObjectMapper objectMapper = new ObjectMapper();
SimpleModule module = new SimpleModule();
module.addSerializer(ref, serializer);
// Add Path support to custom serializer case as well
module.addSerializer(Path.class, new PathSerializer());
module.addDeserializer(Path.class, new PathDeserializer());
module.addKeyDeserializer(Path.class, new PathKeyDeserializer());
objectMapper.registerModule(module);
String json = objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(object);
saveJsonString(json, path, forceSync);
}
private static void saveJsonString(String json, Path path, boolean forceSync) throws IOException {
var file = path.toFile();
if (file.getParentFile() != null && !file.getParentFile().exists()) {

View File

@@ -1,83 +0,0 @@
/*
* Copyright (C) Photon Vision.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.photonvision.jni;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.List;
import org.photonvision.common.hardware.Platform;
import org.photonvision.common.logging.LogGroup;
import org.photonvision.common.logging.Logger;
public abstract class PhotonJNICommon {
public abstract boolean isLoaded();
public abstract void setLoaded(boolean state);
protected static Logger logger = null;
protected static synchronized void forceLoad(
PhotonJNICommon instance, Class<?> clazz, List<String> libraries) throws IOException {
if (instance.isLoaded()) return;
if (logger == null) logger = new Logger(clazz, LogGroup.Camera);
for (var libraryName : libraries) {
try {
// We always extract the shared object (we could hash each so, but that's a lot of work)
var arch_name = Platform.getNativeLibraryFolderName();
var nativeLibName = System.mapLibraryName(libraryName);
var in = clazz.getResourceAsStream("/nativelibraries/" + arch_name + "/" + nativeLibName);
if (in == null) {
instance.setLoaded(false);
return;
}
// It's important that we don't mangle the names of these files on Windows at least
File temp = new File(System.getProperty("java.io.tmpdir"), nativeLibName);
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();
System.load(temp.getAbsolutePath());
logger.info("Successfully loaded shared object " + temp.getName());
} catch (UnsatisfiedLinkError e) {
logger.error("Couldn't load shared object " + libraryName, e);
e.printStackTrace();
// logger.error(System.getProperty("java.library.path"));
instance.setLoaded(false);
return;
}
}
instance.setLoaded(true);
}
protected static synchronized void forceLoad(
PhotonJNICommon instance, Class<?> clazz, String libraryName) throws IOException {
forceLoad(instance, clazz, List.of(libraryName));
}
}

View File

@@ -1,53 +0,0 @@
/*
* Copyright (C) Photon Vision.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.photonvision.jni;
import java.io.IOException;
import java.util.List;
import org.photonvision.common.util.TestUtils;
public class RknnDetectorJNI extends PhotonJNICommon {
private boolean isLoaded;
private static RknnDetectorJNI instance = null;
private RknnDetectorJNI() {
isLoaded = false;
}
public static RknnDetectorJNI getInstance() {
if (instance == null) instance = new RknnDetectorJNI();
return instance;
}
public static synchronized void forceLoad() throws IOException {
TestUtils.loadLibraries();
forceLoad(getInstance(), RknnDetectorJNI.class, List.of("rga", "rknnrt", "rknn_jni"));
}
@Override
public boolean isLoaded() {
return isLoaded;
}
@Override
public void setLoaded(boolean state) {
isLoaded = state;
}
}

View File

@@ -1,56 +0,0 @@
/*
* Copyright (C) Photon Vision.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.photonvision.jni;
import java.io.IOException;
import java.util.List;
import org.photonvision.common.util.TestUtils;
public class RubikDetectorJNI extends PhotonJNICommon {
private boolean isLoaded;
private static RubikDetectorJNI instance = null;
private RubikDetectorJNI() {
isLoaded = false;
}
public static RubikDetectorJNI getInstance() {
if (instance == null) instance = new RubikDetectorJNI();
return instance;
}
public static synchronized void forceLoad() throws IOException {
TestUtils.loadLibraries();
forceLoad(
getInstance(),
RubikDetectorJNI.class,
List.of("tensorflowlite", "tensorflowlite_c", "external_delegate", "rubik_jni"));
}
@Override
public boolean isLoaded() {
return isLoaded;
}
@Override
public void setLoaded(boolean state) {
isLoaded = state;
}
}

View File

@@ -1,82 +0,0 @@
/*
* Copyright (C) Photon Vision.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.photonvision.mrcal;
import java.io.IOException;
import java.util.List;
import org.photonvision.common.hardware.Platform;
import org.photonvision.common.util.TestUtils;
import org.photonvision.jni.PhotonJNICommon;
public class MrCalJNILoader extends PhotonJNICommon {
private boolean isLoaded;
private static MrCalJNILoader instance = null;
private MrCalJNILoader() {
isLoaded = false;
}
public static synchronized MrCalJNILoader getInstance() {
if (instance == null) instance = new MrCalJNILoader();
return instance;
}
public static synchronized void forceLoad() throws IOException {
// Force load opencv
TestUtils.loadLibraries();
// Library naming is dumb and has "lib" appended for Windows when it ought not to
if (Platform.isWindows()) {
// Order is correct to match dependencies of libraries
forceLoad(
MrCalJNILoader.getInstance(),
MrCalJNILoader.class,
List.of(
"libamd",
"libcamd",
"libcolamd",
"libccolamd",
"openblas",
"libwinpthread-1",
"libgcc_s_seh-1",
"libquadmath-0",
"libgfortran-5",
"liblapack",
"libcholmod",
"mrcal_jni"));
} else {
// Nothing else to do on linux
forceLoad(MrCalJNILoader.getInstance(), MrCalJNILoader.class, List.of("mrcal_jni"));
}
if (!MrCalJNILoader.getInstance().isLoaded()) {
throw new IOException("Unable to load mrcal JNI!");
}
}
@Override
public boolean isLoaded() {
return isLoaded;
}
@Override
public void setLoaded(boolean state) {
isLoaded = state;
}
}

View File

@@ -1,53 +0,0 @@
/*
* Copyright (C) Photon Vision.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.photonvision.raspi;
import java.io.IOException;
import java.util.List;
import org.photonvision.jni.PhotonJNICommon;
/** Helper for extracting photon-libcamera-gl-driver shared library files. */
public class LibCameraJNILoader extends PhotonJNICommon {
private boolean libraryLoaded = false;
private static LibCameraJNILoader instance = null;
public static synchronized LibCameraJNILoader getInstance() {
if (instance == null) instance = new LibCameraJNILoader();
return instance;
}
public static synchronized void forceLoad() throws IOException {
forceLoad(
LibCameraJNILoader.getInstance(), LibCameraJNILoader.class, List.of("photonlibcamera"));
}
@Override
public boolean isLoaded() {
return libraryLoaded;
}
@Override
public void setLoaded(boolean state) {
libraryLoaded = state;
}
public boolean isSupported() {
return libraryLoaded && LibCameraJNI.isSupported();
}
}

View File

@@ -26,13 +26,12 @@ import org.opencv.core.Size;
import org.photonvision.common.logging.LogGroup;
import org.photonvision.common.logging.Logger;
import org.photonvision.common.util.ColorHelper;
import org.photonvision.jni.RknnDetectorJNI;
import org.photonvision.rknn.RknnJNI;
import org.photonvision.vision.pipe.impl.NeuralNetworkPipeResult;
/** Manages an object detector using the rknn backend. */
public class RknnObjectDetector implements ObjectDetector {
private static final Logger logger = new Logger(RknnDetectorJNI.class, LogGroup.General);
private static final Logger logger = new Logger(RknnObjectDetector.class, LogGroup.General);
/** Cleaner instance to release the detector when it goes out of scope */
private final Cleaner cleaner = Cleaner.create();

View File

@@ -26,13 +26,12 @@ import org.opencv.core.Size;
import org.photonvision.common.logging.LogGroup;
import org.photonvision.common.logging.Logger;
import org.photonvision.common.util.ColorHelper;
import org.photonvision.jni.RubikDetectorJNI;
import org.photonvision.rubik.RubikJNI;
import org.photonvision.vision.pipe.impl.NeuralNetworkPipeResult;
/** Manages an object detector using the rubik backend. */
public class RubikObjectDetector implements ObjectDetector {
private static final Logger logger = new Logger(RubikDetectorJNI.class, LogGroup.General);
private static final Logger logger = new Logger(RubikObjectDetector.class, LogGroup.General);
/** Cleaner instance to release the detector when it goes out of scope */
private final Cleaner cleaner = Cleaner.create();

View File

@@ -27,12 +27,13 @@ import org.apache.commons.io.FileUtils;
import org.opencv.calib3d.Calib3d;
import org.opencv.core.*;
import org.opencv.imgcodecs.Imgcodecs;
import org.photonvision.common.LoadJNI;
import org.photonvision.common.LoadJNI.JNITypes;
import org.photonvision.common.logging.LogGroup;
import org.photonvision.common.logging.Logger;
import org.photonvision.common.util.math.MathUtils;
import org.photonvision.mrcal.MrCalJNI;
import org.photonvision.mrcal.MrCalJNI.MrCalResult;
import org.photonvision.mrcal.MrCalJNILoader;
import org.photonvision.vision.calibration.BoardObservation;
import org.photonvision.vision.calibration.CameraCalibrationCoefficients;
import org.photonvision.vision.calibration.CameraLensModel;
@@ -94,7 +95,7 @@ public class Calibrate3dPipe
CameraCalibrationCoefficients ret;
var start = System.nanoTime();
if (MrCalJNILoader.getInstance().isLoaded() && params.useMrCal) {
if (LoadJNI.hasLoaded(JNITypes.MRCAL) && params.useMrCal) {
logger.debug("Calibrating with mrcal!");
ret =
calibrateMrcal(

View File

@@ -27,6 +27,8 @@ import java.util.Optional;
import java.util.function.Predicate;
import java.util.regex.Pattern;
import java.util.stream.Stream;
import org.photonvision.common.LoadJNI;
import org.photonvision.common.LoadJNI.JNITypes;
import org.photonvision.common.configuration.CameraConfiguration;
import org.photonvision.common.configuration.ConfigManager;
import org.photonvision.common.dataflow.DataChangeService;
@@ -40,7 +42,6 @@ import org.photonvision.common.logging.LogGroup;
import org.photonvision.common.logging.Logger;
import org.photonvision.common.util.TimedTaskManager;
import org.photonvision.raspi.LibCameraJNI;
import org.photonvision.raspi.LibCameraJNILoader;
import org.photonvision.vision.camera.CameraType;
import org.photonvision.vision.camera.FileVisionSource;
import org.photonvision.vision.camera.PVCameraInfo;
@@ -301,7 +302,7 @@ public class VisionSourceManager {
.filter(c -> !(String.join("", c.otherPaths()).contains("csi-video")))
.filter(c -> !c.name().equals("unicam"))
.forEach(cameraInfos::add);
if (LibCameraJNILoader.getInstance().isSupported()) {
if (LoadJNI.hasLoaded(JNITypes.LIBCAMERA)) {
// find all CSI cameras (Raspberry Pi cameras)
Stream.of(LibCameraJNI.getCameraNames())
.map(

View File

@@ -40,7 +40,7 @@ import org.photonvision.vision.pipeline.result.CVPipelineResult;
public class BenchmarkTest {
@BeforeAll
public static void init() {
TestUtils.loadLibraries();
LoadJNI.loadLibraries();
}
@Test

View File

@@ -56,7 +56,7 @@ import org.photonvision.vision.pipeline.result.CVPipelineResult;
public class ShapeBenchmarkTest {
@BeforeAll
public static void init() {
TestUtils.loadLibraries();
LoadJNI.loadLibraries();
}
@Test

View File

@@ -28,10 +28,10 @@ import java.nio.file.Path;
import java.nio.file.Paths;
import org.apache.commons.io.FileUtils;
import org.junit.jupiter.api.*;
import org.photonvision.common.LoadJNI;
import org.photonvision.common.logging.LogGroup;
import org.photonvision.common.logging.LogLevel;
import org.photonvision.common.logging.Logger;
import org.photonvision.common.util.TestUtils;
import org.photonvision.common.util.file.JacksonUtils;
import org.photonvision.vision.camera.PVCameraInfo;
import org.photonvision.vision.pipeline.AprilTagPipelineSettings;
@@ -51,7 +51,7 @@ public class ConfigTest {
@BeforeAll
public static void init() {
TestUtils.loadLibraries();
LoadJNI.loadLibraries();
var path = Path.of("testconfigdir");
configMgr = new ConfigManager(path, new LegacyConfigProvider(path));
configMgr.load();
@@ -79,7 +79,7 @@ public class ConfigTest {
@Test
@Order(1)
public void serializeConfig() {
TestUtils.loadLibraries();
LoadJNI.loadLibraries();
Logger.setLevel(LogGroup.General, LogLevel.TRACE);
configMgr.getConfig().addCameraConfig(cameraConfig);

View File

@@ -0,0 +1,51 @@
/*
* 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.configuration;
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
import static org.junit.jupiter.api.Assertions.assertEquals;
import java.nio.file.Path;
import java.util.LinkedList;
import org.junit.jupiter.api.Test;
import org.photonvision.common.configuration.NeuralNetworkModelManager.Family;
import org.photonvision.common.configuration.NeuralNetworkModelManager.Version;
import org.photonvision.common.configuration.NeuralNetworkPropertyManager.ModelProperties;
import org.photonvision.common.util.file.JacksonUtils;
public class NeuralNetworkPropertyManagerTest {
@Test
void testSerialization() {
var nnpm = new NeuralNetworkPropertyManager();
// Path is always serialized as absolute; for the test to pass, this must also be made absolute
nnpm.addModelProperties(
new ModelProperties(
Path.of("test", "yolov8nCOCO.rknn").toAbsolutePath(),
"COCO",
new LinkedList<>(),
640,
640,
Family.RKNN,
Version.YOLOV8));
String result = assertDoesNotThrow(() -> JacksonUtils.serializeToString(nnpm));
var deserializedNnpm =
assertDoesNotThrow(
() -> JacksonUtils.deserialize(result, NeuralNetworkPropertyManager.class));
assertEquals(nnpm.getModels()[0], deserializedNnpm.getModels()[0]);
}
}

View File

@@ -27,6 +27,7 @@ import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Order;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import org.photonvision.common.LoadJNI;
import org.photonvision.common.util.TestUtils;
import org.photonvision.vision.camera.CameraQuirk;
import org.photonvision.vision.camera.PVCameraInfo;
@@ -39,7 +40,7 @@ public class SQLConfigTest {
@BeforeAll
public static void init() {
TestUtils.loadLibraries();
LoadJNI.loadLibraries();
}
@Test

View File

@@ -25,12 +25,13 @@ import edu.wpi.first.math.geometry.Transform3d;
import edu.wpi.first.math.geometry.Translation3d;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.photonvision.common.LoadJNI;
import org.photonvision.common.util.math.MathUtils;
public class CoordinateConversionTest {
@BeforeAll
public static void Init() {
TestUtils.loadLibraries();
LoadJNI.loadLibraries();
}
@Test

View File

@@ -19,12 +19,15 @@ package org.photonvision.hardware;
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
import com.diozero.internal.spi.NativeDeviceFactoryInterface;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.io.IOException;
import org.junit.jupiter.api.Test;
import org.photonvision.common.configuration.HardwareConfig;
import org.photonvision.common.hardware.GPIO.CustomGPIO;
import org.photonvision.common.hardware.HardwareManager;
import org.photonvision.common.hardware.gpio.CustomDeviceFactory;
import org.photonvision.common.util.TestUtils;
public class HardwareConfigTest {
@@ -41,7 +44,9 @@ public class HardwareConfigTest {
assertEquals(config.cpuThrottleReasonCmd, "");
assertEquals(config.diskUsageCommand, "");
assertArrayEquals(config.ledPins.stream().mapToInt(i -> i).toArray(), new int[] {2, 13});
CustomGPIO.setConfig(config);
NativeDeviceFactoryInterface deviceFactory = HardwareManager.configureCustomGPIO(config);
assertTrue(deviceFactory instanceof CustomDeviceFactory);
deviceFactory.close();
} catch (IOException e) {
e.printStackTrace();

View File

@@ -1,49 +0,0 @@
/*
* Copyright (C) Photon Vision.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.photonvision.hardware;
import org.junit.jupiter.api.Assumptions;
import org.junit.jupiter.api.Test;
import org.photonvision.common.hardware.GPIO.pi.PigpioException;
import org.photonvision.common.hardware.GPIO.pi.PigpioSocket;
import org.photonvision.common.hardware.HardwareManager;
import org.photonvision.common.hardware.Platform;
import org.photonvision.common.logging.LogGroup;
import org.photonvision.common.logging.Logger;
public class HardwareManagerTest {
public static final Logger logger = new Logger(HardwareManager.class, LogGroup.General);
@Test
public void managementTest() throws InterruptedException {
Assumptions.assumeTrue(Platform.isRaspberryPi());
var socket = new PigpioSocket();
try {
socket.gpioWrite(18, false);
socket.gpioWrite(13, false);
Thread.sleep(500);
for (int i = 0; i < 1000000; i++) {
int duty = 1000000 - i;
socket.hardwarePWM(18, 1000000, duty);
Thread.sleep(0, 25);
}
} catch (PigpioException e) {
logger.error("error", e);
}
}
}

View File

@@ -17,21 +17,31 @@
package org.photonvision.hardware;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
import com.diozero.internal.provider.builtin.DefaultDeviceFactory;
import com.diozero.internal.spi.NativeDeviceFactoryInterface;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.io.IOException;
import java.util.HashSet;
import java.util.List;
import org.junit.jupiter.api.Assumptions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.photonvision.common.hardware.GPIO.CustomGPIO;
import org.photonvision.common.hardware.GPIO.GPIOBase;
import org.photonvision.common.hardware.GPIO.pi.PigpioPin;
import org.photonvision.common.LoadJNI;
import org.photonvision.common.configuration.HardwareConfig;
import org.photonvision.common.hardware.HardwareManager;
import org.photonvision.common.hardware.Platform;
import org.photonvision.common.hardware.VisionLED;
import org.photonvision.common.hardware.metrics.MetricsManager;
import org.photonvision.common.util.TestUtils;
public class HardwareTest {
@Test
public void testHardware() {
TestUtils.loadLibraries();
LoadJNI.loadLibraries();
MetricsManager mm = new MetricsManager();
if (!Platform.isRaspberryPi()) return;
@@ -51,44 +61,71 @@ public class HardwareTest {
}
@Test
public void testGPIO() {
GPIOBase gpio;
if (Platform.isRaspberryPi()) {
gpio = new PigpioPin(18);
} else {
gpio = new CustomGPIO(18);
public void testNativeGPIO() {
try (NativeDeviceFactoryInterface deviceFactory = new DefaultDeviceFactory()) {
Assumptions.assumeTrue(deviceFactory.getBoardInfo().isRecognised());
try (VisionLED led = new VisionLED(deviceFactory, List.of(2, 13), false, 0, 100, 0, null)) {
// Verify states can be set
led.setState(true);
assertEquals(1, deviceFactory.getGpioValue(2));
assertEquals(1, deviceFactory.getGpioValue(13));
led.setState(false);
assertEquals(0, deviceFactory.getGpioValue(2));
assertEquals(0, deviceFactory.getGpioValue(13));
}
}
gpio.setOn(); // HIGH
assertTrue(gpio.getState());
gpio.setOff(); // LOW
assertFalse(gpio.getState());
gpio.togglePin(); // HIGH
assertTrue(gpio.getState());
gpio.togglePin(); // LOW
assertFalse(gpio.getState());
gpio.setState(true); // HIGH
assertTrue(gpio.getState());
gpio.setState(false); // LOW
assertFalse(gpio.getState());
var success = gpio.shutdown();
assertTrue(success);
}
@Test
public void testBlink() {
if (!Platform.isRaspberryPi()) return;
GPIOBase pwm = new PigpioPin(18);
pwm.blink(125, 3);
var startms = System.currentTimeMillis();
while (true) {
if (System.currentTimeMillis() - startms > 4500) break;
@Nested
class CustomGPIOTest {
HardwareConfig hardwareConfig = null;
NativeDeviceFactoryInterface deviceFactory = null;
@BeforeEach
void setup() throws IOException {
System.out.println("Loading Hardware configs...");
hardwareConfig =
new ObjectMapper().readValue(TestUtils.getHardwareConfigJson(), HardwareConfig.class);
deviceFactory = HardwareManager.configureCustomGPIO(hardwareConfig);
}
@Test
public void testCustomGPIO() throws IOException {
try (VisionLED led = new VisionLED(deviceFactory, List.of(2, 13), false, 0, 100, 0, null)) {
// Verify states can be set
led.setState(true);
assertEquals(1, deviceFactory.getGpioValue(2));
assertEquals(1, deviceFactory.getGpioValue(13));
led.setState(false);
assertEquals(0, deviceFactory.getGpioValue(2));
assertEquals(0, deviceFactory.getGpioValue(13));
}
}
@Test
public void testBlink() throws InterruptedException, IOException {
try (VisionLED led = new VisionLED(deviceFactory, List.of(2, 13), false, 0, 100, 0, null)) {
// Verify blinking toggles between states
HashSet<Integer> seenValues = new HashSet<>();
led.blink(125, 3);
var startms = System.currentTimeMillis();
while (System.currentTimeMillis() - startms < 1000) {
seenValues.add(deviceFactory.getGpioValue(2));
}
assertEquals(2, seenValues.size());
assertTrue(seenValues.contains(0));
assertTrue(seenValues.contains(1));
seenValues.clear();
// Verify that after blinking, toggling has stopped
startms = System.currentTimeMillis();
while (System.currentTimeMillis() - startms < 250) {
seenValues.add(deviceFactory.getGpioValue(2));
}
assertEquals(1, seenValues.size());
}
}
}
}

View File

@@ -23,13 +23,14 @@ import java.nio.file.Files;
import java.nio.file.Paths;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.photonvision.common.LoadJNI;
import org.photonvision.common.util.TestUtils;
import org.photonvision.vision.frame.Frame;
public class FileFrameProviderTest {
@BeforeAll
public static void initPath() {
TestUtils.loadLibraries();
LoadJNI.loadLibraries();
}
@Test

View File

@@ -25,12 +25,12 @@ import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.opencv.core.MatOfPoint;
import org.opencv.core.Point;
import org.photonvision.common.util.TestUtils;
import org.photonvision.common.LoadJNI;
public class ContourTest {
@BeforeEach
public void Init() {
TestUtils.loadLibraries();
LoadJNI.loadLibraries();
}
@Test

View File

@@ -22,6 +22,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals;
import edu.wpi.first.math.geometry.Translation3d;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.photonvision.common.LoadJNI;
import org.photonvision.common.configuration.ConfigManager;
import org.photonvision.common.util.TestUtils;
import org.photonvision.vision.apriltag.AprilTagFamily;
@@ -33,7 +34,7 @@ import org.photonvision.vision.target.TargetModel;
public class AprilTagTest {
@BeforeEach
public void setup() {
TestUtils.loadLibraries();
LoadJNI.loadLibraries();
ConfigManager.getInstance().load();
}

View File

@@ -22,6 +22,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals;
import edu.wpi.first.math.geometry.Translation3d;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.photonvision.common.LoadJNI;
import org.photonvision.common.configuration.ConfigManager;
import org.photonvision.common.util.TestUtils;
import org.photonvision.vision.apriltag.AprilTagFamily;
@@ -33,7 +34,7 @@ import org.photonvision.vision.target.TargetModel;
public class ArucoPipelineTest {
@BeforeEach
public void setup() {
TestUtils.loadLibraries();
LoadJNI.loadLibraries();
ConfigManager.getInstance().load();
}

View File

@@ -32,12 +32,12 @@ import org.opencv.calib3d.Calib3d;
import org.opencv.core.Mat;
import org.opencv.core.Size;
import org.opencv.imgcodecs.Imgcodecs;
import org.photonvision.common.LoadJNI;
import org.photonvision.common.configuration.ConfigManager;
import org.photonvision.common.logging.LogGroup;
import org.photonvision.common.logging.LogLevel;
import org.photonvision.common.logging.Logger;
import org.photonvision.common.util.TestUtils;
import org.photonvision.mrcal.MrCalJNILoader;
import org.photonvision.vision.calibration.CameraCalibrationCoefficients;
import org.photonvision.vision.camera.QuirkyCamera;
import org.photonvision.vision.frame.Frame;
@@ -51,8 +51,8 @@ import org.photonvision.vision.pipeline.UICalibrationData.TagFamily;
public class Calibrate3dPipeTest {
@BeforeAll
public static void init() throws IOException {
TestUtils.loadLibraries();
MrCalJNILoader.forceLoad();
LoadJNI.loadLibraries();
LoadJNI.forceLoad(LoadJNI.JNITypes.MRCAL);
var logLevel = LogLevel.DEBUG;
Logger.setLevel(LogGroup.Camera, logLevel);

View File

@@ -29,13 +29,12 @@ import org.junitpioneer.jupiter.cartesian.CartesianTest;
import org.junitpioneer.jupiter.cartesian.CartesianTest.Enum;
import org.opencv.core.Point;
import org.opencv.core.Size;
import org.photonvision.common.LoadJNI;
import org.photonvision.common.configuration.ConfigManager;
import org.photonvision.common.logging.LogGroup;
import org.photonvision.common.logging.LogLevel;
import org.photonvision.common.logging.Logger;
import org.photonvision.common.util.TestUtils;
import org.photonvision.estimation.OpenCVHelp;
import org.photonvision.mrcal.MrCalJNILoader;
import org.photonvision.vision.calibration.CameraCalibrationCoefficients;
import org.photonvision.vision.calibration.CameraLensModel;
import org.photonvision.vision.calibration.JsonMatOfDouble;
@@ -49,8 +48,8 @@ import org.photonvision.vision.target.TrackedTarget;
public class CalibrationRotationPipeTest {
@BeforeAll
public static void init() throws IOException {
TestUtils.loadLibraries();
MrCalJNILoader.forceLoad();
LoadJNI.loadLibraries();
LoadJNI.forceLoad(LoadJNI.JNITypes.MRCAL);
var logLevel = LogLevel.DEBUG;
Logger.setLevel(LogGroup.Camera, logLevel);

View File

@@ -22,6 +22,7 @@ import static org.junit.jupiter.api.Assertions.assertNotNull;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.photonvision.common.LoadJNI;
import org.photonvision.common.util.TestUtils;
import org.photonvision.vision.calibration.CameraCalibrationCoefficients;
import org.photonvision.vision.camera.QuirkyCamera;
@@ -40,7 +41,7 @@ public class CirclePNPTest {
@BeforeEach
public void Init() {
TestUtils.loadLibraries();
LoadJNI.loadLibraries();
}
@Test
@@ -132,7 +133,7 @@ public class CirclePNPTest {
// used to run VisualVM for profiling, which won't run on unit tests.
public static void main(String[] args) {
TestUtils.loadLibraries();
LoadJNI.loadLibraries();
var frameProvider =
new FileFrameProvider(
TestUtils.getWPIImagePath(TestUtils.WPI2019Image.kCargoStraightDark72in_HighRes, false),

View File

@@ -18,6 +18,7 @@
package org.photonvision.vision.pipeline;
import org.junit.jupiter.api.Test;
import org.photonvision.common.LoadJNI;
import org.photonvision.common.util.TestUtils;
import org.photonvision.vision.camera.QuirkyCamera;
import org.photonvision.vision.frame.Frame;
@@ -89,7 +90,7 @@ public class ColoredShapePipelineTest {
}
public static void main(String[] args) {
TestUtils.loadLibraries();
LoadJNI.loadLibraries();
System.out.println(
TestUtils.getWPIImagePath(TestUtils.WPI2020Image.kBlueGoal_108in_Center, false));
var frameProvider =

View File

@@ -21,6 +21,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
import org.junit.jupiter.api.Test;
import org.photonvision.common.LoadJNI;
import org.photonvision.common.util.TestUtils;
import org.photonvision.vision.camera.QuirkyCamera;
import org.photonvision.vision.frame.Frame;
@@ -34,7 +35,7 @@ import org.photonvision.vision.pipeline.result.CVPipelineResult;
public class ReflectivePipelineTest {
@Test
public void test2019() {
TestUtils.loadLibraries();
LoadJNI.loadLibraries();
var pipeline = new ReflectivePipeline();
pipeline.getSettings().hsvHue.set(60, 100);
pipeline.getSettings().hsvSaturation.set(100, 255);
@@ -72,7 +73,7 @@ public class ReflectivePipelineTest {
@Test
public void test2020() {
TestUtils.loadLibraries();
LoadJNI.loadLibraries();
var pipeline = new ReflectivePipeline();
pipeline.getSettings().hsvHue.set(60, 100);
@@ -108,7 +109,7 @@ public class ReflectivePipelineTest {
// used to run VisualVM for profiling. It won't run on unit tests.
public static void main(String[] args) {
TestUtils.loadLibraries();
LoadJNI.loadLibraries();
var frameProvider =
new FileFrameProvider(
TestUtils.getWPIImagePath(TestUtils.WPI2019Image.kCargoStraightDark72in_HighRes, false),

View File

@@ -26,6 +26,7 @@ import edu.wpi.first.math.geometry.Translation3d;
import edu.wpi.first.math.util.Units;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.photonvision.common.LoadJNI;
import org.photonvision.common.util.TestUtils;
import org.photonvision.vision.calibration.CameraCalibrationCoefficients;
import org.photonvision.vision.camera.QuirkyCamera;
@@ -44,7 +45,7 @@ public class SolvePNPTest {
@BeforeEach
public void Init() {
TestUtils.loadLibraries();
LoadJNI.loadLibraries();
}
@Test
@@ -213,7 +214,7 @@ public class SolvePNPTest {
// used to run VisualVM for profiling, which won't run on unit tests.
public static void main(String[] args) {
TestUtils.loadLibraries();
LoadJNI.loadLibraries();
var frameProvider =
new FileFrameProvider(
TestUtils.getWPIImagePath(TestUtils.WPI2019Image.kCargoStraightDark72in_HighRes, false),

View File

@@ -23,15 +23,15 @@ import java.util.ArrayList;
import java.util.List;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.photonvision.common.LoadJNI;
import org.photonvision.common.configuration.ConfigManager;
import org.photonvision.common.util.TestUtils;
import org.photonvision.vision.pipeline.DriverModePipelineSettings;
import org.photonvision.vision.pipeline.PipelineType;
public class PipelineManagerTest {
@BeforeAll
public static void init() {
TestUtils.loadLibraries();
LoadJNI.loadLibraries();
}
@Test

View File

@@ -27,6 +27,7 @@ import java.util.HashMap;
import java.util.List;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.photonvision.common.LoadJNI;
import org.photonvision.common.configuration.CameraConfiguration;
import org.photonvision.common.configuration.ConfigManager;
import org.photonvision.common.dataflow.CVPipelineResultConsumer;
@@ -46,7 +47,7 @@ public class VisionModuleManagerTest {
String classpathStr = System.getProperty("java.class.path");
System.out.print(classpathStr);
TestUtils.loadLibraries();
LoadJNI.loadLibraries();
if (!LibraryLoader.loadTargeting()) fail();
}

View File

@@ -29,6 +29,7 @@ import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.photonvision.common.LoadJNI;
import org.photonvision.common.configuration.CameraConfiguration;
import org.photonvision.common.configuration.ConfigManager;
import org.photonvision.common.util.TestUtils;
@@ -58,7 +59,7 @@ public class VisionSourceManagerTest {
@BeforeAll
public static void loadLibraries() {
assertTrue(TestUtils.loadLibraries());
assertTrue(LoadJNI.loadLibraries());
// Broadcast all still calls into configmanager (ew) so set that up here
ConfigManager.getInstance().load();

View File

@@ -32,7 +32,7 @@ import org.junit.jupiter.params.provider.MethodSource;
import org.opencv.calib3d.Calib3d;
import org.opencv.core.*;
import org.opencv.imgproc.Imgproc;
import org.photonvision.common.util.TestUtils;
import org.photonvision.common.LoadJNI;
import org.photonvision.common.util.numbers.DoubleCouple;
import org.photonvision.vision.calibration.CameraCalibrationCoefficients;
import org.photonvision.vision.calibration.CameraLensModel;
@@ -63,7 +63,7 @@ public class TargetCalculationsTest {
@BeforeAll
public static void setup() {
TestUtils.loadLibraries();
LoadJNI.loadLibraries();
}
@Test

Some files were not shown because too many files have changed in this diff Show More