Compare commits

...

91 Commits

Author SHA1 Message Date
samfreund
f022130bfa do stuff 2025-11-26 21:10:02 -06:00
Sam Freund
5457db947e Merge branch 'main' into py-docs 2025-11-26 20:01:24 -06:00
Jade
8c7ca1697e Fix OV9281 resolution options when libcamera (#2100)
Resolves https://github.com/PhotonVision/photonvision/issues/2087

Values are taken from the
[datasheet](https://www.ovt.com/wp-content/uploads/2022/01/OV9281-OV9282-PB-v1.3-WEB.pdf)
and have been tested on the LL4

Signed-off-by: Jade Turner <spacey-sooty@proton.me>
2025-11-27 01:00:46 +08:00
samfreund
a7329c48a3 lint 2025-11-25 15:31:11 -06:00
Sam Freund
ce0b00ee03 Merge branch 'main' into py-docs 2025-11-25 15:25:17 -06:00
Gold856
a8d825919e Fix PipelineType index mismatch (#2204)
## Description

#2180 added an additional value to PipelineType.java. Enums are
serialized with `ordinal()`, and the frontend wasn't updated to account
for this, causing an off-by-one error where the UI thought the pipeline
was actually the next pipeline over. This resyncs the enum on the
frontend to the backend and adjusts the mapping from PipelineType to
WebsocketPipelineType to make everything match.

Fixes #2201.

closes #2202 

## 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_
- [x] 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
- [x] 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-11-24 02:12:22 -05:00
Michael Jansen
63593b873a Catch other boards in isRK3588 check (#2199) 2025-11-22 23:50:51 +00:00
Tyler Veness
aa64bfe82e [ci] Upgrade to wpiformat 2025.75 (#2198) 2025-11-22 07:39:30 -08:00
Sam Freund
d27b3d0775 Modal template for deletion confirmation (#2190)
## 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 adds a template modal that can be used for confirming that the user
wants to delete something. The main goal is to reduce complication and
duplicated code, and standardize the way we handle deletion.

closes #2175

## 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_
- [x] 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: Devolian <devondoyle@outlook.com>
2025-11-18 03:41:20 -05:00
Tyler Veness
77e5545eef [ci] Upgrade to wpiformat 2025.69 (#2193) 2025-11-17 06:48:36 +00:00
ElectricTurtle32
618072c3dd Add Camera Focus Mode (#2180)
## Description
 
Camera focus tool pipeline using a Laplacian and finding the variance.
Similar to Limelight.


closes #1597 

## 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_
- [x] This PR has been
[linted](https://docs.photonvision.org/en/latest/docs/contributing/linting.html).
- [x] 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
- [x] 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-11-17 00:15:42 +00:00
Dan Katzuv
7d2c69dbdb Fix "ArUco" and "ChAruCo" spellings (#2184)
## Description
Looks like this is the convention from a quick Google search.
## 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-11-13 16:07:33 -05:00
Tyler Veness
a2b19c080e Upgrade to wpiformat 2025.48 (#2186) 2025-11-11 05:28:28 +00:00
Alan Everett
def3b9faa8 Only show images during tests when requested via project property (#2177)
## Description

Revision of #2164. Instead of only running headless tests during a
build, now only the images are disabled. To reenable showing images, the
`enableTestUi` project property needs to be passed to gradle.

```bash
./gradlew test -PenableTestUi
```

## 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_
- [X] This PR has been
[linted](https://docs.photonvision.org/en/latest/docs/contributing/linting.html).
- [x] 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-11-06 19:38:07 +00:00
Charlotte
25c355ebc2 Add warning about arducam-pivariety incompatibility (#2178) 2025-11-06 11:04:02 -06:00
Sam Freund
dad7f0a82d Add support for removing calib coefficients (#2150)
## Description

Adds the ability to remove old calibrations. This might be helpful if
you have a bad calibration.

closes #1262

## 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: Gold856 <117957790+Gold856@users.noreply.github.com>
2025-11-02 21:17:22 +00:00
Sam Freund
6f2603f0cb Fix bug with import nickname (#2176)
## Description

`.` is a special regex character, which means we weren't actually
matching the `.` in the string, but rather the character before it. This
resulted in the last letter in a model nickname getting cut off on
import. This fix resolves that issue.

## 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_
- [x] 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-11-02 20:14:57 +00:00
Jade
f499e4fb50 [NFC] Invert AprilTag family default (#2124)
## Description

Switch default tag family used in AprilTag pipeline. We already
functionally changed the default in
https://github.com/PhotonVision/photonvision/pull/1333 but since 2024
FIRST appears to have been using 36h11 so switching here seems to make
sense as well.

## Meta

Merge checklist:
- [ ] Pull Request title is [short, imperative
summary](https://cbea.ms/git-commit/) of proposed changes
- [x] The description documents the _what_ and _why_
- [x] 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

---------

Signed-off-by: Jade Turner <spacey-sooty@proton.me>
2025-11-02 13:32:39 -05:00
Rikhil Chilka
e5c8859c57 Add notice to notebook about running on linux (#2163) 2025-11-02 18:29:08 +00:00
Sam Freund
2cde701cff Add documentation for linting (#2166)
## Description

This adds documentation for how to lint PhotonVision, as it is not
available elsewhere and people have historically needed to figure it out
for themselves. It also adds an alias for linting PV.

## 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-11-02 03:38:51 +00:00
Sam Freund
d649a9cb9e Use progress bar for file uploads (#2148)
## Description
<img width="3840" height="2160" alt="image"
src="https://github.com/user-attachments/assets/c0289923-a6c8-48b9-84c1-ce92c7acbc9d"
/>
<img width="3840" height="2160" alt="image"
src="https://github.com/user-attachments/assets/3b58c7d0-e12e-45d6-b328-c3061949349a"
/>


closes #871

## 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-11-02 01:56:12 +00:00
Sam Freund
695742bfcf Add axios post util (#2153)
## Description

This replaces boilerplate that checks whether we've successfully sent
the request, whether there was an error, etc.

closes #2151

## 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-11-01 17:48:28 -04:00
Alan Everett
5df9137256 Gradle build task improvements (#2164)
## Description

This fixes a few problems with the Gradle `build` task and subtasks.
1. Spotless was being run on `node_modules`, resulting in errors for
out-of-source files. This is now disabled.
2. All tests were running from the `build` task, resulting in unexpected
windows popping up. Now only headless tests are run.
3. Headless tests were updated to run from the same root directory as
the other tests.

## 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-11-01 17:42:30 -04:00
Sam Freund
36b437323f Add cross-compilation toolchain to docs (#2172) 2025-10-31 21:23:33 -05:00
Sam Freund
5d39ef5b62 Fix client linting in CI (#2167) 2025-10-30 18:52:14 -05:00
Gold856
2bb59f8437 Add Merch link to header on desktop (#2165) 2025-10-30 15:49:01 -05:00
Charlotte
cd502a22c7 Add message discouraging chessboard usage (#2160) 2025-10-30 16:49:57 +00:00
Sam Freund
f16ffe3cd2 Add progress indicator for single model OD upload (#2154) 2025-10-30 01:11:25 -05:00
Chris Gerth
45236b872a Add Merch link to header (#2161)
## Description

added a merch store link


## 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-10-29 21:32:02 -05:00
Sam Freund
6b20dc3c1b Fix rubik_jni bounding box bug (#2162) 2025-10-28 21:48:44 -07:00
Sam Freund
99ca8228a1 Merge branch 'main' into py-docs 2025-10-23 16:14:46 -05:00
samfreund
46e71703ef trigger docs run 2025-06-18 16:06:54 -05:00
samfreund
6fbb41fb76 this should fix importing? 2025-05-09 13:06:42 -05:00
samfreund
05fcf876cd debugggggging 2025-05-09 13:03:05 -05:00
samfreund
1637be6044 change path 2025-05-09 12:57:42 -05:00
samfreund
6f2fd19351 add files to index 2025-05-09 12:47:58 -05:00
samfreund
892e240b18 change theme 2025-05-09 12:44:31 -05:00
samfreund
326c77fa38 dammit, it was a typo 2025-05-09 12:41:57 -05:00
samfreund
8cf48bee57 update requirements.txt 2025-05-09 12:35:33 -05:00
samfreund
26f08a6fdf switch to sphinx 2025-05-09 12:29:47 -05:00
Sam Freund
abb8ccf4e9 add conditionals for publishing 2025-05-09 11:06:18 -05:00
Sam Freund
50adef1672 publish release docs 2025-05-09 11:04:26 -05:00
samfreund
cf68403182 Merge branch 'py-docs' of github.com:photonvision/photonvision into py-docs 2025-05-09 11:01:09 -05:00
samfreund
dc0985dfb5 fix docstrings style 2025-05-09 11:01:05 -05:00
Sam Freund
8fb29ff5c4 Update photon-api-docs.yml 2025-05-09 10:17:45 -05:00
Sam Freund
476cd6df8b Update photon-api-docs.yml 2025-05-09 10:17:00 -05:00
samfreund
783ed82d50 Merge branch 'py-docs' of github.com:photonvision/photonvision into py-docs 2025-05-08 23:36:29 -05:00
samfreund
416e2f7607 update versions 2025-05-08 23:36:23 -05:00
Sam Freund
ebd1071553 Merge branch 'main' into py-docs 2025-05-08 23:33:50 -05:00
samfreund
fa8b60fe27 convert to requirements.txt 2025-05-08 23:33:00 -05:00
Sam Freund
c2581f3e99 limit publishing 2025-05-08 18:34:20 -05:00
samfreund
96b0938dc0 Merge branch 'py-docs' of github.com:photonvision/photonvision into py-docs 2025-05-08 18:31:35 -05:00
samfreund
697e52f886 lint 2025-05-08 18:30:42 -05:00
Sam Freund
87b219d9be Merge branch 'main' into py-docs 2025-05-08 18:29:37 -05:00
samfreund
abcd6b8f50 add docs to readme 2025-05-08 17:30:16 -05:00
Sam Freund
b22371d7c0 Merge branch 'main' into py-docs 2025-04-14 21:41:09 -05:00
samfreund
3eea79f0d4 Merge branch 'py-docs' of github.com:photonvision/photonvision into py-docs 2025-04-14 00:36:07 -05:00
samfreund
0147a44100 remove source 2025-04-14 00:36:02 -05:00
Sam Freund
0bec1f239c Update photon-api-docs.yml 2025-04-13 23:31:09 -05:00
samfreund
44b46cf117 yeah that's good enough 2025-04-13 23:29:32 -05:00
Sam Freund
ffdda9ddfa Update photon-api-docs.yml 2025-04-13 14:57:00 -05:00
Sam Freund
a5bc63878d Merge branch 'main' into py-docs 2025-04-13 14:50:54 -05:00
Sam Freund
a5b1cc0ded Update photon-api-docs.yml 2025-04-13 14:48:39 -05:00
Sam Freund
aacbdf5010 Update index.md 2025-04-13 10:53:34 -05:00
samfreund
3547d0584b exclude generated files 2025-04-13 10:51:39 -05:00
Sam Freund
40815020de Update photon-api-docs.yml 2025-04-13 10:47:52 -05:00
samfreund
e522642a48 linting 2025-04-13 10:43:40 -05:00
Sam Freund
3cdda8a84e Add instructions for excluding a file 2025-04-13 10:38:44 -05:00
Sam Freund
228caf47f2 Update photon-api-docs.yml 2025-04-13 10:30:34 -05:00
samfreund
d1761d07e9 update to exclude packet.py 2025-04-13 10:22:10 -05:00
samfreund
331f4f0218 Merge branch 'py-docs' of github.com:photonvision/photonvision into py-docs 2025-04-13 08:09:23 -05:00
samfreund
eb85834180 add brand color 2025-04-13 08:09:17 -05:00
Sam Freund
871ca61c8d Merge branch 'main' into py-docs 2025-04-13 07:56:26 -05:00
samfreund
358f5747ab man I hate linting 2025-04-12 22:54:50 -05:00
Sam Freund
e334d26459 Update python.yml 2025-04-12 22:53:16 -05:00
Sam Freund
77d5388a35 Update photon-api-docs.yml 2025-04-12 22:46:27 -05:00
samfreund
88a1e789ad actually add the logo 2025-04-12 22:41:36 -05:00
samfreund
abc67bdd95 add logo 2025-04-12 22:37:18 -05:00
Sam Freund
05309b1e25 Update mkdocs.yml 2025-04-12 22:35:20 -05:00
Sam Freund
ddacff7079 Update photon-api-docs.yml 2025-04-12 22:32:18 -05:00
samfreund
32e4f0029b Merge branch 'py-docs' of github.com:photonvision/photonvision into py-docs 2025-04-12 22:30:08 -05:00
samfreund
34057f223d autogen files 2025-04-12 22:29:41 -05:00
Sam Freund
55303ccd9c Update index.md 2025-04-12 17:33:57 -05:00
Sam Freund
3c73b68ba3 Update index.md 2025-04-12 17:33:37 -05:00
Sam Freund
6d816b5053 Update photon-api-docs.yml 2025-04-12 17:19:46 -05:00
Sam Freund
7c6bab1dfa Update photon-api-docs.yml 2025-04-12 17:17:21 -05:00
Sam Freund
462a2aa629 Update photon-api-docs.yml 2025-04-12 17:16:07 -05:00
samfreund
47b799a0ce add docs for python 2025-04-12 17:12:13 -05:00
Sam Freund
35c72e8446 Update photon-api-docs.yml 2025-04-12 16:49:42 -05:00
Sam Freund
c440ce57ce Update photon-api-docs.yml 2025-04-12 16:49:06 -05:00
Sam Freund
8452527589 Update photon-api-docs.yml 2025-04-12 16:44:25 -05:00
126 changed files with 1827 additions and 1122 deletions

View File

@@ -68,19 +68,26 @@ ForEachMacros:
- BOOST_FOREACH
IncludeBlocks: Regroup
IncludeCategories:
- Regex: '^<ext/.*\.h>'
Priority: 2
SortPriority: 0
- Regex: '^<.*\.h>'
Priority: 1
SortPriority: 0
- Regex: '^<.*'
Priority: 2
SortPriority: 0
- Regex: '.*'
Priority: 3
SortPriority: 0
IncludeIsMainRegex: '([-_](test|unittest))?$'
# C standard library headers
#
# https://en.cppreference.com/w/cpp/header:
# * C compatibility headers
# * Special C compatibility headers
# * Empty C headers
# * Meaningless C headers
# * Unsupported C headers
- Regex: '^<(assert\.h|ctype\.h|errno\.h|fenv\.h|float\.h|inttypes\.h|limits\.h|locale\.h|math\.h|setjmp\.h|signal\.h|stdarg\.h|stddef\.h|stdint\.h|stdio\.h|stdlib\.h|string\.h|time\.h|uchar\.h|wchar\.h|wctype\.h|stdatomic\.h|ccomplex|complex\.h|ctgmath|tgmath\.h|ciso646|cstdalign|cstdbool|iso646\.h|stdalign\.h|stdbool\.h|stdatomic\.h|stdnoreturn\.h|threads\.h)>'
Priority: 1
# C++ standard library headers (lowercase and underscores with no .h suffix)
- Regex: '^<[a-z_]+>'
Priority: 2
# Other library headers (angle brackets)
- Regex: '^<.*'
Priority: 3
# Project headers (double quotes)
- Regex: '^".*'
Priority: 4
IncludeIsMainRegex: '(Test|_test)?$'
IncludeIsMainSourceRegex: ''
IndentCaseLabels: true
IndentGotoLabels: true
@@ -136,7 +143,7 @@ RawStringFormats:
CanonicalDelimiter: ''
BasedOnStyle: google
ReflowComments: true
SortIncludes: false
SortIncludes: true
SortUsingDeclarations: true
SpaceAfterCStyleCast: false
SpaceAfterLogicalNot: false

View File

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

View File

@@ -88,10 +88,8 @@ jobs:
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
run: ./gradlew testHeadless --stacktrace
- name: Gradle Coverage
run: ./gradlew jacocoTestReport
- name: Gradle Tests and Coverage
run: ./gradlew test jacocoTestReport --stacktrace
build-offline-docs:
name: "Build Offline Docs"
runs-on: ubuntu-22.04

View File

@@ -31,7 +31,7 @@ jobs:
with:
python-version: 3.11
- name: Install wpiformat
run: pip3 install wpiformat==2025.34
run: pip3 install wpiformat==2025.75
- name: Run
run: wpiformat
- name: Check output

View File

@@ -46,7 +46,7 @@ jobs:
run: pnpm run build-demo
- uses: actions/upload-artifact@v4
with:
name: built-demo
name: demo
path: photon-client/dist/
run_java_cpp_docs:
@@ -74,9 +74,39 @@ jobs:
name: docs-java-cpp
path: photon-docs/build/docs
run_py_docs:
name: Build Python API Docs
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.10'
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -r photon-lib/py/docs/requirements.txt
- name: Build Sphinx site
run: |
sphinx-apidoc -o docs/source photonlibpy
make -C docs html
working-directory: photon-lib/py
- name: Upload built site as artifact
uses: actions/upload-artifact@v4
with:
name: docs-python
path: photon-lib/py/docs/build/html
publish_api_docs:
name: Publish API Docs
needs: [run_java_cpp_docs]
needs: [ run_java_cpp_docs, run_py_docs ]
runs-on: ubuntu-22.04
steps:
# Download docs artifact
@@ -85,7 +115,7 @@ jobs:
pattern: docs-*
- run: find .
- name: Publish Docs To Development
if: github.ref == 'refs/heads/main'
# if: github.ref == 'refs/heads/main'
uses: up9cloud/action-rsync@v1.4
env:
HOST: ${{ secrets.WEBMASTER_SSH_HOST }}
@@ -108,7 +138,7 @@ jobs:
steps:
- uses: actions/download-artifact@v4
with:
name: built-demo
name: demo
- run: find .
- name: Publish demo
if: github.ref == 'refs/heads/main'

View File

@@ -29,7 +29,7 @@ jobs:
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install setuptools wheel pytest mypy
pip install setuptools wheel pytest mypy mkdocs mkdocs-gen-files
- name: Build wheel
working-directory: ./photon-lib/py

4
.gitignore vendored
View File

@@ -142,9 +142,13 @@ venv
.venv/*
.venv
networktables.json
# Web stuff
photon-server/src/main/resources/web/*
node_modules
dist
components.d.ts
# Py docs stuff
photon-lib/py/docs/build
photon-server/src/main/resources/web/index.html

View File

@@ -1,41 +1,25 @@
cppHeaderFileInclude {
\.h$
\.hpp$
\.inc$
\.inl$
}
cppSrcFileInclude {
\.cpp$
}
modifiableFileExclude {
\.jpg$
\.jpeg$
\.png$
\.gif$
\.so$
\.dll$
\.webp$
\.gif$
\.ico$
\.rknn$
\.tflite$
\.jpeg$
\.jpg$
\.mp4$
\.pdf$
\.png$
\.rknn$
\.so$
\.svg$
\.tflite$
\.ttf$
\.webp$
\.woff2$
gradlew
photon-lib/py/photonlibpy/generated/
photon-targeting/src/main/native/cpp/photon/constrained_solvepnp/generate/
photon-targeting/src/generated/
}
includeProject {
^photonLib/
}
includeOtherLibs {
^frc/
^networktables/
^units/
^wpi/
photon-targeting/src/main/native/cpp/photon/constrained_solvepnp/generate/
}

View File

@@ -20,6 +20,7 @@ If you are interested in contributing code or documentation to the project, plea
- Photon UI demo: [http://photonvision.global/](http://photonvision.global/)
- Javadocs: [javadocs.photonvision.org](https://javadocs.photonvision.org)
- C++ Doxygen [cppdocs.photonvision.org](https://cppdocs.photonvision.org)
- Python Documentation [pydocs.photonvision.org](https://pydocs.photonvision.org)
## Building

View File

@@ -39,7 +39,7 @@ ext {
javalinVersion = "6.7.0"
libcameraDriverVersion = "v2025.0.4"
rknnVersion = "dev-v2025.0.0-5-g666c0c6"
rubikVersion = "dev-v2025.1.0-8-g067a316"
rubikVersion = "dev-v2025.1.0-6-g4a5e508"
frcYear = "2025"
mrcalVersion = "v2025.0.0";
@@ -92,7 +92,7 @@ spotless {
format 'misc', {
target fileTree('.') {
include '**/*.md', '**/.gitignore'
exclude '**/build/**', '**/build-*/**'
exclude '**/build/**', '**/build-*/**', '**/node_modules/**'
}
trimTrailingWhitespace()
indentWithSpaces(2)

View File

@@ -1,18 +0,0 @@
modifiableFileExclude {
\.jpg$
\.jpeg$
\.png$
\.gif$
\.so$
\.pdf$
\.mp4$
\.dll$
\.webp$
\.ico$
\.rknn$
\.tflite$
\.svg$
\.woff2$
gradlew
}

View File

@@ -2,7 +2,7 @@
## Tracking AprilTags
Before you get started tracking AprilTags, ensure that you have followed the previous sections on installation, wiring and networking. Next, open the Web UI, go to the top right card, and switch to the "AprilTag" or "Aruco" type. You should see a screen similar to the one below.
Before you get started tracking AprilTags, ensure that you have followed the previous sections on installation, wiring and networking. Next, open the Web UI, go to the top right card, and switch to the "AprilTag" or "ArUco" type. You should see a screen similar to the one below.
```{image} images/apriltag.png
:align: center
@@ -12,7 +12,7 @@ You are now able to detect and track AprilTags in 2D (yaw, pitch, roll, etc.). I
## Tuning AprilTags
AprilTag pipelines come with reasonable defaults to get you up and running with tracking. However, in order to optimize your performance and accuracy, you must tune your AprilTag pipeline using the settings below. Note that the settings below are different between the AprilTag and Aruco detectors but the concepts are the same.
AprilTag pipelines come with reasonable defaults to get you up and running with tracking. However, in order to optimize your performance and accuracy, you must tune your AprilTag pipeline using the settings below. Note that the settings below are different between the AprilTag and ArUco detectors but the concepts are the same.
```{image} images/apriltag-tune.png
:align: center

View File

@@ -8,8 +8,8 @@ Note that both of these pipeline types detect AprilTag markers and are just two
## AprilTag
The AprilTag pipeline type is based on the [AprilTag](https://april.eecs.umich.edu/software/apriltag.html) library from the University of Michigan and we recommend it for most use cases. It is (to our understanding) most accurate pipeline type, but is also ~2x slower than AruCo. This was the pipeline type used by teams in the 2023 season and is well tested.
The AprilTag pipeline type is based on the [AprilTag](https://april.eecs.umich.edu/software/apriltag.html) library from the University of Michigan and we recommend it for most use cases. It is (to our understanding) most accurate pipeline type, but is also ~2x slower than ArUco. This was the pipeline type used by teams in the 2023 season and is well tested.
## AruCo
## ArUco
The AruCo pipeline is based on the [AruCo](https://docs.opencv.org/4.8.0/d9/d6a/group__aruco.html) library implementation from OpenCV. It is ~2x higher fps and ~2x lower latency than the AprilTag pipeline type, but is less accurate. We recommend this pipeline type for teams that need to run at a higher framerate or have a lower powered device. This pipeline type was new for the 2024 season.
The ArUco pipeline is based on the [ArUco](https://docs.opencv.org/4.8.0/d9/d6a/group__aruco.html) library implementation from OpenCV. It is ~2x higher fps and ~2x lower latency than the AprilTag pipeline type, but is less accurate. We recommend this pipeline type for teams that need to run at a higher framerate or have a lower powered device. This pipeline type was new for the 2024 season.

View File

@@ -4,7 +4,7 @@
In order to detect AprilTags and use 3D mode, your camera must be calibrated at the desired resolution! Inaccurate calibration will lead to poor performance.
:::
To calibrate a camera, images of a Charuco board (or chessboard) are taken. By comparing where the grid corners should be in object space (for example, a corner once every inch in an 8x6 grid) with where they appear in the camera image, we can find a least-squares estimate for intrinsic camera properties like focal lengths, center point, and distortion coefficients. For more on camera calibration, please review the [OpenCV documentation](https://docs.opencv.org/4.x/dc/dbb/tutorial_py_calibration.html).
To calibrate a camera, images of a ChArUco board (or chessboard) are taken. By comparing where the grid corners should be in object space (for example, a corner once every inch in an 8x6 grid) with where they appear in the camera image, we can find a least-squares estimate for intrinsic camera properties like focal lengths, center point, and distortion coefficients. For more on camera calibration, please review the [OpenCV documentation](https://docs.opencv.org/4.x/dc/dbb/tutorial_py_calibration.html).
:::{warning}
While any resolution can be calibrated, higher resolutions may be too performance-intensive for some coprocessors to handle. Therefore, we recommend experimenting to see what works best for your coprocessor.
@@ -16,6 +16,10 @@ The calibration data collected during calibration is specific to each physical c
## Calibration Tips
:::{warning}
The usage of chessboards can result in bad calibration results if multiple similar images are taken. We strongly recommend that teams use ChArUco boards instead!
:::
Accurate camera calibration is required in order to get accurate pose measurements when using AprilTags and 3D mode. The tips below should help ensure success:
01. Ensure the images you take have the target in different positions and angles, with as big of a difference between angles as possible. It is important to make sure the target overlay still lines up with the board while doing this. Tilt no more than 45 degrees.
@@ -34,11 +38,11 @@ Following the ideas above should help in getting an accurate calibration.
### 1. Navigate to the calibration section in the UI.
The Cameras tab of the UI houses PhotonVision's camera calibration tooling. It assists users with calibrating their cameras, as well as allows them to view previously calibrated resolutions. We support both charuco and chessboard calibrations.
The Cameras tab of the UI houses PhotonVision's camera calibration tooling. It assists users with calibrating their cameras, as well as allows them to view previously calibrated resolutions. We support both ChArUco and chessboard calibrations.
### 2. Print out the calibration target.
In the Camera Calibration tab, we'll print out the calibration target using the "Download" button. This should be printed on 8.5x11 printer paper. This page shows using an 8x8 charuco board (or chessboard depending on the selected calibration type).
In the Camera Calibration tab, we'll print out the calibration target using the "Download" button. This should be printed on 8.5x11 printer paper. This page shows using an 8x8 ChArUco board (or chessboard depending on the selected calibration type).
:::{warning}
Ensure that there is no scaling applied during printing (it should be at 100%) and that the PDF is printed as is on regular printer paper. Check the square size with calipers or an accurate measuring device after printing to ensure squares are sized properly, and enter the true size of the square in the UI text box. For optimal results, various resources are available online to calibrate your specific printer if needed.
@@ -46,13 +50,13 @@ Ensure that there is no scaling applied during printing (it should be at 100%) a
### 3. Select calibration resolution and fill in appropriate target data.
We'll next select a resolution to calibrate and populate our pattern spacing, marker size, and board size. The provided chessboard and charuco board are an 8x8 grid of 1 inch square. The provided charuco board uses the 4x4 dictionary with a marker size of 0.75 inches (this board does not need the old OpenCV pattern selector selected). Printers are not perfect, and you need to measure your calibration target and enter the correct marker size (size of the aruco marker) and pattern spacing (aka size of the black square) using calipers or similar. Finally, once our entered data is correct, we'll click "start calibration."
We'll next select a resolution to calibrate and populate our pattern spacing, marker size, and board size. The provided chessboard and ChArUco board are an 8x8 grid of 1 inch square. The provided ChArUco board uses the 4x4 dictionary with a marker size of 0.75 inches (this board does not need the old OpenCV pattern selector selected). Printers are not perfect, and you need to measure your calibration target and enter the correct marker size (size of the ArUco marker) and pattern spacing (aka size of the black square) using calipers or similar. Finally, once our entered data is correct, we'll click "start calibration."
:::{warning} Old OpenCV Pattern selector. This should be used in the case that the calibration image is generated from a version of OpenCV before version 4.6.0. This would include targets created by calib.io. If this selector is not set correctly the calibration will be completely invalid. For more info view [this GitHub issue](https://github.com/opencv/opencv_contrib/issues/3291).
:::
:::{note}
If you have a [calib.io](https://calib.io/) CharuCo Target you will have to enter the paramaters of your target. For example if your target says "9x12 | Checker Size: 30 mm | Marker Size: 22 mm | Dictionary: AruCo DICT 5x5", you would have to set the board type to Dict_5x5_1000, the pattern spacing to 1.1811 in (30 mm converted to inches), the marker size 0.866142 in (22 mm converted to inches), the board width to 12 and the board height to 9. If you chose the wrong tag family the board wont be detected during calibration. If you swap the width and height your calibration will have a very high error.
If you have a [calib.io](https://calib.io/) ChArUco Target you will have to enter the paramaters of your target. For example if your target says "9x12 | Checker Size: 30 mm | Marker Size: 22 mm | Dictionary: ArUco DICT 5x5", you would have to set the board type to Dict_5x5_1000, the pattern spacing to 1.1811 in (30 mm converted to inches), the marker size 0.866142 in (22 mm converted to inches), the board width to 12 and the board height to 9. If you chose the wrong tag family the board wont be detected during calibration. If you swap the width and height your calibration will have a very high error.
:::
### 4. Take at calibration images from various angles.

View File

@@ -1,5 +1,9 @@
# Arducam Cameras
:::{warning}
Arducam Pivariety cameras are **incompatible** with PhotonVision as they require a custom camera library not compatible with PhotonVision.
:::
Arducam cameras are supported for setups with multiple devices. This is possible because Arducam provides software that allows you to assign truly different device names to each camera. This feature is particularly useful in complex setups where multiple cameras are used simultaneously.
## Setting Up Arducam Cameras

View File

@@ -18,6 +18,10 @@ This section contains the build instructions from the source code available at [
[pnpm](https://pnpm.io/) is the package manager used to download dependencies for the UI. To install pnpm, follow [the instructions on the official pnpm website](https://pnpm.io/installation).
**Cross-Compilation Toolchains (Optional):**
If you plan to deploy PhotonVision to a coprocessor like a Raspberry Pi, you will need to install the appropriate cross-compilation toolchain for your platform. For `linuxarm64` devices, this can be accomplished by running `./gradlew installArm64Toolchain` in the root folder of the project.
## Compiling Instructions
### Getting the Source Code
@@ -173,6 +177,29 @@ With the VSCode [Extension Pack for Java](https://marketplace.visualstudio.com/i
To correctly run PhotonVision tests this way, you must [delegate the tests to Gradle](https://code.visualstudio.com/docs/java/java-build#_delegate-tests-to-gradle). Debugging tests like this will [**not** currently](https://github.com/microsoft/build-server-for-gradle/issues/119) collect outputs.
### Running Tests With UI
By default, tests are run with UI disabled so they are not obtrusive during a build. All tests should be useful when the UI is disabled. However, if a particular test would benefit from having UI access (i.e. for debugging info), the UI can be enabled by passing the `enableTestUi` project property to Gradle. This will run all tests by default, but the Gradle `--tests` option can be used to [filter for specific tests](https://docs.gradle.org/current/userguide/java_testing.html#test_filtering).
```{eval-rst}
.. tab-set::
.. tab-item:: Linux
:sync: linux
``./gradlew test -PenableTestUi``
.. tab-item:: macOS
:sync: macos
``./gradlew test -PenableTestUi``
.. tab-item:: Windows (cmd)
:sync: windows
``gradlew test -PenableTestUi``
```
### Debugging PhotonVision Running Locally
Unit tests can instead be debugged through the ``test`` Gradle task for a specific subproject in VSCode, found in the Gradle tab:

View File

@@ -3,6 +3,7 @@
```{toctree}
building-photon
building-docs
linting
developer-docs/index
design-descriptions/index
```

View File

@@ -0,0 +1,43 @@
# Linting the PhotonVision Codebase
## Versions
:::{note}
If you work on other projects that use different versions of the same linters as PhotonVision, you may find it beneficial to use a [venv](https://docs.python.org/3/library/venv.html) instead of installing the linters globally. This will allow you to have different versions of the same linter installed for different projects.
:::
The correct versions for each linter can be found under the linting workflow located [here](https://github.com/PhotonVision/photonvision/tree/main/.github/workflows). For *doc8*, the version can be found in `docs/requirements.txt`. If you've linted, and are still unable to pass CI, please check the versions of your linters.
## Frontend
### Linting the frontend
In order to lint the frontend, run `pnpm -C photon-client lint && pnpm -C photon-client format`. This should be done from the base level of the repo.
## Backend
### wpiformat installation
To lint the backend, PhotonVision uses *wpiformat* and *spotless*. Spotless is included with gradle, which means installation is not needed. To install wpiformat, run `pipx install wpiformat`. To install a specific version, run `pipx install wpiformat==<version>`.
### Linting the backend
To lint, run `./gradlew spotlessApply` and `wpiformat`.
## Documentation
### doc8 installation
To install *doc8*, the python tool we use to lint our documentation, run `pipx install doc8`. To install a specific version, run `pipx install doc8==<version>`.
### Linting the documentation
To lint the documentation, run `doc8 docs` from the root level of the docs.
## Alias
The following [alias](https://www.computerworld.com/article/1373210/how-to-use-aliases-in-linux-shell-commands.html) can be added to your shell config, which will allow you to lint the entirety of the PhotonVision project by running `pvLint`. The alias will work on Linux, macOS, Git Bash on Windows, and WSL.
```sh
alias pvLint='wpiformat -v && ./gradlew spotlessApply && pnpm -C photon-client lint && pnpm -C photon-client format && doc8 docs'
```

View File

@@ -6,7 +6,7 @@ The following example is from the PhotonLib example repository ([Java](https://g
- A Robot
- A camera mounted rigidly to the robot's frame, centered and pointed forward.
- A coprocessor running PhotonVision with an AprilTag or Aruco 2D Pipeline.
- A coprocessor running PhotonVision with an AprilTag or ArUco 2D Pipeline.
- [A printout of AprilTag 7](https://firstfrc.blob.core.windows.net/frc2025/FieldAssets/Apriltag_Images_and_User_Guide.pdf), mounted on a rigid and flat surface.
## Code

View File

@@ -14,6 +14,6 @@ PhotonVision currently ONLY supports 640x640 Ultralytics YOLOv5, YOLOv8, and YOL
Only quantized models are supported, so take care when exporting to select the option for quantization.
:::
PhotonVision now ships with a {{ '[Python Notebook](https://github.com/PhotonVision/photonvision/blob/{}/scripts/rknn_conversion.ipynb)'.format(git_tag_ref) }} that you can use in [Google Colab](https://colab.research.google.com) or in a local environment. In Google Colab, you can simply paste the PhotonVision GitHub URL into the "GitHub" tab and select the `rknn_conversion.ipynb` notebook without needing to manually download anything.
PhotonVision now ships with a {{ '[Python Notebook](https://github.com/PhotonVision/photonvision/blob/{}/scripts/rknn_conversion.ipynb)'.format(git_tag_ref) }} that you can use in [Google Colab](https://colab.research.google.com) or in a local **Linux** environment (since `rknn-toolkit2` only supports Linux). In Google Colab, you can simply paste the PhotonVision GitHub URL into the "GitHub" tab and select the `rknn_conversion.ipynb` notebook without needing to manually download anything.
Please ensure that the model you are attempting to convert is among the {ref}`supported models <docs/objectDetection/opi:Supported Models>` and using the PyTorch format.

View File

@@ -14,7 +14,7 @@ PhotonVision currently ONLY supports 640x640 Ultralytics YOLOv8 and YOLOv11 mode
Only quantized models are supported, so take care when exporting to select the option for quantization.
:::
PhotonVision now ships with a {{ '[Python Notebook](https://github.com/PhotonVision/photonvision/blob/{}/scripts/rubik_conversion.ipynb)'.format(git_tag_ref) }} that you can use in [Google Colab](https://colab.research.google.com) or in a local environment. In Google Colab, you can simply paste the PhotonVision GitHub URL into the "GitHub" tab and select the `rubik_conversion.ipynb` notebook without needing to manually download anything.
PhotonVision now ships with a {{ '[Python Notebook](https://github.com/PhotonVision/photonvision/blob/{}/scripts/rubik_conversion.ipynb)'.format(git_tag_ref) }} that you can use in [Google Colab](https://colab.research.google.com), [Kaggle](https://kaggle.com/code), or in a local environment. In Google Colab, you can simply paste the PhotonVision GitHub URL into the "GitHub" tab and select the `rubik_conversion.ipynb` notebook without needing to manually download anything.
Please ensure that the model you are attempting to convert is among the {ref}`supported models <docs/objectDetection/rubik:Supported Models>` and using the PyTorch format.

View File

@@ -10,7 +10,7 @@ A vision pipeline represents a series of steps that are used to acquire an image
## Types of Pipelines
### AprilTag / AruCo
### AprilTag / ArUco
This pipeline type is based on detecting AprilTag fiducial markers. More information about AprilTags can be found in the [WPILib documentation](https://docs.wpilib.org/en/stable/docs/software/vision-processing/apriltag/apriltag-intro.html). This pipeline provides easy to use 3D pose information which allows localization.
@@ -56,7 +56,7 @@ Each pipeline has a set of tabs that are used to configure the pipeline. All pip
Pipielines also have additional tabs that are specific to the pipeline type. Listed below are the tabs for each pipeline type.
### AprilTag / AruCo Pipelines
### AprilTag / ArUco Pipelines
- AprilTag: This tab includes AprilTag specific tuning parameters, such as decimate, blur, threads, pose iterations, and more.

View File

@@ -9,8 +9,8 @@ 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.
- Use the Charuco calibration board:
- Board Type: Charuco
- Use the ChArUco calibration board:
- Board Type: ChAruCo
- Tag Family: 4x4
- Pattern Spacing: 1.00in
- Marker Size: 0.75in

View File

@@ -0,0 +1,19 @@
# Camera Focusing
## Prepare Camera
:::{warning}
Refocusing your camera **will** make your calibration inaccurate, make sure to recalibrate after focusing.
:::
To ensure that your camera is focused properly, mount it to a secure surface and ensure it does not move drastically. Point your camera at a detailed surface like a calibration board, and make sure that it not too close to the camera.
## Using Focus Mode
:::{important}
When you enable Focus Mode, it will assign a *Score* to the current focus, this score depends on your environment and the lighting. This score cannot be compared to a focus score collected from other environments.
:::
- In the Cameras tab, turn on Focus Mode.
- Rotate the lens on your camera to try and get the focus score as high as possible.
- Once you cannot get a higher score, this indicates that your camera is fully focused and can be set in place using glue if desired.
```{image} images/focusModeExample.png
:scale: 50%
```

Binary file not shown.

After

Width:  |  Height:  |  Size: 313 KiB

View File

@@ -9,5 +9,6 @@ wiring
networking
camera-matching
camera-calibration
camera-focusing
quick-configure
```

View File

@@ -138,5 +138,7 @@ docs/contributing/index
Java <https://javadocs.photonvision.org>
C++ <https://cppdocs.photonvision.org/>
C++ <https://cppdocs.photonvision.org>
Python <https://pydocs.photonvision.org>
```

View File

@@ -10,7 +10,7 @@
"build-demo": "vite build --mode demo",
"lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix",
"format": "prettier --write src/",
"lint-ci": "eslint . --max-warnings 0 --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix",
"lint-ci": "eslint . --max-warnings 0 --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts",
"format-ci": "prettier --check src/"
},
"dependencies": {

View File

@@ -13,5 +13,15 @@ import { useStateStore } from "@/stores/StateStore";
<p style="padding: 0; margin: 0; text-align: center">
{{ useStateStore().snackbarData.message }}
</p>
<v-progress-linear
v-if="useStateStore().snackbarData.progressBar != -1"
v-model="useStateStore().snackbarData.progressBar"
height="15"
:color="useStateStore().snackbarData.progressBarColor"
>
<template #default="{ value }">
<strong> {{ Math.ceil(value) }}% </strong>
</template>
</v-progress-linear>
</v-snackbar>
</template>

View File

@@ -141,7 +141,7 @@ const downloadCalibBoard = async () => {
break;
case CalibrationBoardTypes.Charuco:
// Add pregenerated charuco
// Add pregenerated ChArUco
const charucoImage = new Image();
charucoImage.src = CharucoImage;
doc.addImage(charucoImage, "PNG", 0.25, 1.5, 8, 8);
@@ -297,20 +297,30 @@ const setSelectedVideoFormat = (format: VideoFormat) => {
:select-cols="8"
:disabled="isCalibrating"
tooltip="Resolution to calibrate at (you will have to calibrate every resolution you use 3D mode on)"
:items="getUniqueVideoResolutionStrings()"
@update:model-value="
useStateStore().calibrationData.videoFormatIndex =
getUniqueVideoResolutionStrings().find((v) => v.value === $event)?.value || 0
"
:items="getUniqueVideoResolutionStrings()"
/>
<pv-select
v-model="boardType"
label="Board Type"
tooltip="Calibration board pattern to use"
:select-cols="8"
:items="['Chessboard', 'Charuco']"
:items="['Chessboard', 'ChArUco']"
:disabled="isCalibrating"
/>
<v-alert
v-if="boardType !== CalibrationBoardTypes.Charuco"
closable
density="compact"
variant="tonal"
color="warning"
icon="mdi-alert-box"
text="The usage of chessboards can result in bad calibration results if multiple
similar images are taken. We strongly recommend that teams use ChArUco boards instead!"
/>
<pv-select
v-if="boardType !== CalibrationBoardTypes.Charuco"
v-model="useCameraSettingsStore().currentPipelineSettings.streamingFrameDivisor"
@@ -326,7 +336,7 @@ const setSelectedVideoFormat = (format: VideoFormat) => {
v-if="boardType === CalibrationBoardTypes.Charuco"
v-model="tagFamily"
label="Tag Family"
tooltip="Dictionary of aruco markers on the charuco board"
tooltip="Dictionary of ArUco markers on the ChArUco board"
:select-cols="8"
:items="['Dict_4X4_1000', 'Dict_5X5_1000', 'Dict_6X6_1000', 'Dict_7X7_1000']"
:disabled="isCalibrating"

View File

@@ -3,8 +3,9 @@ import type { CameraCalibrationResult, VideoFormat } from "@/types/SettingTypes"
import { useCameraSettingsStore } from "@/stores/settings/CameraSettingsStore";
import { useStateStore } from "@/stores/StateStore";
import { computed, inject, ref } from "vue";
import { getResolutionString, parseJsonFile } from "@/lib/PhotonUtils";
import { axiosPost, getResolutionString, parseJsonFile } from "@/lib/PhotonUtils";
import { useTheme } from "vuetify";
import PvDeleteModal from "@/components/common/pv-delete-modal.vue";
const theme = useTheme();
@@ -12,6 +13,16 @@ const props = defineProps<{
videoFormat: VideoFormat;
}>();
const confirmRemoveDialog = ref({ show: false, vf: props.videoFormat as VideoFormat });
const removeCalibration = (vf: VideoFormat) => {
axiosPost("/calibration/remove", "delete a camera calibration", {
cameraUniqueName: useCameraSettingsStore().currentCameraSettings.uniqueName,
width: vf.resolution.width,
height: vf.resolution.height
});
};
const exportCalibration = ref();
const openExportCalibrationPrompt = () => {
exportCalibration.value.click();
@@ -93,18 +104,19 @@ const calibrationImageURL = (index: number) =>
</script>
<template>
<v-card color="surface" dark>
<div class="d-flex flex-wrap pt-2 pl-2 pr-2">
<div class="d-flex flex-wrap pt-2 pl-2 pr-2 align-center">
<v-col cols="12" md="6">
<v-card-title class="pa-0"> Calibration Details </v-card-title>
</v-col>
<v-col cols="6" md="3" class="d-flex align-center pt-0 pt-md-3 pl-6 pl-md-3">
<v-col cols="12" md="6" class="d-flex align-center pt-0 pt-md-3">
<v-btn
color="buttonPassive"
style="width: 100%"
class="mr-2"
style="flex: 1"
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
@click="openUploadPhotonCalibJsonPrompt"
>
<v-icon start size="large"> mdi-import</v-icon>
<v-icon start size="large">mdi-import</v-icon>
<span>Import</span>
</v-btn>
<input
@@ -114,12 +126,11 @@ const calibrationImageURL = (index: number) =>
style="display: none"
@change="importCalibration"
/>
</v-col>
<v-col cols="6" md="3" class="d-flex align-center pt-0 pt-md-3 pr-6 pr-md-3">
<v-btn
color="buttonPassive"
class="mr-2"
:disabled="!currentCalibrationCoeffs"
style="width: 100%"
style="flex: 1"
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
@click="openExportCalibrationPrompt"
>
@@ -132,6 +143,16 @@ const calibrationImageURL = (index: number) =>
:href="exportCalibrationURL"
target="_blank"
/>
<v-btn
color="error"
:disabled="!currentCalibrationCoeffs"
style="flex: 1"
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
@click="() => (confirmRemoveDialog = { show: true, vf: props.videoFormat })"
>
<v-icon start size="large">mdi-delete</v-icon>
<span>Delete</span>
</v-btn>
</v-col>
</div>
<v-card-title class="pt-0 pb-0"
@@ -289,6 +310,14 @@ const calibrationImageURL = (index: number) =>
</v-data-table>
</v-card-text>
</v-card>
<pv-delete-modal
v-model="confirmRemoveDialog.show"
:width="500"
:title="'Delete Calibration'"
:description="`Are you sure you want to delete the calibration for '${confirmRemoveDialog.vf.resolution.width}x${confirmRemoveDialog.vf.resolution.height}'? This action cannot be undone.`"
:on-confirm="() => removeCalibration(confirmRemoveDialog.vf)"
/>
</template>
<style scoped>

View File

@@ -1,13 +1,14 @@
<script setup lang="ts">
import PvSelect, { type SelectItem } from "@/components/common/pv-select.vue";
import PvInput from "@/components/common/pv-input.vue";
import PvDeleteModal from "@/components/common/pv-delete-modal.vue";
import PvNumberInput from "@/components/common/pv-number-input.vue";
import PvSwitch from "@/components/common/pv-switch.vue";
import { useCameraSettingsStore } from "@/stores/settings/CameraSettingsStore";
import { useStateStore } from "@/stores/StateStore";
import { computed, ref, watchEffect } from "vue";
import { type CameraSettingsChangeRequest, ValidQuirks } from "@/types/SettingTypes";
import axios from "axios";
import { useTheme } from "vuetify";
import { axiosPost } from "@/lib/PhotonUtils";
const theme = useTheme();
@@ -15,7 +16,14 @@ const tempSettingsStruct = ref<CameraSettingsChangeRequest>({
fov: useCameraSettingsStore().currentCameraSettings.fov.value,
quirksToChange: Object.assign({}, useCameraSettingsStore().currentCameraSettings.cameraQuirks.quirks)
});
const focusMode = computed<boolean>({
get: () => useCameraSettingsStore().isFocusMode,
set: (v) =>
useCameraSettingsStore().changeCurrentPipelineIndex(
v ? -3 : useCameraSettingsStore().currentCameraSettings.lastPipelineIndex || 0,
true
)
});
const arducamSelectWrapper = computed<number>({
get: () => {
if (tempSettingsStruct.value.quirksToChange.ArduOV9281Controls) return 1;
@@ -112,44 +120,10 @@ watchEffect(() => {
});
const showDeleteCamera = ref(false);
const yesDeleteMySettingsText = ref("");
const deletingCamera = ref(false);
const deleteThisCamera = () => {
if (deletingCamera.value) return;
deletingCamera.value = true;
const payload = { cameraUniqueName: useStateStore().currentCameraUniqueName };
axios
.post("/utils/nukeOneCamera", payload)
.then(() => {
useStateStore().showSnackbarMessage({
message: "Successfully dispatched the delete command. Waiting for backend to start back up",
color: "success"
});
})
.catch((error) => {
if (error.response) {
useStateStore().showSnackbarMessage({
message: "The backend is unable to fulfil the request to delete this camera.",
color: "error"
});
} else if (error.request) {
useStateStore().showSnackbarMessage({
message: "Error while trying to process the request! The backend didn't respond.",
color: "error"
});
} else {
useStateStore().showSnackbarMessage({
message: "An error occurred while trying to process the request.",
color: "error"
});
}
})
.finally(() => {
deletingCamera.value = false;
showDeleteCamera.value = false;
});
axiosPost("/utils/nukeOneCamera", "delete this camera", {
cameraUniqueName: useStateStore().currentCameraUniqueName
});
};
const wrappedCameras = computed<SelectItem[]>(() =>
Object.keys(useCameraSettingsStore().cameras).map((cameraUniqueName) => ({
@@ -192,6 +166,11 @@ const wrappedCameras = computed<SelectItem[]>(() =>
]"
:select-cols="8"
/>
<pv-switch
v-model="focusMode"
tooltip="Enable Focus Mode to start focusing the lens on your camera"
label="Focus Mode"
></pv-switch>
</v-card-text>
<v-card-text class="d-flex pt-0">
<v-col cols="6" class="pa-0 pr-2">
@@ -221,45 +200,13 @@ const wrappedCameras = computed<SelectItem[]>(() =>
</v-col>
</v-card-text>
<v-dialog v-model="showDeleteCamera" width="800">
<v-card color="surface" flat>
<v-card-title> Delete {{ useCameraSettingsStore().currentCameraSettings.nickname }}? </v-card-title>
<v-card-text class="pt-0 pb-10px">
Are you sure you want to delete "{{ useCameraSettingsStore().currentCameraSettings.nickname }}"? This cannot
be undone.
</v-card-text>
<v-card-text class="pt-0 pb-10px">
<pv-input
v-model="yesDeleteMySettingsText"
:label="'Type &quot;' + useCameraSettingsStore().currentCameraName + '&quot;:'"
:label-cols="6"
:input-cols="6"
/>
</v-card-text>
<v-card-actions class="pa-5 pt-0">
<v-btn
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
color="primary"
class="text-black"
@click="showDeleteCamera = false"
>
Cancel
</v-btn>
<v-btn
color="error"
:disabled="
yesDeleteMySettingsText.toLowerCase() !== useCameraSettingsStore().currentCameraName.toLowerCase()
"
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
:loading="deletingCamera"
@click="deleteThisCamera"
>
<v-icon start class="open-icon" size="large"> mdi-trash-can-outline </v-icon>
<span class="open-label">Delete</span>
</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
<pv-delete-modal
v-model="showDeleteCamera"
title="Delete Camera"
:description="`Are you sure you want to delete the camera '${useCameraSettingsStore().currentCameraSettings.nickname}'? This action cannot be undone.`"
:expected-confirmation-text="useCameraSettingsStore().currentCameraSettings.nickname"
:on-confirm="deleteThisCamera"
/>
</v-card>
</template>

View File

@@ -58,6 +58,15 @@ const fpsTooLow = computed<boolean>(() => {
<v-chip v-else label color="red" variant="text" style="font-size: 1rem; padding: 0; margin: 0">
<span class="pr-1">Camera not connected</span>
</v-chip>
<v-chip
v-if="useCameraSettingsStore().isFocusMode"
label
color="primary"
variant="text"
style="font-size: 1rem; padding: 0; margin: auto"
>
<span class="pr-1"> Focus: {{ Math.round(useStateStore().currentPipelineResults?.focus || 0) }} </span>
</v-chip>
<v-switch
v-model="driverMode"
:disabled="useCameraSettingsStore().isCalibrationMode || useCameraSettingsStore().pipelineNames.length === 0"
@@ -95,7 +104,11 @@ const fpsTooLow = computed<boolean>(() => {
color="buttonPassive"
class="fill"
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
:disabled="useCameraSettingsStore().isDriverMode || useCameraSettingsStore().isCalibrationMode"
:disabled="
useCameraSettingsStore().isDriverMode ||
useCameraSettingsStore().isCalibrationMode ||
useCameraSettingsStore().isFocusMode
"
>
<v-icon start class="mode-btn-icon" size="large">mdi-import</v-icon>
<span class="mode-btn-label">Raw</span>
@@ -104,7 +117,11 @@ const fpsTooLow = computed<boolean>(() => {
color="buttonPassive"
class="fill"
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
:disabled="useCameraSettingsStore().isDriverMode || useCameraSettingsStore().isCalibrationMode"
:disabled="
useCameraSettingsStore().isDriverMode ||
useCameraSettingsStore().isCalibrationMode ||
useCameraSettingsStore().isFocusMode
"
>
<v-icon start class="mode-btn-icon" size="large">mdi-export</v-icon>
<span class="mode-btn-label">Processed</span>

View File

@@ -0,0 +1,85 @@
<script setup lang="ts">
import { ref } from "vue";
import { useTheme } from "vuetify";
import pvInput from "./pv-input.vue";
const theme = useTheme();
const value = defineModel<boolean | undefined>({ required: true });
const props = withDefaults(
defineProps<{
expectedConfirmationText?: string;
onBackup?: () => void;
onConfirm: () => void;
title: string;
description?: string;
deleteText?: string;
width?: number;
}>(),
{
width: 700
}
);
const confirmationText = ref("");
</script>
<template>
<v-dialog v-model="value" :width="props.width" dark>
<v-card color="surface" flat>
<v-card-title style="display: flex; justify-content: center">
{{ title }}
</v-card-title>
<v-card-text class="pt-0 pb-10px">
<span> {{ description }} </span>
</v-card-text>
<v-card-text v-if="expectedConfirmationText" class="pt-0 pb-0">
<pv-input
v-model="confirmationText"
:label="'Type &quot;' + expectedConfirmationText + '&quot;:'"
:label-cols="6"
:input-cols="6"
/>
</v-card-text>
<v-card-text class="pt-10px">
<v-row class="align-center text-white">
<v-col v-if="onBackup" cols="6">
<v-btn
color="buttonActive"
style="float: right"
width="100%"
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
@click="onBackup"
>
<v-icon start class="open-icon" size="large"> mdi-export </v-icon>
<span class="open-label">Backup Data</span>
</v-btn>
</v-col>
<v-col v-if="description" :cols="onBackup ? '6' : '12'">
<v-btn
color="error"
width="100%"
:disabled="
expectedConfirmationText
? confirmationText.toLowerCase() !== expectedConfirmationText.toLowerCase()
: false
"
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
@click="
onConfirm();
confirmationText = '';
value = false;
"
>
<v-icon start class="open-icon" size="large"> mdi-trash-can-outline </v-icon>
<span class="open-label">
{{ deleteText ?? title }}
</span>
</v-btn>
</v-col>
</v-row>
</v-card-text>
</v-card>
</v-dialog>
</template>

View File

@@ -9,6 +9,7 @@ import PvInput from "@/components/common/pv-input.vue";
import { PipelineType } from "@/types/PipelineTypes";
import { useSettingsStore } from "@/stores/settings/GeneralSettingsStore";
import { useTheme } from "vuetify";
import PvDeleteModal from "@/components/common/pv-delete-modal.vue";
const theme = useTheme();
@@ -92,6 +93,9 @@ const pipelineNamesWrapper = computed<SelectItem[]>(() => {
if (useCameraSettingsStore().isDriverMode) {
pipelineNames.push({ name: "Driver Mode", value: WebsocketPipelineType.DriverMode });
}
if (useCameraSettingsStore().isFocusMode) {
pipelineNames.push({ name: "Focus Mode", value: WebsocketPipelineType.FocusCamera });
}
if (useCameraSettingsStore().isCalibrationMode) {
pipelineNames.push({ name: "3D Calibration Mode", value: WebsocketPipelineType.Calib3d });
}
@@ -130,7 +134,7 @@ const validNewPipelineTypes = computed(() => {
{ name: "Reflective", value: WebsocketPipelineType.Reflective },
{ name: "Colored Shape", value: WebsocketPipelineType.ColoredShape },
{ name: "AprilTag", value: WebsocketPipelineType.AprilTag },
{ name: "Aruco", value: WebsocketPipelineType.Aruco }
{ name: "ArUco", value: WebsocketPipelineType.Aruco }
];
if (useSettingsStore().general.supportedBackends.length > 0) {
pipelineTypes.push({ name: "Object Detection", value: WebsocketPipelineType.ObjectDetection });
@@ -168,7 +172,7 @@ const pipelineTypesWrapper = computed<{ name: string; value: number }[]>(() => {
{ name: "Reflective", value: WebsocketPipelineType.Reflective },
{ name: "Colored Shape", value: WebsocketPipelineType.ColoredShape },
{ name: "AprilTag", value: WebsocketPipelineType.AprilTag },
{ name: "Aruco", value: WebsocketPipelineType.Aruco }
{ name: "ArUco", value: WebsocketPipelineType.Aruco }
];
if (useSettingsStore().general.supportedBackends.length > 0) {
pipelineTypes.push({ name: "Object Detection", value: WebsocketPipelineType.ObjectDetection });
@@ -177,6 +181,9 @@ const pipelineTypesWrapper = computed<{ name: string; value: number }[]>(() => {
if (useCameraSettingsStore().isDriverMode) {
pipelineTypes.push({ name: "Driver Mode", value: WebsocketPipelineType.DriverMode });
}
if (useCameraSettingsStore().isFocusMode) {
pipelineTypes.push({ name: "Focus Mode", value: WebsocketPipelineType.FocusCamera });
}
if (useCameraSettingsStore().isCalibrationMode) {
pipelineTypes.push({ name: "3D Calibration Mode", value: WebsocketPipelineType.Calib3d });
}
@@ -187,6 +194,7 @@ const pipelineType = ref<WebsocketPipelineType>(useCameraSettingsStore().current
const currentPipelineType = computed<WebsocketPipelineType>({
get: () => {
if (useCameraSettingsStore().isDriverMode) return WebsocketPipelineType.DriverMode;
if (useCameraSettingsStore().isFocusMode) return WebsocketPipelineType.FocusCamera;
if (useCameraSettingsStore().isCalibrationMode) return WebsocketPipelineType.Calib3d;
return pipelineType.value;
},
@@ -290,6 +298,7 @@ const wrappedCameras = computed<SelectItem[]>(() =>
tooltip="Each pipeline runs on a camera output and stores a unique set of processing settings"
:disabled="
useCameraSettingsStore().isDriverMode ||
useCameraSettingsStore().isFocusMode ||
useCameraSettingsStore().isCalibrationMode ||
!useCameraSettingsStore().hasConnected
"
@@ -366,6 +375,7 @@ const wrappedCameras = computed<SelectItem[]>(() =>
tooltip="Changes the pipeline type, which changes the type of processing that will happen on input frames"
:disabled="
useCameraSettingsStore().isDriverMode ||
useCameraSettingsStore().isFocusMode ||
useCameraSettingsStore().isCalibrationMode ||
!useCameraSettingsStore().hasConnected
"
@@ -413,33 +423,13 @@ const wrappedCameras = computed<SelectItem[]>(() =>
</v-card-actions>
</v-card>
</v-dialog>
<v-dialog v-model="showPipelineDeletionConfirmationDialog" width="500">
<v-card color="surface">
<v-card-title class="pb-0">Delete Pipeline</v-card-title>
<v-card-text>
Are you sure you want to delete
<span style="color: white">"{{ useCameraSettingsStore().currentPipelineSettings.pipelineNickname }}"</span>?
This cannot be undone.
</v-card-text>
<v-card-actions class="pa-5 pt-0">
<v-btn
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
color="primary"
class="text-black"
@click="showPipelineDeletionConfirmationDialog = false"
>
Cancel
</v-btn>
<v-btn
color="error"
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
@click="confirmDeleteCurrentPipeline"
>
Delete
</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
<pv-delete-modal
v-model="showPipelineDeletionConfirmationDialog"
:width="500"
title="Delete Pipeline"
description="Are you sure you want to delete the current pipeline? This action cannot be undone."
:on-confirm="confirmDeleteCurrentPipeline"
/>
<v-dialog v-model="showPipelineTypeChangeDialog" persistent width="600">
<v-card color="surface" dark>
<v-card-title class="pb-0">Change Pipeline Type</v-card-title>

View File

@@ -29,7 +29,7 @@ const allTabs = Object.freeze({
thresholdTab: { tabName: "Threshold", component: ThresholdTab },
contoursTab: { tabName: "Contours", component: ContoursTab },
apriltagTab: { tabName: "AprilTag", component: AprilTagTab },
arucoTab: { tabName: "Aruco", component: ArucoTab },
arucoTab: { tabName: "ArUco", component: ArucoTab },
objectDetectionTab: { tabName: "Object Detection", component: ObjectDetectionTab },
outputTab: { tabName: "Output", component: OutputTab },
targetsTab: { tabName: "Targets", component: TargetsTab },
@@ -99,8 +99,8 @@ const tabGroups = computed<ConfigOption[][]>(() => {
!((isAprilTag || isAruco || isObjectDetection) && tabConfig.tabName === "Threshold") && //Filter out threshold tab if we're doing AprilTags
!((isAprilTag || isAruco || isObjectDetection) && tabConfig.tabName === "Contours") && //Filter out contours if we're doing AprilTags
!(!isAprilTag && tabConfig.tabName === "AprilTag") && //Filter out apriltag unless we actually are doing AprilTags
!(!isAruco && tabConfig.tabName === "Aruco") &&
!(!isObjectDetection && tabConfig.tabName === "Object Detection") //Filter out aruco unless we actually are doing Aruco
!(!isAruco && tabConfig.tabName === "ArUco") &&
!(!isObjectDetection && tabConfig.tabName === "Object Detection") //Filter out ArUco unless we actually are doing ArUco
)
)
.filter((it) => it.length); // Remove empty tab groups

View File

@@ -21,7 +21,9 @@ const processingMode = computed<number>({
<template>
<v-card
:disabled="useCameraSettingsStore().isDriverMode || useStateStore().colorPickingMode"
:disabled="
useCameraSettingsStore().isDriverMode || useCameraSettingsStore().isFocusMode || useStateStore().colorPickingMode
"
class="mt-3 rounded-12"
color="surface"
style="flex-grow: 1; display: flex; flex-direction: column"

View File

@@ -2,60 +2,17 @@
import { inject, ref } from "vue";
import { useStateStore } from "@/stores/StateStore";
import PvSelect from "@/components/common/pv-select.vue";
import PvInput from "@/components/common/pv-input.vue";
import axios from "axios";
import PvDeleteModal from "@/components/common/pv-delete-modal.vue";
import { useTheme } from "vuetify";
import { axiosPost } from "@/lib/PhotonUtils";
const theme = useTheme();
const restartProgram = () => {
axios
.post("/utils/restartProgram")
.then(() => {
useStateStore().showSnackbarMessage({ message: "Successfully sent program restart request", color: "success" });
})
.catch((error) => {
// This endpoint always return 204 regardless of outcome
if (error.request) {
useStateStore().showSnackbarMessage({
message: "Error while trying to process the request! The backend didn't respond.",
color: "error"
});
} else {
useStateStore().showSnackbarMessage({
message: "An error occurred while trying to process the request.",
color: "error"
});
}
});
axiosPost("/utils/restartProgram", "restart PhotonVision");
};
const restartDevice = () => {
axios
.post("/utils/restartDevice")
.then(() => {
useStateStore().showSnackbarMessage({
message: "Successfully dispatched the restart command. It isn't confirmed if a device restart will occur.",
color: "success"
});
})
.catch((error) => {
if (error.response) {
useStateStore().showSnackbarMessage({
message: "The backend is unable to fulfil the request to restart the device.",
color: "error"
});
} else if (error.request) {
useStateStore().showSnackbarMessage({
message: "Error while trying to process the request! The backend didn't respond.",
color: "error"
});
} else {
useStateStore().showSnackbarMessage({
message: "An error occurred while trying to process the request.",
color: "error"
});
}
});
axiosPost("/utils/restartDevice", "restart the device");
};
const address = inject<string>("backendHost");
@@ -77,47 +34,27 @@ const handleOfflineUpdate = () => {
timeout: -1
});
axios
.post("/utils/offlineUpdate", formData, {
headers: { "Content-Type": "multipart/form-data" },
onUploadProgress: ({ progress }) => {
const uploadPercentage = (progress || 0) * 100.0;
if (uploadPercentage < 99.5) {
useStateStore().showSnackbarMessage({
message: "New Software Upload in Process, " + uploadPercentage.toFixed(2) + "% complete",
color: "secondary",
timeout: -1
});
} else {
useStateStore().showSnackbarMessage({
message: "Installing uploaded software...",
color: "secondary",
timeout: -1
});
}
}
})
.then((response) => {
useStateStore().showSnackbarMessage({ message: response.data.text || response.data, color: "success" });
})
.catch((error) => {
if (error.response) {
axiosPost("/utils/offlineUpdate", "upload new software", formData, {
headers: { "Content-Type": "multipart/form-data" },
onUploadProgress: ({ progress }) => {
const uploadPercentage = (progress || 0) * 100.0;
if (uploadPercentage < 99.5) {
useStateStore().showSnackbarMessage({
color: "error",
message: error.response.data.text || error.response.data
});
} else if (error.request) {
useStateStore().showSnackbarMessage({
color: "error",
message: "Error while trying to process the request! The backend didn't respond."
message: "New Software Upload in Progress",
color: "secondary",
timeout: -1,
progressBar: uploadPercentage,
progressBarColor: "primary"
});
} else {
useStateStore().showSnackbarMessage({
color: "error",
message: "An error occurred while trying to process the request."
message: "Installing uploaded software...",
color: "secondary",
timeout: -1
});
}
});
}
});
};
const exportLogFile = ref();
@@ -166,29 +103,9 @@ const handleSettingsImport = () => {
break;
}
axios
.post(`/settings${settingsEndpoint}`, formData, { headers: { "Content-Type": "multipart/form-data" } })
.then((response) => {
useStateStore().showSnackbarMessage({ message: response.data.text || response.data, color: "success" });
})
.catch((error) => {
if (error.response) {
useStateStore().showSnackbarMessage({
color: "error",
message: error.response.data.text || error.response.data
});
} else if (error.request) {
useStateStore().showSnackbarMessage({
color: "error",
message: "Error while trying to process the request! The backend didn't respond."
});
} else {
useStateStore().showSnackbarMessage({
color: "error",
message: "An error occurred while trying to process the request."
});
}
});
axiosPost(`/settings${settingsEndpoint}`, "import settings", formData, {
headers: { "Content-Type": "multipart/form-data" }
});
showImportDialog.value = false;
importType.value = undefined;
@@ -196,36 +113,8 @@ const handleSettingsImport = () => {
};
const showFactoryReset = ref(false);
const expected = "Delete Everything";
const yesDeleteMySettingsText = ref("");
const nukePhotonConfigDirectory = () => {
axios
.post("/utils/nukeConfigDirectory")
.then(() => {
useStateStore().showSnackbarMessage({
message: "Successfully dispatched the reset command. Waiting for backend to start back up",
color: "success"
});
})
.catch((error) => {
if (error.response) {
useStateStore().showSnackbarMessage({
message: "The backend is unable to fulfill the request to reset the device.",
color: "error"
});
} else if (error.request) {
useStateStore().showSnackbarMessage({
message: "Error while trying to process the request! The backend didn't respond.",
color: "error"
});
} else {
useStateStore().showSnackbarMessage({
message: "An error occurred while trying to process the request.",
color: "error"
});
}
});
showFactoryReset.value = false;
axiosPost("/utils/nukeConfigDirectory", "delete the config directory");
};
</script>
@@ -387,63 +276,15 @@ const nukePhotonConfigDirectory = () => {
</v-col>
</v-row>
</div>
<v-dialog v-model="showFactoryReset" width="800" dark>
<v-card color="surface" flat>
<v-card-title style="display: flex; justify-content: center">
<span class="open-label">
<v-icon end color="red" class="open-icon ma-1" size="large">mdi-alert-outline</v-icon>
Factory Reset PhotonVision
<v-icon end color="red" class="open-icon ma-1" size="large">mdi-alert-outline</v-icon>
</span>
</v-card-title>
<v-card-text class="pt-0 pb-10px">
<v-row class="align-center text-white">
<v-col cols="12" md="6">
<span> This will delete ALL OF YOUR SETTINGS and restart PhotonVision. </span>
</v-col>
<v-col cols="12" md="6">
<v-btn
color="primary"
style="float: right"
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
@click="openExportSettingsPrompt"
>
<v-icon start class="open-icon" size="large"> mdi-export </v-icon>
<span class="open-label">Backup Settings</span>
<a
ref="exportSettings"
style="color: black; text-decoration: none; display: none"
:href="`http://${address}/api/settings/photonvision_config.zip`"
download="photonvision-settings.zip"
target="_blank"
/>
</v-btn>
</v-col>
</v-row>
</v-card-text>
<v-card-text class="pt-0 pb-0">
<pv-input
v-model="yesDeleteMySettingsText"
:label="'Type &quot;' + expected + '&quot;:'"
:label-cols="6"
:input-cols="6"
/>
</v-card-text>
<v-card-text class="pt-10px">
<v-btn
color="error"
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
:disabled="yesDeleteMySettingsText.toLowerCase() !== expected.toLowerCase()"
@click="nukePhotonConfigDirectory"
>
<v-icon start class="open-icon" size="large"> mdi-trash-can-outline </v-icon>
<span class="open-label">
{{ $vuetify.display.mdAndUp ? "Delete everything, I have backed up what I need" : "Delete Everything" }}
</span>
</v-btn>
</v-card-text>
</v-card>
</v-dialog>
<pv-delete-modal
v-model="showFactoryReset"
title="Factory Reset PhotonVision"
description="This will delete all settings and configurations stored on this device, including network settings. This action cannot be undone."
expected-confirmation-text="Delete Everything"
:on-confirm="nukePhotonConfigDirectory"
:on-backup="openExportSettingsPrompt"
delete-text="Factory reset"
/>
</v-card>
</template>

View File

@@ -315,23 +315,23 @@ watchEffect(() => {
<v-col class="text-center">
Background
<v-color-picker
v-model:model-value="backgroundColor"
class="ma-auto pt-3"
elevation="0"
mode="hex"
:modes="['hex']"
v-model:model-value="backgroundColor"
v-on:update:model-value="(hex) => setThemeColor(theme, 'background', hex)"
@update:model-value="(hex) => setThemeColor(theme, 'background', hex)"
></v-color-picker>
</v-col>
<v-col class="text-center">
Surface
<v-color-picker
v-model:model-value="surfaceColor"
class="ma-auto pt-3"
elevation="0"
mode="hex"
:modes="['hex']"
v-model:model-value="surfaceColor"
v-on:update:model-value="(hex) => setThemeColor(theme, 'surface', hex)"
@update:model-value="(hex) => setThemeColor(theme, 'surface', hex)"
></v-color-picker>
</v-col>
</v-row>
@@ -339,23 +339,23 @@ watchEffect(() => {
<v-col class="text-center">
Primary
<v-color-picker
v-model:model-value="primaryColor"
class="ma-auto pt-3"
elevation="0"
mode="hex"
:modes="['hex']"
v-model:model-value="primaryColor"
v-on:update:model-value="(hex) => setThemeColor(theme, 'primary', hex)"
@update:model-value="(hex) => setThemeColor(theme, 'primary', hex)"
></v-color-picker>
</v-col>
<v-col class="text-center">
Secondary
<v-color-picker
v-model:model-value="secondaryColor"
class="ma-auto pt-3"
elevation="0"
mode="hex"
:modes="['hex']"
v-model:model-value="secondaryColor"
v-on:update:model-value="(hex) => setThemeColor(theme, 'secondary', hex)"
@update:model-value="(hex) => setThemeColor(theme, 'secondary', hex)"
></v-color-picker>
</v-col>
</v-row>

View File

@@ -1,11 +1,11 @@
<script setup lang="ts">
import { ref, computed, inject } from "vue";
import axios from "axios";
import { useStateStore } from "@/stores/StateStore";
import { useSettingsStore } from "@/stores/settings/GeneralSettingsStore";
import type { ObjectDetectionModelProperties } from "@/types/SettingTypes";
import pvInput from "@/components/common/pv-input.vue";
import { type ObjectDetectionModelProperties } from "@/types/SettingTypes";
import PvDeleteModal from "@/components/common/pv-delete-modal.vue";
import { useTheme } from "vuetify";
import { axiosPost } from "@/lib/PhotonUtils";
const theme = useTheme();
const showImportDialog = ref(false);
@@ -43,34 +43,25 @@ const handleImport = async () => {
timeout: -1
});
axios
.post("/objectdetection/import", formData, {
headers: { "Content-Type": "multipart/form-data" }
})
.then((response) => {
useStateStore().showSnackbarMessage({
message: response.data.text || response.data,
color: "success"
});
})
.catch((error) => {
if (error.response) {
axiosPost("/objectdetection/import", "import an object detection model", formData, {
headers: { "Content-Type": "multipart/form-data" },
onUploadProgress: ({ progress }) => {
const uploadPercentage = (progress || 0) * 100.0;
if (uploadPercentage < 99.5) {
useStateStore().showSnackbarMessage({
color: "error",
message: error.response.data.text || error.response.data
});
} else if (error.request) {
useStateStore().showSnackbarMessage({
color: "error",
message: "Error while trying to process the request! The backend didn't respond."
message: "Object Detection Model Upload in Process, " + uploadPercentage.toFixed(2) + "% complete",
color: "secondary",
timeout: -1
});
} else {
useStateStore().showSnackbarMessage({
color: "error",
message: "An error occurred while trying to process the request."
message: "Processing uploaded Object Detection Model...",
color: "secondary",
timeout: -1
});
}
});
}
});
showImportDialog.value = false;
@@ -82,41 +73,9 @@ const handleImport = async () => {
};
const deleteModel = async (model: ObjectDetectionModelProperties) => {
useStateStore().showSnackbarMessage({
message: "Deleting Object Detection Model...",
color: "secondary",
timeout: -1
axiosPost("/objectdetection/delete", "delete an object detection model", {
modelPath: model.modelPath
});
axios
.post("/objectdetection/delete", {
modelPath: model.modelPath
})
.then((response) => {
useStateStore().showSnackbarMessage({
message: response.data.text || response.data,
color: "success"
});
})
.catch((error) => {
if (error.response) {
useStateStore().showSnackbarMessage({
color: "error",
message: error.response.data.text || error.response.data
});
} else if (error.request) {
useStateStore().showSnackbarMessage({
color: "error",
message: "Error while trying to process the request! The backend didn't respond."
});
} else {
useStateStore().showSnackbarMessage({
color: "error",
message: "An error occurred while trying to process the request."
});
}
});
confirmDeleteDialog.value.show = false;
};
const renameModel = async (model: ObjectDetectionModelProperties, newName: string) => {
@@ -126,35 +85,10 @@ const renameModel = async (model: ObjectDetectionModelProperties, newName: strin
timeout: -1
});
axios
.post("/objectdetection/rename", {
modelPath: model.modelPath.replace("file:", ""),
newName: newName
})
.then((response) => {
useStateStore().showSnackbarMessage({
message: response.data.text || response.data,
color: "success"
});
})
.catch((error) => {
if (error.response) {
useStateStore().showSnackbarMessage({
color: "error",
message: error.response.data.text || error.response.data
});
} else if (error.request) {
useStateStore().showSnackbarMessage({
color: "error",
message: "Error while trying to process the request! The backend didn't respond."
});
} else {
useStateStore().showSnackbarMessage({
color: "error",
message: "An error occurred while trying to process the request."
});
}
});
axiosPost("/objectdetection/rename", "rename an object detection model", {
modelPath: model.modelPath.replace("file:", ""),
newName: newName
});
showRenameDialog.value.show = false;
};
@@ -181,36 +115,8 @@ const openExportIndividualModelPrompt = () => {
};
const showNukeDialog = ref(false);
const expected = "Delete Models";
const yesDeleteMyModelsText = ref("");
const nukeModels = () => {
axios
.post("/objectdetection/nuke")
.then(() => {
useStateStore().showSnackbarMessage({
message: "Successfully dispatched the clear models command.",
color: "success"
});
})
.catch((error) => {
if (error.response) {
useStateStore().showSnackbarMessage({
message: "The backend is unable to fulfill the request to clear the models.",
color: "error"
});
} else if (error.request) {
useStateStore().showSnackbarMessage({
message: "Error while trying to process the request! The backend didn't respond.",
color: "error"
});
} else {
useStateStore().showSnackbarMessage({
message: "An error occurred while trying to process the request.",
color: "error"
});
}
});
showNukeDialog.value = false;
axiosPost("/objectdetection/nuke", "clear and reset object detection models");
};
const showBulkImportDialog = ref(false);
@@ -221,51 +127,27 @@ const handleBulkImport = () => {
const formData = new FormData();
formData.append("data", importFile.value);
axios
.post("/objectdetection/bulkimport", formData, {
headers: { "Content-Type": "multipart/form-data" },
onUploadProgress: ({ progress }) => {
const uploadPercentage = (progress || 0) * 100.0;
if (uploadPercentage < 99.5) {
useStateStore().showSnackbarMessage({
message: "Object Detection Models Upload in Process, " + uploadPercentage.toFixed(2) + "% complete",
color: "secondary",
timeout: -1
});
} else {
useStateStore().showSnackbarMessage({
message: "Importing New Object Detection Models...",
color: "secondary",
timeout: -1
});
}
}
})
.then((response) => {
useStateStore().showSnackbarMessage({
message: response.data.text || response.data,
color: "success"
});
})
.catch((error) => {
if (error.response) {
axiosPost("/objectdetection/bulkimport", "import object detection models", formData, {
headers: { "Content-Type": "multipart/form-data" },
onUploadProgress: ({ progress }) => {
const uploadPercentage = (progress || 0) * 100.0;
if (uploadPercentage < 99.5) {
useStateStore().showSnackbarMessage({
color: "error",
message: error.response.data.text || error.response.data
});
} else if (error.request) {
useStateStore().showSnackbarMessage({
color: "error",
message: "Error while trying to process the request! The backend didn't respond."
message: "Object Detection Models Upload in Progress",
color: "secondary",
timeout: -1,
progressBar: uploadPercentage,
progressBarColor: "primary"
});
} else {
useStateStore().showSnackbarMessage({
color: "error",
message: "An error occurred while trying to process the request."
message: "Importing New Object Detection Models...",
color: "secondary",
timeout: -1
});
}
});
}
});
showImportDialog.value = false;
importFile.value = null;
};
@@ -485,35 +367,20 @@ const handleBulkImport = () => {
</tbody>
</v-table>
<v-dialog v-model="confirmDeleteDialog.show" width="600">
<v-card color="surface" dark>
<v-card-title>Delete Object Detection Model</v-card-title>
<v-card-text class="pt-0">
Are you sure you want to delete the model {{ confirmDeleteDialog.model.nickname }}?
<v-card-actions class="pt-5 pb-0 pr-0" style="justify-content: flex-end">
<v-btn
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
color="buttonPassive"
@click="confirmDeleteDialog.show = false"
>
Cancel
</v-btn>
<v-btn
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
color="error"
@click="deleteModel(confirmDeleteDialog.model)"
>
Delete
</v-btn>
</v-card-actions>
</v-card-text>
</v-card>
</v-dialog>
<pv-delete-modal
v-model="confirmDeleteDialog.show"
:width="500"
:on-confirm="() => deleteModel(confirmDeleteDialog.model)"
title="Delete Object Detection Model"
:description="`Are you sure you want to delete the model ${confirmDeleteDialog.model.nickname}?`"
delete-text="Delete model"
/>
<v-dialog v-model="showRenameDialog.show" width="600">
<v-card color="surface" dark>
<v-card-title>Rename Object Detection Model</v-card-title>
<v-card-text class="pt-0">
Enter a new name for the model {{ showRenameDialog.model.nickname }}:
Enter a new name for the model "{{ showRenameDialog.model.nickname }}":
<div class="pa-5 pb-0">
<v-text-field v-model="showRenameDialog.newName" hide-details label="New Name" variant="underlined" />
</div>
@@ -569,64 +436,15 @@ const handleBulkImport = () => {
</v-row>
</div>
<v-dialog v-model="showNukeDialog" width="800" dark>
<v-card color="surface" flat>
<v-card-title style="display: flex; justify-content: center">
<span class="open-label">
<v-icon end color="error" class="open-icon ma-1" size="large">mdi-alert-outline</v-icon>
Clear and Reset Object Detection Models
<v-icon end color="error" class="open-icon ma-1" size="large">mdi-alert-outline</v-icon>
</span>
</v-card-title>
<v-card-text class="pt-0 pb-10px">
<v-row class="align-center text-white">
<v-col cols="12" md="6">
<span> This will delete ALL OF YOUR MODELS and re-extract the default models. </span>
</v-col>
<v-col cols="12" md="6">
<v-btn
color="buttonActive"
style="float: right"
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
@click="openExportPrompt"
>
<v-icon start class="open-icon" size="large"> mdi-export </v-icon>
<span class="open-label">Backup Models</span>
<a
ref="exportModels"
style="color: black; text-decoration: none; display: none"
:href="`http://${address}/api/objectdetection/export`"
download="photonvision-object-detection-models-export.zip"
target="_blank"
/>
</v-btn>
</v-col>
</v-row>
</v-card-text>
<v-card-text class="pt-0 pb-0">
<pv-input
v-model="yesDeleteMyModelsText"
:label="'Type &quot;' + expected + '&quot;:'"
:label-cols="6"
:input-cols="6"
/>
</v-card-text>
<v-card-text class="pt-10px">
<v-btn
color="error"
width="100%"
:disabled="yesDeleteMyModelsText.toLowerCase() !== expected.toLowerCase()"
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
@click="nukeModels"
>
<v-icon start class="open-icon" size="large"> mdi-trash-can-outline </v-icon>
<span class="open-label">
{{ $vuetify.display.mdAndUp ? "Delete models, I have backed up what I need" : "Delete Models" }}
</span>
</v-btn>
</v-card-text>
</v-card>
</v-dialog>
<pv-delete-modal
v-model="showNukeDialog"
:on-backup="openExportPrompt"
:on-confirm="nukeModels"
title="Delete and Reset All Object Detection Models"
:description="'This will delete ALL object detection models and re-extract the default object detection models. This action cannot be undone.'"
:expected-confirmation-text="'Delete Models'"
delete-text="Delete all models"
/>
</v-card>
</template>

View File

@@ -1,4 +1,6 @@
import { useStateStore } from "@/stores/StateStore";
import type { Resolution } from "@/types/SettingTypes";
import axios from "axios";
export const resolutionsAreEqual = (a: Resolution, b: Resolution) => {
return a.height === b.height && a.width === b.width;
@@ -18,3 +20,41 @@ export const parseJsonFile = async <T extends Record<string, any>>(file: File):
fileReader.readAsText(file);
});
};
/**
* A helper function to make POST requests using axios with standardized success and error handling.
*
* @param url The endpoint URL to which the POST request is sent
* @param description A brief description of the request for users, e.g., "import object detection models".
* @param data Payload to be sent in the POST request
* @param config Optional axios request configuration
* @returns A promise that resolves when the POST request is complete
*/
export const axiosPost = (url: string, description: string, data?: any, config?: any): Promise<void> => {
return axios
.post(url, data, config)
.then(() => {
useStateStore().showSnackbarMessage({
message: "Successfully dispatched the request to " + description + ". Waiting for backend to respond",
color: "success"
});
})
.catch((error) => {
if (error.response) {
useStateStore().showSnackbarMessage({
message: "The backend is unable to fulfill the request to " + description + ".",
color: "error"
});
} else if (error.request) {
useStateStore().showSnackbarMessage({
message: "Error while trying to process the request to " + description + "! The backend didn't respond.",
color: "error"
});
} else {
useStateStore().showSnackbarMessage({
message: "An error occurred while trying to process the request to " + description + ".",
color: "error"
});
}
});
};

View File

@@ -39,6 +39,8 @@ interface StateStore {
snackbarData: {
show: boolean;
progressBar: number;
progressBarColor: string;
message: string;
color: string;
timeout: number;
@@ -86,6 +88,8 @@ export const useStateStore = defineStore("state", {
snackbarData: {
show: false,
progressBar: -1,
progressBarColor: "info",
message: "No Message",
color: "info",
timeout: 2000
@@ -158,11 +162,19 @@ export const useStateStore = defineStore("state", {
updateDiscoveredCameras(data: VsmState) {
this.vsmState = data;
},
showSnackbarMessage(data: { message: string; color: string; timeout?: number }) {
showSnackbarMessage(data: {
message: string;
color: string;
timeout?: number;
progressBar?: number;
progressBarColor?: string;
}) {
this.snackbarData = {
show: true,
progressBar: data.progressBar || -1,
message: data.message,
color: data.color,
progressBarColor: data.progressBarColor || "",
timeout: data.timeout || 2000
};
}

View File

@@ -45,7 +45,7 @@ export const useCameraSettingsStore = defineStore("cameraSettings", {
},
// This method only exists due to just how lazy I am and my dislike of consolidating the pipeline type enums (which mind you, suck as is)
currentWebsocketPipelineType(): WebsocketPipelineType {
return this.currentPipelineType - 2;
return this.currentPipelineType - 3;
},
currentVideoFormat(): VideoFormat {
return this.currentCameraSettings.validVideoFormats[this.currentPipelineSettings.cameraVideoModeIndex];
@@ -76,6 +76,9 @@ export const useCameraSettingsStore = defineStore("cameraSettings", {
isCalibrationMode(): boolean {
return this.currentCameraSettings.currentPipelineIndex == WebsocketPipelineType.Calib3d;
},
isFocusMode(): boolean {
return this.currentCameraSettings.currentPipelineIndex == WebsocketPipelineType.FocusCamera;
},
isCSICamera(): boolean {
return this.currentCameraSettings.isCSICamera;
},

View File

@@ -70,6 +70,8 @@ export interface PipelineResult {
sequenceID: number;
fps: number;
latency: number;
// Focus pipeline
focus?: number;
targets: PhotonTarget[];
// undefined if multitag failed or non-tag pipeline
multitagResult?: MultitagResult;

View File

@@ -1,13 +1,16 @@
import type { WebsocketNumberPair } from "@/types/WebsocketDataTypes";
import type { ObjectDetectionModelProperties } from "@/types/SettingTypes";
/**
* The on-wire form of PipelineType.java (the enum is serialized with `ordinal()`)
*/
export enum PipelineType {
DriverMode = 1,
Reflective = 2,
ColoredShape = 3,
AprilTag = 4,
Aruco = 5,
ObjectDetection = 6
DriverMode = 2,
Reflective = 3,
ColoredShape = 4,
AprilTag = 5,
Aruco = 6,
ObjectDetection = 7
}
export enum AprilTagFamily {

View File

@@ -110,6 +110,7 @@ export interface IncomingWebsocketData {
}
export enum WebsocketPipelineType {
FocusCamera = -3,
Calib3d = -2,
DriverMode = -1,
Reflective = 0,

View File

@@ -7,16 +7,13 @@ import {
PVCameraInfo,
type PVCSICameraInfo,
type PVFileCameraInfo,
type PVUsbCameraInfo,
type UiCameraConfiguration
type PVUsbCameraInfo
} from "@/types/SettingTypes";
import { getResolutionString } from "@/lib/PhotonUtils";
import { axiosPost, getResolutionString } from "@/lib/PhotonUtils";
import PhotonCameraStream from "@/components/app/photon-camera-stream.vue";
import PvInput from "@/components/common/pv-input.vue";
import PvDeleteModal from "@/components/common/pv-delete-modal.vue";
import PvCameraInfoCard from "@/components/common/pv-camera-info-card.vue";
import axios from "axios";
import PvCameraMatchCard from "@/components/common/pv-camera-match-card.vue";
import type { WebsocketCameraSettingsUpdate } from "@/types/WebsocketDataTypes";
import { useTheme } from "vuetify";
const theme = useTheme();
@@ -28,33 +25,9 @@ const activateModule = (moduleUniqueName: string) => {
if (activatingModule.value) return;
activatingModule.value = true;
axios
.post("/utils/activateMatchedCamera", { cameraUniqueName: moduleUniqueName })
.then(() => {
useStateStore().showSnackbarMessage({
message: "Camera activated successfully",
color: "success"
});
})
.catch((error) => {
if (error.response) {
useStateStore().showSnackbarMessage({
message: "The backend is unable to fulfil the request to activate this camera.",
color: "error"
});
} else if (error.request) {
useStateStore().showSnackbarMessage({
message: "Error while trying to process the request! The backend didn't respond.",
color: "error"
});
} else {
useStateStore().showSnackbarMessage({
message: "An error occurred while trying to process the request.",
color: "error"
});
}
})
.finally(() => (activatingModule.value = false));
axiosPost("/utils/activateMatchedCamera", "activate a matched camera", {
cameraUniqueName: moduleUniqueName
}).finally(() => (activatingModule.value = false));
};
const assigningCamera = ref(false);
@@ -66,106 +39,29 @@ const assignCamera = (cameraInfo: PVCameraInfo) => {
cameraInfo: cameraInfo
};
axios
.post("/utils/assignUnmatchedCamera", payload)
.then(() => {
useStateStore().showSnackbarMessage({
message: "Unmatched camera assigned successfully",
color: "success"
});
})
.catch((error) => {
if (error.response) {
useStateStore().showSnackbarMessage({
message: "The backend is unable to fulfil the request to assign this unmatched camera.",
color: "error"
});
} else if (error.request) {
useStateStore().showSnackbarMessage({
message: "Error while trying to process the request! The backend didn't respond.",
color: "error"
});
} else {
useStateStore().showSnackbarMessage({
message: "An error occurred while trying to process the request.",
color: "error"
});
}
})
.finally(() => (assigningCamera.value = false));
axiosPost("/utils/assignUnmatchedCamera", "assign an unmatched camera", payload).finally(
() => (assigningCamera.value = false)
);
};
const deactivatingModule = ref(false);
const deactivateModule = (cameraUniqueName: string) => {
if (deactivatingModule.value) return;
deactivatingModule.value = true;
axios
.post("/utils/unassignCamera", { cameraUniqueName: cameraUniqueName })
.then(() => {
useStateStore().showSnackbarMessage({
message: "Camera deactivated successfully",
color: "success"
});
})
.catch((error) => {
if (error.response) {
useStateStore().showSnackbarMessage({
message: "The backend is unable to fulfil the request to deactivate this camera.",
color: "error"
});
} else if (error.request) {
useStateStore().showSnackbarMessage({
message: "Error while trying to process the request! The backend didn't respond.",
color: "error"
});
} else {
useStateStore().showSnackbarMessage({
message: "An error occurred while trying to process the request.",
color: "error"
});
}
})
.finally(() => (deactivatingModule.value = false));
axiosPost("/utils/unassignCamera", "unassign a camera", { cameraUniqueName: cameraUniqueName }).finally(
() => (deactivatingModule.value = false)
);
};
const deletingCamera = ref(false);
const deleteThisCamera = (cameraName: string) => {
if (deletingCamera.value) return;
deletingCamera.value = true;
const payload = {
cameraUniqueName: cameraName
};
const confirmDeleteDialog = ref({ show: false, nickname: "", cameraUniqueName: "" });
const deletingCamera = ref<string | null>(null);
axios
.post("/utils/nukeOneCamera", payload)
.then(() => {
useStateStore().showSnackbarMessage({
message: "Camera deleted successfully",
color: "success"
});
})
.catch((error) => {
if (error.response) {
useStateStore().showSnackbarMessage({
message: "The backend is unable to fulfil the request to delete this camera.",
color: "error"
});
} else if (error.request) {
useStateStore().showSnackbarMessage({
message: "Error while trying to process the request! The backend didn't respond.",
color: "error"
});
} else {
useStateStore().showSnackbarMessage({
message: "An error occurred while trying to process the request.",
color: "error"
});
}
})
.finally(() => {
setCameraDeleting(null);
deletingCamera.value = false;
});
const deleteThisCamera = (cameraUniqueName: string) => {
if (deletingCamera.value) return;
deletingCamera.value = cameraUniqueName;
axiosPost("/utils/nukeOneCamera", "delete a camera", { cameraUniqueName: cameraUniqueName }).finally(() => {
deletingCamera.value = null;
});
};
const cameraConnected = (uniquePath: string): boolean => {
@@ -209,15 +105,6 @@ const setCameraView = (camera: PVCameraInfo | null, isConnected: boolean | null)
viewingCamera.value = [camera, isConnected];
};
const viewingDeleteCamera = ref(false);
const cameraToDelete = ref<UiCameraConfiguration | WebsocketCameraSettingsUpdate | null>(null);
const setCameraDeleting = (camera: UiCameraConfiguration | WebsocketCameraSettingsUpdate | null) => {
yesDeleteMySettingsText.value = "";
viewingDeleteCamera.value = camera !== null;
cameraToDelete.value = camera;
};
const yesDeleteMySettingsText = ref("");
/**
* Get the connection-type-specific camera info from the given PVCameraInfo object.
*/
@@ -373,8 +260,16 @@ const getMatchedDevice = (info: PVCameraInfo | undefined): PVCameraInfo => {
class="pa-0"
color="error"
style="width: 100%"
:loading="module.uniqueName === deletingCamera"
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
@click="setCameraDeleting(module)"
@click="
() =>
(confirmDeleteDialog = {
show: true,
nickname: module.nickname,
cameraUniqueName: module.uniqueName
})
"
>
<v-icon size="x-large">mdi-trash-can-outline</v-icon>
</v-btn>
@@ -459,8 +354,16 @@ const getMatchedDevice = (info: PVCameraInfo | undefined): PVCameraInfo => {
class="pa-0"
color="error"
style="width: 100%"
:loading="module.uniqueName === deletingCamera"
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
@click="setCameraDeleting(module)"
@click="
() =>
(confirmDeleteDialog = {
show: true,
nickname: module.nickname,
cameraUniqueName: module.uniqueName
})
"
>
<v-icon size="x-large">mdi-trash-can-outline</v-icon>
</v-btn>
@@ -564,43 +467,13 @@ const getMatchedDevice = (info: PVCameraInfo | undefined): PVCameraInfo => {
</v-card>
</v-dialog>
<!-- Camera delete modal -->
<v-dialog v-model="viewingDeleteCamera" width="800">
<v-card v-if="cameraToDelete !== null" class="dialog-container" color="surface" flat>
<v-card-title> Delete {{ cameraToDelete.nickname }}? </v-card-title>
<v-card-text class="pb-10px">
Are you sure you want to delete "{{ cameraToDelete.nickname }}"? This cannot be undone.
</v-card-text>
<v-card-text class="pt-0 pb-10px">
<pv-input
v-model="yesDeleteMySettingsText"
:label="'Type &quot;' + cameraToDelete.nickname + '&quot;:'"
:label-cols="6"
:input-cols="6"
/>
</v-card-text>
<v-card-actions class="pa-5 pt-0">
<v-btn
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
color="primary"
class="text-black"
@click="cameraToDelete = null"
>
Cancel
</v-btn>
<v-btn
color="error"
:disabled="yesDeleteMySettingsText.toLowerCase() !== cameraToDelete.nickname.toLowerCase()"
:loading="deletingCamera"
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
@click="deleteThisCamera(cameraToDelete.uniqueName)"
>
<v-icon start class="open-icon" size="large"> mdi-trash-can-outline </v-icon>
<span class="open-label">Delete</span>
</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
<pv-delete-modal
v-model="confirmDeleteDialog.show"
title="Delete Camera"
:description="`Are you sure you want to delete the camera '${useCameraSettingsStore().currentCameraSettings.nickname}'? This action cannot be undone.`"
:expected-confirmation-text="confirmDeleteDialog.nickname"
:on-confirm="() => deleteThisCamera(confirmDeleteDialog.cameraUniqueName)"
/>
</div>
</template>

View File

@@ -12,8 +12,13 @@ const cameraViewType = computed<number[]>({
// Only show the input stream in Color Picking Mode
if (useStateStore().colorPickingMode) return [0];
// Only show the output stream in Driver Mode or Calibration Mode
if (useCameraSettingsStore().isDriverMode || useCameraSettingsStore().isCalibrationMode) return [1];
// Only show the output stream in Driver Mode or Calibration Mode or Focus Mode
if (
useCameraSettingsStore().isDriverMode ||
useCameraSettingsStore().isCalibrationMode ||
useCameraSettingsStore().isFocusMode
)
return [1];
const ret: number[] = [];
if (useCameraSettingsStore().currentPipelineSettings.inputShouldShow) {

View File

@@ -17,8 +17,13 @@ const cameraViewType = computed<number[]>({
// Only show the input stream in Color Picking Mode
if (useStateStore().colorPickingMode) return [0];
// Only show the output stream in Driver Mode or Calibration Mode
if (useCameraSettingsStore().isDriverMode || useCameraSettingsStore().isCalibrationMode) return [1];
// Only show the output stream in Driver Mode or Calibration Mode or Focus Mode
if (
useCameraSettingsStore().isDriverMode ||
useCameraSettingsStore().isCalibrationMode ||
useCameraSettingsStore().isFocusMode
)
return [1];
const ret: number[] = [];
if (useCameraSettingsStore().currentPipelineSettings.inputShouldShow) {

View File

@@ -24,6 +24,7 @@ import edu.wpi.first.cscore.UsbCameraInfo;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
import org.opencv.core.Size;
import org.photonvision.common.dataflow.websocket.UICameraConfiguration;
import org.photonvision.common.logging.LogGroup;
import org.photonvision.common.logging.Logger;
@@ -189,6 +190,23 @@ public class CameraConfiguration {
calibrations.add(calibration);
}
/**
* Remove a calibration from our list.
*
* @param calibration The calibration to remove
*/
public void removeCalibration(Size unrotatedImageSize) {
logger.info("deleting calibration " + unrotatedImageSize);
calibrations.stream()
.filter(it -> it.unrotatedImageSize.equals(unrotatedImageSize))
.findAny()
.ifPresent(
(it) -> {
it.release();
calibrations.remove(it);
});
}
/**
* cscore will auto-reconnect to the camera path we give it. v4l does not guarantee that if i swap
* cameras around, the same /dev/videoN ID will be assigned to that camera. So instead default to

View File

@@ -27,6 +27,7 @@ import org.photonvision.common.logging.Logger;
import org.photonvision.common.util.SerializationUtils;
import org.photonvision.vision.pipeline.result.CVPipelineResult;
import org.photonvision.vision.pipeline.result.CalibrationPipelineResult;
import org.photonvision.vision.pipeline.result.FocusPipelineResult;
public class UIDataPublisher implements CVPipelineResultConsumer {
private static final Logger logger = new Logger(UIDataPublisher.class, LogGroup.VisionModule);
@@ -77,6 +78,10 @@ public class UIDataPublisher implements CVPipelineResultConsumer {
var uiMap = new HashMap<String, HashMap<String, Object>>();
uiMap.put(uniqueName, dataMap);
if (result instanceof FocusPipelineResult focusResult) {
dataMap.put("focus", focusResult.focus);
}
DataChangeService.getInstance()
.publishEvent(OutgoingUIEvent.wrappedOf("updatePipelineResult", uiMap));
lastUIResultUpdateTime = now;

View File

@@ -90,7 +90,7 @@ public class PhotonArucoDetector implements Releasable {
// each detection has a Mat of corners
Mat cornerMat = cornerMats.get(i);
// Aruco detection returns corners (BR, BL, TL, TR).
// ArUco detection returns corners (BR, BL, TL, TR).
// For parity with AprilTags and photonlib, we want (BL, BR, TR, TL).
double[] xCorners = {
cornerMat.get(0, 1)[0],

View File

@@ -77,10 +77,9 @@ public class LibcameraGpuSettables extends VisionSourceSettables {
videoModes.put(5, new FPSRatedVideoMode(PixelFormat.kUnknown, 3280 / 2, 2464 / 2, 15, 20, 1));
videoModes.put(6, new FPSRatedVideoMode(PixelFormat.kUnknown, 3280 / 4, 2464 / 4, 15, 20, 1));
} else if (sensorModel == LibCameraJNI.SensorModel.OV9281) {
videoModes.put(0, new FPSRatedVideoMode(PixelFormat.kUnknown, 320, 240, 30, 30, .39));
videoModes.put(1, new FPSRatedVideoMode(PixelFormat.kUnknown, 1280 / 2, 800 / 2, 60, 60, 1));
videoModes.put(2, new FPSRatedVideoMode(PixelFormat.kUnknown, 640, 480, 65, 90, .39));
videoModes.put(3, new FPSRatedVideoMode(PixelFormat.kUnknown, 1280, 800, 60, 60, 1));
// Taken from https://www.ovt.com/wp-content/uploads/2022/01/OV9281-OV9282-PB-v1.3-WEB.pdf
videoModes.put(0, new FPSRatedVideoMode(PixelFormat.kUnknown, 640, 400, 120, 240, 1));
videoModes.put(1, new FPSRatedVideoMode(PixelFormat.kUnknown, 1280, 800, 120, 120, 1));
} else {
if (sensorModel == LibCameraJNI.SensorModel.IMX477) {

View File

@@ -280,14 +280,14 @@ public class FindBoardCornersPipe
}
board.matchImagePoints(detectedCornersList, detectedIds, objPoints, imgPoints);
// draw the charuco board
// Draw the ChArUco board
Objdetect.drawDetectedCornersCharuco(
outFrame, detectedCorners, detectedIds, new Scalar(0, 0, 255)); // Red Text
imgPoints.copyTo(outBoardCorners);
objPoints.copyTo(objPts);
// Since charuco can still detect without the whole board we need to send "fake" (all
// Since ChaArUco can still detect without the whole board we need to send "fake" (all
// values less than zero) points and then tell it to ignore that corner by setting the
// corresponding level to -1. Calibrate3dPipe deals with piping this into the correct format
// for each backend
@@ -321,7 +321,7 @@ public class FindBoardCornersPipe
detectedCorners.release();
detectedIds.release();
} else { // If not Charuco then do chessboard
} else { // If not ChArUco then do chessboard
// Reduce the image size to be much more manageable
// Note that opencv will copy the frame if no resize is requested; we can skip
// this since we

View File

@@ -0,0 +1,56 @@
/*
* 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.vision.pipe.impl;
import org.opencv.core.Core;
import org.opencv.core.CvType;
import org.opencv.core.Mat;
import org.opencv.core.MatOfDouble;
import org.opencv.imgproc.Imgproc;
import org.photonvision.vision.pipe.CVPipe;
public class FocusPipe extends CVPipe<Mat, FocusPipe.FocusResult, FocusPipe.FocusParams> {
private double maxVariance = 0.0;
@Override
protected FocusResult process(Mat in) {
var outputMat = new Mat();
Imgproc.Laplacian(in, outputMat, CvType.CV_64F, 3);
var mean = new MatOfDouble();
var stddev = new MatOfDouble();
Core.meanStdDev(outputMat, mean, stddev);
var sd = stddev.get(0, 0)[0];
var variance = sd * sd;
return new FocusResult(outputMat, variance);
}
public static class FocusResult {
public final Mat frame;
public final double variance;
public FocusResult(Mat frame, double variance) {
this.frame = frame;
this.variance = variance;
}
}
public static class FocusParams {}
}

View File

@@ -73,13 +73,13 @@ public class AprilTagPipeline extends CVPipeline<CVPipelineResult, AprilTagPipel
settings.threads = Math.max(1, settings.threads);
// for now, hard code tag width based on enum value
// 2023/other: best guess is 6in
double tagWidth = Units.inchesToMeters(6);
TargetModel tagModel = TargetModel.kAprilTag16h5;
if (settings.tagFamily == AprilTagFamily.kTag36h11) {
// 2024 tag, 6.5in
tagWidth = Units.inchesToMeters(6.5);
tagModel = TargetModel.kAprilTag36h11;
// From 2024 best guess is 6.5
double tagWidth = Units.inchesToMeters(6.5);
TargetModel tagModel = TargetModel.kAprilTag36h11;
if (settings.tagFamily == AprilTagFamily.kTag16h5) {
// 2023 tag, 6in
tagWidth = Units.inchesToMeters(6);
tagModel = TargetModel.kAprilTag16h5;
}
var config = new AprilTagDetector.Config();

View File

@@ -0,0 +1,94 @@
/*
* 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.vision.pipeline;
import org.opencv.core.Mat;
import org.photonvision.common.util.math.MathUtils;
import org.photonvision.vision.frame.Frame;
import org.photonvision.vision.frame.FrameThresholdType;
import org.photonvision.vision.opencv.CVMat;
import org.photonvision.vision.pipe.impl.CalculateFPSPipe;
import org.photonvision.vision.pipe.impl.FocusPipe;
import org.photonvision.vision.pipe.impl.ResizeImagePipe;
import org.photonvision.vision.pipeline.result.FocusPipelineResult;
public class FocusPipeline extends CVPipeline<FocusPipelineResult, FocusPipelineSettings> {
private final FocusPipe focusPipe = new FocusPipe();
private final CalculateFPSPipe calculateFPSPipe = new CalculateFPSPipe();
private final ResizeImagePipe resizeImagePipe = new ResizeImagePipe();
private static final FrameThresholdType PROCESSING_TYPE = FrameThresholdType.NONE;
public FocusPipeline() {
super(PROCESSING_TYPE);
settings = new FocusPipelineSettings();
}
public FocusPipeline(FocusPipelineSettings settings) {
super(PROCESSING_TYPE);
this.settings = settings;
}
@Override
protected void setPipeParamsImpl() {
resizeImagePipe.setParams(
new ResizeImagePipe.ResizeImageParams(settings.streamingFrameDivisor));
}
@Override
public FocusPipelineResult process(Frame frame, FocusPipelineSettings settings) {
long totalNanos = 0;
var inputMat = frame.colorImage.getMat();
boolean emptyIn = inputMat.empty();
Mat displayMat = new Mat();
double variance = 0.0;
if (!emptyIn) {
totalNanos += resizeImagePipe.run(inputMat).nanosElapsed;
var focusResult = focusPipe.run(inputMat);
totalNanos += focusResult.nanosElapsed;
variance = focusResult.output.variance;
displayMat = focusResult.output.frame;
}
var fpsResult = calculateFPSPipe.run(null);
var fps = fpsResult.output;
var processedCVMat = new CVMat(displayMat);
return new FocusPipelineResult(
frame.sequenceID,
MathUtils.nanosToMillis(totalNanos),
fps,
new Frame(
frame.sequenceID,
frame.colorImage,
processedCVMat,
frame.type,
frame.frameStaticProperties),
variance);
}
@Override
public void release() {
// we never actually need to give resources up since pipelinemanager only makes
// one of us
}
}

View File

@@ -0,0 +1,33 @@
/*
* 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.vision.pipeline;
import com.fasterxml.jackson.annotation.JsonTypeName;
import org.photonvision.vision.processes.PipelineManager;
@JsonTypeName("FocusPipelineSettings")
public class FocusPipelineSettings extends CVPipelineSettings {
public FocusPipelineSettings() {
super();
pipelineNickname = "Focus Camera";
pipelineIndex = PipelineManager.FOCUS_INDEX;
pipelineType = PipelineType.FocusCamera;
inputShouldShow = true;
cameraAutoExposure = true;
}
}

View File

@@ -19,6 +19,7 @@ package org.photonvision.vision.pipeline;
@SuppressWarnings("rawtypes")
public enum PipelineType {
FocusCamera(-3, FocusPipeline.class),
Calib3d(-2, Calibrate3dPipeline.class),
DriverMode(-1, DriverModePipeline.class),
Reflective(0, ReflectivePipeline.class),

View File

@@ -0,0 +1,31 @@
/*
* 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.vision.pipeline.result;
import java.util.List;
import org.photonvision.vision.frame.Frame;
public class FocusPipelineResult extends CVPipelineResult {
public final double focus;
public FocusPipelineResult(
long seq, double latencyNanos, double fps, Frame outputFrame, double focus) {
super(seq, latencyNanos, fps, List.of(), outputFrame);
this.focus = focus;
}
}

View File

@@ -36,10 +36,12 @@ public class PipelineManager {
private static final Logger logger = new Logger(PipelineManager.class, LogGroup.VisionModule);
public static final int DRIVERMODE_INDEX = -1;
public static final int FOCUS_INDEX = -3;
public static final int CAL_3D_INDEX = -2;
protected final List<CVPipelineSettings> userPipelineSettings;
protected final Calibrate3dPipeline calibration3dPipeline;
protected final FocusPipeline focusPipeline = new FocusPipeline();
protected final DriverModePipeline driverModePipeline = new DriverModePipeline();
/** Index of the currently active pipeline. Defaults to 0. */
@@ -93,6 +95,7 @@ public class PipelineManager {
return switch (index) {
case DRIVERMODE_INDEX -> driverModePipeline.getSettings();
case CAL_3D_INDEX -> calibration3dPipeline.getSettings();
case FOCUS_INDEX -> focusPipeline.getSettings();
default -> {
for (var setting : userPipelineSettings) {
if (setting.pipelineIndex == index) yield setting;
@@ -112,6 +115,7 @@ public class PipelineManager {
return switch (index) {
case DRIVERMODE_INDEX -> driverModePipeline.getSettings().pipelineNickname;
case CAL_3D_INDEX -> calibration3dPipeline.getSettings().pipelineNickname;
case FOCUS_INDEX -> focusPipeline.getSettings().pipelineNickname;
default -> {
for (var setting : userPipelineSettings) {
if (setting.pipelineIndex == index) yield setting.pipelineNickname;
@@ -153,6 +157,7 @@ public class PipelineManager {
return switch (currentPipelineIndex) {
case CAL_3D_INDEX -> calibration3dPipeline;
case DRIVERMODE_INDEX -> driverModePipeline;
case FOCUS_INDEX -> focusPipeline;
// Just return the current user pipeline, we're not on a built-in one
default -> currentUserPipeline;
};
@@ -253,7 +258,7 @@ public class PipelineManager {
new AprilTagPipeline((AprilTagPipelineSettings) desiredPipelineSettings);
}
case Aruco -> {
logger.debug("Creating Aruco Pipeline");
logger.debug("Creating ArUco Pipeline");
currentUserPipeline = new ArucoPipeline((ArucoPipelineSettings) desiredPipelineSettings);
}
case ObjectDetection -> {
@@ -261,7 +266,7 @@ public class PipelineManager {
currentUserPipeline =
new ObjectDetectionPipeline((ObjectDetectionPipelineSettings) desiredPipelineSettings);
}
case Calib3d, DriverMode -> {}
case Calib3d, DriverMode, FocusCamera -> {}
}
}
@@ -335,7 +340,7 @@ public class PipelineManager {
case AprilTag -> new AprilTagPipelineSettings();
case Aruco -> new ArucoPipelineSettings();
case ObjectDetection -> new ObjectDetectionPipelineSettings();
case Calib3d, DriverMode -> {
case Calib3d, DriverMode, FocusCamera -> {
logger.error("Got invalid pipeline type: " + type);
yield null;
}

View File

@@ -680,6 +680,16 @@ public class VisionModule {
saveAndBroadcastAll();
}
public void removeCalibrationFromConfig(Size unrotatedImageSize) {
if (unrotatedImageSize != null) {
visionSource.getSettables().removeCalibration(unrotatedImageSize);
} else {
logger.error("Got null size?");
}
saveAndBroadcastAll();
}
/**
* Add/remove quirks from the camera we're controlling
*

View File

@@ -19,6 +19,7 @@ package org.photonvision.vision.processes;
import edu.wpi.first.cscore.VideoMode;
import java.util.HashMap;
import org.opencv.core.Size;
import org.photonvision.common.configuration.CameraConfiguration;
import org.photonvision.common.logging.LogGroup;
import org.photonvision.common.logging.Logger;
@@ -120,6 +121,11 @@ public abstract class VisionSourceSettables {
calculateFrameStaticProps();
}
public void removeCalibration(Size unrotatedImageSize) {
configuration.removeCalibration(unrotatedImageSize);
calculateFrameStaticProps();
}
protected void calculateFrameStaticProps() {
var videoMode = getCurrentVideoMode();
this.frameStaticProperties =

View File

@@ -25,6 +25,7 @@ import java.io.File;
import java.io.IOException;
import java.nio.file.Path;
import org.junit.jupiter.api.Test;
import org.photonvision.common.util.TestUtils;
public class NetworkConfigTest {
@Test
@@ -39,13 +40,13 @@ public class NetworkConfigTest {
@Test
public void testDeserializeTeamNumberOrNtServerAddress() {
{
var folder = Path.of("test-resources/network-old-team-number");
var folder = TestUtils.getResourcesFolderPath(true).resolve("network-old-team-number");
var configMgr = new ConfigManager(folder, new LegacyConfigProvider(folder));
configMgr.load();
assertEquals("9999", configMgr.getConfig().getNetworkConfig().ntServerAddress);
}
{
var folder = Path.of("test-resources/network-new-team-number");
var folder = TestUtils.getResourcesFolderPath(true).resolve("network-new-team-number");
var configMgr = new ConfigManager(folder, new LegacyConfigProvider(folder));
configMgr.load();
assertEquals("9999", configMgr.getConfig().getNetworkConfig().ntServerAddress);

View File

@@ -109,7 +109,6 @@ public class AprilTagTest {
pipeline.getSettings().solvePNPEnabled = true;
pipeline.getSettings().cornerDetectionAccuracyPercentage = 4;
pipeline.getSettings().cornerDetectionUseConvexHulls = true;
pipeline.getSettings().targetModel = TargetModel.kAprilTag6p5in_36h11;
pipeline.getSettings().tagFamily = AprilTagFamily.kTag16h5;
var frameProvider =

View File

@@ -0,0 +1,20 @@
# Minimal makefile for Sphinx documentation
#
# You can set these variables from the command line, and also
# from the environment for the first two.
SPHINXOPTS ?=
SPHINXBUILD ?= sphinx-build
SOURCEDIR = source
BUILDDIR = build
# Put it first so that "make" without argument is like "make help".
help:
@$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
.PHONY: help Makefile
# Catch-all target: route all unknown targets to Sphinx using the new
# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS).
%: Makefile
@$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)

View File

@@ -0,0 +1,5 @@
# Minimal wpimath stub for Sphinx docs build
from . import geometry
from . import units
__all__ = ["geometry", "units"]

View File

@@ -0,0 +1,4 @@
# Stub module to match wpimath compiled module names
# This file exists so imports like `wpimath._init__wpimath` succeed during docs build.
# no-op

View File

@@ -0,0 +1,130 @@
# Minimal geometry stubs for Sphinx documentation
class Rotation3d:
def __init__(self, roll=0.0, pitch=0.0, yaw=0.0):
# store yaw as the primary rotation for simple stubs
self.roll = roll
self.pitch = pitch
self.yaw = yaw
def toRotation2d(self):
# convert yaw to a Rotation2d for simple compatibility in docs build
return Rotation2d(self.yaw)
class Translation3d:
def __init__(self, x=0.0, y=0.0, z=0.0):
# Support both (x, y, z) and (distance, Rotation3d) forms used by the real wpimath
# If y is a Rotation3d, compute a point at 'distance' along its yaw/pitch
try:
from math import cos, sin
except Exception:
def cos(x):
return x
def sin(x):
return x
if hasattr(y, "yaw") and hasattr(y, "pitch"):
# interpret constructor as Translation3d(distance, Rotation3d)
distance = float(x)
pitch = float(getattr(y, "pitch", 0.0))
yaw = float(getattr(y, "yaw", 0.0))
# approximate spherical -> cartesian
self._x = distance * cos(pitch) * cos(yaw)
self._y = distance * cos(pitch) * sin(yaw)
self._z = distance * sin(pitch)
else:
self._x = float(x)
self._y = float(y)
self._z = float(z)
def X(self):
return self._x
def Y(self):
return self._y
def Z(self):
return self._z
class Pose3d:
def __init__(self, *args, **kwargs):
pass
class Rotation2d:
def __init__(self, *args):
# Accept several initialization forms used in the real wpimath Rotation2d
# - Rotation2d(angle)
# - Rotation2d(fx, xOffset) used by SimCameraProperties.getPixelYaw
if len(args) == 0:
self._angle = 0.0
elif len(args) == 1:
self._angle = float(args[0])
else:
# fallback: when called with fx, xOffset, approximate angle as 0.0
self._angle = 0.0
def degrees(self):
from math import degrees
return degrees(self._angle)
def radians(self):
return float(self._angle)
def __add__(self, other):
# allow Rotation2d + Rotation2d or Rotation2d + numeric
if hasattr(other, "_angle"):
return Rotation2d(self._angle + float(other._angle))
try:
return Rotation2d(self._angle + float(other))
except Exception:
return NotImplemented
def __radd__(self, other):
# numeric + Rotation2d
return self.__add__(other)
def __sub__(self, other):
if hasattr(other, "_angle"):
return Rotation2d(self._angle - float(other._angle))
try:
return Rotation2d(self._angle - float(other))
except Exception:
return NotImplemented
def __neg__(self):
return Rotation2d(-self._angle)
def __repr__(self):
return f"Rotation2d({self._angle})"
class Translation2d:
def __init__(self, x=0.0, y=0.0):
self._x = float(x)
self._y = float(y)
def X(self):
return self._x
def Y(self):
return self._y
class Pose2d:
def __init__(self, *args, **kwargs):
pass
def __repr__(self) -> str:
return "Pose2d()"
class Transform3d:
def __init__(self, *args, **kwargs):
pass
class Quaternion:
def __init__(self, *args, **kwargs):
pass
# Expose names commonly used by photonlibpy
__all__ = ["Rotation3d", "Translation3d", "Pose3d", "Rotation2d", "Translation2d", "Pose2d"]

View File

@@ -0,0 +1,12 @@
# Minimal interpolation stub for docs
class TimeInterpolatableRotation2dBuffer:
def __init__(self, *args, **kwargs):
pass
def addSample(self, *args, **kwargs):
pass
def sample(self, *args, **kwargs):
return None
__all__ = ["TimeInterpolatableRotation2dBuffer"]

View File

@@ -0,0 +1,3 @@
from ._interpolation import *
__all__ = ["TimeInterpolatableRotation2dBuffer"]

View File

@@ -0,0 +1,25 @@
# Minimal interpolation submodule stub for docs
class TimeInterpolatableRotation2dBuffer:
def __init__(self, *args, **kwargs):
pass
def addSample(self, *args, **kwargs):
pass
def sample(self, *args, **kwargs):
return None
class TimeInterpolatablePose3dBuffer:
def __init__(self, *args, **kwargs):
# buffer of Pose3d-like objects for docs import
pass
def addSample(self, *args, **kwargs):
pass
def sample(self, *args, **kwargs):
return None
__all__ = ["TimeInterpolatableRotation2dBuffer", "TimeInterpolatablePose3dBuffer"]

View File

@@ -0,0 +1,31 @@
"""Minimal wpimath.units stub for documentation builds."""
def degreesToRadians(deg: float) -> float:
from math import pi
return deg * (pi / 180.0)
# Represent seconds as a float alias for annotations
seconds = float
__all__ = ["degreesToRadians", "seconds"]
# Common unit aliases used in type annotations in WPILib stubs
meters = float
meters_per_second = float
meters_per_second_squared = float
kilograms = float
kilogram_square_meters = float
__all__.extend([
"meters",
"meters_per_second",
"meters_per_second_squared",
"kilograms",
"kilogram_square_meters",
"hertz",
])
# frequency
hertz = float

View File

@@ -0,0 +1,35 @@
@ECHO OFF
pushd %~dp0
REM Command file for Sphinx documentation
if "%SPHINXBUILD%" == "" (
set SPHINXBUILD=sphinx-build
)
set SOURCEDIR=source
set BUILDDIR=build
%SPHINXBUILD% >NUL 2>NUL
if errorlevel 9009 (
echo.
echo.The 'sphinx-build' command was not found. Make sure you have Sphinx
echo.installed, then set the SPHINXBUILD environment variable to point
echo.to the full path of the 'sphinx-build' executable. Alternatively you
echo.may add the Sphinx directory to PATH.
echo.
echo.If you don't have Sphinx installed, grab it from
echo.https://www.sphinx-doc.org/
exit /b 1
)
if "%1" == "" goto help
%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%
goto end
:help
%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%
:end
popd

View File

@@ -0,0 +1,3 @@
sphinx==7.2.6
sphinx-autodoc-typehints==1.25.2
sphinx-rtd-theme==1.3.0

View File

@@ -0,0 +1,53 @@
import os
import sys
# This adds the 'py/' directory to the Python path
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..")))
# Configuration file for the Sphinx documentation builder.
#
# For the full list of built-in configuration values, see the documentation:
# https://www.sphinx-doc.org/en/master/usage/configuration.html
# -- Project information -----------------------------------------------------
# https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information
project = "PhotonVision"
copyright = "2025, Matt Morley, Banks Troutman"
author = "Matt Morley, Banks Troutman"
# -- General configuration ---------------------------------------------------
# https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration
extensions = [
"sphinx.ext.autodoc",
"sphinx.ext.napoleon", # for Google/NumPy docstrings
"sphinx_autodoc_typehints", # for type hints in docs
]
import os
import sys
sys.path.insert(
0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "_stubs"))
) # add docs stubs first so they shadow unavailable third-party packages
sys.path.insert(
0, os.path.abspath("../../photonlibpy")
) # adjust based on your project layout
# Mock imports that aren't available in the docs build environment so autodoc
# can import the local modules even if optional runtime deps (like wpimath)
# aren't installed. Add other names here if you see warnings for missing
# third-party packages during the build.
autodoc_mock_imports = [
"wpilib",
]
templates_path = ["_templates"]
exclude_patterns = []
# -- Options for HTML output -------------------------------------------------
# https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output
html_theme = "sphinx_rtd_theme"
html_static_path = ["_static"]

View File

@@ -0,0 +1,17 @@
.. PhotonVision documentation master file, created by
sphinx-quickstart on Fri May 9 12:16:37 2025.
You can adapt this file completely to your liking, but it should at least
contain the root `toctree` directive.
PhotonLib Python Documentation
===============================
The main documentation for PhotonVision can be found at `photonvision.org <https://photonvision.org>`_.
.. toctree::
:maxdepth: 2
:caption: Contents:
modules
photonlibpy

View File

@@ -0,0 +1,7 @@
photonlibpy
===========
.. toctree::
:maxdepth: 4
photonlibpy

View File

@@ -0,0 +1,53 @@
photonlibpy.estimation package
==============================
Submodules
----------
photonlibpy.estimation.cameraTargetRelation module
--------------------------------------------------
.. automodule:: photonlibpy.estimation.cameraTargetRelation
:members:
:undoc-members:
:show-inheritance:
photonlibpy.estimation.openCVHelp module
----------------------------------------
.. automodule:: photonlibpy.estimation.openCVHelp
:members:
:undoc-members:
:show-inheritance:
photonlibpy.estimation.rotTrlTransform3d module
-----------------------------------------------
.. automodule:: photonlibpy.estimation.rotTrlTransform3d
:members:
:undoc-members:
:show-inheritance:
photonlibpy.estimation.targetModel module
-----------------------------------------
.. automodule:: photonlibpy.estimation.targetModel
:members:
:undoc-members:
:show-inheritance:
photonlibpy.estimation.visionEstimation module
----------------------------------------------
.. automodule:: photonlibpy.estimation.visionEstimation
:members:
:undoc-members:
:show-inheritance:
Module contents
---------------
.. automodule:: photonlibpy.estimation
:members:
:undoc-members:
:show-inheritance:

View File

@@ -0,0 +1,61 @@
photonlibpy.generated package
=============================
Submodules
----------
photonlibpy.generated.MultiTargetPNPResultSerde module
------------------------------------------------------
.. automodule:: photonlibpy.generated.MultiTargetPNPResultSerde
:members:
:undoc-members:
:show-inheritance:
photonlibpy.generated.PhotonPipelineMetadataSerde module
--------------------------------------------------------
.. automodule:: photonlibpy.generated.PhotonPipelineMetadataSerde
:members:
:undoc-members:
:show-inheritance:
photonlibpy.generated.PhotonPipelineResultSerde module
------------------------------------------------------
.. automodule:: photonlibpy.generated.PhotonPipelineResultSerde
:members:
:undoc-members:
:show-inheritance:
photonlibpy.generated.PhotonTrackedTargetSerde module
-----------------------------------------------------
.. automodule:: photonlibpy.generated.PhotonTrackedTargetSerde
:members:
:undoc-members:
:show-inheritance:
photonlibpy.generated.PnpResultSerde module
-------------------------------------------
.. automodule:: photonlibpy.generated.PnpResultSerde
:members:
:undoc-members:
:show-inheritance:
photonlibpy.generated.TargetCornerSerde module
----------------------------------------------
.. automodule:: photonlibpy.generated.TargetCornerSerde
:members:
:undoc-members:
:show-inheritance:
Module contents
---------------
.. automodule:: photonlibpy.generated
:members:
:undoc-members:
:show-inheritance:

View File

@@ -0,0 +1,21 @@
photonlibpy.networktables package
=================================
Submodules
----------
photonlibpy.networktables.NTTopicSet module
-------------------------------------------
.. automodule:: photonlibpy.networktables.NTTopicSet
:members:
:undoc-members:
:show-inheritance:
Module contents
---------------
.. automodule:: photonlibpy.networktables
:members:
:undoc-members:
:show-inheritance:

View File

@@ -0,0 +1,58 @@
photonlibpy package
===================
Subpackages
-----------
.. toctree::
:maxdepth: 4
photonlibpy.estimation
photonlibpy.generated
photonlibpy.networktables
photonlibpy.simulation
photonlibpy.targeting
photonlibpy.timesync
Submodules
----------
photonlibpy.estimatedRobotPose module
-------------------------------------
.. automodule:: photonlibpy.estimatedRobotPose
:members:
:undoc-members:
:show-inheritance:
photonlibpy.packet module
-------------------------
.. automodule:: photonlibpy.packet
:members:
:undoc-members:
:show-inheritance:
photonlibpy.photonCamera module
-------------------------------
.. automodule:: photonlibpy.photonCamera
:members:
:undoc-members:
:show-inheritance:
photonlibpy.photonPoseEstimator module
--------------------------------------
.. automodule:: photonlibpy.photonPoseEstimator
:members:
:undoc-members:
:show-inheritance:
Module contents
---------------
.. automodule:: photonlibpy
:members:
:undoc-members:
:show-inheritance:

View File

@@ -0,0 +1,53 @@
photonlibpy.simulation package
==============================
Submodules
----------
photonlibpy.simulation.photonCameraSim module
---------------------------------------------
.. automodule:: photonlibpy.simulation.photonCameraSim
:members:
:undoc-members:
:show-inheritance:
photonlibpy.simulation.simCameraProperties module
-------------------------------------------------
.. automodule:: photonlibpy.simulation.simCameraProperties
:members:
:undoc-members:
:show-inheritance:
photonlibpy.simulation.videoSimUtil module
------------------------------------------
.. automodule:: photonlibpy.simulation.videoSimUtil
:members:
:undoc-members:
:show-inheritance:
photonlibpy.simulation.visionSystemSim module
---------------------------------------------
.. automodule:: photonlibpy.simulation.visionSystemSim
:members:
:undoc-members:
:show-inheritance:
photonlibpy.simulation.visionTargetSim module
---------------------------------------------
.. automodule:: photonlibpy.simulation.visionTargetSim
:members:
:undoc-members:
:show-inheritance:
Module contents
---------------
.. automodule:: photonlibpy.simulation
:members:
:undoc-members:
:show-inheritance:

View File

@@ -0,0 +1,45 @@
photonlibpy.targeting package
=============================
Submodules
----------
photonlibpy.targeting.TargetCorner module
-----------------------------------------
.. automodule:: photonlibpy.targeting.TargetCorner
:members:
:undoc-members:
:show-inheritance:
photonlibpy.targeting.multiTargetPNPResult module
-------------------------------------------------
.. automodule:: photonlibpy.targeting.multiTargetPNPResult
:members:
:undoc-members:
:show-inheritance:
photonlibpy.targeting.photonPipelineResult module
-------------------------------------------------
.. automodule:: photonlibpy.targeting.photonPipelineResult
:members:
:undoc-members:
:show-inheritance:
photonlibpy.targeting.photonTrackedTarget module
------------------------------------------------
.. automodule:: photonlibpy.targeting.photonTrackedTarget
:members:
:undoc-members:
:show-inheritance:
Module contents
---------------
.. automodule:: photonlibpy.targeting
:members:
:undoc-members:
:show-inheritance:

View File

@@ -0,0 +1,21 @@
photonlibpy.timesync package
============================
Submodules
----------
photonlibpy.timesync.timeSyncServer module
------------------------------------------
.. automodule:: photonlibpy.timesync.timeSyncServer
:members:
:undoc-members:
:show-inheritance:
Module contents
---------------
.. automodule:: photonlibpy.timesync
:members:
:undoc-members:
:show-inheritance:

View File

@@ -70,19 +70,11 @@ class Packet:
return retVal
def getData(self) -> bytes:
"""
* Returns the packet data.
*
* @return The packet data.
"""
"""Return the packet data."""
return self.packetData
def setData(self, data: bytes):
"""
* Sets the packet data.
*
* @param data The packet data.
"""
"""Set the packet data."""
self.clear()
self.packetData = data
self.size = len(self.packetData)
@@ -101,74 +93,42 @@ class Packet:
return value
def decode8(self) -> int:
"""
* Returns a single decoded byte from the packet.
*
* @return A decoded byte from the packet.
"""
"""Return a single decoded byte from the packet."""
return self._decodeGeneric("<b", 1)
def decode16(self) -> int:
"""
* Returns a single decoded short from the packet.
*
* @return A decoded short from the packet.
"""
"""Return a single decoded short from the packet."""
return self._decodeGeneric("<h", 2)
def decodeInt(self) -> int:
"""
* Returns a decoded int (32 bytes) from the packet.
*
* @return A decoded int from the packet.
"""
"""Return a decoded 32-bit integer from the packet."""
return self._decodeGeneric("<l", 4)
def decodeFloat(self) -> float:
"""
* Returns a decoded float from the packet.
*
* @return A decoded float from the packet.
"""
"""Return a decoded float from the packet."""
return self._decodeGeneric("<f", 4)
def decodeLong(self) -> int:
"""
* Returns a decoded int64 from the packet.
*
* @return A decoded int64 from the packet.
"""
"""Return a decoded 64-bit integer from the packet."""
return self._decodeGeneric("<q", 8)
def decodeDouble(self) -> float:
"""
* Returns a decoded double from the packet.
*
* @return A decoded double from the packet.
"""
"""Return a decoded double from the packet."""
return self._decodeGeneric("<d", 8)
def decodeBoolean(self) -> bool:
"""
* Returns a decoded boolean from the packet.
*
* @return A decoded boolean from the packet.
"""
"""Return a decoded boolean from the packet."""
return self.decode8() == 1
def decodeDoubleArray(self, length: int) -> list[float]:
"""
* Returns a decoded array of floats from the packet.
"""
"""Return a decoded list of doubles of the given length from the packet."""
ret = []
for _ in range(length):
ret.append(self.decodeDouble())
return ret
def decodeShortList(self) -> list[int]:
"""
* Returns a decoded array of shorts from the packet.
"""
"""Return a decoded list of shorts from the packet (length-prefixed)."""
length = self.decode8()
ret = []
for _ in range(length):
@@ -176,11 +136,7 @@ class Packet:
return ret
def decodeTransform(self) -> Transform3d:
"""
* Returns a decoded Transform3d
*
* @return A decoded Tansform3d from the packet.
"""
"""Return a decoded Transform3d from the packet."""
x = self.decodeDouble()
y = self.decodeDouble()
z = self.decodeDouble()

View File

@@ -261,18 +261,18 @@ class PhotonPoseEstimator:
def update(
self, cameraResult: Optional[PhotonPipelineResult] = None
) -> Optional[EstimatedRobotPose]:
"""
Updates the estimated position of the robot. Returns empty if:
"""Update the estimated robot position.
- The timestamp of the provided pipeline result is the same as in the previous call to
``update()``.
Returns empty if one of the following is true:
- No targets were found in the pipeline results.
- The timestamp of the provided pipeline result is the same as in the previous call to
``update()``.
- No targets were found in the pipeline results.
:param cameraResult: The latest pipeline result from the camera
:param cameraResult: The latest pipeline result from the camera.
:returns: an :class:`EstimatedRobotPose` with an estimated pose, timestamp, and targets used to
create the estimate.
:returns: An :class:`EstimatedRobotPose` with an estimated pose, timestamp, and targets used
to create the estimate, or ``None`` if no estimate could be made.
"""
if not cameraResult:
if not self._camera:

View File

@@ -306,14 +306,13 @@ class SimCameraProperties:
:param a: The initial translation of the line
:param b: The final translation of the line
:returns: A Pair of Doubles. The values may be null:
:returns: A pair of floats (t_min, t_max) where each may be ``None``:
- {Double, Double} : Two parametrized values(t), minimum first, representing which
segment of the line is visible in the camera frustum.
- {Double, null} : One value(t) representing a single intersection point. For example,
the line only intersects the intersection of two adjacent viewplanes.
- {null, null} : No values. The line segment is not visible in the camera frustum.
"""
- ``(float, float)``: Two t values (minimum first) representing the visible segment.
- ``(float, None)``: A single intersection point.
- ``(None, None)``: No intersection; the segment is not visible.
"""
# translations relative to the camera
relA = camRt.applyTranslation(a)

View File

@@ -24,9 +24,6 @@
#include "photon/PhotonCamera.h"
#include <hal/FRCUsageReporting.h>
#include <net/TimeSyncServer.h>
#include <stdexcept>
#include <string>
#include <string_view>
@@ -36,6 +33,8 @@
#include <frc/Errors.h>
#include <frc/RobotController.h>
#include <frc/Timer.h>
#include <hal/FRCUsageReporting.h>
#include <net/TimeSyncServer.h>
#include <opencv2/core.hpp>
#include <opencv2/core/mat.hpp>
#include <wpi/json.h>

View File

@@ -24,8 +24,6 @@
#include "photon/PhotonPoseEstimator.h"
#include <hal/FRCUsageReporting.h>
#include <cmath>
#include <iostream>
#include <limits>
@@ -41,6 +39,7 @@
#include <frc/geometry/Pose3d.h>
#include <frc/geometry/Rotation3d.h>
#include <frc/geometry/Transform3d.h>
#include <hal/FRCUsageReporting.h>
#include <opencv2/calib3d.hpp>
#include <opencv2/core/mat.hpp>
#include <opencv2/core/types.hpp>

View File

@@ -24,7 +24,16 @@
#pragma once
#include <algorithm>
#include <limits>
#include <string>
#include <utility>
#include <vector>
#include <cameraserver/CameraServer.h>
#include <frc/Timer.h>
#include <frc/apriltag/AprilTagFieldLayout.h>
#include <frc/apriltag/AprilTagFields.h>
#include <photon/PhotonCamera.h>
#include <photon/PhotonTargetSortMode.h>
#include <photon/estimation/CameraTargetRelation.h>
@@ -33,16 +42,6 @@
#include <photon/simulation/SimCameraProperties.h>
#include <photon/simulation/VideoSimUtil.h>
#include <photon/simulation/VisionTargetSim.h>
#include <algorithm>
#include <limits>
#include <string>
#include <utility>
#include <vector>
#include <frc/Timer.h>
#include <frc/apriltag/AprilTagFieldLayout.h>
#include <frc/apriltag/AprilTagFields.h>
#include <units/math.h>
#include <wpi/timestamp.h>

View File

@@ -24,8 +24,6 @@
#pragma once
#include <photon/estimation/OpenCVHelp.h>
#include <algorithm>
#include <random>
#include <string>
@@ -37,6 +35,7 @@
#include <frc/MathUtil.h>
#include <frc/geometry/Rotation2d.h>
#include <frc/geometry/Translation3d.h>
#include <photon/estimation/OpenCVHelp.h>
#include <units/frequency.h>
#include <units/time.h>

View File

@@ -24,8 +24,6 @@
#pragma once
#include <cscore_cv.h>
#include <algorithm>
#include <numeric>
#include <string>
@@ -33,6 +31,7 @@
#include <utility>
#include <vector>
#include <cscore_cv.h>
#include <frc/apriltag/AprilTag.h>
#include <opencv2/core.hpp>
#include <opencv2/imgcodecs.hpp>

View File

@@ -22,20 +22,19 @@
* SOFTWARE.
*/
#include <string>
#include <vector>
#include <fmt/ranges.h>
#include <frc/smartdashboard/SmartDashboard.h>
#include <gtest/gtest.h>
#include <hal/HAL.h>
#include <net/TimeSyncClient.h>
#include <net/TimeSyncServer.h>
#include <networktables/NetworkTableInstance.h>
#include <photon/PhotonCamera.h>
#include <photon/simulation/PhotonCameraSim.h>
#include <string>
#include <vector>
#include <frc/smartdashboard/SmartDashboard.h>
#include <networktables/NetworkTableInstance.h>
TEST(TimeSyncProtocolTest, Smoketest) {
using namespace wpi::tsp;
using namespace std::chrono_literals;

View File

@@ -22,6 +22,8 @@
* SOFTWARE.
*/
#include "photon/PhotonPoseEstimator.h"
#include <map>
#include <utility>
#include <vector>
@@ -30,13 +32,12 @@
#include <frc/geometry/Pose3d.h>
#include <frc/geometry/Rotation3d.h>
#include <frc/geometry/Transform3d.h>
#include <gtest/gtest.h>
#include <units/angle.h>
#include <units/length.h>
#include <wpi/SmallVector.h>
#include "gtest/gtest.h"
#include "photon/PhotonCamera.h"
#include "photon/PhotonPoseEstimator.h"
#include "photon/dataflow/structures/Packet.h"
#include "photon/simulation/PhotonCameraSim.h"
#include "photon/simulation/SimCameraProperties.h"

View File

@@ -22,7 +22,8 @@
* SOFTWARE.
*/
#include "gtest/gtest.h"
#include "photon/PhotonUtils.h"
#include <gtest/gtest.h>
TEST(PhotonUtilsTest, Include) {}

View File

@@ -24,8 +24,9 @@
#include <iostream>
#include <gtest/gtest.h>
#include "PhotonVersion.h"
#include "gtest/gtest.h"
TEST(VersionTest, PrintVersion) {
std::cout << photon::PhotonVersion::versionString << std::endl;

View File

@@ -22,16 +22,17 @@
* SOFTWARE.
*/
#include "photon/simulation/VisionSystemSim.h"
#include <chrono>
#include <thread>
#include <tuple>
#include <vector>
#include <gtest/gtest.h>
#include <wpi/deprecated.h>
#include "gtest/gtest.h"
#include "photon/PhotonUtils.h"
#include "photon/simulation/VisionSystemSim.h"
// Ignore GetLatestResult warnings
WPI_IGNORE_DEPRECATED

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