Compare commits

...

110 Commits

Author SHA1 Message Date
Sam Freund
9f6d8caf48 Fix calibration resolution default bug (#2156) 2025-10-23 23:42:04 -05:00
Rikhil Chilka
3cbac8117e Merge rknn conversion scripts into notebook (#2157) 2025-10-23 23:41:49 -05:00
Rikhil Chilka
8e88a9a780 Update notebook links in docs to point to docs version (#2155)
Co-authored-by: Sam Freund <samf.236@proton.me>
2025-10-23 16:35:40 -05:00
Rikhil Chilka
7cb3b7a37b Add downgrade fix for ONNX error during RKNN conversion (#2136) 2025-10-23 17:17:31 +00:00
Alan
054ed8b6a1 Add camera mismatch banner to dashboard (#1921)
## Description

Detects if a camera mismatch is present in any camera and displays a
banner in the dashboard for better visibility to the user. All detection
occurs in the backend, and is sent to the frontend via use of a mismatch
boolean included in each vision module.

<img width="1235" alt="image"
src="https://github.com/user-attachments/assets/19219a56-c366-4c56-8c4b-cb5a36fe4a04"
/>

Closes #1920

## 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
- [x] If this PR touches configuration, this is backwards compatible
with settings back to v2024.3.1
- [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

---------

Co-authored-by: Sam Freund <techguy763@gmail.com>
Co-authored-by: samfreund <samf.236@proton.me>
Co-authored-by: Matt Morley <matthew.morley.ca@gmail.com>
2025-10-21 20:53:22 -04:00
Riley Brewer
d44480ddad Fix typo: s/Specifc/Specific (#2143)
## Description

Simple typo fix:
s/Specifc/Specific

## 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] If this PR changes behavior or adds a feature, user documentation
is updated
- [x] If this PR touches photon-serde, all messages have been
regenerated and hashes have not changed unexpectedly
- [x] 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
- [x] If this PR addresses a bug, a regression test for it is added
2025-10-22 00:18:01 +00:00
Sam Freund
c71921c41e Add documentation for forcing OD UI (#2018) 2025-10-21 19:09:42 -05:00
Michael Jansen
ee4501f1d6 Add Luma P1 support (#2135)
## Description

Adds support for building images for the Luma P1. This bumps the image
modifier pin to v2025.0.4. This pulls in:

* Allow users to install any release via install.sh by @crschardt in
https://github.com/PhotonVision/photon-image-modifier/pull/49
* Exit install script if run on systemcore by @crschardt in
https://github.com/PhotonVision/photon-image-modifier/pull/58
* Fix --list-versions in install.sh by @crschardt in
https://github.com/PhotonVision/photon-image-modifier/pull/59
* Remove large folders of firmware that (probably) isn't needed by
@crschardt in
https://github.com/PhotonVision/photon-image-modifier/pull/41
* Cancel in progress runs by @spacey-sooty in
https://github.com/PhotonVision/photon-image-modifier/pull/65
* Add limelight 4 support by @spacey-sooty in
https://github.com/PhotonVision/photon-image-modifier/pull/52

**Full Changelog**:
https://github.com/PhotonVision/photon-image-modifier/compare/v2025.0.3...v2025.0.4


## 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-10-21 10:20:42 -07:00
Gold856
d9b86a718e Revert "Make HardwareConfig a record" (#2142) 2025-10-21 10:56:17 -05:00
Sam Freund
1ac185c247 Fix versioning helper (#2141)
## Description

When we tagged `v2026.0.0-alpha-1`, we broke the versioning-helper
logic. It doesn't expect `alpha` in the version string. This PR adds
matching for lowercase alphanumeric to the versioning helper, which
resolves that issue.

Also turns out `getProviders()` is now broken as a gradle method. Sad.

## 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-10-21 01:14:57 -04:00
Sam Freund
7170c29efe Version rubik pi image (#2129) 2025-10-21 04:43:33 +00:00
Gold856
4f549ba579 Use the tool plugin to include photon-targeting into photon-core (#2137)
## Description

This allows photon-targeting to be loaded using the same mechanism as
the rest of the WPILib libraries, fixing issues with libraries not being
able to find and load their dependent libraries.

## 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] 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-20 07:45:54 -07:00
ElectricTurtle32
b531fe6b81 Made steam overlay buttons primary color (#2139) 2025-10-19 00:59:16 +00:00
Jade
373ed2ff05 Fix most gradle deprecation warnings (#2093) 2025-10-15 22:22:55 -04:00
Jade
115bc09f2e Update LimeLight installation documentation (#2133) 2025-10-15 17:51:43 +00:00
Gold856
7497566f56 [ci] Fix releaser (#2127) 2025-10-14 00:55:14 -05:00
Henry Martin
85f155c77b chore: Bump Javalin (#2126) 2025-10-13 20:58:14 -05:00
Sam Freund
797936865f Add support for building rubik image (#2110) 2025-10-13 17:56:23 -05:00
Sam Freund
831df409f7 Disable 3d mode for OD (#2121)
## Description

There isn't anything that 3D mode adds for OD, and the results are
typically messed up. Thus, we disable 3D mode when we're using an OD
pipeline.

## 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-10-13 21:56:16 +00:00
Drew Williams
b89ab49d34 Add Constrained PNP Pose Strategies to C++ photonlib (#1908)
This adds the two missing pose strategies from the java version of
photonlib (Constrained PNP and the Trig solve), to C++ photonlib

---------

Co-authored-by: Matthew Morley <matthew.morley.ca@gmail.com>
2025-10-12 12:26:12 -07:00
Sam Freund
099c88e0b7 Remove gh-action-releaser (#2119)
## Description

See https://github.com/photonvision/photon-image-modifier/pull/77

## 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-10-11 09:48:20 -07:00
Sam Freund
1ee2ecb608 Make NT client name the same as hostname (#2107) 2025-10-10 22:07:27 -07:00
Gold856
82d6b6b845 Make exported journalctl logs less verbose and always display everything (#2101)
## Description

There was a recent occurance of journalctl logs saying something like
`[66B blob data]`. We don't log anything binary, so journalctl might be
hiding some lines, thinking they're actually binary data when they're
actually plain text. Use `-a` to always log everything. If it's binary
data anyways, we'll want to know what it is anyways. Use `--output cat`
because we output our own timestamps and we don't want journalctl's own
timestamping. Filter the output through `sed` to remove ANSI color codes
to make reading logs easier.

## 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] 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-11 00:25:54 -04:00
Sam Freund
8ec041493a Conglomerate release artifacts (#2115) 2025-10-10 23:10:40 -05:00
Gold856
da608a5070 Fix file uploads not overwriting existing files (#2116)
## Description

#2023 changed how file uploads were handled to use `Files.copy`, but
incorrectly didn't specify the `REPLACE_EXISTING` copy option, causing
file uploads to fail if the file already existed.

## 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-10-10 04:44:43 +00:00
Alan Everett
17c23b0390 Fix documentation tab set syncing (#2109) 2025-10-09 16:27:58 -05:00
Gold856
e486ff5a50 Modify build-image to use global variable for image version (#2113) 2025-10-09 17:22:23 +00:00
Gold856
e84e3e7c7c Refactor LibCameraJNILoader to use PhotonJNICommon (#2048) 2025-09-16 08:04:25 -07:00
Jade
a1d06a9920 [ci] Set versions for image builds to 24.04 (#2092)
This should fix the [LL4 CI](https://github.com/PhotonVision/photonvision/actions/runs/17751849602/job/50450131089).
From [here](https://github.com/pguyot/arm-runner-action?tab=readme-ov-file#cpu)
we can see that 24.04 is required for the cortex-a76 used by the pi5,
> cortex-a76 equivalent to max:cortex-a76. Note that this requires a
newer version of qemu, for example with runner ubuntu 24.

Signed-off-by: Jade Turner <spacey-sooty@proton.me>
2025-09-16 12:56:41 +08:00
Gold856
04e9bffeb7 Properly declare inputs and output for buildClient (#2086)
Co-authored-by: samfreund <samf.236@proton.me>
2025-09-16 04:35:33 +00:00
Jade
4b01b66ab7 Add limelight 4 support (#1807)
Resolves #1804

---------

Signed-off-by: Jade Turner <spacey-sooty@proton.me>
2025-09-16 09:48:02 +08:00
Craig Schardt
ee56bf7597 Update libcameraDriverVersion to v2025.0.4 (#1769)
Signed-off-by: Jade Turner <spacey-sooty@proton.me>
Co-authored-by: Jade <spacey-sooty@proton.me>
2025-09-15 03:05:53 -04:00
Jade
058ca19262 Publish FPS with camera (#2083) 2025-09-12 09:10:37 -05:00
Devon Doyle
b43d0dde20 Add custom theming (#2081)
Adds support for user-created custom themes. Custom theme interface is
tucked into the global settings in a non-invasive manner to avoid major
design changes. Builds on the theme structure established by the dark
theme update.

<img width="1486" height="953" alt="image"
src="https://github.com/user-attachments/assets/716bcfc7-af74-41dc-b14a-cfc2f2d2caa9"
/>

<img width="1486" height="956" alt="image"
src="https://github.com/user-attachments/assets/a00f9620-0b1d-4f67-b010-e94dda5dc212"
/>



Here's a few examples of what teams could do, using a few color schemes
from local teams. Imagine the possibilities!

<img width="1485" height="951" alt="image"
src="https://github.com/user-attachments/assets/c3da37b8-f6be-4152-81e0-533297f517fc"
/>

<img width="1483" height="951" alt="image"
src="https://github.com/user-attachments/assets/0d453f7a-cf6f-4c27-97db-603b54c1f73e"
/>

<img width="1485" height="952" alt="image"
src="https://github.com/user-attachments/assets/bf8c7770-e60d-4875-9580-ed7e54e089f4"
/>

<img width="1484" height="952" alt="image"
src="https://github.com/user-attachments/assets/326d89e6-dd6e-4e05-a9fa-c9fc6f880847"
/>

<img width="1482" height="951" alt="image"
src="https://github.com/user-attachments/assets/eb5a2a5d-c103-482c-a62a-5ccd5ba21cc5"
/>

<img width="1482" height="950" alt="image"
src="https://github.com/user-attachments/assets/4831ca56-f322-4345-97af-8963ae8539b1"
/>



Looking for high contrast? Just moments away:
<img width="1484" height="949" alt="image"
src="https://github.com/user-attachments/assets/7ffc65c6-7000-4566-b4f0-c8247f75fb3d"
/>
2025-09-07 00:33:37 -04:00
Kevin Cooney
3300b90823 Set the initial capacity of ArrayLists in getAllUnreadResults() (#2079)
If there are more than 10 queued results,
PhotonCamera.getAllUnreadResults() would resize two arrays at least once
each.

## Meta

Merge checklist:
- [x] Pull Request title is [short, imperative
summary](https://cbea.ms/git-commit/) of proposed changes
- [x] The description documents the _what_ and _why_
- [ ] If this PR changes behavior or adds a feature, user documentation
is updated
- [ ] If this PR touches photon-serde, all messages have been
regenerated and hashes have not changed unexpectedly
- [ ] If this PR touches configuration, this is backwards compatible
with settings back to v2024.3.1
- [ ] If this PR touches pipeline settings or anything related to data
exchange, the frontend typing is updated
- [ ] If this PR addresses a bug, a regression test for it is added
2025-09-01 23:36:02 -04:00
Sam Freund
f58416fe16 Update compatible version to 2025 (#2068)
## Description

The version of settings that needs to be compatible is last years, in
this case 2025.

## Meta

Merge checklist:
- [x] Pull Request title is [short, imperative
summary](https://cbea.ms/git-commit/) of proposed changes
- [x] The description documents the _what_ and _why_
- [ ] If this PR changes behavior or adds a feature, user documentation
is updated
- [ ] If this PR touches photon-serde, all messages have been
regenerated and hashes have not changed unexpectedly
- [ ] If this PR touches configuration, this is backwards compatible
with settings back to v2024.3.1
- [ ] If this PR touches pipeline settings or anything related to data
exchange, the frontend typing is updated
- [ ] If this PR addresses a bug, a regression test for it is added
2025-09-01 23:34:58 -04:00
Graham
62eb66a493 Set canonical root URL for documentation (#2078) 2025-08-29 02:19:50 +00:00
Gold856
fcbc392d83 Use deploy-utils instead of an external ssh plugin (#2077) 2025-08-22 06:10:13 +00:00
Kevin Cooney
7d927aca3b Fix 'Resource leak: <variable> is never closed' warnings (#2023)
Fix numerous places where using AutoCloseable objects without closing
them.

Changes:
- Upgrade JUnit from 5.10.0 to 5.11.4 (so `@AutoClose` can be used)
- Use `Files.copy()` to copy files
- Use try-with-resources when calling `Files.list()` or `Files.walk()`
- Use try-with-resources or `@AutoClose` to close `PhotonCamera` and
  `PhotonCameraSim` objects created by tests
- Update `SQLConfigTest` to use `@TempDir`

## Meta

Merge checklist:
- [x] Pull Request title is [short, imperative
summary](https://cbea.ms/git-commit/) of proposed changes
- [x] The description documents the _what_ and _why_
- [ ] If this PR changes behavior or adds a feature, user documentation
is updated
- [ ] If this PR touches photon-serde, all messages have been
regenerated and hashes have not changed unexpectedly
- [ ] If this PR touches configuration, this is backwards compatible
with settings back to v2024.3.1
- [ ] If this PR touches pipeline settings or anything related to data
exchange, the frontend typing is updated
- [ ] If this PR addresses a bug, a regression test for it is added
2025-08-18 23:37:00 -04:00
Sam Freund
bd2c5062f9 Bump website and docs dependencies (#2075) 2025-08-19 01:30:47 +00:00
Sam Freund
354f4e945e Remove codecov from workflow (#2070)
## Description

It's unused and fails when run, so I'm removing it.

## Meta

Merge checklist:
- [x] Pull Request title is [short, imperative
summary](https://cbea.ms/git-commit/) of proposed changes
- [x] The description documents the _what_ and _why_
- [ ] If this PR changes behavior or adds a feature, user documentation
is updated
- [ ] If this PR touches photon-serde, all messages have been
regenerated and hashes have not changed unexpectedly
- [ ] If this PR touches configuration, this is backwards compatible
with settings back to v2024.3.1
- [ ] If this PR touches pipeline settings or anything related to data
exchange, the frontend typing is updated
- [ ] If this PR addresses a bug, a regression test for it is added
2025-08-17 01:54:01 +00:00
Rikhil Chilka
2eb224a55f Preload OD models before import to check quantization (#2056)
Co-authored-by: samfreund <samf.236@proton.me>
Co-authored-by: Sam Freund <techguy763@gmail.com>
2025-08-16 00:59:22 -05:00
Sam Freund
2ab7a2e389 Bump Gradle to 8.14.3 (#2064) 2025-08-15 22:07:12 -04:00
Sam Freund
27a1cfcb12 Bump wpiformat to 2025.34 (#2066) 2025-08-15 22:05:54 -04:00
Sam Freund
c7f5edc262 Exclude license from being loaded as a model (#2063) 2025-08-15 15:17:21 -05:00
Gold856
6fe96316a4 Fix calibrationUtils.py (#2055) 2025-08-13 06:16:49 -07:00
Gold856
b32d9c6ee3 Only update UI when there's been a change in conflict detection (#2054)
## Description

After #1991, the program state was always resent in an attempt to
simplify logic, but this had the side effect of causing the settings UI
to reset periodically when the hostname check was performed. This
restores the original logic in #1791 to check for differences in the
conflict state, and to only send the program state if it's changed.

## Meta

Merge checklist:
- [x] Pull Request title is [short, imperative
summary](https://cbea.ms/git-commit/) of proposed changes
- [x] The description documents the _what_ and _why_
- [ ] If this PR changes behavior or adds a feature, user documentation
is updated
- [ ] If this PR touches photon-serde, all messages have been
regenerated and hashes have not changed unexpectedly
- [ ] If this PR touches configuration, this is backwards compatible
with settings back to v2024.3.1
- [ ] If this PR touches pipeline settings or anything related to data
exchange, the frontend typing is updated
- [ ] If this PR addresses a bug, a regression test for it is added
2025-08-11 18:21:19 -04:00
Gold856
7766d99ca6 Only make vendor-json-repo PR if the repo is ours (#2053)
## Description

This prevents jobs from failing on forks of the repo.
Closes #1975.

## Meta

Merge checklist:
- [x] Pull Request title is [short, imperative
summary](https://cbea.ms/git-commit/) of proposed changes
- [x] The description documents the _what_ and _why_
- [ ] If this PR changes behavior or adds a feature, user documentation
is updated
- [ ] If this PR touches photon-serde, all messages have been
regenerated and hashes have not changed unexpectedly
- [ ] If this PR touches configuration, this is backwards compatible
with settings back to v2024.3.1
- [ ] If this PR touches pipeline settings or anything related to data
exchange, the frontend typing is updated
- [ ] If this PR addresses a bug, a regression test for it is added
2025-08-10 19:26:50 -04:00
Gold856
81a5a48ac4 Use new vendor JSON repo action (#2049) 2025-08-10 13:59:20 -07:00
Craig Schardt
29e183660f Remove log spam from periodic network and IP address queries (#2051) 2025-08-10 07:22:21 -07:00
cuttestkittensrule
8676649ebc Add PNP_DISTANCE_TRIG_SOLVE strategy to C++ (#2021) 2025-08-10 06:55:04 -07:00
Matt Morley
35dcc3ce5a Verify that nmcli installed (#1929)
## Description

Previously, NetworkManager would happily go asking for networkmanager to
do things even if it wasn't installed. This should never be the case,
but we should bail out early if it is regardless IMO. This prevents logs
and the UI from looking suspiciously "working", if you ignore the exit
code. Up for debate if we actually need this feature.

## Meta

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

---------

Co-authored-by: Craig Schardt <crschardt@fastem.com>
2025-08-09 22:40:55 -04:00
Sam Freund
9277960018 Add support for object detection on Rubik Pi 3 (#2005) 2025-08-09 10:02:55 -05:00
Gold856
e23df8c9a4 Make dashboard theme transition smooth and fix link color (#2046) 2025-08-08 13:06:26 -07:00
Gold856
22490b8c38 Add an error when the connection for the end calibration request fails (#1840) 2025-08-08 13:06:08 -07:00
Sam Freund
3ac509b40d Expose and document NMS slider (#2028) 2025-08-08 13:05:54 -07:00
Sam Freund
0ea108e17f Run metrics with debug false to reduce log spam (#2027) 2025-08-08 08:14:20 -05:00
Sam Freund
da715244cb Add COCO trained model for RKNN (#2035)
## Description

See #2026 for the previous iteration of this PR.

This adds the RKNN model trained on the COCO dataset as one of the
models shipped with PV. This model is fairly general, and has been
trained to identify a number of objects, including people, animals,
cars, and more. This model is meant for teams to test object detection,
particularly for teams who might not have access to the game elements
that our other models are trained on.

It additionally acknowledges Ultralytics for the model, and includes the
AGPL copyleft license.

## 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] If this PR changes behavior or adds a feature, user documentation
is updated
- [x] If this PR touches photon-serde, all messages have been
regenerated and hashes have not changed unexpectedly
- [x] If this PR touches configuration, this is backwards compatible
with settings back to v2024.3.1
- [x] If this PR touches pipeline settings or anything related to data
exchange, the frontend typing is updated
- [x] If this PR addresses a bug, a regression test for it is added
2025-08-08 05:55:14 +00:00
Kevin Cooney
ab854e91e5 [photon-lib] Invalidate pose cache when setting referencePose (#2040) 2025-08-08 00:14:44 -05:00
Rikhil Chilka
a930852bee Add yes pipe for rubik conversion cmd (#2043) 2025-08-07 19:11:43 -05:00
Sam Freund
65c214ac2d Add notebook for Rubik Model conversion (#2006) 2025-08-07 11:11:35 -05:00
Rikhil Chilka
bf8073ab26 Update RKNN Conversion notebook permalinks (#2042) 2025-08-06 22:09:58 +00:00
Rikhil Chilka
2bf166bc3f Update notebook links (#2037) 2025-08-06 16:33:03 -05:00
Gold856
2c98d10a92 Fix labeler labelling everything as backend (#2041) 2025-08-06 16:31:36 -05:00
Gold856
923f9564dc Fix buildAndCopyUI and update build instructions (#2036) 2025-08-05 17:19:37 -05:00
Gold856
ad64bfeaa9 Switch to pnpm (and update some dependencies) (#2032) 2025-08-04 13:59:45 -07:00
Gold856
ffd4d1f80e Add PR labeler (#2031)
## Description

It's nice to be able to filter PRs by what components they've modified.
The 5 labels that have been selected are `frontend`, `backend`,
`documentation`, `photonlib`, and `website`, since those are the primary
components in the monorepo.

## Meta

Merge checklist:
- [x] Pull Request title is [short, imperative
summary](https://cbea.ms/git-commit/) of proposed changes
- [x] The description documents the _what_ and _why_
- [ ] If this PR changes behavior or adds a feature, user documentation
is updated
- [ ] If this PR touches photon-serde, all messages have been
regenerated and hashes have not changed unexpectedly
- [ ] If this PR touches configuration, this is backwards compatible
with settings back to v2024.3.1
- [ ] If this PR touches pipeline settings or anything related to data
exchange, the frontend typing is updated
- [ ] If this PR addresses a bug, a regression test for it is added
2025-08-04 12:18:52 -07:00
Gold856
1310640e10 Revert "Add COCO model for RKNN (#2026)" (#2033)
This reverts commit 753123844b.

## Description

The COCO dataset contains images that use the NC and/or ND variants of
the CC license, and distributing a model based on that dataset is most
likely a violation of licenses. Additionally, the model is licensed under AGPL,
which might be a concern for PhotonVision, and at a minimum, there's no
license file bundled with the model right now.

## 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] If this PR changes behavior or adds a feature, user documentation
is updated
- [ ] If this PR touches photon-serde, all messages have been
regenerated and hashes have not changed unexpectedly
- [ ] If this PR touches configuration, this is backwards compatible
with settings back to v2024.3.1
- [ ] If this PR touches pipeline settings or anything related to data
exchange, the frontend typing is updated
- [ ] If this PR addresses a bug, a regression test for it is added
2025-08-04 13:51:24 -04:00
Sam Freund
753123844b Add COCO model for RKNN (#2026)
## Description

This adds the RKNN model trained on the COCO dataset as one of the
models shipped with PV. This model is fairly general, and has been
trained to identify a number of objects, including people, animals,
cars, and more. This model is meant for teams to test object detection,
particularly for teams who might not have access to the game elements
that our other models are trained on.

## 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] If this PR changes behavior or adds a feature, user documentation
is updated
- [ ] If this PR touches photon-serde, all messages have been
regenerated and hashes have not changed unexpectedly
- [ ] If this PR touches configuration, this is backwards compatible
with settings back to v2024.3.1
- [ ] If this PR touches pipeline settings or anything related to data
exchange, the frontend typing is updated
- [ ] If this PR addresses a bug, a regression test for it is added
2025-08-04 05:20:51 +00:00
Rikhil Chilka
ba1c0db7e1 RKNN conversion tool (#2024) 2025-08-04 05:15:54 +00:00
Devon Doyle
fce54d12c1 Dark mode and minor interface tweaks (#2016)
Co-authored-by: Sam Freund <samf.236@proton.me>
2025-08-04 05:15:33 +00:00
Gold856
3e19cd45cc Disable linkcheck for www.gnu.org (#2030) 2025-08-03 22:54:08 -05:00
Rikhil Chilka
6b49e92d00 Add model benchmark data (#2025) 2025-08-03 23:37:16 +00:00
Kevin Cooney
29e24bbac2 [photon-lib] Python support for PNP_DISTANCE_TRIG_SOLVE (#2015)
This adds support for PNP_DISTANCE_TRIG_SOLVE in the the python
PhotonPoseEstimator, mirroring the implementation in the Java
PhotonPoseEstimator.

Changes:
- Add PoseStrategy.PNP_DISTANCE_TRIG_SOLVE
- Add addHeadingData() and resetHeadingData() to PhotonPoseEstimator
- Fix PhotonCameraSim.process() to set ntReceiveTimestampMicros in the
result
- Minor readability improvements to PhotonPipelineResult
- Minor test improvements to PhotonPoseEstimatorTest
- Add .vscode/settings.json (to make running python tests in VSCode
easier)

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

---------

Co-authored-by: Sam948-byte <samf.236@proton.me>
2025-08-01 11:04:01 -07:00
Kevin Cooney
cefaa313df Add an overload of resetHeadingData() which takes in a Rotation3d (#2013) 2025-07-27 19:28:16 -05:00
Sam Freund
4b5bc6ae84 Update logic for metric publisher topic name (#2011) 2025-07-23 23:23:10 -05:00
Sam Freund
758fbb9110 Update metrics publisher hostname when hostname is changed (#2008) 2025-07-22 23:56:32 +00:00
Sam Freund
02e6b6d3e2 Move metrics subtable to root PV table (#2007) 2025-07-22 18:28:48 -05:00
Sam Freund
af689b61d5 Add Gradle wrapper validation (#2004)
Check Gradle wrapper with gradle/actions/wrapper-validation to avoid supply chain attacks
2025-07-19 18:59:26 -04:00
Gold856
8215cafbae Fix camera calibration card contents completely vanishing during calibration (#1998)
## Description

Per
https://github.com/PhotonVision/photonvision/pull/1972#issuecomment-3066574742,
camera calibration got broken because I accidently hid the entire
contents of the camera calibration card in #1972. Now, v-show is only
applied to the calibration table so that only the calibration table is
hidden during calibration.

## Meta

Merge checklist:
- [x] Pull Request title is [short, imperative
summary](https://cbea.ms/git-commit/) of proposed changes
- [x] The description documents the _what_ and _why_
- [ ] If this PR changes behavior or adds a feature, user documentation
is updated
- [ ] If this PR touches photon-serde, all messages have been
regenerated and hashes have not changed unexpectedly
- [ ] If this PR touches configuration, this is backwards compatible
with settings back to v2024.3.1
- [ ] If this PR touches pipeline settings or anything related to data
exchange, the frontend typing is updated
- [ ] If this PR addresses a bug, a regression test for it is added
2025-07-13 21:56:19 -04:00
Gold856
ed58f69275 Dynamically import dependencies that are not critical to viewing the UI (#2001) 2025-07-13 14:28:45 -07:00
Gold856
a62d5e0eee Make testHeadless output the same stuff as test from photon-lib (#2000) 2025-07-13 06:24:09 -07:00
Gold856
2e97c95be1 Restore original video mode index order (#1999) 2025-07-13 06:23:35 -07:00
Gold856
6610b21b6e Refactor MAC address detection (#1991)
Co-authored-by: Sam Freund <techguy763@gmail.com>
2025-07-13 04:59:16 +00:00
Jade
ef5e6463cb Clarify and fix OpenCV/WPILib version checking error (#1963)
## Description

Fixed the error in the OpenCV/WPILib version checking crash and
clarified it, since it's not PhotonVision that needs updating, but
rather WPILib.

Reported on chief
https://www.chiefdelphi.com/t/opencv-is-version-4-6-0-and-needs-to-be-4-10-0/501751/7

## Meta

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

---------

Signed-off-by: Jade Turner <spacey-sooty@proton.me>
Co-authored-by: Gold856 <117957790+Gold856@users.noreply.github.com>
2025-07-12 05:44:55 -04:00
Sam Freund
7f6edcd567 feat: add metrics publisher for NT (#1791)
Publishes metrics to NT using a protobuf under
`photonvision/coprocessors/metrics` using the device host name as the
key.

Refactors metrics to use numbers where possible, instead of strings.

Removes GPU mem display from metrics card when it can't be determined.

Updates UI metrics periodically.

Resolves #1988

Closes #830

---------

Co-authored-by: Matt <matthew.morley.ca@gmail.com>
Co-authored-by: Gold856 <117957790+Gold856@users.noreply.github.com>
2025-07-12 09:40:58 +00:00
Gold856
d7e536dda9 Clean up spacing and other things in various parts of the UI (#1972)
## Description

After the Vue 3 upgrade, the spacing for various UI elements was left
inconsistent in many places. Dialogs were hit especially hard and had
some very inconsistent spacing. Additionally, the 24 pixels of padding
around all cards was noted as a waste of space and unnecessary, so it
has been shrunk down to 20 pixels to make the UI a tiny bit more compact
and to make it visually closer to some parts of the UI that have 16
pixels of padding (the camera views are the most notable example).
Padding between input elements has also been reduced to 20 pixels (this
required some hackery to get consistent sizes on input elements, since
switches and sliders have different heights.)

Some other minor UI tweaks were made, such as removing the divider
between dialog contents and dialog buttons because it visually looks
better, shrinking the banner padding so it doesn't displace as much
content, making the banner background one uniform color instead of a
highlight around the icon, fixing the targets tab so that the columns
stop shifting around when the values change, preserving newlines in the
log view, cleaning up the object detection UI, and making the import
dialogs have consistently inset input elements.

Old dashboard:

![image](https://github.com/user-attachments/assets/409c7ddd-4b7d-4535-9f3f-3970d9dd85f8)

New dashboard:

![image](https://github.com/user-attachments/assets/587ac540-1d6d-40e5-9c6b-00697bab6cbc)

Old Camera tab:

![image](https://github.com/user-attachments/assets/2f1d50a1-131f-4fb7-8617-e1cb4dc5504c)

New Camera tab:

![image](https://github.com/user-attachments/assets/6d5581b7-faff-400a-8e34-e3abf00e0af6)

Old Calibration Info:

![image](https://github.com/user-attachments/assets/81133cc1-c861-4746-9b1e-8320312037de)

New Calibration Info:

![image](https://github.com/user-attachments/assets/0de5935c-84a7-4606-bbc1-8e6d227b7b60)

Old Log Viewer:

![image](https://github.com/user-attachments/assets/f2c32a10-3353-4781-93d7-8e0ffa8ca7fe)

New Log Viewer:

![image](https://github.com/user-attachments/assets/0aeee866-c182-4e80-9025-56bf383d714f)

Old Pipeline Creation Dialog:

![image](https://github.com/user-attachments/assets/a0eb368d-d9af-4cb3-8d9c-fcd12a5caf36)

New Pipeline Creation Dialog:

![image](https://github.com/user-attachments/assets/f05f34a3-f42e-4e8f-9ccd-171a48980b8f)

Old Factory Reset:

![image](https://github.com/user-attachments/assets/9c16a7f7-a454-4ee4-8574-98abf9b94e2d)

New Factory Reset:

![image](https://github.com/user-attachments/assets/fb67888c-c4f1-4e8e-9d02-6943e7a918eb)

Old Pipeline Change:

![image](https://github.com/user-attachments/assets/3acb215a-6639-4d50-a4e6-18b50c3ec1bd)

New Pipeline Change:

![image](https://github.com/user-attachments/assets/a2b18582-cdbd-407c-9690-f11aecf78c76)

Old Import Dialog:

![image](https://github.com/user-attachments/assets/ff43b0bd-3f99-44e5-97fa-c250cd331790)

New Import Dialog:

![image](https://github.com/user-attachments/assets/7ec46023-d47a-45d7-80b8-6881b812300e)

## Meta

Merge checklist:
- [x] Pull Request title is [short, imperative
summary](https://cbea.ms/git-commit/) of proposed changes
- [x] The description documents the _what_ and _why_
- [ ] If this PR changes behavior or adds a feature, user documentation
is updated
- [ ] If this PR touches photon-serde, all messages have been
regenerated and hashes have not changed unexpectedly
- [ ] If this PR touches configuration, this is backwards compatible
with settings back to v2024.3.1
- [ ] If this PR touches pipeline settings or anything related to data
exchange, the frontend typing is updated
- [ ] If this PR addresses a bug, a regression test for it is added
2025-07-12 00:02:23 -04:00
Gold856
dbbb00f955 Reenable and fix flaky tests (#1837) 2025-07-09 21:16:44 -07:00
Gold856
78f57600cc Set a more sane timeout for WS connections and log WS errors (#1992) 2025-07-08 21:40:21 -07:00
Sam Freund
d341ebbadf Initial hardware support for Rubik pi (#1989) 2025-07-06 19:39:29 +00:00
Sam Freund
d88ea4a75d De-conflict camera names and hostnames by use of a banner (#1982) 2025-07-04 21:43:17 +00:00
Sam Freund
46ac1baa69 Delete photon-server/photonvision_config_from_2024.3.1 (#1985)
Seems to be unused, since the settings import test we use pulls from test-resources.
2025-07-01 01:34:53 -04:00
Sam Freund
f802e8c10c Update server index wording (#1984) 2025-07-01 00:23:07 -04:00
Jade
647c238987 Fix usage reporting. (#1964)
## Description

Fixes the amount of cameras and pose estimators reported by usage reporting.

## Meta

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

---------

Signed-off-by: Jade Turner <spacey-sooty@proton.me>
2025-06-30 23:08:23 -04:00
Sam Freund
4a648b302a Migrate NNM Settings to SQLITE (#1894)
Co-authored-by: Gold856 <117957790+Gold856@users.noreply.github.com>
Co-authored-by: Chris Gerth <gerth2@users.noreply.github.com>
2025-06-30 22:02:44 -05:00
Gold856
cc7923eeb4 Fix camera setup modal not closing and navigation not working (#1979) 2025-06-28 23:18:54 -05:00
Sam Freund
a9c26202a0 Fix logic for no cameras detected modal (#1978)
fixes #1977 

Previously, the logic was checking for the camera object to be the same
as the placeholder camera object. Logic has been changed to check only
the name of the camera object.
2025-06-28 16:09:35 -05:00
Matthew Liang
783d9d73be Update list of coprocessors on quick-install.md (#1976) 2025-06-25 21:07:35 -05:00
Jade
efc997dfbd [ci] Run on push or PR (#1974)
## Description

CI will now run whenever a branch is pushed or a PR is opened. Fixes
2027 behaviour and allows running CI on forks

## Meta

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

Signed-off-by: Jade Turner <spacey-sooty@proton.me>
2025-06-23 23:50:13 -04:00
Gold856
3df58485c2 Disable linkcheck for gnu.org 2025-06-22 03:26:31 -07:00
Sam Freund
25851525ce Fix Broken Docs (#1971)
Some dummy updated the
[Caddyfile](https://github.com/PhotonVision/ansible-playbooks/blob/main/files/caddy/Caddyfile)
on the server but never updated the playbook repo or the CI on main to
reflect that. 🤷 Anyways, we're keeping the new Caddyfile because
it makes pydocs work, and it more accurately reflects our CI process.
2025-06-18 17:26:20 -04:00
Gold856
4b740a5485 Disable Alerts test in PhotonCameraTest (#1969)
## Description

Disable the Alerts test in PhotonCameraTest because it's consistently
failing on Linux.

## Meta

Merge checklist:
- [x] Pull Request title is [short, imperative
summary](https://cbea.ms/git-commit/) of proposed changes
- [x] The description documents the _what_ and _why_
- [ ] If this PR changes behavior or adds a feature, user documentation
is updated
- [ ] If this PR touches photon-serde, all messages have been
regenerated and hashes have not changed unexpectedly
- [ ] If this PR touches configuration, this is backwards compatible
with settings back to v2024.3.1
- [ ] If this PR touches pipeline settings or anything related to data
exchange, the frontend typing is updated
- [ ] If this PR addresses a bug, a regression test for it is added
2025-06-18 03:26:52 +00:00
Matthew Morley
413cf8c4af Fuse off mypy 2025-06-16 15:37:15 -07:00
Matthew Morley
d2193037f9 Nix merch because we can't have nice things 2025-06-16 15:37:03 -07:00
Sam Freund
52125067ac Remove unused template from docs (#1960) 2025-05-08 20:53:48 -07:00
Sam Freund
db591f720c Remove manual links from README (#1959) 2025-05-08 20:51:35 -07:00
Sam Freund
c81d4addb9 [docs] upgrade dependencies (#1958)
Upgrade dependencies for docs page
2025-05-08 20:47:47 -07:00
Sam Freund
aa0760e97a Add basic linux troubleshooting tips (#1885) 2025-05-07 16:09:11 -05:00
Graham
74322affde Clean up client dependencies (#1954)
Co-authored-by: samfreund <techguy763@gmail.com>
Co-authored-by: Gold856 <117957790+Gold856@users.noreply.github.com>
2025-05-07 11:06:07 -05:00
Graham
bec8092660 Vue 3 Upgrade (#1900)
## Description

Upgrades to Vue 3 and necessary associated dependencies. Also fixes some
issues with the layout and adds validation for object detection models.

Closes #885, closes #1943, closes #1449.
## Meta

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

---------

Co-authored-by: Matt M <matthew.morley.ca@gmail.com>
Co-authored-by: Gold856 <117957790+Gold856@users.noreply.github.com>
Co-authored-by: samfreund <techguy763@gmail.com>
2025-05-06 18:21:41 -04:00
237 changed files with 13317 additions and 10849 deletions

15
.github/labeler.yml vendored Normal file
View File

@@ -0,0 +1,15 @@
"backend":
- changed-files:
- any-glob-to-any-file: [photon-core/**, photon-server/**]
"documentation":
- changed-files:
- any-glob-to-any-file: [docs/**, photon-docs/**]
"frontend":
- changed-files:
- any-glob-to-any-file: photon-client/**
"photonlib":
- changed-files:
- any-glob-to-any-file: photon-lib*/**
"website":
- changed-files:
- any-glob-to-any-file: website/**

View File

@@ -13,6 +13,6 @@ Merge checklist:
- [ ] The description documents the _what_ and _why_
- [ ] If this PR changes behavior or adds a feature, user documentation is updated
- [ ] If this PR touches photon-serde, all messages have been regenerated and hashes have not changed unexpectedly
- [ ] If this PR touches configuration, this is backwards compatible with settings back to v2024.3.1
- [ ] If this PR touches 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

View File

@@ -3,38 +3,23 @@ name: Build
on:
# Run on pushes to main and pushed tags, and on pull requests against main, but ignore the docs folder
push:
branches: [ main ]
tags:
- 'v*'
pull_request:
branches: [ main ]
merge_group:
concurrency:
group: ${{ github.workflow }}-${{ github.head_ref || github.ref }}
cancel-in-progress: true
env:
IMAGE_VERSION: v2026.0.4
jobs:
build-client:
name: "PhotonClient Build"
defaults:
run:
working-directory: photon-client
runs-on: ubuntu-22.04
validation:
name: "Validation"
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 18
- name: Install Dependencies
run: npm ci
- name: Build Production Client
run: npm run build
- uses: actions/upload-artifact@v4
with:
name: built-client
path: photon-client/dist/
- uses: gradle/actions/wrapper-validation@v4
build-examples:
strategy:
@@ -49,6 +34,7 @@ jobs:
name: "Photonlib - Build Examples - ${{ matrix.os }}"
runs-on: ${{ matrix.os }}
needs: [validation]
steps:
- name: Checkout code
@@ -76,6 +62,7 @@ jobs:
build-gradle:
name: "Gradle Build"
runs-on: ubuntu-22.04
needs: [validation]
steps:
# Checkout code.
- name: Checkout code
@@ -89,22 +76,22 @@ jobs:
with:
java-version: 17
distribution: temurin
- name: Install pnpm
uses: pnpm/action-setup@v4
with:
version: 10
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 22
- name: Install mrcal deps
run: sudo apt-get update && sudo apt-get install -y libcholmod3 liblapack3 libsuitesparseconfig5
- name: Gradle Build
run: ./gradlew photon-targeting:build photon-core:build photon-server:build -x check
- name: Gradle Tests
run: ./gradlew testHeadless -i --stacktrace
run: ./gradlew testHeadless --stacktrace
- name: Gradle Coverage
run: ./gradlew jacocoTestReport
- name: Publish Coverage Report
uses: codecov/codecov-action@v4
with:
file: ./photon-server/build/reports/jacoco/test/jacocoTestReport.xml
- name: Publish Core Coverage Report
uses: codecov/codecov-action@v4
with:
file: ./photon-core/build/reports/jacoco/test/jacocoTestReport.xml
build-offline-docs:
name: "Build Offline Docs"
runs-on: ubuntu-22.04
@@ -135,6 +122,7 @@ jobs:
build-photonlib-vendorjson:
name: "Build Vendor JSON"
runs-on: ubuntu-22.04
needs: [validation]
steps:
- uses: actions/checkout@v4
with:
@@ -179,6 +167,7 @@ jobs:
name: "Photonlib - Build Host - ${{ matrix.artifact-name }}"
runs-on: ${{ matrix.os }}
needs: [validation]
steps:
- uses: actions/checkout@v4
with:
@@ -190,7 +179,7 @@ jobs:
distribution: temurin
architecture: ${{ matrix.architecture }}
- run: git fetch --tags --force
- run: ./gradlew photon-targeting:build photon-lib:build -i
- run: ./gradlew photon-targeting:build photon-lib:build
name: Build with Gradle
- run: ./gradlew photon-lib:publish photon-targeting:publish
name: Publish
@@ -222,6 +211,7 @@ jobs:
runs-on: ubuntu-22.04
container: ${{ matrix.container }}
name: "Photonlib - Build Docker - ${{ matrix.artifact-name }}"
needs: [validation]
steps:
- uses: actions/checkout@v4
with:
@@ -231,7 +221,7 @@ jobs:
git config --global --add safe.directory /__w/photonvision/photonvision
- name: Build PhotonLib
# We don't need to run tests, since we specify only non-native platforms
run: ./gradlew photon-targeting:build photon-lib:build ${{ matrix.build-options }} -i -x test
run: ./gradlew photon-targeting:build photon-lib:build ${{ matrix.build-options }} -x test
- name: Publish
run: ./gradlew photon-lib:publish photon-targeting:publish ${{ matrix.build-options }}
env:
@@ -270,7 +260,7 @@ jobs:
path: output/*.zip
build-package:
needs: [build-client, build-gradle, build-offline-docs]
needs: [build-gradle, build-offline-docs]
strategy:
fail-fast: false
@@ -310,21 +300,19 @@ jobs:
java-version: 17
distribution: temurin
architecture: ${{ matrix.architecture }}
- name: Install pnpm
uses: pnpm/action-setup@v4
with:
version: 10
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 22
cache: pnpm
cache-dependency-path: photon-client/pnpm-lock.yaml
- name: Install Arm64 Toolchain
run: ./gradlew installArm64Toolchain
if: ${{ (matrix.artifact-name) == 'LinuxArm64' }}
- run: |
rm -rf photon-server/src/main/resources/web/*
mkdir -p photon-server/src/main/resources/web/docs
if: ${{ (matrix.os) != 'windows-latest' }}
- run: |
del photon-server\src\main\resources\web\*.*
mkdir photon-server\src\main\resources\web\docs
if: ${{ (matrix.os) == 'windows-latest' }}
- uses: actions/download-artifact@v4
with:
name: built-client
path: photon-server/src/main/resources/web/
- uses: actions/download-artifact@v4
with:
name: built-docs
@@ -374,7 +362,7 @@ jobs:
- run: |
sudo apt-get update
sudo apt-get install --yes libcholmod3 liblapack3 libsuitesparseconfig5
if: ${{ (matrix.os) == 'ubuntu-22.04' }}
if: ${{ (matrix.os) == 'ubuntu-24.04' }}
# and actually run the jar
- run: java -jar ${{ matrix.extraOpts }} *.jar --smoketest
if: ${{ (matrix.os) != 'windows-latest' }}
@@ -388,10 +376,10 @@ jobs:
fail-fast: false
matrix:
include:
- os: ubuntu-22.04
- os: ubuntu-24.04
artifact-name: LinuxArm64
image_suffix: RaspberryPi
image_url: https://github.com/PhotonVision/photon-image-modifier/releases/download/v2025.0.3/photonvision_raspi.img.xz
image_url: https://github.com/PhotonVision/photon-image-modifier/releases/download/$IMAGE_VERSION/photonvision_raspi.img.xz
cpu: cortex-a7
image_additional_mb: 0
extraOpts: -Djdk.lang.Process.launchMechanism=vfork
@@ -427,69 +415,81 @@ jobs:
fail-fast: false
matrix:
include:
- os: ubuntu-22.04
- os: ubuntu-24.04
artifact-name: LinuxArm64
image_suffix: RaspberryPi
image_url: https://github.com/PhotonVision/photon-image-modifier/releases/download/v2025.0.3/photonvision_raspi.img.xz
image_url: https://github.com/PhotonVision/photon-image-modifier/releases/download/$IMAGE_VERSION/photonvision_raspi.img.xz
cpu: cortex-a7
image_additional_mb: 0
- os: ubuntu-22.04
- os: ubuntu-24.04
artifact-name: LinuxArm64
image_suffix: limelight2
image_url: https://github.com/PhotonVision/photon-image-modifier/releases/download/v2025.0.3/photonvision_limelight.img.xz
image_url: https://github.com/PhotonVision/photon-image-modifier/releases/download/$IMAGE_VERSION/photonvision_limelight.img.xz
cpu: cortex-a7
image_additional_mb: 0
- os: ubuntu-22.04
- os: ubuntu-24.04
artifact-name: LinuxArm64
image_suffix: limelight3
image_url: https://github.com/PhotonVision/photon-image-modifier/releases/download/v2025.0.3/photonvision_limelight3.img.xz
image_url: https://github.com/PhotonVision/photon-image-modifier/releases/download/$IMAGE_VERSION/photonvision_limelight3.img.xz
cpu: cortex-a7
image_additional_mb: 0
- os: ubuntu-22.04
- os: ubuntu-24.04
artifact-name: LinuxArm64
image_suffix: limelight3G
image_url: https://github.com/PhotonVision/photon-image-modifier/releases/download/v2025.0.3/photonvision_limelight3g.img.xz
image_url: https://github.com/PhotonVision/photon-image-modifier/releases/download/$IMAGE_VERSION/photonvision_limelight3g.img.xz
cpu: cortex-a7
image_additional_mb: 0
- os: ubuntu-22.04
- os: ubuntu-24.04
artifact-name: LinuxArm64
image_suffix: limelight4
image_url: https://github.com/PhotonVision/photon-image-modifier/releases/download/$IMAGE_VERSION/photonvision_limelight4.img.xz
cpu: cortex-a76
image_additional_mb: 0
- os: ubuntu-24.04
artifact-name: LinuxArm64
image_suffix: luma_p1
image_url: https://github.com/PhotonVision/photon-image-modifier/releases/download/$IMAGE_VERSION/photonvision_luma_p1.img.xz
cpu: cortex-a76
image_additional_mb: 0
- os: ubuntu-24.04
artifact-name: LinuxArm64
image_suffix: orangepi5
image_url: https://github.com/PhotonVision/photon-image-modifier/releases/download/v2025.0.3/photonvision_opi5.img.xz
image_url: https://github.com/PhotonVision/photon-image-modifier/releases/download/$IMAGE_VERSION/photonvision_opi5.img.xz
cpu: cortex-a8
image_additional_mb: 1024
- os: ubuntu-22.04
- os: ubuntu-24.04
artifact-name: LinuxArm64
image_suffix: orangepi5b
image_url: https://github.com/PhotonVision/photon-image-modifier/releases/download/v2025.0.3/photonvision_opi5b.img.xz
image_url: https://github.com/PhotonVision/photon-image-modifier/releases/download/$IMAGE_VERSION/photonvision_opi5b.img.xz
cpu: cortex-a8
image_additional_mb: 1024
- os: ubuntu-22.04
- os: ubuntu-24.04
artifact-name: LinuxArm64
image_suffix: orangepi5plus
image_url: https://github.com/PhotonVision/photon-image-modifier/releases/download/v2025.0.3/photonvision_opi5plus.img.xz
image_url: https://github.com/PhotonVision/photon-image-modifier/releases/download/$IMAGE_VERSION/photonvision_opi5plus.img.xz
cpu: cortex-a8
image_additional_mb: 1024
- os: ubuntu-22.04
- os: ubuntu-24.04
artifact-name: LinuxArm64
image_suffix: orangepi5pro
image_url: https://github.com/PhotonVision/photon-image-modifier/releases/download/v2025.0.3/photonvision_opi5pro.img.xz
image_url: https://github.com/PhotonVision/photon-image-modifier/releases/download/$IMAGE_VERSION/photonvision_opi5pro.img.xz
cpu: cortex-a8
image_additional_mb: 1024
- os: ubuntu-22.04
- os: ubuntu-24.04
artifact-name: LinuxArm64
image_suffix: orangepi5max
image_url: https://github.com/PhotonVision/photon-image-modifier/releases/download/v2025.0.3/photonvision_opi5max.img.xz
image_url: https://github.com/PhotonVision/photon-image-modifier/releases/download/$IMAGE_VERSION/photonvision_opi5max.img.xz
cpu: cortex-a8
image_additional_mb: 1024
- os: ubuntu-22.04
- os: ubuntu-24.04
artifact-name: LinuxArm64
image_suffix: rock5c
image_url: https://github.com/PhotonVision/photon-image-modifier/releases/download/v2025.0.3/photonvision_rock5c.img.xz
image_url: https://github.com/PhotonVision/photon-image-modifier/releases/download/$IMAGE_VERSION/photonvision_rock5c.img.xz
cpu: cortex-a8
image_additional_mb: 1024
runs-on: ${{ matrix.os }}
name: "Build image - ${{ matrix.image_url }}"
name: "Build image - ${{ matrix.image_suffix }}"
steps:
- name: Checkout code
@@ -523,8 +523,40 @@ jobs:
with:
name: image-${{ matrix.image_suffix }}
path: photonvision*.xz
build-rubik-image:
needs: [build-package]
if: ${{ github.event_name != 'pull_request' }}
runs-on: ubuntu-24.04
name: "Build image - Rubik Pi 3"
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: actions/download-artifact@v4
with:
name: jar-LinuxArm64
- name: Generate image
run: |
wget https://raw.githubusercontent.com/PhotonVision/photon-image-modifier/refs/tags/$IMAGE_VERSION/mount_rubikpi3.sh
chmod +x mount_rubikpi3.sh
./mount_rubikpi3.sh https://github.com/PhotonVision/photon-image-modifier/releases/download/$IMAGE_VERSION/photonvision_rubikpi3.tar.xz /tmp/build/scripts/armrunner.sh
- name: Compress image
run: |
new_jar=$(realpath $(find . -name photonvision\*-linuxarm64.jar))
new_image_name=$(basename "${new_jar/.jar/_rubikpi3.img}")
mv photonvision_rubikpi3 $new_image_name
tar -I 'xz -T0' -cf ${new_image_name}.tar.xz $new_image_name --checkpoint=10000 --checkpoint-action=echo='%T'
- uses: actions/upload-artifact@v4
name: Upload image
with:
name: image-rubikpi3
path: photonvision*.xz
release:
needs: [build-package, build-image, combine]
needs: [build-photonlib-vendorjson, build-package, build-image, build-rubik-image, combine]
runs-on: ubuntu-22.04
steps:
# Download all fat JARs
@@ -550,11 +582,12 @@ jobs:
- run: find
# Push to dev release
- uses: pyTooling/Actions/releaser@r0
- uses: pyTooling/Actions/releaser@r6
with:
token: ${{ secrets.GITHUB_TOKEN }}
tag: 'Dev'
rm: true
snapshots: false
files: |
**/*.xz
**/*linux*.jar
@@ -562,38 +595,12 @@ jobs:
**/photonlib*.json
**/photonlib*.zip
if: github.event_name == 'push'
# Upload all jars and xz archives
# Split into two uploads to work around max size limits in action-gh-releases
# https://github.com/softprops/action-gh-release/issues/353
- uses: softprops/action-gh-release@v2.0.9
with:
files: |
**/@(*orangepi5*|*rock5*).xz
if: startsWith(github.ref, 'refs/tags/v')
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- uses: softprops/action-gh-release@v2.0.9
with:
files: |
**/!(*orangepi5*|*rock5*).xz
**/*.jar
**/photonlib*.json
**/photonlib*.zip
if: startsWith(github.ref, 'refs/tags/v')
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
dispatch:
name: dispatch
needs: [build-photonlib-vendorjson, release]
runs-on: ubuntu-22.04
steps:
- uses: peter-evans/repository-dispatch@v3
if: |
github.repository == 'PhotonVision/photonvision' &&
startsWith(github.ref, 'refs/tags/v')
- name: Create Vendor JSON Repo PR
uses: wpilibsuite/vendor-json-repo/.github/actions/add_vendordep@main
with:
repo: PhotonVision/vendor-json-repo
token: ${{ secrets.VENDOR_JSON_REPO_PUSH_TOKEN }}
repository: PhotonVision/vendor-json-repo
event-type: tag
client-payload: '{"run_id": "${{ github.run_id }}", "package_version": "${{ github.ref_name }}"}'
vendordep_file: ${{ github.workspace }}/photonlib-${{ github.ref_name }}.json
pr_title: Update photonlib to ${{ github.ref_name }}
pr_branch: photonlib-${{ github.ref_name }}
if: github.repository == 'PhotonVision/photonvision' && startsWith(github.ref, 'refs/tags/v')

14
.github/workflows/labeler.yml vendored Normal file
View File

@@ -0,0 +1,14 @@
name: "Pull Request Labeler"
on:
- pull_request_target
jobs:
labeler:
permissions:
contents: read
pull-requests: write
runs-on: ubuntu-latest
steps:
- uses: actions/labeler@v5
with:
sync-labels: true

View File

@@ -3,18 +3,19 @@ name: Lint and Format
on:
# Run on pushes to main and pushed tags, and on pull requests against main, but ignore the docs folder
push:
branches: [ main ]
tags:
- 'v*'
pull_request:
branches: [ main ]
merge_group:
concurrency:
group: ${{ github.workflow }}-${{ github.head_ref || github.ref }}
cancel-in-progress: true
jobs:
validation:
name: "Validation"
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: gradle/actions/wrapper-validation@v4
wpiformat:
name: "wpiformat"
runs-on: ubuntu-22.04
@@ -30,7 +31,7 @@ jobs:
with:
python-version: 3.11
- name: Install wpiformat
run: pip3 install wpiformat==2025.33
run: pip3 install wpiformat==2025.34
- name: Run
run: wpiformat
- name: Check output
@@ -45,6 +46,7 @@ jobs:
if: ${{ failure() }}
javaformat:
name: "Java Formatting"
needs: [validation]
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v4
@@ -65,25 +67,19 @@ jobs:
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v4
- name: Install pnpm
uses: pnpm/action-setup@v4
with:
version: 10
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 18
node-version: 22
cache: pnpm
cache-dependency-path: photon-client/pnpm-lock.yaml
- name: Install Dependencies
run: npm ci
run: pnpm i --frozen-lockfile
- name: Check Linting
run: npm run lint-ci
run: pnpm run lint-ci
- name: Check Formatting
run: npm run format-ci
server-index:
name: "Check server index.html not changed"
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v4
- name: Fetch all history and metadata
run: |
git fetch --prune --unshallow
git checkout -b pr
git branch -f main origin/main
- name: Check index.html not changed
run: git --no-pager diff --exit-code origin/main photon-server/src/main/resources/web/index.html
run: pnpm run format-ci

View File

@@ -3,12 +3,7 @@ name: Photon API Documentation
on:
# Run on pushes to main and pushed tags, and on pull requests against main, but ignore the docs folder
push:
branches: [ main ]
tags:
- 'v*'
pull_request:
branches: [ main ]
merge_group:
concurrency:
group: ${{ github.workflow }}-${{ github.head_ref || github.ref }}
@@ -21,6 +16,12 @@ permissions:
id-token: write
jobs:
validation:
name: "Validation"
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: gradle/actions/wrapper-validation@v4
build_demo:
name: Build PhotonClient Demo
defaults:
@@ -29,21 +30,28 @@ jobs:
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v4
- name: Install pnpm
uses: pnpm/action-setup@v4
with:
version: 10
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 18
node-version: 22
cache: pnpm
cache-dependency-path: photon-client/pnpm-lock.yaml
- name: Install Dependencies
run: npm ci
run: pnpm i --frozen-lockfile
- name: Build Production Client
run: npm run build-demo
run: pnpm run build-demo
- uses: actions/upload-artifact@v4
with:
name: built-demo
path: photon-client/dist/
run_api_docs:
name: Build API Docs
run_java_cpp_docs:
name: Build Java and C++ API Docs
needs: [validation]
runs-on: "ubuntu-22.04"
steps:
- name: Checkout code
@@ -63,18 +71,18 @@ jobs:
./gradlew photon-docs:generateJavaDocs photon-docs:doxygen
- uses: actions/upload-artifact@v4
with:
name: built-docs
name: docs-java-cpp
path: photon-docs/build/docs
publish_api_docs:
name: Publish API Docs
needs: [run_api_docs]
needs: [run_java_cpp_docs]
runs-on: ubuntu-22.04
steps:
# Download docs artifact
- uses: actions/download-artifact@v4
with:
name: built-docs
pattern: docs-*
- run: find .
- name: Publish Docs To Development
if: github.ref == 'refs/heads/main'
@@ -83,7 +91,7 @@ jobs:
HOST: ${{ secrets.WEBMASTER_SSH_HOST }}
USER: ${{ secrets.WEBMASTER_SSH_USERNAME }}
KEY: ${{secrets.WEBMASTER_SSH_KEY}}
TARGET: /var/www/html/photonvision-docs/development
TARGET: /var/www/html/photonvision-docs/development/
- name: Publish Docs To Release
if: startsWith(github.ref, 'refs/tags/v')
uses: up9cloud/action-rsync@v1.4

View File

@@ -2,10 +2,7 @@ name: PhotonVision ReadTheDocs Checks
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
merge_group:
concurrency:
group: ${{ github.workflow }}-${{ github.head_ref || github.ref }}

View File

@@ -5,12 +5,7 @@ permissions:
on:
push:
branches: [ main ]
tags:
- 'v*'
pull_request:
branches: [ main ]
merge_group:
concurrency:
group: ${{ github.workflow }}-${{ github.head_ref || github.ref }}
@@ -47,13 +42,14 @@ jobs:
pip install --no-cache-dir dist/*.whl
pytest
- name: Run mypy type checking
uses: liskin/gh-problem-matcher-wrap@v3
with:
linters: mypy
run: |
mypy --show-column-numbers --config-file photon-lib/py/pyproject.toml photon-lib
# Disable due to robotpy issue. See
# https://github.com/PhotonVision/photonvision/issues/1968
# - name: Run mypy type checking
# uses: liskin/gh-problem-matcher-wrap@v3
# with:
# linters: mypy
# run: |
# mypy --show-column-numbers --config-file photon-lib/py/pyproject.toml photon-lib
- name: Upload artifacts
uses: actions/upload-artifact@master

View File

@@ -2,13 +2,7 @@ name: Website
on:
push:
# For now, run on all commits to main
branches: [ main ]
tags:
- 'v*'
pull_request:
branches: [ main ]
merge_group:
jobs:
rsync:
@@ -16,13 +10,21 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install pnpm
uses: pnpm/action-setup@v4
with:
version: 10
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: 22
cache: pnpm
cache-dependency-path: website/pnpm-lock.yaml
- name: Install packages
run: npm ci
run: pnpm i --frozen-lockfile
working-directory: website
- name: Build project
run: npm run build
run: pnpm run build
working-directory: website
- uses: up9cloud/action-rsync@v1.4
if: github.ref == 'refs/heads/main'
@@ -38,11 +40,19 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install pnpm
uses: pnpm/action-setup@v4
with:
version: 10
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: 22
cache: pnpm
cache-dependency-path: website/pnpm-lock.yaml
- name: Install Packages
run: npm ci
run: pnpm i --frozen-lockfile
working-directory: website
- name: Run Formatting Check
run: npx prettier -c .
run: pnpm prettier -c .
working-directory: website

4
.gitignore vendored
View File

@@ -5,7 +5,8 @@ __pycache__/
/.vs
backend/settings/
.vscode/
.vscode/*
!.vscode/settings.json
# Docs
_build
# Compiled class file
@@ -146,3 +147,4 @@ photon-server/src/main/resources/web/*
node_modules
dist
components.d.ts
photon-server/src/main/resources/web/index.html

1
.python-version Normal file
View File

@@ -0,0 +1 @@
3.11

View File

@@ -19,6 +19,7 @@ modifiableFileExclude {
\.webp$
\.ico$
\.rknn$
\.tflite$
\.mp4$
\.ttf$
\.woff2$

5
.vscode/settings.json vendored Normal file
View File

@@ -0,0 +1,5 @@
{
"python.testing.unittestEnabled": false,
"python.testing.pytestEnabled": true,
"python.testing.cwd": "photon-lib/py"
}

View File

@@ -17,13 +17,13 @@ If you are interested in contributing code or documentation to the project, plea
## Documentation
- Our main documentation page: [docs.photonvision.org](https://docs.photonvision.org)
- Photon UI demo: [http://photonvision.global/](http://photonvision.global/) (or [manual link](https://photonvision.github.io/photonvision/built-client/))
- Javadocs: [javadocs.photonvision.org](https://javadocs.photonvision.org) (or [manual link](https://photonvision.github.io/photonvision/built-docs/javadoc/))
- C++ Doxygen [cppdocs.photonvision.org](https://cppdocs.photonvision.org) (or [manual link](https://photonvision.github.io/photonvision/built-docs/doxygen/html/))
- 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)
## Building
Gradle is used for all C++ and Java code, and NPM is used for the web UI. Instructions to compile PhotonVision yourself can be found [in our docs](https://docs.photonvision.org/en/latest/docs/contributing/building-photon.html#compiling-instructions).
Gradle is used for all C++ and Java code, and pnpm is used for the web UI. Instructions to compile PhotonVision yourself can be found [in our docs](https://docs.photonvision.org/en/latest/docs/contributing/building-photon.html#compiling-instructions).
You can run one of the many built in examples straight from the command line, too! They contain a fully featured robot project, and some include simulation support. The projects can be found inside the [`photonlib-java-examples`](photonlib-java-examples) and [`photonlib-cpp-examples`](photonlib-cpp-examples) subdirectories, respectively. Instructions for running these examples directly from the repo are found [in the docs](https://docs.photonvision.org/en/latest/docs/contributing/building-photon.html#running-examples).
@@ -41,6 +41,8 @@ Note that these are case sensitive!
* linuxarm64
* linuxathena
- `-PtgtIP`: Specifies where `./gradlew deploy` should try to copy the fat JAR to
- `-PtgtUser`: Specifies custom username for `./gradlew deploy` to SSH into
- `-PtgtPw`: Specifies custom password for `./gradlew deploy` to SSH into
- `-Pprofile`: enables JVM profiling
- `-PwithSanitizers`: On Linux, enables `-fsanitize=address,undefined,leak`

View File

@@ -8,10 +8,9 @@ plugins {
id 'edu.wpi.first.WpilibTools' version '1.3.0'
id 'com.google.protobuf' version '0.9.3' apply false
id 'edu.wpi.first.GradleJni' version '1.1.0'
id "org.ysb33r.doxygen" version "1.0.4" apply false
id "org.ysb33r.doxygen" version "2.0.0" apply false
id 'com.gradleup.shadow' version '8.3.4' apply false
id "com.github.node-gradle.node" version "7.0.1" apply false
id "org.hidetake.ssh" version "2.11.2" apply false
}
allprojects {
@@ -37,9 +36,10 @@ ext {
wpimathVersion = wpilibVersion
openCVYear = "2025"
openCVversion = "4.10.0-3"
javalinVersion = "5.6.2"
libcameraDriverVersion = "v2025.0.3"
rknnVersion = "dev-v2025.0.0-1-g33b6263"
javalinVersion = "6.7.0"
libcameraDriverVersion = "v2025.0.4"
rknnVersion = "dev-v2025.0.0-5-g666c0c6"
rubikVersion = "dev-v2025.1.0-8-g067a316"
frcYear = "2025"
mrcalVersion = "v2025.0.0";
@@ -101,7 +101,7 @@ spotless {
}
wrapper {
gradleVersion '8.11'
gradleVersion = '8.14.3'
}
ext.getCurrentArch = {

View File

@@ -1,10 +1,8 @@
import argparse
import base64
import json
import os
from dataclasses import dataclass
import cv2
import mrcal
import numpy as np
from wpimath.geometry import Quaternion as _Quat
@@ -12,8 +10,8 @@ from wpimath.geometry import Quaternion as _Quat
@dataclass
class Size:
width: int
height: int
width: float
height: float
@dataclass
@@ -24,14 +22,6 @@ class JsonMatOfDoubles:
data: list[float]
@dataclass
class JsonMat:
rows: int
cols: int
type: int
data: str # Base64-encoded PNG data
@dataclass
class Point2:
x: float
@@ -84,8 +74,7 @@ class Observation:
# If we should use this observation when re-calculating camera calibration
includeObservationInCalibration: bool
snapshotName: str
# The actual image the snapshot is from
snapshotData: JsonMat
snapshotDataLocation: str
@dataclass
@@ -97,6 +86,7 @@ class CameraCalibration:
calobjectWarp: list[float]
calobjectSize: Size
calobjectSpacing: float
lensmodel: str
def __convert_cal_to_mrcal_cameramodel(
@@ -127,6 +117,13 @@ def __convert_cal_to_mrcal_cameramodel(
]
return np.concatenate((r, t))
imagersize = (int(cal.resolution.width), int(cal.resolution.height))
def fill_missing_corners(observations: list[list[float]], width: int, height: int):
num_corners = width * height
observations += [[0, 0, -1] for x in range(num_corners - len(observations))]
return observations
imagersize = (cal.resolution.width, cal.resolution.height)
# Always weight=1 for Photon data
@@ -135,8 +132,12 @@ def __convert_cal_to_mrcal_cameramodel(
[
# note that we expect row-major observations here. I think this holds
np.array(
list(map(lambda it: [it.x, it.y, WEIGHT], o.locationInImageSpace))
).reshape((cal.calobjectSize.width, cal.calobjectSize.height, 3))
fill_missing_corners(
list(map(lambda it: [it.x, it.y, WEIGHT], o.locationInImageSpace)),
int(cal.calobjectSize.width),
int(cal.calobjectSize.height),
)
).reshape((int(cal.calobjectSize.width), int(cal.calobjectSize.height), 3))
for o in cal.observations
]
)
@@ -206,14 +207,6 @@ def convert_photon_to_mrcal(photon_cal_json_path: str, output_folder: str):
if not os.path.exists(output_folder):
os.makedirs(output_folder)
# Decode each image and save it as a png
for obs in camera_cal_data.observations:
image = obs.snapshotData.data
decoded_data = base64.b64decode(image)
np_data = np.frombuffer(decoded_data, np.uint8)
img = cv2.imdecode(np_data, cv2.IMREAD_UNCHANGED)
cv2.imwrite(f"{output_folder}/{obs.snapshotName}", img)
# And create a VNL file for use with mrcal
with open(f"{output_folder}/corners.vnl", "w+") as vnl_file:
vnl_file.write("# filename x y level\n")

View File

@@ -11,6 +11,7 @@ modifiableFileExclude {
\.webp$
\.ico$
\.rknn$
\.tflite$
\.svg$
\.woff2$
gradlew

View File

@@ -1,16 +1,16 @@
alabaster==1.0.0
alabaster==0.7.16
anyio==4.9.0
babel==2.17.0
beautifulsoup4==4.13.3
certifi==2025.1.31
charset-normalizer==3.4.1
beautifulsoup4==4.13.4
certifi==2025.4.26
charset-normalizer==3.4.2
click==8.1.8
colorama==0.4.6
doc8==1.1.2
docopt==0.6.2
docutils==0.21.2
docutils==0.20.1
furo==2024.8.6
h11==0.14.0
h11==0.16.0
idna==3.10
imagesize==1.4.1
Jinja2==3.1.6
@@ -19,20 +19,20 @@ MarkupSafe==3.0.2
mdit-py-plugins==0.4.2
mdurl==0.1.2
myst-parser==4.0.1
packaging==24.2
packaging==25.0
pbr==6.1.1
pipreqs==0.4.13
pipreqs==0.5.0
Pygments==2.19.1
PyYAML==6.0.2
requests==2.32.3
restructuredtext_lint==1.4.0
requests==2.32.4
restructuredtext-lint==1.4.0
roman-numerals-py==3.1.0
setuptools==77.0.3
setuptools==80.3.1
six==1.17.0
sniffio==1.3.1
snowballstemmer==2.2.0
soupsieve==2.6
Sphinx==8.2.3
snowballstemmer==3.0.0.1
soupsieve==2.7
Sphinx==8.1.3
sphinx-autobuild==2024.10.3
sphinx-basic-ng==1.0.0b2
sphinx-notfound-page==1.1.0
@@ -47,13 +47,13 @@ sphinxcontrib-jquery==4.1
sphinxcontrib-jsmath==1.0.1
sphinxcontrib-qthelp==2.0.0
sphinxcontrib-serializinghtml==2.0.0
sphinxext-opengraph==0.9.1
sphinxext-opengraph==0.10.0
sphinxext-remoteliteralinclude==0.5.0
starlette==0.46.1
starlette==0.47.2
stevedore==5.4.1
typing_extensions==4.12.2
urllib3==2.3.0
uvicorn==0.34.0
watchfiles==1.0.4
typing_extensions==4.13.2
urllib3==2.5.0
uvicorn==0.34.2
watchfiles==1.0.5
websockets==15.0.1
yarg==0.1.10
yarg==0.1.9

View File

@@ -1,74 +0,0 @@
{# Import the theme's layout. #}
{% extends '!layout.html' %}
{%- block extrahead %}
<script>
if (localStorage.getItem("colorTheme") === "dark") {
document.documentElement.setAttribute('data-theme', 'dark');
} else if (localStorage.getItem("colorTheme") === "light") {
document.documentElement.setAttribute('data-theme', 'light');
} else {
var userPrefersDark = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches;
if (userPrefersDark) {
document.documentElement.setAttribute('data-theme', 'dark');
} else {
document.documentElement.setAttribute('data-theme', 'light');
}
}
</script>
{# Call the parent block #}
{{ super() }}
{% endblock %}
{%- block extrafooter %}
{# Add custom things to the head HTML tag #}
<div class="dark-mode-toggle-container">
<strong class="light-label md-icon">&#xE430</strong>
<div class="dark-mode-toggle">
<input type="checkbox" id="switch" name="theme"/><label class="toggle" for="switch">Toggle</label>
</div>
<strong class="dark-label md-icon">&#xE42D</strong>
</div>
<script>
var checkbox = document.querySelector('input[name=theme]');
var element = document.documentElement.getAttribute('data-theme');
if (element == 'dark') {
// Auto check the checkbox if the set theme is "dark".
checkbox.checked = true;
}
checkbox.addEventListener('change', function() {
if (this.checked) {
document.documentElement.setAttribute('data-theme', 'dark');
localStorage.setItem("colorTheme", "dark");
} else {
document.documentElement.setAttribute('data-theme', 'light');
localStorage.setItem("colorTheme", "light");
}
});
window.matchMedia('(prefers-color-scheme: dark)')
.addEventListener('change', event => {
if (event.matches) {
document.documentElement.setAttribute('data-theme', 'dark');
localStorage.setItem("colorTheme", "dark");
checkbox.checked = true;
} else {
document.documentElement.setAttribute('data-theme', 'light');
localStorage.setItem("colorTheme", "light");
checkbox.checked = false;
}
});
</script>
{# Call the parent block #}
{{ super() }}
{%- endblock %}

View File

@@ -21,6 +21,29 @@ project = "PhotonVision"
copyright = "2024, PhotonVision"
author = "Banks Troutman, Matt Morley"
# -- Git configuration -----------------------------------------------------
import subprocess
try:
# Use closest tag
git_tag_ref = (
subprocess.check_output(
[
"git",
"describe",
"--tags",
],
stderr=subprocess.DEVNULL,
)
.strip()
.decode()
)
except subprocess.CalledProcessError:
# Couldn't find closest tag, fallback to main
git_tag_ref = "main"
myst_substitutions = {"git_tag_ref": git_tag_ref}
# -- General configuration ---------------------------------------------------
# Add any Sphinx extension module names here, as strings. They can be
@@ -30,7 +53,6 @@ extensions = [
"sphinx_rtd_theme",
"sphinx.ext.autosectionlabel",
"sphinx.ext.todo",
"sphinx_tabs.tabs",
"notfound.extension",
"sphinxext.remoteliteralinclude",
"sphinxext.opengraph",
@@ -47,9 +69,6 @@ ogp_site_url = "https://docs.photonvision.org/en/latest/"
ogp_site_name = "PhotonVision Documentation"
ogp_image = "https://raw.githubusercontent.com/PhotonVision/photonvision-docs/main/source/assets/RectLogo.png"
# Add any paths that contain templates here, relative to this directory.
templates_path = ["_templates"]
# List of patterns, relative to source directory, that match files and
# directories to ignore when looking for source files.
# This pattern also affects html_static_path and html_extra_path.
@@ -70,6 +89,10 @@ html_title = "PhotonVision Docs"
html_theme = "furo"
html_favicon = "assets/RoundLogo.png"
# Specify canonical root
# This tells search engines that this domain is preferred
html_baseurl = "https://docs.photonvision.org/en/latest/"
# Add any paths that contain custom static files (such as style sheets) here,
# relative to this directory. They are copied after the builtin static files,
# so a file named "default.css" will overwrite the builtin "default.css".
@@ -147,11 +170,15 @@ sphinx_tabs_valid_builders = ["epub", "linkcheck"]
# Excluded links for linkcheck
# These should be periodically checked by hand to ensure that they are still functional
linkcheck_ignore = [R"https://www.raspberrypi.com/software/", R"http://10\..+"]
linkcheck_ignore = [
R"https://www.raspberrypi.com/software/",
R"http://10\..+",
R"https://www.gnu.org/",
]
token = os.environ.get("GITHUB_TOKEN", None)
if token:
linkcheck_auth = [(R"https://github.com/.+", token)]
# MyST configuration (https://myst-parser.readthedocs.io/en/latest/configuration.html)
myst_enable_extensions = ["colon_fence"]
myst_enable_extensions = ["colon_fence", "substitution"]

View File

@@ -28,7 +28,7 @@ This multi-target pose estimate can be accessed using PhotonLib. We suggest usin
```{eval-rst}
.. tab-set-code::
.. code-block:: Java
.. code-block:: java
var results = camera.getAllUnreadResults();
for (var result : results) {
@@ -39,7 +39,7 @@ This multi-target pose estimate can be accessed using PhotonLib. We suggest usin
}
.. code-block:: C++
.. code-block:: c++
auto results = camera.GetAllUnreadResults();
for (auto &result : results)
@@ -51,7 +51,7 @@ This multi-target pose estimate can be accessed using PhotonLib. We suggest usin
}
.. code-block:: Python
.. code-block:: python
results = camera.getAllUnreadResults()
for result in results:

View File

@@ -0,0 +1,8 @@
# Performance Benchmarks
```{toctree}
:maxdepth: 0
:titlesonly: true
rknn-model-benchmarks
```

View File

@@ -0,0 +1,125 @@
# RKNN Benchmarks
## Description
This benchmark compares the performance of four object detection models: YOLOv5, YOLOv5u, YOLOv8, and YOLOv11 on the [COCO 2017 Validation Set](http://images.cocodataset.org/zips/val2017.zip). The main purpose is to assess and compare the inference speed and detection accuracy of these models when deployed on the Orange Pi devices using the RKNN framework and int8 quantization.
## Methodology
- **Dataset**: [COCO 2017 Validation Set](http://images.cocodataset.org/zips/val2017.zip) (5,000 images)
- **Platform**: Orange Pi 5 with RK3588
- **Quantization**: int8 using 20 randomly selected images from the validation set
- **Framework**: RKNN Toolkit 2
## Operator-Level Benchmark Results
The following tables break down the average CPU time, NPU time, and total execution time (in microseconds) for each operator used by the models. Each value represents the mean ± standard deviation across 5,000 inferences.
### YOLOv5
| OpType | CPU Time (μs) | NPU Time (μs) | Total Time (μs) | Time Ratio (%) | Number of Times Called |
|-----------------|---------------------|----------------------|-----------------------|---------------------|-----------------------|
| ConvExSwish | 0.00 ± 0.00 | 10968.81 ± 1126.00 | 10968.81 ± 1126.00 | 73.06 ± 0.94 | 57 |
| ConvSigmoid | 0.00 ± 0.00 | 1243.49 ± 67.66 | 1243.49 ± 67.66 | 8.33 ± 0.57 | 3 |
| Concat | 0.00 ± 0.00 | 1080.68 ± 259.40 | 1080.68 ± 259.40 | 7.09 ± 0.87 | 13 |
| Conv | 0.00 ± 0.00 | 732.15 ± 29.42 | 732.15 ± 29.42 | 4.92 ± 0.42 | 1 |
| Add | 0.00 ± 0.00 | 473.71 ± 131.48 | 473.71 ± 131.48 | 3.10 ± 0.50 | 7 |
| MaxPool | 0.00 ± 0.00 | 272.40 ± 110.52 | 272.40 ± 110.52 | 1.76 ± 0.51 | 6 |
| Resize | 0.00 ± 0.00 | 147.61 ± 38.89 | 147.61 ± 38.89 | 0.97 ± 0.15 | 2 |
| OutputOperator | 106.60 ± 15.00 | 0.00 ± 0.00 | 106.60 ± 15.00 | 0.72 ± 0.13 | 3 |
| InputOperator | 8.64 ± 1.79 | 0.00 ± 0.00 | 8.64 ± 1.79 | 0.06 ± 0.02 | 1 |
| **Total** | **115.24 ± 16.16** | **14918.85 ± 1735.45**| **15034.09 ± 1734.28**| | **93** |
### YOLOv5u
| OpType | CPU Time (μs) | NPU Time (μs) | Total Time (μs) | Time Ratio (%) | Number of Times Called |
|-----------------|---------------------|----------------------|-----------------------|---------------------|-----------------------|
| ConvExSwish | 0.00 ± 0.00 | 16828.24 ± 1332.73 | 16828.24 ± 1332.73 | 83.04 ± 1.61 | 69 |
| Concat | 0.00 ± 0.00 | 1265.94 ± 250.24 | 1265.94 ± 250.24 | 6.17 ± 0.69 | 13 |
| ConvSigmoid | 0.00 ± 0.00 | 613.88 ± 62.97 | 613.88 ± 62.97 | 3.03 ± 0.15 | 3 |
| Add | 0.00 ± 0.00 | 553.75 ± 131.17 | 553.75 ± 131.17 | 2.69 ± 0.44 | 7 |
| Conv | 0.00 ± 0.00 | 298.61 ± 72.72 | 298.61 ± 72.72 | 1.45 ± 0.25 | 3 |
| ConvClip | 0.00 ± 0.00 | 256.02 ± 64.48 | 256.02 ± 64.48 | 1.24 ± 0.23 | 3 |
| MaxPool | 0.00 ± 0.00 | 178.68 ± 58.72 | 178.68 ± 58.72 | 0.86 ± 0.23 | 3 |
| Resize | 0.00 ± 0.00 | 170.87 ± 40.14 | 170.87 ± 40.14 | 0.83 ± 0.13 | 2 |
| OutputOperator | 126.89 ± 16.53 | 0.00 ± 0.00 | 126.89 ± 16.53 | 0.63 ± 0.10 | 9 |
| InputOperator | 8.69 ± 1.45 | 0.00 ± 0.00 | 8.69 ± 1.45 | 0.04 ± 0.01 | 1 |
| **Total** | **135.57 ± 17.51** | **20165.99 ± 1963.70**| **20301.56 ± 1965.88**| | **113** |
### YOLOv8
| OpType | CPU Time (μs) | NPU Time (μs) | Total Time (μs) | Time Ratio (%) | Number of Times Called |
|-----------------|---------------------|----------------------|-----------------------|---------------------|-----------------------|
| ConvExSwish | 0.00 ± 0.00 | 13017.04 ± 1165.76 | 13017.04 ± 1165.76 | 75.66 ± 1.96 | 57 |
| Concat | 0.00 ± 0.00 | 1489.94 ± 257.22 | 1489.94 ± 257.22 | 8.58 ± 0.53 | 13 |
| Split | 0.00 ± 0.00 | 681.47 ± 166.62 | 681.47 ± 166.62 | 3.89 ± 0.53 | 8 |
| ConvSigmoid | 0.00 ± 0.00 | 596.08 ± 75.01 | 596.08 ± 75.01 | 3.45 ± 0.18 | 3 |
| Add | 0.00 ± 0.00 | 443.60 ± 118.05 | 443.60 ± 118.05 | 2.53 ± 0.41 | 6 |
| Conv | 0.00 ± 0.00 | 269.61 ± 78.65 | 269.61 ± 78.65 | 1.54 ± 0.30 | 3 |
| Resize | 0.00 ± 0.00 | 236.79 ± 37.74 | 236.79 ± 37.74 | 1.37 ± 0.08 | 2 |
| ConvClip | 0.00 ± 0.00 | 231.82 ± 68.44 | 231.82 ± 68.44 | 1.32 ± 0.27 | 3 |
| MaxPool | 0.00 ± 0.00 | 156.85 ± 56.94 | 156.85 ± 56.94 | 0.89 ± 0.23 | 3 |
| OutputOperator | 124.86 ± 20.74 | 0.00 ± 0.00 | 124.86 ± 20.74 | 0.73 ± 0.15 | 9 |
| InputOperator | 8.47 ± 1.66 | 0.00 ± 0.00 | 8.47 ± 1.66 | 0.05 ± 0.01 | 1 |
| **Total** | **133.33 ± 21.95** | **17123.19 ± 1985.72**| **17256.52 ± 1986.77** | | **108** |
---
### YOLOv11
| OpType | CPU Time (μs) | NPU Time (μs) | Total Time (μs) | Time Ratio (%) | Number of Times Called |
|-----------------|---------------------|----------------------|-----------------------|---------------------|-----------------------|
| ConvExSwish | 0.00 ± 0.00 | 16034.00 ± 1331.95 | 16034.00 ± 1331.95 | 69.90 ± 1.55 | 77 |
| Concat | 0.00 ± 0.00 | 1888.89 ± 293.99 | 1888.89 ± 293.99 | 8.17 ± 0.51 | 17 |
| exSDPAttention | 0.00 ± 0.00 | 1210.88 ± 17.73 | 1210.88 ± 17.73 | 5.32 ± 0.52 | 1 |
| Split | 0.00 ± 0.00 | 908.30 ± 183.92 | 908.30 ± 183.92 | 3.91 ± 0.45 | 10 |
| Add | 0.00 ± 0.00 | 871.64 ± 212.79 | 871.64 ± 212.79 | 3.73 ± 0.60 | 12 |
| ConvSigmoid | 0.00 ± 0.00 | 617.61 ± 59.61 | 617.61 ± 59.61 | 2.69 ± 0.16 | 3 |
| Conv | 0.00 ± 0.00 | 419.72 ± 89.88 | 419.72 ± 89.88 | 1.80 ± 0.24 | 5 |
| Resize | 0.00 ± 0.00 | 272.09 ± 49.91 | 272.09 ± 49.91 | 1.18 ± 0.12 | 2 |
| ConvClip | 0.00 ± 0.00 | 260.08 ± 59.12 | 260.08 ± 59.12 | 1.12 ± 0.18 | 3 |
| MaxPool | 0.00 ± 0.00 | 181.93 ± 53.32 | 181.93 ± 53.32 | 0.78 ± 0.18 | 3 |
| OutputOperator | 131.48 ± 22.93 | 0.00 ± 0.00 | 131.48 ± 22.93 | 0.58 ± 0.12 | 9 |
| ConvAdd | 0.00 ± 0.00 | 126.79 ± 35.28 | 126.79 ± 35.28 | 0.54 ± 0.11 | 2 |
| Reshape | 0.00 ± 0.00 | 56.61 ± 18.03 | 56.61 ± 18.03 | 0.24 ± 0.06 | 3 |
| InputOperator | 8.66 ± 1.59 | 0.00 ± 0.00 | 8.66 ± 1.59 | 0.04 ± 0.01 | 1 |
| **Total** | **140.14 ± 24.26** | **22848.54 ± 2351.95**| **22988.68 ± 2355.97**| | **148** |
## Model Summary and Accuracy Metrics
The table below summarizes the mean average precision (mAP) and total inference time for each model. These metrics provide a high-level view of how each model performs in terms of both detection accuracy and runtime efficiency.
### Mean Average Precision (mAP) by Model
| Metric | YOLOv5 | YOLOv5u | YOLOv8 | YOLOv11 |
|--------|------------|------------|------------|------------|
| **mAP** | 0.2243 | 0.2745 | 0.3051 | 0.3251 |
| **mAP50** | 0.3538 | 0.3834 | 0.4145 | 0.4406 |
| **mAP75** | 0.2432 | 0.2997 | 0.3349 | 0.3568 |
| **mAP85** | 0.3054 | 0.3472 | 0.3867 | 0.4068 |
| **mAP95** | 0.3708 | 0.4822 | 0.5483 | 0.5858 |
### Model Execution Time and Call Frequency
| Model | Total Time (μs) | Number of Processing Calls |
|---------|------------------------|----------------------------|
| **YOLOv5** | 15034.09 ± 1734.28 | 93 |
| **YOLOv5u** | 20301.56 ± 1965.88 | 113 |
| **YOLOv8** | 17256.52 ± 1986.77 | 108 |
| **YOLOv11** | 22988.68 ± 2355.97 | 148 |
## Conclusion
The benchmark reveals a clear performance trade-off between inference time and detection accuracy:
- **YOLOv5** is the fastest model with the lowest total inference time, making it well-suited for situations where speed is more important than high detection precision.
- **YOLOv11** achieves the highest accuracy (mAP) across all IoU thresholds but comes with the longest inference time, which may limit its use in real-time applications.
- **YOLOv8** offers a strong balance between speed and accuracy, making it a practical choice when both factors matter.
- **YOLOv5u** improves accuracy compared to YOLOv5 but falls behind YOLOv8 in both speed and detection quality.
When choosing a model for edge devices like the Orange Pi 5, its important to weigh how much latency your system can tolerate versus how much accuracy you need. A faster model may give quicker results, while a more accurate one may offer better detection reliability, but at the cost of speed.

View File

@@ -1,4 +1,4 @@
# Camera-Specifc Configuration
# Camera-Specific Configuration
```{toctree}
:maxdepth: 2

View File

@@ -12,17 +12,11 @@ This section contains the build instructions from the source code available at [
**Node JS:**
The UI is written in Node JS. To compile the UI, Node 18.20.4 to Node 20.0.0 is required. To install Node JS follow the instructions for your platform [on the official Node JS website](https://nodejs.org/en/download/). However, modify this line
The UI is written in Node JS. To compile the UI, Node 22 or later is required. To install Node JS, follow the instructions for your platform [on the official Node JS website](https://nodejs.org/en/download/).
```bash
nvm install 20
```
**pnpm:**
so that it instead reads
```javascript
nvm install 18.20.4
```
[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).
## Compiling Instructions
@@ -46,27 +40,7 @@ or alternatively download the source code from GitHub and extract the zip:
In the photon-client directory:
```bash
npm install
```
### Build and Copy UI to Java Source
In the root directory:
```{eval-rst}
.. tab-set::
.. tab-item:: Linux
``./gradlew buildAndCopyUI``
.. tab-item:: macOS
``./gradlew buildAndCopyUI``
.. tab-item:: Windows (cmd)
``gradlew buildAndCopyUI``
pnpm install
```
### Using hot reload on the UI
@@ -74,7 +48,7 @@ In the root directory:
In the photon-client directory:
```bash
npm run dev
pnpm run dev
```
This allows you to make UI changes quickly without having to spend time rebuilding the jar. Hot reload is enabled, so changes that you make and save are reflected in the UI immediately. Running this command will give you the URL for accessing the UI, which is on a different port than normal. You must use the printed URL to use hot reload.
@@ -87,14 +61,17 @@ To compile and run the project, issue the following command in the root director
.. tab-set::
.. tab-item:: Linux
:sync: linux
``./gradlew run``
.. tab-item:: macOS
:sync: macos
``./gradlew run``
.. tab-item:: Windows (cmd)
:sync: windows
``gradlew run``
```
@@ -105,21 +82,24 @@ Running the following command under the root directory will build the jar under
.. tab-set::
.. tab-item:: Linux
:sync: linux
``./gradlew shadowJar``
.. tab-item:: macOS
:sync: macos
``./gradlew shadowJar``
.. tab-item:: Windows (cmd)
:sync: windows
``gradlew shadowJar``
```
### Build and Run PhotonVision on a Raspberry Pi Coprocessor
As a convenience, the build has a built-in `deploy` command which builds, deploys, and starts the current source code on a coprocessor.
As a convenience, the build has a built-in `deploy` command which builds, deploys, and starts the current source code on a coprocessor. It uses [deploy-utils](https://github.com/wpilibsuite/deploy-utils/blob/main/README.md), so it works very similarly to deploys on robot projects.
An architecture override is required to specify the deploy target's architecture.
@@ -127,18 +107,21 @@ An architecture override is required to specify the deploy target's architecture
.. tab-set::
.. tab-item:: Linux
:sync: linux
``./gradlew clean``
``./gradlew deploy -PArchOverride=linuxarm64``
.. tab-item:: macOS
:sync: macos
``./gradlew clean``
``./gradlew deploy -PArchOverride=linuxarm64``
.. tab-item:: Windows (cmd)
:sync: windows
``gradlew clean``
@@ -157,14 +140,17 @@ The photonlib source can be published to your local maven repository after build
.. tab-set::
.. tab-item:: Linux
:sync: linux
``./gradlew publishToMavenLocal``
.. tab-item:: macOS
:sync: macos
``./gradlew publishToMavenLocal``
.. tab-item:: Windows (cmd)
:sync: windows
``gradlew publishToMavenLocal``
```
@@ -207,7 +193,7 @@ Similarly, a local instance of PhotonVision can be debugged in the same way usin
Set up a VSCode configuration in {code}`launch.json`
```
```json
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
@@ -289,3 +275,9 @@ Using the [GitHub CLI](https://cli.github.com/), we can download artifacts from
MacOS builds are not published to releases as MacOS is not an officially
supported platform. However, MacOS builds are still available from the MacOS
build action, which can be found [here](https://github.com/PhotonVision/photonvision/actions/workflows/build.yml).
#### Forcing Object Detection in the UI
In order to force the Object Detection interface to be visible, it's necessary to hardcode the platform that `Platform.java` returns. This can be done by changing the function that detects the RK3588S/QCS6490 platform to always return true, and changing the `getCurrentPlatform()` function to always return the RK3588S/QCS6490 architecture.
Alternatively, it's possible to modify the frontend code by changing all instances of `useSettingsStore().general.supportedBackends.length > 0` to `true`, which will force the card to render.
Make sure to revert these changes before submitting a Pull Request.

View File

@@ -14,8 +14,10 @@ To do this, we'll use the _pitch_ of the target in the camera image and trigonom
```{eval-rst}
.. tab-set::
:sync-group: code
.. tab-item:: Java
:sync: java
.. rli:: https://raw.githubusercontent.com/PhotonVision/photonvision/abe95dfaa055bbe3609f72cfcaaba0f96ee7978c/photonlib-java-examples/aimandrange/src/main/java/frc/robot/Robot.java
:language: java
@@ -24,6 +26,7 @@ To do this, we'll use the _pitch_ of the target in the camera image and trigonom
:lineno-start: 84
.. tab-item:: C++ (Header)
:sync: c++
.. rli:: https://raw.githubusercontent.com/PhotonVision/photonvision/abe95dfaa055bbe3609f72cfcaaba0f96ee7978c/photonlib-cpp-examples/aimandrange/src/main/include/Robot.h
:language: c++
@@ -32,6 +35,7 @@ To do this, we'll use the _pitch_ of the target in the camera image and trigonom
:lineno-start: 25
.. tab-item:: C++ (Source)
:sync: c++
.. rli:: https://raw.githubusercontent.com/PhotonVision/photonvision/abe95dfaa055bbe3609f72cfcaaba0f96ee7978c/photonlib-cpp-examples/aimandrange/src/main/cpp/Robot.cpp
:language: c++
@@ -40,6 +44,7 @@ To do this, we'll use the _pitch_ of the target in the camera image and trigonom
:lineno-start: 58
.. tab-item:: Python
:sync: python
.. rli:: https://raw.githubusercontent.com/PhotonVision/photonvision/abe95dfaa055bbe3609f72cfcaaba0f96ee7978c/photonlib-python-examples/aimandrange/robot.py
:language: python

View File

@@ -19,8 +19,10 @@ In this example, while the operator holds a button down, the robot will turn tow
```{eval-rst}
.. tab-set::
:sync-group: code
.. tab-item:: Java
:sync: java
.. rli:: https://raw.githubusercontent.com/PhotonVision/photonvision/abe95dfaa055bbe3609f72cfcaaba0f96ee7978c/photonlib-java-examples/aimattarget/src/main/java/frc/robot/Robot.java
:language: java
@@ -29,6 +31,7 @@ In this example, while the operator holds a button down, the robot will turn tow
:lineno-start: 77
.. tab-item:: C++ (Header)
:sync: c++
.. rli:: https://raw.githubusercontent.com/PhotonVision/photonvision/abe95dfaa055bbe3609f72cfcaaba0f96ee7978c/photonlib-cpp-examples/aimattarget/src/main/include/Robot.h
:language: c++
@@ -37,6 +40,7 @@ In this example, while the operator holds a button down, the robot will turn tow
:lineno-start: 25
.. tab-item:: C++ (Source)
:sync: c++
.. rli:: https://raw.githubusercontent.com/PhotonVision/photonvision/abe95dfaa055bbe3609f72cfcaaba0f96ee7978c/photonlib-cpp-examples/aimattarget/src/main/cpp/Robot.cpp
:language: c++
@@ -45,6 +49,7 @@ In this example, while the operator holds a button down, the robot will turn tow
:lineno-start: 56
.. tab-item:: Python
:sync: python
.. rli:: https://raw.githubusercontent.com/PhotonVision/photonvision/abe95dfaa055bbe3609f72cfcaaba0f96ee7978c/photonlib-python-examples/aimattarget/robot.py
:language: python

View File

@@ -21,32 +21,24 @@ Please reference the [WPILib documentation](https://docs.wpilib.org/en/stable/do
We use the 2024 game's AprilTag Locations:
```{eval-rst}
.. tab-set::
.. tab-set-code::
.. rli:: https://raw.githubusercontent.com/PhotonVision/photonvision/abe95dfaa055bbe3609f72cfcaaba0f96ee7978c/photonlib-java-examples/poseest/src/main/java/frc/robot/Vision.java
:language: java
:lines: 68-68
:linenos:
:lineno-start: 68
.. tab-item:: Java
:sync: java
.. rli:: https://raw.githubusercontent.com/PhotonVision/photonvision/abe95dfaa055bbe3609f72cfcaaba0f96ee7978c/photonlib-cpp-examples/poseest/src/main/include/Constants.h
:language: c++
:lines: 42-43
:linenos:
:lineno-start: 42
.. rli:: https://raw.githubusercontent.com/PhotonVision/photonvision/abe95dfaa055bbe3609f72cfcaaba0f96ee7978c/photonlib-java-examples/poseest/src/main/java/frc/robot/Vision.java
:language: java
:lines: 68-68
:linenos:
:lineno-start: 68
.. tab-item:: C++
.. rli:: https://raw.githubusercontent.com/PhotonVision/photonvision/abe95dfaa055bbe3609f72cfcaaba0f96ee7978c/photonlib-cpp-examples/poseest/src/main/include/Constants.h
:language: c++
:lines: 42-43
:linenos:
:lineno-start: 42
.. tab-item:: Python
.. rli:: https://raw.githubusercontent.com/PhotonVision/photonvision/abe95dfaa055bbe3609f72cfcaaba0f96ee7978c/photonlib-python-examples/poseest/robot.py
:language: python
:lines: 46-46
:linenos:
:lineno-start: 46
.. rli:: https://raw.githubusercontent.com/PhotonVision/photonvision/abe95dfaa055bbe3609f72cfcaaba0f96ee7978c/photonlib-python-examples/poseest/robot.py
:language: python
:lines: 46-46
:linenos:
:lineno-start: 46
```
@@ -56,63 +48,47 @@ To incorporate PhotonVision, we need to create a {code}`PhotonCamera`:
```{eval-rst}
.. tab-set::
.. tab-set-code::
.. rli:: https://raw.githubusercontent.com/PhotonVision/photonvision/abe95dfaa055bbe3609f72cfcaaba0f96ee7978c/photonlib-java-examples/poseest/src/main/java/frc/robot/Vision.java
:language: java
:lines: 57-57
:linenos:
:lineno-start: 57
.. tab-item:: Java
:sync: java
.. rli:: https://raw.githubusercontent.com/PhotonVision/photonvision/abe95dfaa055bbe3609f72cfcaaba0f96ee7978c/photonlib-cpp-examples/poseest/src/main/include/Vision.h
:language: c++
:lines: 145-145
:linenos:
:lineno-start: 145
.. rli:: https://raw.githubusercontent.com/PhotonVision/photonvision/abe95dfaa055bbe3609f72cfcaaba0f96ee7978c/photonlib-java-examples/poseest/src/main/java/frc/robot/Vision.java
:language: java
:lines: 57-57
:linenos:
:lineno-start: 57
.. tab-item:: C++
.. rli:: https://raw.githubusercontent.com/PhotonVision/photonvision/abe95dfaa055bbe3609f72cfcaaba0f96ee7978c/photonlib-cpp-examples/poseest/src/main/include/Vision.h
:language: c++
:lines: 145-145
:linenos:
:lineno-start: 145
.. tab-item:: Python
.. rli:: https://raw.githubusercontent.com/PhotonVision/photonvision/abe95dfaa055bbe3609f72cfcaaba0f96ee7978c/photonlib-python-examples/poseest/robot.py
:language: python
:lines: 44-44
:linenos:
:lineno-start: 44
.. rli:: https://raw.githubusercontent.com/PhotonVision/photonvision/abe95dfaa055bbe3609f72cfcaaba0f96ee7978c/photonlib-python-examples/poseest/robot.py
:language: python
:lines: 44-44
:linenos:
:lineno-start: 44
```
During periodic execution, we read back camera results. If we see AprilTags in the image, we calculate the camera-measured pose of the robot and pass it to the {code}`Drivetrain`.
```{eval-rst}
.. tab-set::
.. tab-set-code::
.. rli:: https://raw.githubusercontent.com/PhotonVision/photonvision/abe95dfaa055bbe3609f72cfcaaba0f96ee7978c/photonlib-java-examples/poseest/src/main/java/frc/robot/Robot.java
:language: java
:lines: 64-74
:linenos:
:lineno-start: 64
.. tab-item:: Java
:sync: java
.. rli:: https://raw.githubusercontent.com/PhotonVision/photonvision/abe95dfaa055bbe3609f72cfcaaba0f96ee7978c/photonlib-cpp-examples/poseest/src/main/cpp/Robot.cpp
:language: c++
:lines: 38-46
:linenos:
:lineno-start: 38
.. rli:: https://raw.githubusercontent.com/PhotonVision/photonvision/abe95dfaa055bbe3609f72cfcaaba0f96ee7978c/photonlib-java-examples/poseest/src/main/java/frc/robot/Robot.java
:language: java
:lines: 64-74
:linenos:
:lineno-start: 64
.. tab-item:: C++
.. rli:: https://raw.githubusercontent.com/PhotonVision/photonvision/abe95dfaa055bbe3609f72cfcaaba0f96ee7978c/photonlib-cpp-examples/poseest/src/main/cpp/Robot.cpp
:language: c++
:lines: 38-46
:linenos:
:lineno-start: 38
.. tab-item:: Python
.. rli:: https://raw.githubusercontent.com/PhotonVision/photonvision/abe95dfaa055bbe3609f72cfcaaba0f96ee7978c/photonlib-python-examples/poseest/robot.py
:language: python
:lines: 54-56
:linenos:
:lineno-start: 54
.. rli:: https://raw.githubusercontent.com/PhotonVision/photonvision/abe95dfaa055bbe3609f72cfcaaba0f96ee7978c/photonlib-python-examples/poseest/robot.py
:language: python
:lines: 54-56
:linenos:
:lineno-start: 54
```
@@ -121,56 +97,45 @@ During periodic execution, we read back camera results. If we see AprilTags in t
First, we create a new {code}`VisionSystemSim` to represent our camera and coprocessor running PhotonVision, and moving around our simulated field.
```{eval-rst}
.. tab-set::
.. tab-set-code::
.. rli:: https://raw.githubusercontent.com/PhotonVision/photonvision/abe95dfaa055bbe3609f72cfcaaba0f96ee7978c/photonlib-java-examples/poseest/src/main/java/frc/robot/Vision.java
:language: java
:lines: 65-69
:linenos:
:lineno-start: 65
.. tab-item:: Java
:sync: java
.. rli:: https://raw.githubusercontent.com/PhotonVision/photonvision/abe95dfaa055bbe3609f72cfcaaba0f96ee7978c/photonlib-cpp-examples/poseest/src/main/include/Vision.h
:language: c++
:lines: 49-52
:linenos:
:lineno-start: 49
.. rli:: https://raw.githubusercontent.com/PhotonVision/photonvision/abe95dfaa055bbe3609f72cfcaaba0f96ee7978c/photonlib-java-examples/poseest/src/main/java/frc/robot/Vision.java
:language: java
:lines: 65-69
:linenos:
:lineno-start: 65
.. code-block:: python
.. tab-item:: C++
.. rli:: https://raw.githubusercontent.com/PhotonVision/photonvision/abe95dfaa055bbe3609f72cfcaaba0f96ee7978c/photonlib-cpp-examples/poseest/src/main/include/Vision.h
:language: c++
:lines: 49-52
:linenos:
:lineno-start: 49
.. tab-item:: Python
# Coming Soon!
# Coming Soon!
```
Then, we add configure the simulated vision system to match the camera system being simulated.
```{eval-rst}
.. tab-set::
.. tab-set-code::
.. tab-item:: Java
:sync: java
.. rli:: https://raw.githubusercontent.com/PhotonVision/photonvision/abe95dfaa055bbe3609f72cfcaaba0f96ee7978c/photonlib-java-examples/poseest/src/main/java/frc/robot/Vision.java
:language: java
:lines: 69-82
:linenos:
:lineno-start: 69
.. rli:: https://raw.githubusercontent.com/PhotonVision/photonvision/abe95dfaa055bbe3609f72cfcaaba0f96ee7978c/photonlib-java-examples/poseest/src/main/java/frc/robot/Vision.java
:language: java
:lines: 69-82
:linenos:
:lineno-start: 69
.. rli:: https://raw.githubusercontent.com/PhotonVision/photonvision/abe95dfaa055bbe3609f72cfcaaba0f96ee7978c/photonlib-cpp-examples/poseest/src/main/include/Vision.h
:language: c++
:lines: 53-65
:linenos:
:lineno-start: 53
.. tab-item:: C++
.. code-block:: python
.. rli:: https://raw.githubusercontent.com/PhotonVision/photonvision/abe95dfaa055bbe3609f72cfcaaba0f96ee7978c/photonlib-cpp-examples/poseest/src/main/include/Vision.h
:language: c++
:lines: 53-65
:linenos:
:lineno-start: 53
.. tab-item:: Python
# Coming Soon!
# Coming Soon!
```
@@ -179,28 +144,23 @@ Then, we add configure the simulated vision system to match the camera system be
During simulation, we periodically update the simulated vision system.
```{eval-rst}
.. tab-set::
.. tab-set-code::
.. tab-item:: Java
:sync: java
.. rli:: https://raw.githubusercontent.com/PhotonVision/photonvision/abe95dfaa055bbe3609f72cfcaaba0f96ee7978c/photonlib-java-examples/poseest/src/main/java/frc/robot/Robot.java
:language: java
:lines: 114-132
:linenos:
:lineno-start: 114
.. rli:: https://raw.githubusercontent.com/PhotonVision/photonvision/abe95dfaa055bbe3609f72cfcaaba0f96ee7978c/photonlib-java-examples/poseest/src/main/java/frc/robot/Robot.java
:language: java
:lines: 114-132
:linenos:
:lineno-start: 114
.. rli:: https://raw.githubusercontent.com/PhotonVision/photonvision/abe95dfaa055bbe3609f72cfcaaba0f96ee7978c/photonlib-cpp-examples/poseest/src/main/cpp/Robot.cpp
:language: c++
:lines: 95-109
:linenos:
:lineno-start: 95
.. tab-item:: C++
.. code-block:: python
.. rli:: https://raw.githubusercontent.com/PhotonVision/photonvision/abe95dfaa055bbe3609f72cfcaaba0f96ee7978c/photonlib-cpp-examples/poseest/src/main/cpp/Robot.cpp
:language: c++
:lines: 95-109
:linenos:
:lineno-start: 95
.. tab-item:: Python
# Coming Soon!
# Coming Soon!
```
The rest is done behind the scenes.

View File

@@ -43,7 +43,7 @@ A simple way to use a pose estimate is to activate robot functions automatically
```{eval-rst}
.. tab-set-code::
.. code-block:: Java
.. code-block:: java
Pose3d robotPose;
boolean launcherSpinCmd;

View File

@@ -2,9 +2,9 @@
## How does it work?
PhotonVision supports object detection using neural network accelerator hardware built into Orange Pi 5/5+ coprocessors. Please note that the Orange Pi 5/5+ are the only coprocessors that are currently supported. The Neural Processing Unit, or NPU, is [used by PhotonVision](https://github.com/PhotonVision/rknn_jni/tree/main) to massively accelerate certain math operations like those needed for running ML-based object detection.
PhotonVision supports object detection using neural network accelerator hardware, commonly known as an NPU. The two coprocessors currently supported are the {ref}`Orange Pi 5 <docs/objectDetection/opi:Orange Pi 5 (and variants) Object Detection>` and the {ref}`Rubik Pi 3 <docs/objectDetection/rubik:Rubik Pi 3 Object Detection>`.
For the 2025 season, PhotonVision ships with a pretrained ALGAE model. A model to detect coral is not currently stable, and interested teams should ask in the Photonvision discord.
PhotonVision currently ships with a model trained on the [COCO dataset](https://cocodataset.org/) by [Ultralytics](https://github.com/ultralytics/ultralytics) (this model is licensed under [AGPLv3](https://www.gnu.org/licenses/agpl-3.0.en.html)). This model is meant to be used for testing and other miscellaneous purposes. It is not meant to be used in competition. For the 2025 post-season, PhotonVision also ships with a pretrained ALGAE model. A model to detect coral is available in the PhotonVision discord, but will not be distributed with PhotonVision.
## Tracking Objects
@@ -18,7 +18,7 @@ This model output means that while its fairly easy to say that "this rectangle p
## Tuning and Filtering
Compared to other pipelines, object detection exposes very few tuning handles. The Confidence slider changes the minimum confidence that the model needs to have in a given detection to consider it valid, as a number between 0 and 1 (with 0 meaning completely uncertain and 1 meaning maximally certain).
Compared to other pipelines, object detection exposes very few tuning handles. The Confidence slider changes the minimum confidence that the model needs to have in a given detection to consider it valid, as a number between 0 and 1 (with 0 meaning completely uncertain and 1 meaning maximally certain). The Non-Maximum Suppresion (NMS) Threshold slider is used to filter out overlapping detections. Higher values mean more detections are allowed through, but may result in false positives. It's generally recommended that teams leave this set at the default, unless they find they're unable to get usable results with solely the Confidence slider.
```{raw} html
<video width="85%" controls>
@@ -33,31 +33,19 @@ The same area, aspect ratio, and target orientation/sort parameters from {ref}`r
Photonvision will letterbox your camera frame to 640x640. This means that if you select a resolution that is larger than 640 it will be scaled down to fit inside a 640x640 frame with black bars if needed. Smaller frames will be scaled up with black bars if needed.
## Training Custom Models
It is recommended that you select a resolution that results in the smaller dimension being just greater than, or equal to, 640. Anything above this will not see any increased performance.
:::{warning}
Power users only. This requires some setup, such as obtaining your own dataset and installing various tools. It's additionally advised to have a general knowledge of ML before attempting to train your own model. Additionally, this is not officially supported by Photonvision, and any problems that may arise are not attributable to Photonvision.
:::
## Custom Models
Before beginning, it is necessary to install the [rknn-toolkit2](https://github.com/airockchip/rknn-toolkit2). Then, install the relevant [Ultralytics repository](https://github.com/airockchip?tab=repositories&q=yolo&type=&language=&sort=) from this list. After training your model, export it to `rknn`. This will give you an `onnx` file, formatted for conversion. Copy this file to the relevant folder in [rknn_model_zoo](https://github.com/airockchip/rknn_model_zoo), and use the conversion script located there to convert it. If necessary, modify the script to provide the path to your training database for quantization.
For information regarding converting custom models and supported models for each platform, refer to the page detailing information about your specific coprocessor.
## Uploading Custom Models
- {ref}`Orange Pi 5 <docs/objectDetection/opi:Orange Pi 5 (and variants) Object Detection>`
- {ref}`Rubik Pi 3 <docs/objectDetection/rubik:Rubik Pi 3 Object Detection>`
:::{warning}
PhotonVision currently ONLY supports 640x640 Ultralytics YOLOv5, YOLOv8, and YOLOv11 models trained and converted to `.rknn` format for RK3588 CPUs! Other models require different post-processing code and will NOT work. The model conversion process is also highly particular. Proceed with care.
:::
### Training Custom Models
:::{warning}
Non-quantized models are not supported! If you have the option, make sure quantization is enabled when exporting to .rknn format. This will represent the weights and activations of the model as 8-bit integers, instead of 32-bit floats which PhotonVision doesn't support. Quantized models are also much faster.
:::
PhotonVision does not offer any support for training custom models, only conversion. For information on which models are supported for a given coprocessor, use the links above.
In the settings, under `Device Control`, there's an option to upload a new object detection model. Naming convention
should be `name-verticalResolution-horizontalResolution-yolovXXX`. The
`name` should only include alphanumeric characters, periods, and underscores. Additionally, the labels
file ought to have the same name as the RKNN file, with `-labels` appended to the end. For
example, if the RKNN file is named `Algae_1.03.2025-640-640-yolov5s.rknn`, the labels file should be
named `Algae_1.03.2025-640-640-yolov5s-labels.txt`.
### Managing Custom Models
:::{note}
Currently there is no way to delete custom models in the GUI, though this is a planned feature. To do this, you have to SSH into the coprocessor and delete the files manually from `/opt/photonvision/photonvision_config/models`.
:::
Custom models can now be managed from the Object Detection tab in settings. You can upload a custom model by clicking the "Upload Model" button, selecting your model file, and filling out the property fields. Models can also be exported, both individually and in bulk. Models exported in bulk can be imported using the `import bulk` button. Models exported individually must be re-imported as an individual model, and all the relevant metadata is stored in the filename of the model.

View File

@@ -1,8 +1,8 @@
# Object Detection
```{toctree}
:maxdepth: 0
:titlesonly: true
about-object-detection
opi
rubik
```

View File

@@ -0,0 +1,19 @@
# Orange Pi 5 (and variants) Object Detection
## How it works
PhotonVision runs object detection on the Orange Pi 5 by use of the RKNN model architecture, and [this JNI code](https://github.com/PhotonVision/rknn_jni).
## Supported models
PhotonVision currently ONLY supports 640x640 Ultralytics YOLOv5, YOLOv8, and YOLOv11 models trained and converted to `.rknn` format for RK3588 SOCs! Other models require different post-processing code and will NOT work.
## Converting Custom Models
:::{warning}
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.
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

@@ -0,0 +1,25 @@
# Rubik Pi 3 Object Detection
## How it works
PhotonVision runs object detection on the Rubik Pi 3 by use of [TensorflowLite](https://github.com/tensorflow/tensorflow), and [this JNI code](https://github.com/PhotonVision/rubik_jni).
## Supported models
PhotonVision currently ONLY supports 640x640 Ultralytics YOLOv8 and YOLOv11 models trained and converted to `.tflite` format for QCS6490 SOCs! Other models require different post-processing code and will NOT work.
## Converting Custom Models
:::{warning}
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.
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.
## Benchmarking
Before you can perform benchmarking, it's necessary to install `tensorflow-lite-qcom-apps` with apt.
By SSHing into your Rubik Pi and running this command, replacing `PATH/TO/MODEL` with the path to your model, `benchmark_model --graph=src/test/resources/yolov8nCoco.tflite --external_delegate_path=/usr/lib/libQnnTFLiteDelegate.so --external_delegate_options=backend_type:htp --external_delegate_options=htp_use_conv_hmx:1 --external_delegate_options=htp_performance_mode:2` you can determine how long it takes for inference to be performed with your model.

View File

@@ -4,17 +4,17 @@ You can control the vision LEDs of supported hardware via PhotonLib using the `s
```{eval-rst}
.. tab-set-code::
.. code-block:: Java
.. code-block:: java
// Blink the LEDs.
camera.setLED(VisionLEDMode.kBlink);
.. code-block:: C++
.. code-block:: c++
// Blink the LEDs.
camera.SetLED(photonlib::VisionLEDMode::kBlink);
.. code-block:: Python
.. code-block:: python
# Coming Soon!
```

View File

@@ -9,17 +9,17 @@ You can use the `setDriverMode()`/`SetDriverMode()` (Java and C++ respectively)
```{eval-rst}
.. tab-set-code::
.. code-block:: Java
.. code-block:: java
// Set driver mode to on.
camera.setDriverMode(true);
.. code-block:: C++
.. code-block:: c++
// Set driver mode to on.
camera.SetDriverMode(true);
.. code-block:: Python
.. code-block:: python
# Coming Soon!
```
@@ -31,17 +31,17 @@ You can use the `setPipelineIndex()`/`SetPipelineIndex()` (Java and C++ respecti
```{eval-rst}
.. tab-set-code::
.. code-block:: Java
.. code-block:: java
// Change pipeline to 2
camera.setPipelineIndex(2);
.. code-block:: C++
.. code-block:: c++
// Change pipeline to 2
camera.SetPipelineIndex(2);
.. code-block:: Python
.. code-block:: python
# Coming Soon!
```
@@ -52,17 +52,17 @@ You can also get the pipeline latency from a pipeline result using the `getLaten
```{eval-rst}
.. tab-set-code::
.. code-block:: Java
.. code-block:: java
// Get the pipeline latency.
double latencySeconds = result.getLatencyMillis() / 1000.0;
.. code-block:: C++
.. code-block:: c++
// Get the pipeline latency.
units::second_t latency = result.GetLatency();
.. code-block:: Python
.. code-block:: python
# Coming Soon!
```

View File

@@ -20,7 +20,7 @@ The `PhotonCamera` class has two constructors: one that takes a `NetworkTable` a
:language: c++
:lines: 42-43
.. code-block:: Python
.. code-block:: python
# Change this to match the name of your camera as shown in the web ui
self.camera = PhotonCamera("your_camera_name_here")
@@ -51,7 +51,7 @@ Use the `getLatestResult()`/`GetLatestResult()` (Java and C++ respectively) to o
:language: c++
:lines: 35-36
.. code-block:: Python
.. code-block:: python
# Query the latest result from PhotonVision
result = self.camera.getLatestResult()
@@ -69,17 +69,17 @@ Each pipeline result has a `hasTargets()`/`HasTargets()` (Java and C++ respectiv
```{eval-rst}
.. tab-set-code::
.. code-block:: Java
.. code-block:: java
// Check if the latest result has any targets.
boolean hasTargets = result.hasTargets();
.. code-block:: C++
.. code-block:: c++
// Check if the latest result has any targets.
bool hasTargets = result.HasTargets();
.. code-block:: Python
.. code-block:: python
# Check if the latest result has any targets.
hasTargets = result.hasTargets()
@@ -99,17 +99,17 @@ You can get a list of tracked targets using the `getTargets()`/`GetTargets()` (J
```{eval-rst}
.. tab-set-code::
.. code-block:: Java
.. code-block:: java
// Get a list of currently tracked targets.
List<PhotonTrackedTarget> targets = result.getTargets();
.. code-block:: C++
.. code-block:: c++
// Get a list of currently tracked targets.
wpi::ArrayRef<photonlib::PhotonTrackedTarget> targets = result.GetTargets();
.. code-block:: Python
.. code-block:: python
# Get a list of currently tracked targets.
targets = result.getTargets()
@@ -121,18 +121,18 @@ You can get the {ref}`best target <docs/reflectiveAndShape/contour-filtering:Con
```{eval-rst}
.. tab-set-code::
.. code-block:: Java
.. code-block:: java
// Get the current best target.
PhotonTrackedTarget target = result.getBestTarget();
.. code-block:: C++
.. code-block:: c++
// Get the current best target.
photonlib::PhotonTrackedTarget target = result.GetBestTarget();
.. code-block:: Python
.. code-block:: python
# Coming Soon!
@@ -149,7 +149,7 @@ You can get the {ref}`best target <docs/reflectiveAndShape/contour-filtering:Con
```{eval-rst}
.. tab-set-code::
.. code-block:: Java
.. code-block:: java
// Get information from target.
double yaw = target.getYaw();
@@ -159,7 +159,7 @@ You can get the {ref}`best target <docs/reflectiveAndShape/contour-filtering:Con
Transform2d pose = target.getCameraToTarget();
List<TargetCorner> corners = target.getCorners();
.. code-block:: C++
.. code-block:: c++
// Get information from target.
double yaw = target.GetYaw();
@@ -169,7 +169,7 @@ You can get the {ref}`best target <docs/reflectiveAndShape/contour-filtering:Con
frc::Transform2d pose = target.GetCameraToTarget();
wpi::SmallVector<std::pair<double, double>, 4> corners = target.GetCorners();
.. code-block:: Python
.. code-block:: python
# Get information from target.
yaw = target.getYaw()
@@ -193,7 +193,7 @@ All of the data above (**except skew**) is available when using AprilTags.
```{eval-rst}
.. tab-set-code::
.. code-block:: Java
.. code-block:: java
// Get information from target.
int targetID = target.getFiducialId();
@@ -201,7 +201,7 @@ All of the data above (**except skew**) is available when using AprilTags.
Transform3d bestCameraToTarget = target.getBestCameraToTarget();
Transform3d alternateCameraToTarget = target.getAlternateCameraToTarget();
.. code-block:: C++
.. code-block:: c++
// Get information from target.
int targetID = target.GetFiducialId();
@@ -209,7 +209,7 @@ All of the data above (**except skew**) is available when using AprilTags.
frc::Transform3d bestCameraToTarget = target.getBestCameraToTarget();
frc::Transform3d alternateCameraToTarget = target.getAlternateCameraToTarget();
.. code-block:: Python
.. code-block:: python
# Get information from target.
targetID = target.getFiducialId()
@@ -227,7 +227,7 @@ Images are stored within the PhotonVision configuration directory. Running the "
```{eval-rst}
.. tab-set-code::
.. code-block:: Java
.. code-block:: java
// Capture pre-process camera stream image
camera.takeInputSnapshot();
@@ -235,7 +235,7 @@ Images are stored within the PhotonVision configuration directory. Running the "
// Capture post-process camera stream image
camera.takeOutputSnapshot();
.. code-block:: C++
.. code-block:: c++
// Capture pre-process camera stream image
camera.TakeInputSnapshot();
@@ -243,7 +243,7 @@ Images are stored within the PhotonVision configuration directory. Running the "
// Capture post-process camera stream image
camera.TakeOutputSnapshot();
.. code-block:: Python
.. code-block:: python
# Capture pre-process camera stream image
camera.takeInputSnapshot()

View File

@@ -8,17 +8,17 @@ A `PhotonUtils` class with helpful common calculations is included within `Photo
```{eval-rst}
.. tab-set-code::
.. code-block:: Java
.. code-block:: java
// Calculate robot's field relative pose
if (aprilTagFieldLayout.getTagPose(target.getFiducialId()).isPresent()) {
Pose3d robotPose = PhotonUtils.estimateFieldToRobotAprilTag(target.getBestCameraToTarget(), aprilTagFieldLayout.getTagPose(target.getFiducialId()).get(), cameraToRobot);
}
.. code-block:: C++
.. code-block:: c++
//TODO
.. code-block:: Python
.. code-block:: python
# Coming Soon!
```
@@ -29,19 +29,19 @@ You can get your robot's `Pose2D` on the field using various camera data, target
```{eval-rst}
.. tab-set-code::
.. code-block:: Java
.. code-block:: java
// Calculate robot's field relative pose
Pose2D robotPose = PhotonUtils.estimateFieldToRobot(
kCameraHeight, kTargetHeight, kCameraPitch, kTargetPitch, Rotation2d.fromDegrees(-target.getYaw()), gyro.getRotation2d(), targetPose, cameraToRobot);
.. code-block:: C++
.. code-block:: c++
// Calculate robot's field relative pose
frc::Pose2D robotPose = photonlib::EstimateFieldToRobot(
kCameraHeight, kTargetHeight, kCameraPitch, kTargetPitch, frc::Rotation2d(units::degree_t(-target.GetYaw())), frc::Rotation2d(units::degree_t(gyro.GetRotation2d)), targetPose, cameraToRobot);
.. code-block:: Python
.. code-block:: python
# Coming Soon!
@@ -54,15 +54,15 @@ If your camera is at a fixed height on your robot and the height of the target i
```{eval-rst}
.. tab-set-code::
.. code-block:: Java
.. code-block:: java
// TODO
.. code-block:: C++
.. code-block:: c++
// TODO
.. code-block:: Python
.. code-block:: python
# Coming Soon!
@@ -78,15 +78,15 @@ The C++ version of PhotonLib uses the Units library. For more information, see [
```{eval-rst}
.. tab-set-code::
.. code-block:: Java
.. code-block:: java
double distanceToTarget = PhotonUtils.getDistanceToPose(robotPose, targetPose);
.. code-block:: C++
.. code-block:: c++
//TODO
.. code-block:: Python
.. code-block:: python
# Coming Soon!
```
@@ -97,19 +97,19 @@ You can get a [translation](https://docs.wpilib.org/en/latest/docs/software/adva
```{eval-rst}
.. tab-set-code::
.. code-block:: Java
.. code-block:: java
// Calculate a translation from the camera to the target.
Translation2d translation = PhotonUtils.estimateCameraToTargetTranslation(
distanceMeters, Rotation2d.fromDegrees(-target.getYaw()));
.. code-block:: C++
.. code-block:: c++
// Calculate a translation from the camera to the target.
frc::Translation2d translation = photonlib::PhotonUtils::EstimateCameraToTargetTranslation(
distance, frc::Rotation2d(units::degree_t(-target.GetYaw())));
.. code-block:: Python
.. code-block:: python
# Coming Soon!
@@ -125,14 +125,14 @@ We are negating the yaw from the camera from CV (computer vision) conventions to
```{eval-rst}
.. tab-set-code::
.. code-block:: Java
.. code-block:: java
Rotation2d targetYaw = PhotonUtils.getYawToPose(robotPose, targetPose);
.. code-block:: C++
.. code-block:: c++
//TODO
.. code-block:: Python
.. code-block:: python
# Coming Soon!
```

View File

@@ -75,15 +75,15 @@ If you would like to access your Ethernet-connected vision device from a compute
```{eval-rst}
.. tab-set-code::
.. code-block:: Java
.. code-block:: java
PortForwarder.add(5800, "photonvision.local", 5800);
.. code-block:: C++
.. code-block:: c++
wpi::PortForwarder::GetInstance().Add(5800, "photonvision.local", 5800);
.. code-block:: Python
.. code-block:: python
# Coming Soon!
```
@@ -99,3 +99,7 @@ The camera streams start at 1181 with two ports for each camera (ex. 1181 and 11
:::{warning}
If your camera stream isn't sent to the same port as it's originally found on, its stream will not be visible in the UI.
:::
## SSH Access
For advanced users, SSH access is available for coprocessors running PhotonVision. This allows you to perform advanced configurations and troubleshooting. The default credentials are: `photon:vision` for all devices using an image of `v2026.0.3` or later. The legacy credentials of `pi:raspberry` will still work, but it's recommended to switch to the new credentials as the old ones will be deprecated in a future release.

View File

@@ -1,22 +1,30 @@
# Quick Install
# Quick Installation Guide
## Install the latest image of photonvision for your coprocessor
- For the following supported coprocessors
- {ref}`Raspberry Pi 3,4,5 <docs/quick-start/quick-install:Raspberry Pi and Orange Pi Installation>`
- {ref}`Orange Pi 5, 5B, 5 Pro <docs/quick-start/quick-install:Raspberry Pi and Orange Pi Installation>`
- {ref}`Limelight 2, 2+, 3, 3G, 4 <docs/quick-start/quick-install:LimeLight Installation>`
- {ref}`Rubik Pi 3 <docs/quick-start/quick-install:Rubik Pi 3 Installation>`
- For the supported coprocessors
- RPI 3,4,5
- Orange Pi 5
- Limelight
For installing on non-supported devices {ref}`see. <docs/advanced-installation/sw_install/index:Software Installation>`
For installing on non-supported devices {ref}`see here. <docs/advanced-installation/sw_install/index:Software Installation>`
[Download the latest preconfigured image of photonvision for your coprocessor](https://github.com/PhotonVision/photonvision/releases/latest)
| Coprocessor | Image filename | Jar |
| -------------------- | ---------------------------------------------------- | ------------------------------------- |
| OrangePi 5 | photonvision-{version}-linuxarm64_orangepi5.img.xz | photonvision-{version}-linuxarm64.jar |
| Raspberry Pi 3, 4, 5 | photonvision-{version}-linuxarm64_RaspberryPi.img.xz | photonvision-{version}-linuxarm64.jar |
| Limelight 2 | photonvision-{version}-linuxarm64_limelight2.img.xz | photonvision-{version}-linuxarm64.jar |
| Limelight 3 | photonvision-{version}-linuxarm64_limelight3.img.xz | photonvision-{version}-linuxarm64.jar |
| Coprocessor | Image filename | Jar |
| -------------------- | -------------------------------------------------------- | ------------------------------------- |
| Raspberry Pi 3, 4, 5 | photonvision-{version}-linuxarm64_RaspberryPi.img.xz | photonvision-{version}-linuxarm64.jar |
| OrangePi 5 | photonvision-{version}-linuxarm64_orangepi5.img.xz | photonvision-{version}-linuxarm64.jar |
| OrangePi 5B | photonvision-{version}-linuxarm64_orangepi5b.img.xz | photonvision-{version}-linuxarm64.jar |
| OrangePi 5 Pro | photonvision-{version}-linuxarm64_orangepi5pro.img.xz | photonvision-{version}-linuxarm64.jar |
| Limelight 2 | photonvision-{version}-linuxarm64_limelight2.img.xz | photonvision-{version}-linuxarm64.jar |
| Limelight 3 | photonvision-{version}-linuxarm64_limelight3.img.xz | photonvision-{version}-linuxarm64.jar |
| Limelight 3G | photonvision-{version}-linuxarm64_limelight3G.img.xz | photonvision-{version}-linuxarm64.jar |
| Limelight 4 | photonvision-{version}-linuxarm64_limelight4.img.xz | photonvision-{version}-linuxarm64.jar |
| Rubik Pi 3 | photonvision-{version}-linuxarm64_rubikpi3.tar.xz | photonvision-{version}-linuxarm64.jar |
Unless otherwise noted in release notes or if updating from the prior years version, to update PhotonVision after the initial installation, use the offline update option in the settings page with the downloaded jar file from the latest release.
## Raspberry Pi and Orange Pi Installation
Use the [Raspberry Pi Imager](https://www.raspberrypi.com/software/) to flash the image onto the coprocessors microSD card. Select the downloaded `.img.xz` file, select your microSD card, and flash.
@@ -24,10 +32,25 @@ Use the [Raspberry Pi Imager](https://www.raspberrypi.com/software/) to flash th
Balena Etcher can also be used, but historically has had issues such as bootlooping (the system will repeatedly boot and restart) when imaging your device. Use at your own risk.
:::
Limelights have a different installation processes. Simply connect the limelight to your computer using the proper usb cable. Select the compute module. If it doesnt show up after 30s try using another USB port, initialization may take a while. If prompted, install the recommended missing drivers. Select the image, and flash.
## Limelight Installation
Unless otherwise noted in release notes or if updating from the prior years version, to update PhotonVision after the initial installation, use the offline update option in the settings page with the downloaded jar file from the latest release.
In order to flash your Limelight you should follow the instructions on the Limelight documentation for the relevant version. Make sure to replace the Limelight OS image with the relevant PhotonVision image.
| Limelight Version | Limelight Documentation | PhotonVision Image | |
| ----------------- | ------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------- | --- |
| 2 | [Updating Limelight 2 OS](https://docs.limelightvision.io/docs/docs-limelight/getting-started/limelight-2#4-updating-limelightos) | photonvision-{version}-linuxarm64_limelight2.img.xz | |
| 3 | [Updating Limelight 3 OS](https://docs.limelightvision.io/docs/docs-limelight/getting-started/limelight-3#4-updating-limelightos) | photonvision-{version}-linuxarm64_limelight3.img.xz | |
| 3G | [Updating Limelight 3G OS](https://docs.limelightvision.io/docs/docs-limelight/getting-started/limelight-3g#4-updating-limelightos) | photonvision-{version}-linuxarm64_limelight3g.img.xz | |
| 4 | [Updating Limelight 4 OS](https://docs.limelightvision.io/docs/docs-limelight/getting-started/limelight-4#4-updating-limelightos) | photonvision-{version}-linuxarm64_limelight4.img.xz | |
:::{note}
Limelight 2, 2+, and 3 will need a [custom hardware config file](https://github.com/PhotonVision/photonvision/tree/main/docs/source/docs/advanced-installation/sw_install/files) for lighting to work. Currently only limelight 2 and 2+ files are available.
Limelight models will need a [custom hardware config file](https://github.com/PhotonVision/photonvision/tree/main/docs/source/docs/advanced-installation/sw_install/files) for LEDs or other hardware features to work.
:::
## Rubik Pi 3 Installation
:::{warning}
The Qualcomm Launcher caches files. If you flash multiple times, you may need to clear the cache by navigating to your temp directory, and deleting the `qualcomm-launcher` folder.
:::
To flash the Rubik Pi 3 coprocessor, it's necessary to use the [Qualcomm Launcher](https://softwarecenter.qualcomm.com/catalog/item/Qualcomm_Launcher). Upload a custom image by selecting the *Custom* option in the launcher. Choose the downloaded PhotonVision `.tar.xz` file and follow the prompts to complete the installation. It is recommended to skip the *Configure Login* process, as PhotonVision will handle the necessary settings.

View File

@@ -54,7 +54,7 @@ A `VisionSystemSim` represents the simulated world for one or more cameras, and
```{eval-rst}
.. tab-set-code::
.. code-block:: Java
.. code-block:: java
// A vision system sim labelled as "main" in NetworkTables
VisionSystemSim visionSim = new VisionSystemSim("main");
@@ -67,7 +67,7 @@ Vision targets require a `TargetModel`, which describes the shape of the target.
```{eval-rst}
.. tab-set-code::
.. code-block:: Java
.. code-block:: java
// A 0.5 x 0.25 meter rectangular target
TargetModel targetModel = new TargetModel(0.5, 0.25);
@@ -78,7 +78,7 @@ These `TargetModel` are paired with a target pose to create a `VisionTargetSim`.
```{eval-rst}
.. tab-set-code::
.. code-block:: Java
.. code-block:: java
// The pose of where the target is on the field.
// Its rotation determines where "forward" or the target x-axis points.
@@ -100,7 +100,7 @@ For convenience, an `AprilTagFieldLayout` can also be added to automatically cre
```{eval-rst}
.. tab-set-code::
.. code-block:: Java
.. code-block:: java
// The layout of AprilTags which we want to add to the vision system
AprilTagFieldLayout tagLayout = AprilTagFieldLayout.loadFromResource(AprilTagFields.kDefaultField.m_resourceFile);
@@ -121,7 +121,7 @@ Before adding a simulated camera, we need to define its properties. This is done
```{eval-rst}
.. tab-set-code::
.. code-block:: Java
.. code-block:: java
// The simulated camera properties
SimCameraProperties cameraProp = new SimCameraProperties();
@@ -132,7 +132,7 @@ By default, this will create a 960 x 720 resolution camera with a 90 degree diag
```{eval-rst}
.. tab-set-code::
.. code-block:: Java
.. code-block:: java
// A 640 x 480 camera with a 100 degree diagonal FOV.
cameraProp.setCalibration(640, 480, Rotation2d.fromDegrees(100));
@@ -150,7 +150,7 @@ These properties are used in a `PhotonCameraSim`, which handles generating captu
```{eval-rst}
.. tab-set-code::
.. code-block:: Java
.. code-block:: java
// The PhotonCamera used in the real robot code.
PhotonCamera camera = new PhotonCamera("cameraName");
@@ -164,7 +164,7 @@ The `PhotonCameraSim` can now be added to the `VisionSystemSim`. We have to defi
```{eval-rst}
.. tab-set-code::
.. code-block:: Java
.. code-block:: java
// Our camera is mounted 0.1 meters forward and 0.5 meters up from the robot pose,
// (Robot pose is considered the center of rotation at the floor level, or Z = 0)
@@ -186,7 +186,7 @@ If the camera is mounted on a mobile mechanism (like a turret) this transform ca
```{eval-rst}
.. tab-set-code::
.. code-block:: Java
.. code-block:: java
// The turret the camera is mounted on is rotated 5 degrees
Rotation3d turretRotation = new Rotation3d(0, 0, Math.toRadians(5));
@@ -203,7 +203,7 @@ To update the `VisionSystemSim`, we simply have to pass in the simulated robot p
```{eval-rst}
.. tab-set-code::
.. code-block:: Java
.. code-block:: java
// Update with the simulated drivetrain pose. This should be called every loop in simulation.
visionSim.update(robotPoseMeters);
@@ -218,7 +218,7 @@ Each `VisionSystemSim` has its own built-in `Field2d` for displaying object pose
```{eval-rst}
.. tab-set-code::
.. code-block:: Java
.. code-block:: java
// Get the built-in Field2d used by this VisionSystemSim
visionSim.getDebugField();
@@ -233,7 +233,7 @@ A `PhotonCameraSim` can also draw and publish generated camera frames to a MJPEG
```{eval-rst}
.. tab-set-code::
.. code-block:: Java
.. code-block:: java
// Enable the raw and processed streams. These are enabled by default.
cameraSim.enableRawStream(true);

View File

@@ -7,4 +7,5 @@ common-errors
logging
camera-troubleshooting
networking-troubleshooting
unix-commands
```

View File

@@ -0,0 +1,134 @@
# Useful Unix Commands
## Networking
### SSH
[SSH (Secure Shell)](https://www.mankier.com/1/ssh) is used to securely connect from a local to a remote system (ex. from a laptop to a coprocessor). Unlike other commands on this page, ssh is not Unix specific and can be done on Windows and MacOS from their respective terminals.
:::{note}
You may see a warning similar to `The authenticity of host 'xxx' can't be established...` or `WARNING: REMOTE HOST IDENTIFICATION HAS CHANGED!`, in most cases this can be safely ignored if you have confirmed that you are connecting to the correct host over a secure connection, and the fingerprint will change when your operating system is reinstalled or PhotonVision's coprocessor image is re-flashed. This can also occur if you have multiple coprocessors with the same hostname on your network. You can read more about it [here](https://superuser.com/questions/421997/what-is-a-ssh-key-fingerprint-and-how-is-it-generated)
:::
Example:
```
ssh pi@hostname
```
For PhotonVision, the username will be `pi` and the password will be `raspberry`.
### ip
Run [ip address](https://www.mankier.com/8/ip) with your coprocessor connected to a monitor in order to see its IP address and other network configuration information.
Your output might look something like this:
```
2: end1: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc mq state UP group default qlen 1000
link/ether de:9a:8f:7d:31:aa brd ff:ff:ff:ff:ff:ff
inet 10.88.47.12/24 brd 10.88.47.255 scope global dynamic noprefixroute end1
valid_lft 27367sec preferred_lft 27367sec
```
In this example, the numbers following `inet` (10.88.47.12) are your IP address.
### ping
[ping](https://www.mankier.com/8/ping) is a command-line utility used to test the reachability of a host on an IP network. It also measures the round-trip time for messages sent from the originating host to a destination computer. It can be used to determine if a network interface is available, which can be helpful when debugging.
## File Transfer
All files under `/opt/photonvision` are owned by the root user. This means that if you want to modify them, the commands to do so must be ran as sudo.
### SCP
[SCP (Secure Copy)](https://www.mankier.com/1/scp) is used to securely transfer files between local and remote systems.
Example:
```
scp [file] pi@hostname:/path/to/destination
```
### SFTP
[SFTP (SSH File Transfer Protocol)](https://www.mankier.com/1/sftp#) is another option for transferring files between local and remote systems.
### Filezilla
[Filezilla](https://filezilla-project.org/) is a GUI alternative to SCP and SFTP. It is available for Windows, MacOS, and Linux.
## Miscellaneous
### v4l2-ctl
[v4l2-ctl](https://www.mankier.com/1/v4l2-ctl) is a command-line tool for controlling video devices.
List available video devices (used to verify the device recognized a connected camera):
```
v4l2-ctl --list-devices
```
List supported formats and resolutions for a specific video device:
```
v4l2-ctl --list-formats-ext --device /path/to/video_device
```
List all video device's controls and their values:
```
v4l2-ctl --list-ctrls --device path/to/video_device
```
:::{note}
This command is especially useful in helping to debug when certain camera controls, like exposure, aren't behaving as expected. If you see an error in the logs similar to `WARNING 30: failed to set property [property name] (UsbCameraImpl.cpp:646)`, that means that PhotonVision is trying to use a control that doesn't exist or has a different name on your hardware. If you encounter this issue, please [file an issue](https://github.com/PhotonVision/photonvision/issues) with the necessary logs and output of the `v4l2-ctl --list-ctrls` command.
:::
### systemctl
[systemctl](https://www.mankier.com/1/systemctl) is a command that controls the `systemd` system and service manager.
Start PhotonVision:
```
systemctl start photonvision
```
Stop PhotonVision:
```
systemctl stop photonvision
```
Restart PhotonVision:
```
systemctl restart photonvision
```
Check the status of PhotonVision:
```
systemctl status photonvision
```
### journalctl
[journalctl](https://www.mankier.com/1/journalctl) is a command that queries the systemd journal, which is a logging system used by many Linux distributions.
View the PhotonVision logs:
```
journalctl --output cat -u photonvision
```
View the PhotonVision logs in real-time:
```
journalctl --output cat -u photonvision -f
```
`--output cat` is used to prevent journalctl from printing its own timestamps, because we log our own timestamps.

View File

@@ -127,6 +127,7 @@ docs/troubleshooting/index
docs/additional-resources/best-practices
docs/additional-resources/config
docs/additional-resources/nt-api
docs/benchmarks/index
docs/contributing/index
```

Binary file not shown.

View File

@@ -1,6 +1,6 @@
distributionBase=GRADLE_USER_HOME
distributionPath=permwrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.11-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.3-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME

9
gradlew vendored
View File

@@ -86,8 +86,7 @@ done
# shellcheck disable=SC2034
APP_BASE_NAME=${0##*/}
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s
' "$PWD" ) || exit
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD=maximum
@@ -115,7 +114,7 @@ case "$( uname )" in #(
NONSTOP* ) nonstop=true ;;
esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
CLASSPATH="\\\"\\\""
# Determine the Java command to use to start the JVM.
@@ -206,7 +205,7 @@ fi
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Collect all arguments for the java command:
# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
# and any embedded shellness will be escaped.
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
# treated as '${Hostname}' itself on the command line.
@@ -214,7 +213,7 @@ DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
set -- \
"-Dorg.gradle.appname=$APP_BASE_NAME" \
-classpath "$CLASSPATH" \
org.gradle.wrapper.GradleWrapperMain \
-jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \
"$@"
# Stop when "xargs" is not available.

4
gradlew.bat vendored
View File

@@ -70,11 +70,11 @@ goto fail
:execute
@rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
set CLASSPATH=
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %*
:end
@rem End local scope for the variables with windows NT shell

View File

@@ -1,21 +0,0 @@
{
"root": true,
"extends": [
"plugin:vue/vue3-recommended",
"eslint:recommended",
"@vue/eslint-config-typescript",
"@vue/eslint-config-prettier/skip-formatting"
],
"rules": {
"quotes": ["error", "double"],
"comma-dangle": ["error", "never"],
"comma-spacing": ["error", { "before": false, "after": true }],
"semi": ["error", "always"],
"eol-last": "error",
"object-curly-spacing": ["error", "always"],
"quote-props": ["error", "as-needed"],
"no-case-declarations": "off",
"vue/require-default-prop": "off",
"vue/v-on-event-hyphenation": "off"
}
}

View File

@@ -1 +1,2 @@
src/assets/fonts/PromptRegular.ts
pnpm-lock.yaml

View File

@@ -0,0 +1,38 @@
import pluginVue from "eslint-plugin-vue";
import { defineConfigWithVueTs, vueTsConfigs } from "@vue/eslint-config-typescript";
import skipFormattingConfig from "@vue/eslint-config-prettier/skip-formatting";
export default defineConfigWithVueTs(
pluginVue.configs["flat/recommended"],
vueTsConfigs.recommended,
skipFormattingConfig,
{
ignores: ["**/dist/**"]
},
{
//extends: ["js/recommended"],
rules: {
quotes: ["error", "double"],
"comma-dangle": ["error", "never"],
"comma-spacing": [
"error",
{
before: false,
after: true
}
],
semi: ["error", "always"],
"eol-last": "error",
"object-curly-spacing": ["error", "always"],
"quote-props": ["error", "as-needed"],
"no-case-declarations": "off",
"vue/require-default-prop": "off",
"vue/v-on-event-hyphenation": "off",
"@typescript-eslint/no-explicit-any": "off",
"vue/valid-v-slot": ["error", { allowModifiers: true }]
}
}
);

File diff suppressed because it is too large Load Diff

View File

@@ -5,48 +5,41 @@
"type": "module",
"scripts": {
"dev": "vite",
"build": "run-p build-only",
"preview": "vite preview --port 4173",
"build-only": "vite build",
"build": "vite build",
"build-demo": "vite build --mode demo",
"lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix --ignore-path .gitignore",
"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 --ignore-path .gitignore",
"lint-ci": "eslint . --max-warnings 0 --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix",
"format-ci": "prettier --check src/"
},
"dependencies": {
"@fontsource/prompt": "^5.0.9",
"@fontsource/prompt": "^5.2.6",
"@mdi/font": "^7.4.47",
"@msgpack/msgpack": "^3.0.0-beta2",
"axios": "^1.6.3",
"jspdf": "^2.5.1",
"lodash": "^4.17.21",
"pinia": "^2.1.4",
"three": "^0.160.0",
"vue": "^2.7.14",
"vue-router": "^3.6.5",
"vue-virtual-scroll-list": "^2.3.5",
"vue2-helpers": "^2.1.1",
"vuetify": "^2.7.1"
"@msgpack/msgpack": "^3.1.2",
"axios": "^1.11.0",
"jspdf": "^3.0.1",
"pinia": "^3.0.2",
"three": "^0.178.0",
"vue": "^3.5.13",
"vue-router": "^4.5.1",
"vue3-virtual-scroll-list": "^0.2.1",
"vuetify": "^3.8.3"
},
"devDependencies": {
"@rushstack/eslint-patch": "^1.3.2",
"@types/node": "^18.19.45",
"@types/three": "^0.160.0",
"@vitejs/plugin-vue2": "^2.3.1",
"@vue/eslint-config-prettier": "^9.0.0",
"@vue/eslint-config-typescript": "^12.0.0",
"@vue/tsconfig": "^0.5.1",
"deepmerge": "^4.3.1",
"eslint": "^8.56.0",
"eslint-plugin-vue": "^9.19.2",
"npm-run-all": "^4.1.5",
"prettier": "3.2.2",
"sass": "~1.32",
"sass-loader": "^13.3.2",
"terser": "^5.14.2",
"typescript": "^5.3.3",
"unplugin-vue-components": "^0.26.0",
"vite": "^5.4.2"
"@eslint/js": "^9.31.0",
"@types/node": "^22.15.14",
"@types/three": "^0.178.0",
"@vitejs/plugin-vue": "^6.0.0",
"@vue/eslint-config-prettier": "^10.2.0",
"@vue/eslint-config-typescript": "^14.5.0",
"@vue/tsconfig": "^0.7.0",
"eslint": "^9.31.0",
"eslint-plugin-vue": "^10.3.0",
"prettier": "^3.6.2",
"sass": "^1.89.2",
"typescript": "^5.8.3",
"vite": "^7.0.5",
"vite-plugin-vuetify": "^2.1.1"
}
}

2853
photon-client/pnpm-lock.yaml generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -3,10 +3,12 @@ import { useStateStore } from "@/stores/StateStore";
import { useSettingsStore } from "@/stores/settings/GeneralSettingsStore";
import { useCameraSettingsStore } from "@/stores/settings/CameraSettingsStore";
import { AutoReconnectingWebsocket } from "@/lib/AutoReconnectingWebsocket";
import { inject } from "vue";
import { inject, onBeforeMount } from "vue";
import PhotonSidebar from "@/components/app/photon-sidebar.vue";
import PhotonLogView from "@/components/app/photon-log-view.vue";
import PhotonErrorSnackbar from "@/components/app/photon-error-snackbar.vue";
import { useTheme } from "vuetify";
import { restoreThemeConfig } from "@/lib/ThemeManager";
const is_demo = import.meta.env.MODE === "demo";
if (!is_demo) {
@@ -50,6 +52,11 @@ if (!is_demo) {
);
useStateStore().$patch({ websocket: websocket });
}
const theme = useTheme();
onBeforeMount(() => {
restoreThemeConfig(theme);
});
</script>
<template>
@@ -58,9 +65,9 @@ if (!is_demo) {
<v-main>
<v-container class="main-container" fluid fill-height>
<v-layout>
<v-flex>
<v-container class="align-start pa-0 ma-0" fluid>
<router-view />
</v-flex>
</v-container>
</v-layout>
</v-container>
</v-main>
@@ -71,9 +78,11 @@ if (!is_demo) {
</template>
<style lang="scss">
@import "vuetify/src/styles/settings/_variables";
@use "@/assets/styles/settings";
@use "@/assets/styles/variables";
@use "sass:map";
@media #{map-get($display-breakpoints, 'md-and-down')} {
@media #{map.get(settings.$display-breakpoints, 'md-and-down')} {
html {
font-size: 14px !important;
}
@@ -85,24 +94,27 @@ if (!is_demo) {
}
::-webkit-scrollbar-track {
background: #232c37;
background: rgb(var(--v-theme-background));
}
::-webkit-scrollbar-thumb {
background-color: #ffd843;
background-color: rgb(var(--v-theme-accent));
border-radius: 10px;
}
::-webkit-scrollbar-thumb:hover {
background-color: #e4c33c;
background-color: rgb(var(--v-theme-primary));
}
.main-container {
background-color: #232c37;
padding: 0 !important;
}
#title {
color: #ffd843;
.v-overlay__scrim {
background-color: #111111;
}
div.v-layout {
overflow: unset !important;
}
</style>

View File

@@ -1,4 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100" preserveAspectRatio="xMidYMid" width="200" height="200" style="shape-rendering: auto; display: block; background: rgb(0, 100, 146);" xmlns:xlink="http://www.w3.org/1999/xlink"><g><g transform="translate(80,50)">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100" preserveAspectRatio="xMidYMid" width="200" height="200" style="shape-rendering: auto; display: block; background: rgba(0, 100, 146, 0);" xmlns:xlink="http://www.w3.org/1999/xlink"><g><g transform="translate(80,50)">
<g transform="rotate(0)">
<circle fill-opacity="1" fill="#ffd943" r="6" cy="0" cx="0">
<animateTransform repeatCount="indefinite" dur="0.9345794392523364s" keyTimes="0;1" values="1.5 1.5;1 1" begin="-0.8177570093457943s" type="scale" attributeName="transform"></animateTransform>

Before

Width:  |  Height:  |  Size: 4.3 KiB

After

Width:  |  Height:  |  Size: 4.3 KiB

View File

@@ -0,0 +1,25 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg width="100%" height="100%" viewBox="0 0 508 507" version="1.1" xmlns="http://www.w3.org/2000/svg"
xml:space="preserve"
style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
<g transform="matrix(1,0,0,1,-1279,0)">
<g id="PhotonVision-Icon-BG" transform="matrix(0.264062,0,0,0.469444,1279.5,0)">
<rect x="0" y="0" width="1920" height="1080" style="fill:none;"/>
<clipPath id="_clip1">
<rect x="0" y="0" width="1920" height="1080"/>
</clipPath>
<g clip-path="url(#_clip1)">
<g transform="matrix(4.27015,0,0,2.40196,-20444.8,-3235.56)">
<circle cx="5012.55" cy="1571.77" r="224.918" style="fill:rgb(0,100,146,0);"/>
</g>
<g transform="matrix(4.95901,0,0,2.78944,-13955,-10313.5)">
<path d="M3055.09,3977.51C3050.3,3984.25 3045,3990.56 3039.21,3996.35C2987.91,4047.65 2917.1,4038.77 2881.16,3976.54C2845.23,3914.3 2857.71,3822.13 2909.01,3770.83C2960.31,3719.53 3031.13,3728.41 3067.06,3790.64C3069.85,3795.48 3072.35,3800.49 3074.56,3805.67L3039.78,3811.64C3012.82,3769.64 2962.9,3764.58 2926.45,3801.04C2888.89,3838.59 2879.76,3906.07 2906.07,3951.63C2932.37,3997.19 2984.22,4003.69 3021.77,3966.14L3021.89,3966.01L3055.09,3977.51ZM3085.02,3841.47C3090.86,3875.56 3086.6,3912.35 3073.22,3944.57L3043.91,3934.42C3056.74,3907.59 3060.53,3875.54 3054.13,3846.78L3085.02,3841.47Z" style="fill:white;"/>
</g>
<g transform="matrix(4.95901,0,0,2.78944,-13955,-3827.86)">
<path d="M2906.78,1571.77L3111.02,1642.48L3116.61,1626.34L3147.2,1664.74L3099.42,1675.99L3105,1659.86L2910.03,1592.35C2908.25,1585.69 2907.18,1578.77 2906.78,1571.77ZM2917.45,1517.07L3114.77,1483.17L3111.88,1466.34L3157.2,1485.21L3120.78,1518.13L3117.88,1501.3L2910.22,1536.97C2911.99,1530.09 2914.41,1523.4 2917.45,1517.07Z" style="fill:rgb(255,216,67);"/>
</g>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.2 KiB

View File

@@ -0,0 +1,4 @@
@forward "vuetify/settings" with (
$button-colored-disabled: false,
$button-disabled-opacity: 0.4
);

View File

@@ -1,29 +1,74 @@
@import "@fontsource/prompt";
@use "@fontsource/prompt";
$default-font: "Prompt", sans-serif !default;
$body-font-family: $default-font;
$heading-font-family: $default-font;
$body-background: #282c34;
$body-background: rgb(var(--v-theme-background));
body {
background: $body-background;
}
.v-application {
html {
font-family: $default-font !important;
}
.v-row-group__header {
background: #005281 !important;
}
.theme--dark.v-data-table
> .v-data-table__wrapper
.v-table
> .v-table__wrapper
> table
> tbody
> tr:hover:not(.v-data-table__expanded__content):not(.v-data-table__empty-wrapper) {
background: #005281 !important;
background: rgba(0, 0, 0, 0.2);
}
.v-card__title {
.v-card-title,
.v-dialog > .v-overlay__content > .v-card > .v-card-title,
.v-dialog > .v-overlay__content > form > .v-card > .v-card-title {
padding: 20px;
word-break: break-word !important;
}
.v-card-text,
.v-dialog > .v-overlay__content > .v-card > .v-card-text,
.v-dialog > .v-overlay__content > form > .v-card > .v-card-text {
font-size: 1rem;
padding: 20px;
}
.v-card-subtitle,
.v-dialog > .v-overlay__content > .v-card > .v-card-subtitle,
.v-dialog > .v-overlay__content > form > .v-card > .v-card-subtitle {
font-size: 1rem;
padding: 20px;
}
.v-field__input {
padding: 0px !important;
}
.pb-10px {
padding-bottom: 10px !important;
}
.pt-10px {
padding-top: 10px !important;
}
.pl-10px {
padding-left: 10px !important;
}
.pr-10px {
padding-right: 10px !important;
}
.pa-10px {
padding: 10px !important;
}
.rounded-12 {
border-radius: 12px;
}

View File

@@ -1,21 +1,24 @@
<script setup lang="ts">
import type { PhotonTarget } from "@/types/PhotonTrackingTypes";
// @ts-expect-error Intellisense says these conflict with the dynamic imports below
import type { Mesh, Object3D, PerspectiveCamera, Scene, WebGLRenderer } from "three";
// @ts-expect-error Intellisense says these conflict with the dynamic imports below
import type { TrackballControls } from "three/examples/jsm/controls/TrackballControls";
import { onBeforeUnmount, onMounted, watchEffect } from "vue";
import {
const {
ArrowHelper,
BoxGeometry,
Color,
ConeGeometry,
Mesh,
MeshNormalMaterial,
type Object3D,
PerspectiveCamera,
Quaternion,
Scene,
Vector3,
Scene,
WebGLRenderer
} from "three";
import { TrackballControls } from "three/examples/jsm/controls/TrackballControls";
} = await import("three");
const { TrackballControls } = await import("three/examples/jsm/controls/TrackballControls");
const props = defineProps<{
targets: PhotonTarget[];
@@ -114,7 +117,7 @@ const resetCamThirdPerson = () => {
}
};
onMounted(() => {
onMounted(async () => {
scene = new Scene();
camera = new PerspectiveCamera(75, 800 / 800, 0.1, 1000);

View File

@@ -2,10 +2,10 @@
import { computed, inject, ref, onBeforeUnmount } from "vue";
import { useStateStore } from "@/stores/StateStore";
import { useCameraSettingsStore } from "@/stores/settings/CameraSettingsStore";
import loadingImage from "@/assets/images/loading-transparent.svg";
import type { StyleValue } from "vue/types/jsx";
import type { StyleValue } from "vue";
import PvIcon from "@/components/common/pv-icon.vue";
import type { UiCameraConfiguration } from "@/types/SettingTypes";
import PvLoading from "@/components/common/pv-loading.vue";
const props = defineProps<{
streamType: "Raw" | "Processed";
@@ -92,7 +92,7 @@ onBeforeUnmount(() => {
<template>
<div class="stream-container" :style="containerStyle">
<img :src="loadingImage" class="stream-loading" />
<pv-loading class="stream-loading" />
<img
:id="id"
ref="mjpgStream"
@@ -105,18 +105,21 @@ onBeforeUnmount(() => {
/>
<div class="stream-overlay" :style="overlayStyle">
<pv-icon
color="primary"
icon-name="mdi-camera-image"
tooltip="Capture and save a frame of this stream"
class="ma-1 mr-2"
@click="handleCaptureClick"
/>
<pv-icon
color="primary"
icon-name="mdi-fullscreen"
tooltip="Open this stream in fullscreen"
class="ma-1 mr-2"
@click="handleFullscreenRequest"
/>
<pv-icon
color="primary"
icon-name="mdi-open-in-new"
tooltip="Open this stream in a new window"
class="ma-1 mr-2"

View File

@@ -5,7 +5,8 @@ import { useStateStore } from "@/stores/StateStore";
<template>
<v-snackbar
v-model="useStateStore().snackbarData.show"
top
location="top"
variant="elevated"
:color="useStateStore().snackbarData.color"
:timeout="useStateStore().snackbarData.timeout"
>

View File

@@ -7,13 +7,13 @@ const props = defineProps<{ source: LogMessage }>();
const logColorClass = computed<string>(() => {
switch (props.source.level) {
case LogLevel.ERROR:
return "red--text";
return "text-red";
case LogLevel.WARN:
return "yellow--text";
return "text-yellow";
case LogLevel.INFO:
return "light-blue--text";
return "text-light-blue";
case LogLevel.DEBUG:
return "white--text";
return "text-white";
}
return "";
});
@@ -22,3 +22,8 @@ const logColorClass = computed<string>(() => {
<template>
<div :class="logColorClass">[{{ source.timestamp.toTimeString().split(" ")[0] }}] {{ source.message }}</div>
</template>
<style scoped>
div {
white-space: pre-wrap;
}
</style>

View File

@@ -3,7 +3,7 @@ import { computed, inject, ref, watch } from "vue";
import { LogLevel, type LogMessage } from "@/types/SettingTypes";
import { useStateStore } from "@/stores/StateStore";
import LogEntry from "@/components/app/photon-log-entry.vue";
import VirtualList from "vue-virtual-scroll-list";
import VirtualList from "vue3-virtual-scroll-list";
const backendHost = inject<string>("backendHost");
@@ -74,15 +74,15 @@ document.addEventListener("keydown", (e) => {
<template>
<v-dialog v-model="useStateStore().showLogModal" width="1500" dark>
<v-card dark class="dialog-container pa-6" color="primary" flat>
<v-card class="dialog-container pa-5" color="surface" flat>
<!-- Logs header -->
<v-row class="no-gutters pb-3">
<v-row class="pb-3">
<v-col cols="4">
<v-card-title class="pa-0">Program Logs</v-card-title>
</v-col>
<v-col class="align-self-center pl-3" style="text-align: right">
<v-btn text color="white" @click="handleLogExport">
<v-icon left class="menu-icon"> mdi-download </v-icon>
<v-btn variant="text" color="white" @click="handleLogExport">
<v-icon start class="menu-icon" size="large"> mdi-download </v-icon>
<span class="menu-label">Download</span>
<!-- Special hidden link that gets 'clicked' when the user exports journalctl logs -->
@@ -94,12 +94,12 @@ document.addEventListener("keydown", (e) => {
target="_blank"
/>
</v-btn>
<v-btn text color="white" @click="handleLogClear">
<v-icon left class="menu-icon"> mdi-trash-can-outline </v-icon>
<v-btn variant="text" color="white" @click="handleLogClear">
<v-icon start class="menu-icon" size="large"> mdi-trash-can-outline </v-icon>
<span class="menu-label">Clear Client Logs</span>
</v-btn>
<v-btn text color="white" @click="() => (useStateStore().showLogModal = false)">
<v-icon left class="menu-icon"> mdi-close </v-icon>
<v-btn variant="text" color="white" @click="() => (useStateStore().showLogModal = false)">
<v-icon start class="menu-icon" size="large"> mdi-close </v-icon>
<span class="menu-label">Close</span>
</v-btn>
</v-col>
@@ -109,38 +109,33 @@ document.addEventListener("keydown", (e) => {
<div class="dialog-data">
<!-- Log view options -->
<v-row class="pt-4 pt-md-0 no-gutters">
<v-col cols="12" md="5" class="align-self-center">
<v-row no-gutters class="pt-4 pt-md-0" style="display: flex; justify-content: space-between">
<v-col cols="12" md="7" style="display: flex; align-items: center" class="pr-3">
<v-text-field
v-model="searchQuery"
dark
dense
density="compact"
clearable
hide-details="auto"
prepend-icon="mdi-magnify"
color="accent"
color="primary"
label="Search"
variant="underlined"
/>
</v-col>
<v-col cols="12" md="2" style="display: flex; align-items: center">
<input v-model="timeInput" type="time" step="1" class="white--text pl-0 pl-md-8" />
<v-btn icon class="ml-3" @click="timeInput = undefined">
<v-icon>mdi-close-circle-outline</v-icon>
<input v-model="timeInput" type="time" step="1" class="text-white pl-3" />
<v-btn icon variant="flat" @click="timeInput = undefined">
<v-icon>mdi-close</v-icon>
</v-btn>
</v-col>
<v-col cols="12" md="5" class="pr-3">
<v-row class="no-gutters">
<v-col v-for="level in [0, 1, 2, 3]" :key="level">
<v-row dense align="center">
<v-col cols="6" md="8" style="text-align: right">
{{ getLogLevelFromIndex(level) }}
</v-col>
<v-col cols="6" md="4">
<v-switch v-model="selectedLogLevels[level]" dark color="#ffd843" />
</v-col>
</v-row>
</v-col>
</v-row>
<v-col v-for="level in [0, 1, 2, 3]" :key="level" class="pr-3">
<div class="pb-0 pt-0" style="display: flex; align-items: center; flex: min-content">
{{ getLogLevelFromIndex(level)
}}<v-switch
v-model="selectedLogLevels[level]"
class="pl-2"
hide-details
color="rgb(var(--v-theme-primary))"
></v-switch>
</div>
</v-col>
</v-row>
@@ -178,9 +173,9 @@ document.addEventListener("keydown", (e) => {
.log-display {
/* Dialog data size - options */
height: calc(100% - 66px);
height: calc(100% - 56px);
padding: 10px;
background-color: #232c37 !important;
background-color: rgb(var(--v-theme-logsBackground)) !important;
border-radius: 5px;
}

View File

@@ -1,10 +1,11 @@
<script setup lang="ts">
import { computed, getCurrentInstance } from "vue";
import { computed } from "vue";
import { useSettingsStore } from "@/stores/settings/GeneralSettingsStore";
import { useStateStore } from "@/stores/StateStore";
import { useCameraSettingsStore } from "@/stores/settings/CameraSettingsStore";
import { PlaceholderCameraSettings } from "@/types/SettingTypes";
import { useRoute } from "vue2-helpers/vue-router";
import { useRoute } from "vue-router";
import { useDisplay, useTheme } from "vuetify";
import { toggleTheme } from "@/lib/ThemeManager";
const compact = computed<boolean>({
get: () => {
@@ -14,128 +15,139 @@ const compact = computed<boolean>({
useStateStore().setSidebarFolded(val);
}
});
const { mdAndUp } = useDisplay();
// Vuetify2 doesn't yet support the useDisplay API so this is required to access the prop when using the Composition API
const mdAndUp = computed<boolean>(() => getCurrentInstance()?.proxy.$vuetify.breakpoint.mdAndUp || false);
const theme = useTheme();
const needsCamerasConfigured = computed<boolean>(() => {
return (
Object.values(useCameraSettingsStore().cameras).length === 0 ||
useCameraSettingsStore().cameras["PlaceHolder Name"] === PlaceholderCameraSettings
);
});
const renderCompact = computed<boolean>(() => compact.value || !mdAndUp.value);
</script>
<template>
<v-navigation-drawer dark app permanent :mini-variant="compact || !mdAndUp" color="primary">
<v-list>
<v-navigation-drawer permanent :rail="renderCompact" color="sidebar">
<v-list nav color="primary">
<!-- List item for the heading; note that there are some tricks in setting padding and image width make things look right -->
<v-list-item :class="compact || !mdAndUp ? 'pr-0 pl-0' : ''" style="display: flex; justify-content: center">
<v-list-item-icon class="mr-0">
<img v-if="!(compact || !mdAndUp)" class="logo" src="@/assets/images/logoLarge.svg" alt="large logo" />
<img v-else class="logo" src="@/assets/images/logoSmall.svg" alt="small logo" />
</v-list-item-icon>
<v-list-item :class="renderCompact ? 'pr-0 pl-0' : ''" style="display: flex; justify-content: center">
<template #prepend>
<img v-if="!renderCompact" class="logo" src="@/assets/images/logoLarge.svg" alt="large logo" />
<img v-else class="logo" src="@/assets/images/logoSmallTransparent.svg" alt="small logo" />
</template>
</v-list-item>
<v-list-item link to="/dashboard">
<v-list-item-icon>
<v-icon>mdi-view-dashboard</v-icon>
</v-list-item-icon>
<v-list-item-content>
<v-list-item-title>Dashboard</v-list-item-title>
</v-list-item-content>
<v-list-item link to="/dashboard" prepend-icon="mdi-view-dashboard">
<v-list-item-title>Dashboard</v-list-item-title>
</v-list-item>
<v-list-item link to="/settings">
<v-list-item-icon>
<v-icon>mdi-cog</v-icon>
</v-list-item-icon>
<v-list-item-content>
<v-list-item-title>Settings</v-list-item-title>
</v-list-item-content>
<v-list-item link to="/settings" prepend-icon="mdi-cog">
<v-list-item-title>Settings</v-list-item-title>
</v-list-item>
<v-list-item ref="camerasTabOpener" link to="/cameras">
<v-list-item-icon>
<v-icon>mdi-camera</v-icon>
</v-list-item-icon>
<v-list-item-content>
<v-list-item-title>Camera</v-list-item-title>
</v-list-item-content>
<v-list-item ref="camerasTabOpener" link to="/cameras" prepend-icon="mdi-camera">
<v-list-item-title>Camera</v-list-item-title>
</v-list-item>
<v-list-item
link
to="/cameraConfigs"
:class="{ cameraicon: needsCamerasConfigured && useRoute().path !== '/cameraConfigs' }"
:class="{
cameraicon: useCameraSettingsStore().needsCameraConfiguration && useRoute().path !== '/cameraConfigs'
}"
>
<v-list-item-icon>
<v-icon :class="{ 'red--text': needsCamerasConfigured }">mdi-swap-horizontal-bold</v-icon>
</v-list-item-icon>
<v-list-item-content>
<v-list-item-title :class="{ 'red--text': needsCamerasConfigured }">Camera Matching</v-list-item-title>
</v-list-item-content>
<template #prepend>
<v-icon :class="{ 'text-red': useCameraSettingsStore().needsCameraConfiguration }"
>mdi-swap-horizontal-bold</v-icon
>
</template>
<v-list-item-title :class="{ 'text-red': useCameraSettingsStore().needsCameraConfiguration }"
>Camera Matching</v-list-item-title
>
</v-list-item>
<v-list-item link to="/docs">
<v-list-item-icon>
<v-icon>mdi-bookshelf</v-icon>
</v-list-item-icon>
<v-list-item-content>
<v-list-item-title>Documentation</v-list-item-title>
</v-list-item-content>
<v-list-item link to="/docs" prepend-icon="mdi-bookshelf">
<v-list-item-title>Documentation</v-list-item-title>
</v-list-item>
<div style="position: absolute; bottom: 0; left: 0">
<v-list-item v-if="mdAndUp" link @click="() => (compact = !compact)">
<v-list-item-icon>
<v-icon v-if="compact || !mdAndUp"> mdi-chevron-right </v-icon>
<v-icon v-else> mdi-chevron-left </v-icon>
</v-list-item-icon>
<v-list-item-content>
<v-list-item-title>Compact Mode</v-list-item-title>
</v-list-item-content>
</v-list-item>
<v-list-item>
<v-list-item-icon>
<v-icon v-if="useSettingsStore().network.runNTServer"> mdi-server </v-icon>
<v-icon v-else-if="useStateStore().ntConnectionStatus.connected"> mdi-robot </v-icon>
<v-icon v-else style="border-radius: 100%"> mdi-robot-off </v-icon>
</v-list-item-icon>
<v-list-item-content>
<v-list-item-title v-if="useSettingsStore().network.runNTServer" class="text-wrap">
NetworkTables server running for
<span class="accent--text">{{ useStateStore().ntConnectionStatus.clients || 0 }}</span> clients
</v-list-item-title>
<v-list-item-title
v-else-if="useStateStore().ntConnectionStatus.connected && useStateStore().backendConnected"
class="text-wrap"
style="flex-direction: column; display: flex"
>
NetworkTables Server Connected!
<span class="accent--text">
{{ useStateStore().ntConnectionStatus.address }}
</span>
</v-list-item-title>
<v-list-item-title v-else class="text-wrap" style="flex-direction: column; display: flex">
Not connected to NetworkTables Server!
</v-list-item-title>
</v-list-item-content>
</v-list-item>
<v-list-item>
<v-list-item-icon>
<v-icon v-if="useStateStore().backendConnected"> mdi-server-network </v-icon>
<v-icon v-else style="border-radius: 100%"> mdi-server-network-off </v-icon>
</v-list-item-icon>
<v-list-item-content>
<v-list-item-title class="text-wrap">
{{ useStateStore().backendConnected ? "Backend connected" : "Trying to connect to backend" }}
</v-list-item-title>
</v-list-item-content>
</v-list-item>
</div>
</v-list>
<template #append>
<v-list nav>
<v-list-item
v-if="mdAndUp"
link
:prepend-icon="`mdi-chevron-${compact || !mdAndUp ? 'right' : 'left'}`"
@click="() => (compact = !compact)"
>
<v-list-item-title>Compact</v-list-item-title>
</v-list-item>
<v-list-item
link
:prepend-icon="theme.global.name.value === 'LightTheme' ? 'mdi-white-balance-sunny' : 'mdi-weather-night'"
@click="() => toggleTheme(theme)"
>
<v-list-item-title>Theme</v-list-item-title>
</v-list-item>
<v-list-item>
<template #prepend>
<v-icon
:icon="
useSettingsStore().network.runNTServer
? 'mdi-server'
: useStateStore().ntConnectionStatus.connected
? 'mdi-robot'
: 'mdi-robot-off'
"
:color="
useSettingsStore().network.runNTServer || useStateStore().ntConnectionStatus.connected
? '#00ff00'
: '#ff0000'
"
/>
</template>
<v-list-item-title v-if="useSettingsStore().network.runNTServer" v-show="!renderCompact" class="text-wrap">
NetworkTables server running for
<span class="text-primary">{{ useStateStore().ntConnectionStatus.clients || 0 }}</span> clients
</v-list-item-title>
<v-list-item-title
v-else-if="useStateStore().ntConnectionStatus.connected && useStateStore().backendConnected"
v-show="!renderCompact"
class="text-wrap"
style="flex-direction: column; display: flex"
>
NetworkTables Server Connected!
<span class="text-primary"> {{ useStateStore().ntConnectionStatus.address }} </span>
</v-list-item-title>
<v-list-item-title
v-else
v-show="!renderCompact"
class="text-wrap"
style="flex-direction: column; display: flex"
>
Not connected to NetworkTables Server!
</v-list-item-title>
</v-list-item>
<v-list-item>
<template #prepend>
<v-icon
:icon="useStateStore().backendConnected ? 'mdi-server-network' : 'mdi-server-network-off'"
:color="useStateStore().backendConnected ? '#00ff00' : '#ff0000'"
/>
</template>
<v-list-item-title v-show="!renderCompact" class="text-wrap">
{{ useStateStore().backendConnected ? "Backend connected" : "Trying to connect to backend..." }}
</v-list-item-title>
</v-list-item>
</v-list>
</template>
</v-navigation-drawer>
</template>
<style scoped>
.v-navigation-drawer {
border: none;
}
.v-navigation-drawer--rail {
border: none;
}
.v-list-item-title {
font-size: 1rem !important;
line-height: 1.2rem !important;
}
.logo {
width: 100%;
height: 70px;

View File

@@ -1,9 +1,7 @@
<script setup lang="ts">
import { computed, ref } from "vue";
import { computed, ref, watchEffect } from "vue";
import { useCameraSettingsStore } from "@/stores/settings/CameraSettingsStore";
import { CalibrationBoardTypes, CalibrationTagFamilies, type VideoFormat } from "@/types/SettingTypes";
import JsPDF from "jspdf";
import { font as PromptRegular } from "@/assets/fonts/PromptRegular";
import MonoLogo from "@/assets/images/logoMono.png";
import CharucoImage from "@/assets/images/ChArUco_Marker8x8.png";
import PvSlider from "@/components/common/pv-slider.vue";
@@ -15,6 +13,12 @@ import { WebsocketPipelineType } from "@/types/WebsocketDataTypes";
import { getResolutionString, resolutionsAreEqual } from "@/lib/PhotonUtils";
import CameraCalibrationInfoCard from "@/components/cameras/CameraCalibrationInfoCard.vue";
import { useSettingsStore } from "@/stores/settings/GeneralSettingsStore";
import { useTheme } from "vuetify";
const PromptRegular = import("@/assets/fonts/PromptRegular");
const jspdf = import("jspdf");
const theme = useTheme();
const settingsValid = ref(true);
@@ -75,6 +79,18 @@ const calibrationDivisors = computed(() =>
})
);
const uniqueVideoResolutionString = ref("");
// Use a watchEffect so the value is populated/reacts when the stores become available or update.
// This avoids trying to index into an array that may be empty during page reload.
watchEffect(() => {
const currentIndex = useCameraSettingsStore().currentVideoFormat.index ?? 0;
useStateStore().calibrationData.videoFormatIndex = currentIndex;
const names = useCameraSettingsStore().currentCameraSettings.validVideoFormats.map((f) =>
getResolutionString(f.resolution)
);
uniqueVideoResolutionString.value = names[currentIndex] ?? names[0] ?? "";
});
const squareSizeIn = ref(1);
const markerSizeIn = ref(0.75);
const patternWidth = ref(8);
@@ -88,10 +104,12 @@ const tooManyPoints = computed(
() => useStateStore().calibrationData.imageCount * patternWidth.value * patternHeight.value > 700000
);
const downloadCalibBoard = () => {
const doc = new JsPDF({ unit: "in", format: "letter" });
const downloadCalibBoard = async () => {
const { jsPDF } = await jspdf;
const { font } = await PromptRegular;
const doc = new jsPDF({ unit: "in", format: "letter" });
doc.addFileToVFS("Prompt-Regular.tff", PromptRegular);
doc.addFileToVFS("Prompt-Regular.tff", font);
doc.addFont("Prompt-Regular.tff", "Prompt-Regular", "normal");
doc.setFont("Prompt-Regular");
doc.setFontSize(12);
@@ -101,9 +119,8 @@ const downloadCalibBoard = () => {
switch (boardType.value) {
case CalibrationBoardTypes.Chessboard:
// eslint-disable-next-line no-case-declarations
const chessboardStartX = (paperWidth - patternWidth.value * squareSizeIn.value) / 2;
// eslint-disable-next-line no-case-declarations
const chessboardStartY = (paperHeight - patternWidth.value * squareSizeIn.value) / 2;
for (let squareY = 0; squareY < patternHeight.value; squareY++) {
@@ -129,10 +146,7 @@ const downloadCalibBoard = () => {
charucoImage.src = CharucoImage;
doc.addImage(charucoImage, "PNG", 0.25, 1.5, 8, 8);
doc.text("8 x 8 | 1in & 0.75in", paperWidth - 1, 1.0, {
maxWidth: (paperWidth - 2.0) / 2,
align: "right"
});
doc.text("8 x 8 | 1in & 0.75in", paperWidth - 1, 1.0, { maxWidth: (paperWidth - 2.0) / 2, align: "right" });
break;
}
@@ -180,8 +194,10 @@ const startCalibration = () => {
const showCalibEndDialog = ref(false);
const calibCanceled = ref(false);
const calibSuccess = ref<boolean | undefined>(undefined);
const calibEndpointFail = ref(false);
const endCalibration = () => {
calibSuccess.value = undefined;
calibEndpointFail.value = false;
if (!useStateStore().calibrationData.hasEnoughImages) {
calibCanceled.value = true;
@@ -194,7 +210,13 @@ const endCalibration = () => {
.then(() => {
calibSuccess.value = true;
})
.catch(() => {
.catch((e) => {
if (e.response) {
// Server returned a status code
} else if (e.request) {
// Something went wrong. Unsure if calibration actually worked
calibEndpointFail.value = true;
}
calibSuccess.value = false;
})
.finally(() => {
@@ -203,10 +225,10 @@ const endCalibration = () => {
});
};
let drawAllSnapshots = ref(true);
const drawAllSnapshots = ref(true);
let showCalDialog = ref(false);
let selectedVideoFormat = ref<VideoFormat | undefined>(undefined);
const showCalDialog = ref(false);
const selectedVideoFormat = ref<VideoFormat | undefined>(undefined);
const setSelectedVideoFormat = (format: VideoFormat) => {
selectedVideoFormat.value = format;
showCalDialog.value = true;
@@ -215,11 +237,11 @@ const setSelectedVideoFormat = (format: VideoFormat) => {
<template>
<div>
<v-card class="mb-3" color="primary" dark>
<v-card-title class="pa-6 pb-3">Camera Calibration</v-card-title>
<v-card-text v-show="!isCalibrating">
<v-card-subtitle class="pt-3 pl-2 pb-4 white--text">Current Calibration</v-card-subtitle>
<v-simple-table fixed-header height="100%" dense>
<v-card class="mb-3 rounded-12" color="surface" dark>
<v-card-title>Camera Calibration</v-card-title>
<v-card-text v-if="!isCalibrating" class="pb-0">
<v-card-subtitle class="pa-0 pb-3 text-white">Current Calibrations</v-card-subtitle>
<v-table fixed-header height="100%" density="compact">
<thead>
<tr>
<th>Resolution</th>
@@ -239,275 +261,311 @@ const setSelectedVideoFormat = (format: VideoFormat) => {
<td>{{ value.horizontalFOV !== undefined ? value.horizontalFOV.toFixed(2) + "°" : "-" }}</td>
<td>{{ value.verticalFOV !== undefined ? value.verticalFOV.toFixed(2) + "°" : "-" }}</td>
<td>{{ value.diagonalFOV !== undefined ? value.diagonalFOV.toFixed(2) + "°" : "-" }}</td>
<v-tooltip bottom>
<template #activator="{ on, attrs }">
<td v-bind="attrs" v-on="on" @click="setSelectedVideoFormat(value)">
<v-icon small class="mr-2">mdi-information</v-icon>
<v-tooltip location="bottom">
<template #activator="{ props }">
<td v-bind="props" @click="setSelectedVideoFormat(value)">
<v-icon size="small" color="primary">mdi-information</v-icon>
</td>
</template>
<span>Click for more info on this calibration.</span>
<span>View calibration information</span>
</v-tooltip>
</tr>
</tbody>
</v-simple-table>
</v-table>
</v-card-text>
<v-card-text v-if="useCameraSettingsStore().isConnected" class="d-flex flex-column pa-6 pt-0">
<v-card-subtitle v-show="!isCalibrating" class="pl-0 pb-3 pt-3 white--text"
>Configure New Calibration</v-card-subtitle
>
<v-form ref="form" v-model="settingsValid">
<!-- TODO: the default videoFormatIndex is 0, but the list of unique video mode indexes might not include 0. getUniqueVideoResolutionStrings indexing is also different from the normal video mode indexing -->
<pv-select
v-model="useStateStore().calibrationData.videoFormatIndex"
label="Resolution"
:select-cols="8"
:disabled="isCalibrating"
tooltip="Resolution to calibrate at (you will have to calibrate every resolution you use 3D mode on)"
:items="getUniqueVideoResolutionStrings()"
/>
<pv-select
v-show="isCalibrating && boardType != CalibrationBoardTypes.Charuco"
v-model="useCameraSettingsStore().currentPipelineSettings.streamingFrameDivisor"
label="Decimation"
tooltip="Resolution to which camera frames are downscaled for detection. Calibration still uses full-res"
:items="calibrationDivisors"
:select-cols="8"
@input="(v) => useCameraSettingsStore().changeCurrentPipelineSetting({ streamingFrameDivisor: +v }, false)"
/>
<pv-select
v-model="boardType"
label="Board Type"
tooltip="Calibration board pattern to use"
:select-cols="8"
:items="['Chessboard', 'Charuco']"
:disabled="isCalibrating"
/>
<pv-select
v-show="boardType == CalibrationBoardTypes.Charuco"
v-model="tagFamily"
label="Tag Family"
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"
/>
<pv-number-input
v-model="squareSizeIn"
label="Pattern Spacing (in)"
tooltip="Spacing between pattern features in inches"
:disabled="isCalibrating"
:rules="[(v) => v > 0 || 'Size must be positive']"
:label-cols="4"
/>
<pv-number-input
v-show="boardType == CalibrationBoardTypes.Charuco"
v-model="markerSizeIn"
label="Marker Size (in)"
tooltip="Size of the tag markers in inches must be smaller than pattern spacing"
:disabled="isCalibrating"
:rules="[(v) => v > 0 || 'Size must be positive']"
:label-cols="4"
/>
<pv-number-input
v-model="patternWidth"
label="Board Width (squares)"
tooltip="Width of the board in dots or chessboard squares"
:disabled="isCalibrating"
:rules="[(v) => v >= 4 || 'Width must be at least 4']"
:label-cols="4"
/>
<pv-number-input
v-model="patternHeight"
label="Board Height (squares)"
tooltip="Height of the board in dots or chessboard squares"
:disabled="isCalibrating"
:rules="[(v) => v >= 4 || 'Height must be at least 4']"
:label-cols="4"
<v-card-text class="pt-0">
<div v-if="useCameraSettingsStore().isConnected" class="d-flex flex-column">
<v-card-subtitle v-if="!isCalibrating" class="pl-0 pb-3 pt-3 text-white"
>Configure New Calibration</v-card-subtitle
>
<v-form ref="form" v-model="settingsValid">
<v-alert
closable
density="compact"
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'tonal'"
:color="useSettingsStore().general.mrCalWorking ? 'buttonPassive' : 'error'"
:icon="useSettingsStore().general.mrCalWorking ? 'mdi-check' : 'mdi-close'"
:text="
useSettingsStore().general.mrCalWorking
? 'Mrcal was successfully loaded and will be used!'
: 'MrCal failed to load, check journalctl logs for details.'
"
/>
<pv-select
v-model="uniqueVideoResolutionString"
label="Resolution"
:select-cols="8"
:disabled="isCalibrating"
tooltip="Resolution to calibrate at (you will have to calibrate every resolution you use 3D mode on)"
@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']"
:disabled="isCalibrating"
/>
<pv-select
v-if="boardType !== CalibrationBoardTypes.Charuco"
v-model="useCameraSettingsStore().currentPipelineSettings.streamingFrameDivisor"
label="Decimation"
tooltip="Resolution to which camera frames are downscaled for detection. Calibration still uses full-res"
:items="calibrationDivisors"
:select-cols="8"
@update:modelValue="
(v) => useCameraSettingsStore().changeCurrentPipelineSetting({ streamingFrameDivisor: +v }, false)
"
/>
<pv-select
v-if="boardType === CalibrationBoardTypes.Charuco"
v-model="tagFamily"
label="Tag Family"
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"
/>
<pv-number-input
v-model="squareSizeIn"
label="Pattern Spacing (in)"
tooltip="Spacing between pattern features in inches"
:disabled="isCalibrating"
:rules="[(v) => v > 0 || 'Size must be positive']"
:label-cols="4"
/>
<pv-number-input
v-if="boardType === CalibrationBoardTypes.Charuco"
v-model="markerSizeIn"
label="Marker Size (in)"
tooltip="Size of the tag markers in inches must be smaller than pattern spacing"
:disabled="isCalibrating"
:rules="[(v) => v > 0 || 'Size must be positive']"
:label-cols="4"
/>
<pv-number-input
v-model="patternWidth"
label="Board Width (squares)"
tooltip="Width of the board in dots or chessboard squares"
:disabled="isCalibrating"
:rules="[(v) => v >= 4 || 'Width must be at least 4']"
:label-cols="4"
/>
<pv-number-input
v-model="patternHeight"
label="Board Height (squares)"
tooltip="Height of the board in dots or chessboard squares"
:disabled="isCalibrating"
:rules="[(v) => v >= 4 || 'Height must be at least 4']"
:label-cols="4"
/>
<pv-switch
v-if="boardType === CalibrationBoardTypes.Charuco"
v-model="useOldPattern"
label="Old OpenCV Pattern"
:disabled="isCalibrating"
tooltip="If enabled, Photon will use the old OpenCV pattern for calibration."
:label-cols="4"
/>
</v-form>
</div>
<div v-if="isCalibrating">
<pv-switch
v-model="drawAllSnapshots"
label="Draw Collected Corners"
:switch-cols="8"
tooltip="Draw all snapshots"
@update:modelValue="
(args) => useCameraSettingsStore().changeCurrentPipelineSetting({ drawAllSnapshots: args }, false)
"
/>
<pv-switch
v-show="boardType == CalibrationBoardTypes.Charuco"
v-model="useOldPattern"
label="Old OpenCV Pattern"
:disabled="isCalibrating"
tooltip="If enabled, Photon will use the old OpenCV pattern for calibration."
v-model="useCameraSettingsStore().currentPipelineSettings.cameraAutoExposure"
label="Auto Exposure"
:label-cols="4"
tooltip="Enables or Disables camera automatic adjustment for current lighting conditions"
@update:modelValue="
(args) => useCameraSettingsStore().changeCurrentPipelineSetting({ cameraAutoExposure: args }, false)
"
/>
<v-banner
v-if="useSettingsStore().general.mrCalWorking"
rounded
color="secondary"
text-color="white"
class="mt-3"
icon="mdi-alert-circle-outline"
<pv-slider
v-model="useCameraSettingsStore().currentPipelineSettings.cameraExposureRaw"
:disabled="useCameraSettingsStore().currentCameraSettings.pipelineSettings.cameraAutoExposure"
label="Exposure"
tooltip="Directly controls how long the camera shutter remains open. Units are dependant on the underlying driver."
:min="useCameraSettingsStore().minExposureRaw"
:max="useCameraSettingsStore().maxExposureRaw"
:slider-cols="8"
:step="1"
@update:modelValue="
(args) => useCameraSettingsStore().changeCurrentPipelineSetting({ cameraExposureRaw: args }, false)
"
/>
<pv-slider
v-model="useCameraSettingsStore().currentPipelineSettings.cameraBrightness"
label="Brightness"
:min="0"
:max="100"
:slider-cols="8"
@update:modelValue="
(args) => useCameraSettingsStore().changeCurrentPipelineSetting({ cameraBrightness: args }, false)
"
/>
<pv-slider
v-if="useCameraSettingsStore().currentPipelineSettings.cameraGain >= 0"
v-model="useCameraSettingsStore().currentPipelineSettings.cameraGain"
label="Camera Gain"
tooltip="Controls camera gain, similar to brightness"
:min="0"
:max="100"
:slider-cols="8"
@update:modelValue="
(args) => useCameraSettingsStore().changeCurrentPipelineSetting({ cameraGain: args }, false)
"
/>
<pv-slider
v-if="useCameraSettingsStore().currentPipelineSettings.cameraRedGain !== -1"
v-model="useCameraSettingsStore().currentPipelineSettings.cameraRedGain"
label="Red AWB Gain"
:min="0"
:max="100"
:slider-cols="8"
tooltip="Controls red automatic white balance gain, which affects how the camera captures colors in different conditions"
@update:modelValue="
(args) => useCameraSettingsStore().changeCurrentPipelineSetting({ cameraRedGain: args }, false)
"
/>
<pv-slider
v-if="useCameraSettingsStore().currentPipelineSettings.cameraBlueGain !== -1"
v-model="useCameraSettingsStore().currentPipelineSettings.cameraBlueGain"
label="Blue AWB Gain"
:min="0"
:max="100"
:slider-cols="8"
tooltip="Controls blue automatic white balance gain, which affects how the camera captures colors in different conditions"
@update:modelValue="
(args) => useCameraSettingsStore().changeCurrentPipelineSetting({ cameraBlueGain: args }, false)
"
/>
</div>
<div v-if="isCalibrating" class="d-flex justify-center align-center pt-10px pb-5">
<v-chip
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'tonal'"
label
:color="useStateStore().calibrationData.hasEnoughImages ? 'buttonPassive' : 'light-grey'"
>
Mrcal was successfully loaded and will be used!
</v-banner>
<v-banner v-else rounded color="error" text-color="white" class="mt-3" icon="mdi-alert-circle-outline">
MrCal JNI could not be loaded! Consult journalctl logs for additional details.
</v-banner>
</v-form>
</v-card-text>
<v-card-text v-if="isCalibrating" class="pa-6 pt-0">
<pv-switch
v-model="drawAllSnapshots"
label="Draw Collected Corners"
:switch-cols="8"
tooltip="Draw all snapshots"
@input="(args) => useCameraSettingsStore().changeCurrentPipelineSetting({ drawAllSnapshots: args }, false)"
/>
<pv-switch
v-model="useCameraSettingsStore().currentPipelineSettings.cameraAutoExposure"
label="Auto Exposure"
:label-cols="4"
tooltip="Enables or Disables camera automatic adjustment for current lighting conditions"
@input="(args) => useCameraSettingsStore().changeCurrentPipelineSetting({ cameraAutoExposure: args }, false)"
/>
<pv-slider
v-model="useCameraSettingsStore().currentPipelineSettings.cameraExposureRaw"
:disabled="useCameraSettingsStore().currentCameraSettings.pipelineSettings.cameraAutoExposure"
label="Exposure"
tooltip="Directly controls how long the camera shutter remains open. Units are dependant on the underlying driver."
:min="useCameraSettingsStore().minExposureRaw"
:max="useCameraSettingsStore().maxExposureRaw"
:slider-cols="7"
:step="1"
@input="(args) => useCameraSettingsStore().changeCurrentPipelineSetting({ cameraExposureRaw: args }, false)"
/>
<pv-slider
v-model="useCameraSettingsStore().currentPipelineSettings.cameraBrightness"
label="Brightness"
:min="0"
:max="100"
:slider-cols="7"
@input="(args) => useCameraSettingsStore().changeCurrentPipelineSetting({ cameraBrightness: args }, false)"
/>
<pv-slider
v-if="useCameraSettingsStore().currentPipelineSettings.cameraGain >= 0"
v-model="useCameraSettingsStore().currentPipelineSettings.cameraGain"
label="Camera Gain"
tooltip="Controls camera gain, similar to brightness"
:min="0"
:max="100"
:slider-cols="7"
@input="(args) => useCameraSettingsStore().changeCurrentPipelineSetting({ cameraGain: args }, false)"
/>
<pv-slider
v-if="useCameraSettingsStore().currentPipelineSettings.cameraRedGain !== -1"
v-model="useCameraSettingsStore().currentPipelineSettings.cameraRedGain"
label="Red AWB Gain"
:min="0"
:max="100"
:slider-cols="7"
tooltip="Controls red automatic white balance gain, which affects how the camera captures colors in different conditions"
@input="(args) => useCameraSettingsStore().changeCurrentPipelineSetting({ cameraRedGain: args }, false)"
/>
<pv-slider
v-if="useCameraSettingsStore().currentPipelineSettings.cameraBlueGain !== -1"
v-model="useCameraSettingsStore().currentPipelineSettings.cameraBlueGain"
label="Blue AWB Gain"
:min="0"
:max="100"
:slider-cols="7"
tooltip="Controls blue automatic white balance gain, which affects how the camera captures colors in different conditions"
@input="(args) => useCameraSettingsStore().changeCurrentPipelineSetting({ cameraBlueGain: args }, false)"
/>
<v-banner
Snapshots: {{ useStateStore().calibrationData.imageCount }} of at least
{{ useStateStore().calibrationData.minimumImageCount }}
</v-chip>
</div>
<div>
<v-btn
color="buttonPassive"
size="small"
block
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
:disabled="!settingsValid"
@click="downloadCalibBoard"
>
<v-icon start class="calib-btn-icon" size="large"> mdi-download </v-icon>
<span class="calib-btn-label">Generate Board</span>
</v-btn>
</div>
<v-alert
v-if="tooManyPoints"
rounded
class="mt-3"
class="mt-5"
color="error"
text-color="white"
density="compact"
text="Too many corners. Finish calibration now!"
icon="mdi-alert-circle-outline"
>
Too many corners. Finish calibration now!
</v-banner>
</v-card-text>
<v-card-text v-if="isCalibrating" class="d-flex justify-center align-center pa-6 pt-0">
<v-chip label :color="useStateStore().calibrationData.hasEnoughImages ? 'secondary' : 'gray'">
Snapshots: {{ useStateStore().calibrationData.imageCount }} of at least
{{ useStateStore().calibrationData.minimumImageCount }}
</v-chip>
</v-card-text>
<v-card-text class="d-flex pa-6 pt-0">
<v-col cols="6" class="pa-0 pr-2">
<v-btn
small
block
color="secondary"
:disabled="!settingsValid || tooManyPoints"
@click="isCalibrating ? useCameraSettingsStore().takeCalibrationSnapshot() : startCalibration()"
>
<v-icon left class="calib-btn-icon"> {{ isCalibrating ? "mdi-camera" : "mdi-flag-outline" }} </v-icon>
<span class="calib-btn-label">{{ isCalibrating ? "Take Snapshot" : "Start Calibration" }}</span>
</v-btn>
</v-col>
<v-col cols="6" class="pa-0 pl-2">
<v-btn
small
block
:color="useStateStore().calibrationData.hasEnoughImages ? 'accent' : 'error'"
:class="useStateStore().calibrationData.hasEnoughImages ? 'black--text' : 'white---text'"
:disabled="!isCalibrating || !settingsValid"
@click="endCalibration"
>
<v-icon left class="calib-btn-icon">
{{ useStateStore().calibrationData.hasEnoughImages ? "mdi-flag-checkered" : "mdi-flag-off-outline" }}
</v-icon>
<span class="calib-btn-label">{{
useStateStore().calibrationData.hasEnoughImages ? "Finish Calibration" : "Cancel Calibration"
}}</span>
</v-btn>
</v-col>
</v-card-text>
<v-card-text class="pa-6 pt-0">
<v-btn color="accent" small block outlined :disabled="!settingsValid" @click="downloadCalibBoard">
<v-icon left class="calib-btn-icon"> mdi-download </v-icon>
<span class="calib-btn-label">Generate Board</span>
</v-btn>
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'tonal'"
/>
<div class="d-flex pt-5">
<v-col cols="6" class="pa-0 pr-2">
<v-btn
size="small"
block
color="buttonActive"
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
:disabled="!settingsValid || tooManyPoints"
@click="isCalibrating ? useCameraSettingsStore().takeCalibrationSnapshot() : startCalibration()"
>
<v-icon start class="calib-btn-icon" size="large">
{{ isCalibrating ? "mdi-camera" : "mdi-flag-outline" }}
</v-icon>
<span class="calib-btn-label">{{ isCalibrating ? "Take Snapshot" : "Start Calibration" }}</span>
</v-btn>
</v-col>
<v-col cols="6" class="pa-0 pl-2">
<v-btn
size="small"
block
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
:color="useStateStore().calibrationData.hasEnoughImages ? 'buttonActive' : 'error'"
:disabled="!isCalibrating || !settingsValid"
@click="endCalibration"
>
<v-icon start class="calib-btn-icon" size="large">
{{ useStateStore().calibrationData.hasEnoughImages ? "mdi-flag-checkered" : "mdi-flag-off-outline" }}
</v-icon>
<span class="calib-btn-label">{{
useStateStore().calibrationData.hasEnoughImages ? "Finish Calibration" : "Cancel Calibration"
}}</span>
</v-btn>
</v-col>
</div>
</v-card-text>
</v-card>
<v-dialog v-model="showCalibEndDialog" width="500px" :persistent="true">
<v-card color="primary" dark>
<v-card-title class="pb-8"> Camera Calibration </v-card-title>
<div class="ml-3">
<v-col style="text-align: center">
<template v-if="calibCanceled">
<v-icon color="blue" size="70"> mdi-cancel </v-icon>
<v-card-text
>Camera Calibration has been Canceled, the backend is attempting to cleanly cancel the calibration
process.</v-card-text
>
</template>
<!-- No result reported yet -->
<template v-else-if="calibSuccess === undefined">
<v-progress-circular indeterminate :size="70" :width="8" color="accent" />
<v-card-text>Camera is being calibrated. This process may take several minutes...</v-card-text>
</template>
<!-- Got positive result -->
<template v-else-if="calibSuccess">
<v-icon color="green" size="70"> mdi-check-bold </v-icon>
<v-card-text>
Camera has been successfully calibrated for
{{
getUniqueVideoResolutionStrings().find(
(v) => v.value === useStateStore().calibrationData.videoFormatIndex
)?.name
}}!
</v-card-text>
</template>
<template v-else>
<v-icon color="red" size="70"> mdi-close </v-icon>
<v-card-text
>Camera calibration failed! Make sure that the photos are taken such that the rainbow grid circles align
with the corners of the chessboard, and try again. More information is available in the program
logs.</v-card-text
>
</template>
</v-col>
<v-card color="surface" dark>
<v-card-title> Camera Calibration </v-card-title>
<div style="text-align: center">
<template v-if="calibCanceled">
<v-icon color="primary" size="70"> mdi-cancel </v-icon>
<v-card-text>
Camera calibration has been canceled. The backend is attempting to cleanly cancel the calibration process.
</v-card-text>
</template>
<!-- No result reported yet -->
<template v-else-if="calibSuccess === undefined">
<v-progress-circular indeterminate :size="70" :width="8" color="primary" />
<v-card-text>Camera is being calibrated. This process may take several minutes...</v-card-text>
</template>
<!-- Got positive result -->
<template v-else-if="calibSuccess">
<v-icon color="#00ff00" size="70"> mdi-check </v-icon>
<v-card-text>
Camera has been successfully calibrated for
{{
useCameraSettingsStore().currentCameraSettings.validVideoFormats.map((f) =>
getResolutionString(f.resolution)
)[useStateStore().calibrationData.videoFormatIndex]
}}!
</v-card-text>
</template>
<template v-else-if="calibEndpointFail">
<v-icon color="gray" size="70"> mdi-help-circle-outline </v-icon>
<v-card-text
>Unable to determine if calibration was successful. Refresh this page and manually check if calibration
was successful.</v-card-text
>
</template>
<template v-else>
<v-icon color="red" size="70"> mdi-close </v-icon>
<v-card-text>
Camera calibration failed! Make sure that the photos are taken such that the rainbow grid circles align
with the corners of the chessboard, and try again. More information is available in the program logs.
</v-card-text>
</template>
</div>
<v-card-actions>
<v-card-actions class="pa-5 pt-0">
<v-spacer />
<v-btn v-if="!isCalibrating" color="white" text @click="showCalibEndDialog = false"> OK </v-btn>
<v-btn v-if="!isCalibrating" color="white" variant="text" @click="showCalibEndDialog = false"> OK </v-btn>
</v-card-actions>
</v-card>
</v-dialog>
@@ -518,18 +576,21 @@ const setSelectedVideoFormat = (format: VideoFormat) => {
</template>
<style scoped lang="scss">
.v-data-table {
th {
text-align: center !important;
padding: 0 8px !important;
}
.v-table {
text-align: center;
width: 100%;
th,
td {
background-color: #006492 !important;
font-size: 1rem !important;
}
tbody :hover td {
background-color: #005281 !important;
cursor: pointer;
}
@@ -545,7 +606,7 @@ const setSelectedVideoFormat = (format: VideoFormat) => {
}
::-webkit-scrollbar-thumb {
background-color: #ffd843;
background-color: rgb(var(--v-theme-accent));
border-radius: 10px;
}
}

View File

@@ -4,6 +4,9 @@ import { useCameraSettingsStore } from "@/stores/settings/CameraSettingsStore";
import { useStateStore } from "@/stores/StateStore";
import { computed, inject, ref } from "vue";
import { getResolutionString, parseJsonFile } from "@/lib/PhotonUtils";
import { useTheme } from "vuetify";
const theme = useTheme();
const props = defineProps<{
videoFormat: VideoFormat;
@@ -88,16 +91,20 @@ const exportCalibrationURL = computed<string>(() =>
const calibrationImageURL = (index: number) =>
useCameraSettingsStore().getCalImageUrl(inject<string>("backendHost") as string, props.videoFormat.resolution, index);
</script>
<template>
<v-card color="primary" dark>
<div class="d-flex flex-wrap pr-md-3">
<v-card color="surface" dark>
<div class="d-flex flex-wrap pt-2 pl-2 pr-2">
<v-col cols="12" md="6">
<v-card-title class="pl-3 pb-0 pb-md-4"> Calibration Details </v-card-title>
<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-btn color="secondary" style="width: 100%" @click="openUploadPhotonCalibJsonPrompt">
<v-icon left> mdi-import</v-icon>
<v-btn
color="buttonPassive"
style="width: 100%"
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
@click="openUploadPhotonCalibJsonPrompt"
>
<v-icon start size="large"> mdi-import</v-icon>
<span>Import</span>
</v-btn>
<input
@@ -110,12 +117,13 @@ const calibrationImageURL = (index: number) =>
</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="secondary"
color="buttonPassive"
:disabled="!currentCalibrationCoeffs"
style="width: 100%"
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
@click="openExportCalibrationPrompt"
>
<v-icon left>mdi-export</v-icon>
<v-icon start size="large">mdi-export</v-icon>
<span>Export</span>
</v-btn>
<a
@@ -126,16 +134,21 @@ const calibrationImageURL = (index: number) =>
/>
</v-col>
</div>
<v-card-title class="pt-0 pb-3"
<v-card-title class="pt-0 pb-0"
>{{ useCameraSettingsStore().currentCameraName }}@{{ getResolutionString(videoFormat.resolution) }}</v-card-title
>
<v-card-text v-if="!currentCalibrationCoeffs">
<v-banner rounded color="secondary" text-color="white" class="mt-3" icon="mdi-alert-circle-outline">
The selected video format has not been calibrated.
</v-banner>
<v-alert
class="pt-3 pb-3"
color="primary"
density="compact"
text="The selected video format has not been calibrated."
icon="mdi-alert-circle-outline"
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'tonal'"
/>
</v-card-text>
<v-card-text>
<v-simple-table dense style="width: 100%">
<v-card-text class="pt-0">
<v-table density="compact" style="width: 100%">
<template #default>
<thead>
<tr>
@@ -238,25 +251,36 @@ const calibrationImageURL = (index: number) =>
</tr>
</tbody>
</template>
</v-simple-table>
</v-table>
</v-card-text>
<v-card-title v-if="currentCalibrationCoeffs" class="pt-0">Individual Observations</v-card-title>
<v-card-text v-if="currentCalibrationCoeffs">
<v-card-title v-if="currentCalibrationCoeffs" class="pt-0 pb-0">Individual Observations</v-card-title>
<v-card-text v-if="currentCalibrationCoeffs" class="pt-0">
<v-data-table
dense
density="compact"
style="width: 100%"
:headers="[
{ text: 'Observation Id', value: 'index' },
{ text: 'Mean Reprojection Error', value: 'mean' },
{ text: '', value: 'data-table-expand' }
{ title: 'Observation Id', key: 'index' },
{ title: 'Mean Reprojection Error', key: 'mean' },
{ title: '', key: 'data-table-expand' }
]"
:items="getObservationDetails()"
item-key="index"
item-value="index"
show-expand
expand-icon="mdi-eye"
>
<template #expanded-item="{ headers, item }">
<td :colspan="headers.length">
<template #item.data-table-expand="{ internalItem, toggleExpand }">
<v-btn
icon="mdi-eye"
class="text-none"
color="medium-emphasis"
size="small"
variant="text"
slim
@click="toggleExpand(internalItem)"
></v-btn>
</template>
<template #expanded-row="{ columns, item }">
<td :colspan="columns.length">
<div style="display: flex; justify-content: center; width: 100%">
<img :src="calibrationImageURL(item.index)" alt="observation image" class="snapshot-preview pt-2 pb-2" />
</div>
@@ -268,9 +292,6 @@ const calibrationImageURL = (index: number) =>
</template>
<style scoped>
.v-data-table {
background-color: #006492 !important;
}
.snapshot-preview {
max-width: 55%;
}

View File

@@ -2,6 +2,9 @@
import { ref } from "vue";
import axios from "axios";
import { useStateStore } from "@/stores/StateStore";
import { useTheme } from "vuetify";
const theme = useTheme();
interface SnapshotMetadata {
snapshotName: string;
@@ -91,41 +94,69 @@ const expanded = ref([]);
</script>
<template>
<v-card dark style="background-color: #006492">
<v-card color="surface" class="rounded-12">
<v-card-title>Camera Control</v-card-title>
<v-card-text>
<v-btn color="secondary" @click="fetchSnapshots">
<v-icon left class="open-icon"> mdi-folder </v-icon>
<v-card-text class="pt-0">
<v-btn
color="buttonPassive"
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
@click="fetchSnapshots"
>
<v-icon start class="open-icon" size="large"> mdi-folder </v-icon>
<span class="open-label">Show Saved Snapshots</span>
</v-btn>
</v-card-text>
<v-dialog v-model="showSnapshotViewerDialog">
<v-card dark class="pt-3 pl-5 pr-5" color="primary" flat>
<v-card-title> View Saved Frame Snapshots </v-card-title>
<v-divider />
<v-card-text v-if="imgData.length === 0" style="font-size: 18px; font-weight: 600" class="pt-4">
There are no snapshots saved
<v-card color="surface" flat>
<v-card-title> Saved Frame Snapshots </v-card-title>
<v-card-text v-if="imgData.length === 0" class="pt-0">
<v-alert
color="buttonPassive"
density="compact"
text="There are currently no saved snapshots."
icon="mdi-information-outline"
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'tonal'"
/>
</v-card-text>
<div v-else class="pb-2">
<v-card-text v-else class="pt-0">
<v-alert
closable
color="buttonPassive"
density="compact"
text="Snapshot timestamps depend on when the coprocessor was last connected to the internet."
icon="mdi-information-outline"
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'tonal'"
/>
<v-data-table
v-model:expanded="expanded"
:headers="[
{ text: 'Snapshot Name', value: 'snapshotShortName', sortable: false },
{ text: 'Camera Unique Name', value: 'cameraUniqueName' },
{ text: 'Camera Nickname', value: 'cameraNickname' },
{ text: 'Stream Type', value: 'streamType' },
{ text: 'Time Created', value: 'timeCreated' },
{ text: 'Actions', value: 'actions', sortable: false }
{ title: 'Snapshot Name', key: 'snapshotShortName', sortable: false },
{ title: 'Camera Unique Name', key: 'cameraUniqueName' },
{ title: 'Camera Nickname', key: 'cameraNickname' },
{ title: 'Stream Type', key: 'streamType' },
{ title: 'Time Created', key: 'timeCreated' },
{ title: 'Actions', key: 'actions', sortable: false }
]"
:items="imgData"
group-by="cameraUniqueName"
:group-by="[{ key: 'cameraUniqueName' }]"
class="elevation-0"
item-key="index"
item-value="index"
show-expand
expand-icon="mdi-eye"
>
<template #expanded-item="{ headers, item }">
<td :colspan="headers.length">
<template #item.data-table-expand="{ internalItem, toggleExpand }">
<v-btn
icon="mdi-eye"
class="text-none"
color="medium-emphasis"
size="small"
variant="text"
slim
@click="toggleExpand(internalItem)"
></v-btn>
</template>
<template #expanded-row="{ item, columns }">
<td :colspan="columns.length">
<div style="display: flex; justify-content: center; width: 100%">
<img :src="item.snapshotSrc" alt="snapshot-image" class="snapshot-preview pt-2 pb-2" />
</div>
@@ -135,16 +166,12 @@ const expanded = ref([]);
<template #item.actions="{ item }">
<div style="display: flex; justify-content: center">
<a :download="item.snapshotName" :href="item.snapshotSrc">
<v-icon small> mdi-download </v-icon>
<v-icon size="small"> mdi-download </v-icon>
</a>
</div>
</template>
</v-data-table>
<span
>Snapshot Timestamps may be incorrect as they depend on when the coprocessor was last connected to the
internet</span
>
</div>
</v-card-text>
</v-card>
</v-dialog>
</v-card>
@@ -157,20 +184,14 @@ const expanded = ref([]);
.v-btn {
width: 100%;
}
.v-data-table {
.v-table {
text-align: center;
background-color: #006492 !important;
th,
td {
background-color: #005281 !important;
font-size: 1rem !important;
}
tbody :hover tr {
background-color: #005281 !important;
}
::-webkit-scrollbar {
width: 0;
height: 0.55em;
@@ -183,7 +204,7 @@ const expanded = ref([]);
}
::-webkit-scrollbar-thumb {
background-color: #ffd843;
background-color: rgb(var(--v-theme-accent));
border-radius: 10px;
}
}

View File

@@ -1,11 +1,15 @@
<script setup lang="ts">
import PvSelect, { type SelectItem } from "@/components/common/pv-select.vue";
import PvInput from "@/components/common/pv-input.vue";
import PvNumberInput from "@/components/common/pv-number-input.vue";
import { useCameraSettingsStore } from "@/stores/settings/CameraSettingsStore";
import { useStateStore } from "@/stores/StateStore";
import { computed, inject, ref, watchEffect } from "vue";
import { computed, ref, watchEffect } from "vue";
import { type CameraSettingsChangeRequest, ValidQuirks } from "@/types/SettingTypes";
import axios from "axios";
import { useTheme } from "vuetify";
const theme = useTheme();
const tempSettingsStruct = ref<CameraSettingsChangeRequest>({
fov: useCameraSettingsStore().currentCameraSettings.fov.value,
@@ -72,10 +76,7 @@ const saveCameraSettings = () => {
useCameraSettingsStore()
.updateCameraSettings(tempSettingsStruct.value)
.then((response) => {
useStateStore().showSnackbarMessage({
color: "success",
message: response.data.text || response.data
});
useStateStore().showSnackbarMessage({ color: "success", message: response.data.text || response.data });
// Update the local settings cause the backend checked their validity. Assign is to deref value
useCameraSettingsStore().currentCameraSettings.fov.value = tempSettingsStruct.value.fov;
@@ -111,22 +112,13 @@ watchEffect(() => {
});
const showDeleteCamera = ref(false);
const address = inject<string>("backendHost");
const exportSettings = ref();
const openExportSettingsPrompt = () => {
exportSettings.value.click();
};
const yesDeleteMySettingsText = ref("");
const deletingCamera = ref(false);
const deleteThisCamera = () => {
if (deletingCamera.value) return;
deletingCamera.value = true;
const payload = {
cameraUniqueName: useStateStore().currentCameraUniqueName
};
const payload = { cameraUniqueName: useStateStore().currentCameraUniqueName };
axios
.post("/utils/nukeOneCamera", payload)
@@ -168,9 +160,9 @@ const wrappedCameras = computed<SelectItem[]>(() =>
</script>
<template>
<v-card class="mb-3" color="primary" dark>
<v-card-title class="pa-6 pb-0">Camera Settings</v-card-title>
<v-card-text class="pa-6 pt-3">
<v-card class="mb-3 rounded-12" color="surface" dark>
<v-card-title class="pb-0">Camera Settings</v-card-title>
<v-card-text class="pt-3">
<pv-select
v-model="useStateStore().currentCameraUniqueName"
label="Camera"
@@ -201,45 +193,42 @@ const wrappedCameras = computed<SelectItem[]>(() =>
:select-cols="8"
/>
</v-card-text>
<v-card-text class="d-flex pa-6 pt-0">
<v-card-text class="d-flex pt-0">
<v-col cols="6" class="pa-0 pr-2">
<v-btn block small color="secondary" :disabled="!settingsHaveChanged()" @click="saveCameraSettings">
<v-icon left> mdi-content-save </v-icon>
<v-btn
block
size="small"
color="primary"
:disabled="!settingsHaveChanged()"
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
@click="saveCameraSettings"
>
<v-icon start size="large"> mdi-content-save </v-icon>
Save Changes
</v-btn>
</v-col>
<v-col cols="6" class="pa-0 pl-2">
<v-btn block small color="error" @click="() => (showDeleteCamera = true)">
<v-icon left> mdi-trash-can-outline </v-icon>
<v-btn
block
size="small"
color="error"
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
@click="() => (showDeleteCamera = true)"
>
<v-icon start size="large"> mdi-trash-can-outline </v-icon>
Delete Camera
</v-btn>
</v-col>
</v-card-text>
<v-dialog v-model="showDeleteCamera" dark width="800">
<v-card dark class="dialog-container pa-3 pb-2" color="primary" flat>
<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>
<v-row class="align-center pt-6">
<v-col cols="12" md="6">
<span class="white--text"> This will delete ALL OF YOUR SETTINGS and restart PhotonVision. </span>
</v-col>
<v-col cols="12" md="6">
<v-btn color="secondary" block @click="openExportSettingsPrompt">
<v-icon left class="open-icon"> 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 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>
<v-card-text class="pt-0 pb-10px">
<pv-input
v-model="yesDeleteMySettingsText"
:label="'Type &quot;' + useCameraSettingsStore().currentCameraName + '&quot;:'"
@@ -247,20 +236,28 @@ const wrappedCameras = computed<SelectItem[]>(() =>
:input-cols="6"
/>
</v-card-text>
<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
block
color="error"
:disabled="
yesDeleteMySettingsText.toLowerCase() !== useCameraSettingsStore().currentCameraName.toLowerCase()
"
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
:loading="deletingCamera"
@click="deleteThisCamera"
>
<v-icon left class="open-icon"> mdi-trash-can-outline </v-icon>
<span class="open-label">DELETE (UNRECOVERABLE)</span>
<v-icon start class="open-icon" size="large"> mdi-trash-can-outline </v-icon>
<span class="open-label">Delete</span>
</v-btn>
</v-card-text>
</v-card-actions>
</v-card>
</v-dialog>
</v-card>

View File

@@ -5,20 +5,11 @@ import { useCameraSettingsStore } from "@/stores/settings/CameraSettingsStore";
import { PipelineType } from "@/types/PipelineTypes";
import { useStateStore } from "@/stores/StateStore";
import { useSettingsStore } from "@/stores/settings/GeneralSettingsStore";
import { useTheme } from "vuetify";
const props = defineProps<{
// TODO fully update v-model usage in custom components on Vue3 update
value: number[];
}>();
const theme = useTheme();
const emit = defineEmits<{
(e: "input", value: number[]): void;
}>();
const localValue = computed({
get: () => props.value,
set: (v) => emit("input", v)
});
const value = defineModel<number[]>({ required: true });
const driverMode = computed<boolean>({
get: () => useCameraSettingsStore().isDriverMode,
@@ -41,38 +32,39 @@ const fpsTooLow = computed<boolean>(() => {
</script>
<template>
<v-card id="camera-settings-camera-view-card" class="camera-settings-camera-view-card" color="primary" dark>
<v-card-title class="justify-space-between align-content-center pa-0 pl-6 pr-6">
<div class="d-flex flex-wrap pt-4 pb-4">
<div>
<span class="mr-4" style="white-space: nowrap"> Cameras </span>
</div>
<div>
<v-chip
v-if="useCameraSettingsStore().currentCameraSettings.isConnected"
label
:color="fpsTooLow ? 'error' : 'transparent'"
:text-color="fpsTooLow ? '#C7EA46' : '#ff4d00'"
style="font-size: 1rem; padding: 0; margin: 0"
<v-card
id="camera-settings-camera-view-card"
class="camera-settings-camera-view-card rounded-12"
color="surface"
dark
>
<v-card-title class="justify-space-between align-content-center pt-0 pb-0">
<div class="d-flex flex-wrap align-center pt-4 pb-4">
<span class="mr-4" style="white-space: nowrap"> Cameras </span>
<v-chip
v-if="useCameraSettingsStore().currentCameraSettings.isConnected"
label
:color="fpsTooLow ? 'error' : 'transparent'"
style="font-size: 1rem; padding: 0; margin: 0"
>
<span
class="pr-1"
:style="{ color: fpsTooLow ? 'rgb(var(--v-theme-error))' : 'rgb(var(--v-theme-primary))' }"
>
<span class="pr-1">
{{ Math.round(useStateStore().currentPipelineResults?.fps || 0) }}&nbsp;FPS &ndash;
{{ Math.min(Math.round(useStateStore().currentPipelineResults?.latency || 0), 9999) }} ms latency
</span>
</v-chip>
<v-chip v-else label color="transparent" text-color="red" style="font-size: 1rem; padding: 0; margin: 0">
<span class="pr-1">Camera not connected</span>
</v-chip>
</div>
</div>
<div class="d-flex align-center">
&nbsp;{{ Math.round(useStateStore().currentPipelineResults?.fps || 0) }}&nbsp;FPS &ndash;
{{ Math.min(Math.round(useStateStore().currentPipelineResults?.latency || 0), 9999) }} ms latency
</span>
</v-chip>
<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-switch
v-model="driverMode"
:disabled="useCameraSettingsStore().isCalibrationMode || useCameraSettingsStore().pipelineNames.length === 0"
label="Driver Mode"
style="margin-left: auto"
color="accent"
class="pt-2 pb-2"
color="primary"
density="compact"
hide-details="auto"
/>
</div>
@@ -98,21 +90,23 @@ const fpsTooLow = computed<boolean>(() => {
</div>
</v-card-text>
<v-card-text class="pt-0">
<v-btn-toggle v-model="localValue" :multiple="true" mandatory dark class="fill" style="width: 100%">
<v-btn-toggle v-model="value" :multiple="true" mandatory class="fill" style="width: 100%">
<v-btn
color="secondary"
color="buttonPassive"
class="fill"
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
:disabled="useCameraSettingsStore().isDriverMode || useCameraSettingsStore().isCalibrationMode"
>
<v-icon left class="mode-btn-icon">mdi-import</v-icon>
<v-icon start class="mode-btn-icon" size="large">mdi-import</v-icon>
<span class="mode-btn-label">Raw</span>
</v-btn>
<v-btn
color="secondary"
color="buttonPassive"
class="fill"
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
:disabled="useCameraSettingsStore().isDriverMode || useCameraSettingsStore().isCalibrationMode"
>
<v-icon left class="mode-btn-icon">mdi-export</v-icon>
<v-icon start class="mode-btn-icon" size="large">mdi-export</v-icon>
<span class="mode-btn-label">Processed</span>
</v-btn>
</v-btn-toggle>
@@ -123,7 +117,6 @@ const fpsTooLow = computed<boolean>(() => {
<style scoped>
.v-btn-toggle.fill {
width: 100%;
height: 100%;
}
.v-btn-toggle.fill > .v-btn {
width: 50%;
@@ -134,10 +127,6 @@ th {
text-align: center;
}
.v-input--switch {
margin-top: 0;
}
.stream-container {
display: flex;
justify-content: center;

View File

@@ -24,7 +24,7 @@ const cameraInfoFor: any = (camera: PVCameraInfo) => {
<template>
<div>
<v-simple-table dense :style="{ backgroundColor: 'var(--v-primary-base)' }">
<v-table density="compact" :style="{ backgroundColor: 'var(--v-primary-base)' }">
<tbody>
<tr v-if="cameraInfoFor(camera).dev !== undefined && cameraInfoFor(camera).dev !== null">
<td>Device Number:</td>
@@ -66,6 +66,6 @@ const cameraInfoFor: any = (camera: PVCameraInfo) => {
<td>{{ cameraInfoFor(camera).otherPaths }}</td>
</tr>
</tbody>
</v-simple-table>
</v-table>
</div>
</template>

View File

@@ -1,6 +1,19 @@
<script setup lang="ts">
import { PVCameraInfo } from "@/types/SettingTypes";
import _ from "lodash";
function isEqual<T>(a: T, b: T): boolean {
if (a === b) {
return true;
}
const bothAreObjects = a && b && typeof a === "object" && typeof b === "object";
return (
bothAreObjects &&
Object.keys(a).length === Object.keys(b).length &&
Object.entries(a).every(([k, v]) => isEqual(v, b[k as keyof T]))
);
}
const { saved, current } = defineProps({
saved: {
@@ -29,7 +42,7 @@ const cameraInfoFor = (camera: PVCameraInfo): any => {
<template>
<div>
<v-simple-table dense :style="{ backgroundColor: 'var(--v-primary-base)' }">
<v-table density="compact" :style="{ backgroundColor: 'var(--v-primary-base)' }">
<tbody>
<tr>
<th></th>
@@ -105,14 +118,14 @@ const cameraInfoFor = (camera: PVCameraInfo): any => {
</tr>
<tr
v-if="cameraInfoFor(saved).otherPaths !== undefined && cameraInfoFor(saved).otherPaths !== null"
:class="!_.isEqual(cameraInfoFor(saved).otherPaths, cameraInfoFor(current).otherPaths) ? 'mismatch' : ''"
:class="isEqual(cameraInfoFor(saved).otherPaths, cameraInfoFor(current).otherPaths) ? '' : 'mismatch'"
>
<td>Other Paths:</td>
<td>{{ cameraInfoFor(saved).otherPaths }}</td>
<td>{{ cameraInfoFor(current).otherPaths }}</td>
</tr>
</tbody>
</v-simple-table>
</v-table>
</div>
</template>

View File

@@ -1,5 +1,5 @@
<script setup lang="ts">
const props = withDefaults(
withDefaults(
defineProps<{
iconName: string;
disabled?: boolean;
@@ -18,20 +18,17 @@ const props = withDefaults(
defineEmits<{
(e: "click"): void;
}>();
const hoverClass = props.hover ? "hover" : "";
</script>
<template>
<div>
<v-tooltip :right="right" :bottom="!right" nudge-right="10" :disabled="tooltip === undefined">
<template #activator="{ on, attrs }">
<v-tooltip :right="right" :location="!right ? 'bottom' : undefined" offset="10" :disabled="tooltip === undefined">
<template #activator="{ props }">
<v-icon
:class="hoverClass"
:class="hover ? 'hover' : ''"
:color="color"
v-bind="attrs"
v-bind="props"
:disabled="disabled"
v-on="on"
@click="$emit('click')"
>
{{ iconName }}

View File

@@ -1,13 +1,12 @@
<script setup lang="ts">
import { computed } from "vue";
import TooltippedLabel from "@/components/common/pv-tooltipped-label.vue";
const value = defineModel<string>({ required: true });
const props = withDefaults(
defineProps<{
label?: string;
tooltip?: string;
// TODO fully update v-model usage in custom components on Vue3 update
value: string;
disabled?: boolean;
errorMessage?: string;
placeholder?: string;
@@ -22,49 +21,43 @@ const props = withDefaults(
);
const emit = defineEmits<{
(e: "input", value: string): void;
(e: "onEnter", value: string): void;
(e: "onEscape"): void;
}>();
const localValue = computed({
get: () => props.value,
set: (v) => emit("input", v)
});
const handleKeydown = ({ key }) => {
switch (key) {
case "Enter":
// Explicitly check that all rule props return true
if (!props.rules?.every((rule) => rule(localValue.value) === true)) return;
if (!props.rules?.every((rule) => rule(value.value) === true)) return;
emit("onEnter", localValue.value);
emit("onEnter", value.value);
break;
case "Escape":
emit("onEscape");
break;
}
};
</script>
// TODO: fix error text theming
</script>
<template>
<div class="d-flex">
<v-col :cols="labelCols || 12 - inputCols" class="d-flex align-center pl-0">
<v-col :cols="labelCols || 12 - inputCols" class="d-flex align-center pl-0 pt-10px pb-10px">
<tooltipped-label :tooltip="tooltip" :label="label" />
</v-col>
<v-col :cols="inputCols" class="d-flex align-center pr-0">
<v-col :cols="inputCols" class="d-flex align-center pr-0 pt-10px pb-10px">
<v-text-field
v-model="localValue"
dark
dense
color="accent"
v-model="value"
density="compact"
color="primary"
:placeholder="placeholder"
:disabled="disabled"
:error-messages="errorMessage"
:rules="rules"
hide-details="auto"
class="light-error"
variant="underlined"
@keydown="handleKeydown"
/>
</v-col>
@@ -75,9 +68,3 @@ const handleKeydown = ({ key }) => {
margin-top: 0px;
}
</style>
<style>
.light-error .error--text {
color: red !important;
caret-color: red !important;
}
</style>

View File

@@ -0,0 +1,205 @@
<template>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 100 100"
preserveAspectRatio="xMidYMid"
width="200"
height="200"
style="shape-rendering: auto; display: block; background: rgba(0, 100, 146, 0)"
xmlns:xlink="http://www.w3.org/1999/xlink"
>
<g>
<g transform="translate(80,50)">
<g transform="rotate(0)">
<circle class="loader-circle" fill-opacity="1" fill="#ffd943" r="6" cy="0" cx="0">
<animateTransform
repeatCount="indefinite"
dur="0.9345794392523364s"
keyTimes="0;1"
values="1.5 1.5;1 1"
begin="-0.8177570093457943s"
type="scale"
attributeName="transform"
></animateTransform>
<animate
begin="-0.8177570093457943s"
values="1;0"
repeatCount="indefinite"
dur="0.9345794392523364s"
keyTimes="0;1"
attributeName="fill-opacity"
></animate>
</circle>
</g>
</g>
<g transform="translate(71.21320343559643,71.21320343559643)">
<g transform="rotate(45)">
<circle class="loader-circle" fill-opacity="0.875" fill="#ffd943" r="6" cy="0" cx="0">
<animateTransform
repeatCount="indefinite"
dur="0.9345794392523364s"
keyTimes="0;1"
values="1.5 1.5;1 1"
begin="-0.7009345794392523s"
type="scale"
attributeName="transform"
></animateTransform>
<animate
begin="-0.7009345794392523s"
values="1;0"
repeatCount="indefinite"
dur="0.9345794392523364s"
keyTimes="0;1"
attributeName="fill-opacity"
></animate>
</circle>
</g>
</g>
<g transform="translate(50,80)">
<g transform="rotate(90)">
<circle class="loader-circle" fill-opacity="0.75" fill="#ffd943" r="6" cy="0" cx="0">
<animateTransform
repeatCount="indefinite"
dur="0.9345794392523364s"
keyTimes="0;1"
values="1.5 1.5;1 1"
begin="-0.5841121495327103s"
type="scale"
attributeName="transform"
></animateTransform>
<animate
begin="-0.5841121495327103s"
values="1;0"
repeatCount="indefinite"
dur="0.9345794392523364s"
keyTimes="0;1"
attributeName="fill-opacity"
></animate>
</circle>
</g>
</g>
<g transform="translate(28.786796564403577,71.21320343559643)">
<g transform="rotate(135)">
<circle class="loader-circle" fill-opacity="0.625" fill="#ffd943" r="6" cy="0" cx="0">
<animateTransform
repeatCount="indefinite"
dur="0.9345794392523364s"
keyTimes="0;1"
values="1.5 1.5;1 1"
begin="-0.4672897196261682s"
type="scale"
attributeName="transform"
></animateTransform>
<animate
begin="-0.4672897196261682s"
values="1;0"
repeatCount="indefinite"
dur="0.9345794392523364s"
keyTimes="0;1"
attributeName="fill-opacity"
></animate>
</circle>
</g>
</g>
<g transform="translate(20,50.00000000000001)">
<g transform="rotate(180)">
<circle class="loader-circle" fill-opacity="0.5" fill="#ffd943" r="6" cy="0" cx="0">
<animateTransform
repeatCount="indefinite"
dur="0.9345794392523364s"
keyTimes="0;1"
values="1.5 1.5;1 1"
begin="-0.35046728971962615s"
type="scale"
attributeName="transform"
></animateTransform>
<animate
begin="-0.35046728971962615s"
values="1;0"
repeatCount="indefinite"
dur="0.9345794392523364s"
keyTimes="0;1"
attributeName="fill-opacity"
></animate>
</circle>
</g>
</g>
<g transform="translate(28.78679656440357,28.786796564403577)">
<g transform="rotate(225)">
<circle class="loader-circle" fill-opacity="0.375" fill="#ffd943" r="6" cy="0" cx="0">
<animateTransform
repeatCount="indefinite"
dur="0.9345794392523364s"
keyTimes="0;1"
values="1.5 1.5;1 1"
begin="-0.2336448598130841s"
type="scale"
attributeName="transform"
></animateTransform>
<animate
begin="-0.2336448598130841s"
values="1;0"
repeatCount="indefinite"
dur="0.9345794392523364s"
keyTimes="0;1"
attributeName="fill-opacity"
></animate>
</circle>
</g>
</g>
<g transform="translate(49.99999999999999,20)">
<g transform="rotate(270)">
<circle class="loader-circle" fill-opacity="0.25" fill="#ffd943" r="6" cy="0" cx="0">
<animateTransform
repeatCount="indefinite"
dur="0.9345794392523364s"
keyTimes="0;1"
values="1.5 1.5;1 1"
begin="-0.11682242990654206s"
type="scale"
attributeName="transform"
></animateTransform>
<animate
begin="-0.11682242990654206s"
values="1;0"
repeatCount="indefinite"
dur="0.9345794392523364s"
keyTimes="0;1"
attributeName="fill-opacity"
></animate>
</circle>
</g>
</g>
<g transform="translate(71.21320343559643,28.78679656440357)">
<g transform="rotate(315)">
<circle class="loader-circle" fill-opacity="0.125" fill="#ffd943" r="6" cy="0" cx="0">
<animateTransform
repeatCount="indefinite"
dur="0.9345794392523364s"
keyTimes="0;1"
values="1.5 1.5;1 1"
begin="0s"
type="scale"
attributeName="transform"
></animateTransform>
<animate
begin="0s"
values="1;0"
repeatCount="indefinite"
dur="0.9345794392523364s"
keyTimes="0;1"
attributeName="fill-opacity"
></animate>
</circle>
</g>
</g>
<g></g>
</g>
<!-- [ldio] generated by https://loading.io -->
</svg>
</template>
<style scoped lang="scss">
.loader-circle {
fill: rgb(var(--v-theme-buttonActive));
}
</style>

View File

@@ -1,13 +1,13 @@
<script setup lang="ts">
import TooltippedLabel from "@/components/common/pv-tooltipped-label.vue";
import { computed } from "vue";
const props = withDefaults(
const value = defineModel<number>({
required: true
});
withDefaults(
defineProps<{
label?: string;
tooltip?: string;
// TODO fully update v-model usage in custom components on Vue3 update
value: number;
disabled?: boolean;
labelCols?: number;
rules?: ((v: number) => boolean | string)[];
@@ -20,30 +20,27 @@ const props = withDefaults(
}
);
const emit = defineEmits<{
(e: "input", value: number): void;
}>();
const localValue = computed({
get: () => props.value,
set: (v) => emit("input", parseFloat(v as unknown as string))
get: () => value.value,
set: (v) => (value.value = parseFloat(v as unknown as string))
});
</script>
<template>
<div class="d-flex">
<v-col :cols="labelCols" class="d-flex pl-0 align-center">
<v-col :cols="labelCols" class="d-flex pl-0 pt-10px pb-10px align-center">
<tooltipped-label :tooltip="tooltip" :label="label" />
</v-col>
<v-col class="pr-0">
<v-col class="pr-0 pt-10px pb-10px">
<v-text-field
v-model="localValue"
dark
class="mt-0 pt-0"
density="compact"
hide-details
single-line
color="accent"
color="primary"
type="number"
variant="underlined"
style="width: 70px"
:step="step"
:disabled="disabled"

View File

@@ -1,13 +1,13 @@
<script setup lang="ts">
import { computed } from "vue";
import TooltippedLabel from "@/components/common/pv-tooltipped-label.vue";
const value = defineModel<number>({
required: true
});
const props = withDefaults(
withDefaults(
defineProps<{
label?: string;
tooltip?: string;
// TODO fully update v-model usage in custom components on Vue3 update
value: number;
disabled?: boolean;
inputCols?: number;
list: string[];
@@ -17,39 +17,25 @@ const props = withDefaults(
inputCols: 8
}
);
const emit = defineEmits<{
(e: "input", value: number): void;
}>();
const localValue = computed({
get: () => props.value,
set: (v) => emit("input", v)
});
</script>
<template>
<div class="d-flex">
<v-col :cols="12 - inputCols" class="d-flex align-center pl-0">
<v-col :cols="12 - inputCols" class="d-flex align-center pl-0 pt-10px pb-10px">
<tooltipped-label :tooltip="tooltip" :label="label" />
</v-col>
<v-col :cols="inputCols" class="d-flex align-center pr-0">
<v-radio-group v-model="localValue" row dark :mandatory="true" hide-details="auto">
<v-col :cols="inputCols" class="pr-0 pt-10px pb-10px">
<v-radio-group v-model="value" row:mandatory="true" inline hide-details="auto">
<v-radio
v-for="(radioName, index) in list"
:key="index"
color="#ffd843"
:label="radioName"
:value="index"
color="rgb(var(--v-theme-primary))"
:label="radioName"
:model-value="index"
:disabled="disabled"
/>
</v-radio-group>
</v-col>
</div>
</template>
<style scoped>
.v-input--radio-group {
padding-top: 0;
margin-top: 0;
}
</style>

View File

@@ -1,14 +1,15 @@
<script setup lang="ts">
import { computed } from "vue";
import TooltippedLabel from "@/components/common/pv-tooltipped-label.vue";
import type { WebsocketNumberPair } from "@/types/WebsocketDataTypes";
const value = defineModel<[number, number] | WebsocketNumberPair>({
required: true
});
const props = withDefaults(
defineProps<{
label?: string;
tooltip?: string;
// TODO fully update v-model usage in custom components on Vue3 update
// value: [number, number] | WebsocketNumberPair, // Vue doesnt like Union types for the value prop for some reason.
value: [number, number];
min: number;
max: number;
step?: number;
@@ -24,19 +25,15 @@ const props = withDefaults(
}
);
const emit = defineEmits<{
(e: "input", value: [number, number]): void;
}>();
const localValue = computed<[number, number]>({
get: (): [number, number] => {
return Object.values(props.value) as [number, number];
return Object.values(value.value) as [number, number];
},
set: (v) => {
for (let i = 0; i < v.length; i++) {
v[i] = parseFloat(v[i] as unknown as string);
}
emit("input", v);
value.value = v;
}
});
@@ -69,45 +66,46 @@ const checkNumberRange = (v: string): boolean => {
:min="min"
:disabled="disabled"
hide-details
class="align-center"
dark
:color="inverted ? 'rgba(255, 255, 255, 0.2)' : 'accent'"
:track-color="inverted ? 'accent' : undefined"
thumb-color="accent"
class="align-center ml-0 mr-0"
color="primary"
:track-color="inverted ? 'primary' : undefined"
thumb-color="primary"
:step="step"
>
<template #prepend>
<v-text-field
:value="localValue[0]"
dark
color="accent"
:model-value="localValue[0]"
color="primary"
class="mt-0 pt-0"
density="compact"
hide-details
single-line
variant="underlined"
:max="max"
:min="min"
:step="step"
:rules="[checkNumberRange]"
type="number"
style="width: 60px"
@input="(v) => changeFromSlot(v, 0)"
@update:modelValue="(v) => changeFromSlot(v, 0)"
/>
</template>
<template #append>
<v-text-field
:value="localValue[1]"
dark
color="accent"
:model-value="localValue[1]"
color="primary"
class="mt-0 pt-0"
density="compact"
hide-details
single-line
variant="underlined"
:max="max"
:min="min"
:step="step"
:rules="[checkNumberRange]"
type="number"
style="width: 60px"
@input="(v) => changeFromSlot(v, 1)"
@update:modelValue="(v) => changeFromSlot(v, 1)"
/>
</template>
</v-range-slider>

View File

@@ -7,14 +7,13 @@ export interface SelectItem {
value: string | number;
disabled?: boolean;
}
const value = defineModel<string | number | undefined>({ required: true });
const props = withDefaults(
defineProps<{
label?: string;
tooltip?: string;
selectCols?: number;
// TODO fully update v-model usage in custom components on Vue3 update
value: any;
disabled?: boolean;
items: string[] | number[] | SelectItem[];
}>(),
@@ -24,15 +23,6 @@ const props = withDefaults(
}
);
const emit = defineEmits<{
(e: "input", value: string): void;
}>();
const localValue = computed({
get: () => props.value,
set: (v) => emit("input", v)
});
// Computed in case items changes
const items = computed<SelectItem[]>(() => {
// Trivial case for empty list; we have no data
@@ -50,21 +40,20 @@ const items = computed<SelectItem[]>(() => {
<template>
<div class="d-flex">
<v-col :cols="12 - selectCols" class="d-flex align-center pl-0">
<v-col :cols="12 - selectCols" class="d-flex align-center pl-0 pt-10px pb-10px">
<tooltipped-label :tooltip="tooltip" :label="label" />
</v-col>
<v-col :cols="selectCols" class="d-flex align-center pr-0">
<v-col :cols="selectCols" class="d-flex align-center pr-0 pt-10px pb-10px">
<v-select
v-model="localValue"
v-model="value"
:items="items"
item-text="name"
item-title="name"
item-value="value"
item-disabled="disabled"
dark
color="accent"
item-color="secondary"
item-props.disabled="disabled"
:disabled="disabled"
hide-details="auto"
variant="underlined"
density="compact"
/>
</v-col>
</div>

View File

@@ -1,29 +1,21 @@
<script setup lang="ts">
import { computed } from "vue";
import TooltippedLabel from "@/components/common/pv-tooltipped-label.vue";
import { computed } from "vue";
const props = withDefaults(
defineProps<{
label?: string;
tooltip?: string;
// TODO fully update v-model usage in custom components on Vue3 update
value: number;
modelValue: number;
min: number;
max: number;
step?: number;
disabled?: boolean;
sliderCols?: number;
}>(),
{
step: 1,
disabled: false,
sliderCols: 8
}
{ step: 1, disabled: false, sliderCols: 8 }
);
const emit = defineEmits<{
(e: "input", value: number): void;
}>();
const emit = defineEmits<{ (e: "update:modelValue", value: number): void }>();
// Debounce function
function debounce(func: (...args: any[]) => void, wait: number) {
@@ -35,29 +27,28 @@ function debounce(func: (...args: any[]) => void, wait: number) {
}
const debouncedEmit = debounce((v: number) => {
emit("input", v);
emit("update:modelValue", v);
}, 20);
const localValue = computed({
get: () => props.value,
get: () => props.modelValue,
set: (v) => debouncedEmit(parseFloat(v as unknown as string))
});
</script>
<template>
<div class="d-flex">
<v-col :cols="12 - sliderCols" class="pl-0 d-flex align-center">
<v-col :cols="12 - sliderCols" class="pl-0 pt-10px pb-10px d-flex align-center">
<tooltipped-label :tooltip="tooltip" :label="label" />
</v-col>
<v-col :cols="sliderCols - 1">
<v-col :cols="sliderCols - 1" class="pl-0 pt-10px pb-10px">
<v-slider
v-model="localValue"
dark
class="align-center"
:max="max"
:min="min"
hide-details
color="accent"
color="primary"
:disabled="disabled"
:step="step"
append-icon="mdi-menu-right"
@@ -66,18 +57,19 @@ const localValue = computed({
@click:prepend="localValue -= step"
/>
</v-col>
<v-col :cols="1" class="pr-0">
<v-col :cols="1" class="pr-0 pt-10px pb-10px">
<v-text-field
:value="localValue"
dark
color="accent"
:model-value="localValue"
color="primary"
:max="max"
:min="min"
:disabled="disabled"
class="mt-0 pt-0"
density="compact"
hide-details
single-line
type="number"
variant="underlined"
style="width: 100%"
:step="step"
:hide-spin-buttons="true"

View File

@@ -1,34 +1,11 @@
<script setup lang="ts">
import TooltippedLabel from "@/components/common/pv-tooltipped-label.vue";
import { computed } from "vue";
const props = withDefaults(
defineProps<{
label?: string;
tooltip?: string;
// TODO fully update v-model usage in custom components on Vue3 update
value: boolean;
disabled?: boolean;
labelCols?: number;
switchCols?: number;
dense?: boolean;
}>(),
{
disabled: false,
labelCols: 2,
switchCols: 8,
dense: false
}
const value = defineModel<boolean>();
withDefaults(
defineProps<{ label?: string; tooltip?: string; disabled?: boolean; labelCols?: number; switchCols?: number }>(),
{ disabled: false, labelCols: 2, switchCols: 8 }
);
const emit = defineEmits<{
(e: "input", value: boolean): void;
}>();
const localValue = computed({
get: () => props.value,
set: (v) => emit("input", v)
});
</script>
<template>
@@ -37,12 +14,13 @@ const localValue = computed({
<tooltipped-label :tooltip="tooltip" :label="label" />
</v-col>
<v-col :cols="switchCols || 12 - labelCols" class="d-flex align-center pr-0">
<v-switch v-model="localValue" dark :disabled="disabled" color="#ffd843" hide-details="auto" class="pb-1" />
<v-switch v-model="value" :disabled="disabled" color="primary" hide-details density="compact" />
</v-col>
</div>
</template>
<style scoped>
.v-input--selection-controls {
margin-top: 0px;
.v-col {
padding-top: 6px !important;
padding-bottom: 6px !important;
}
</style>

View File

@@ -7,9 +7,9 @@ defineProps<{
<template>
<div>
<v-tooltip :disabled="tooltip === undefined" right open-delay="300">
<template #activator="{ on, attrs }">
<span style="cursor: text !important" class="white--text" v-bind="attrs" v-on="on">{{ label }}</span>
<v-tooltip :disabled="tooltip === undefined" location="right" open-delay="300">
<template #activator="{ props }">
<span style="cursor: text !important" class="text-white" v-bind="props">{{ label }}</span>
</template>
<span>{{ tooltip }}</span>
</v-tooltip>

View File

@@ -8,6 +8,9 @@ import PvIcon from "@/components/common/pv-icon.vue";
import PvInput from "@/components/common/pv-input.vue";
import { PipelineType } from "@/types/PipelineTypes";
import { useSettingsStore } from "@/stores/settings/GeneralSettingsStore";
import { useTheme } from "vuetify";
const theme = useTheme();
const changeCurrentCameraUniqueName = (cameraUniqueName: string) => {
useCameraSettingsStore().setCurrentCameraUniqueName(cameraUniqueName, true);
@@ -53,10 +56,7 @@ const saveCameraNameEdit = (newName: string) => {
useCameraSettingsStore()
.changeCameraNickname(newName, false)
.then((response) => {
useStateStore().showSnackbarMessage({
color: "success",
message: response.data.text || response.data
});
useStateStore().showSnackbarMessage({ color: "success", message: response.data.text || response.data });
useCameraSettingsStore().currentCameraSettings.nickname = newName;
})
.catch((error) => {
@@ -241,15 +241,15 @@ const wrappedCameras = computed<SelectItem[]>(() =>
</script>
<template>
<v-card color="primary">
<v-row style="padding: 20px 12px 0 30px">
<v-card color="surface" class="rounded-12">
<v-row no-gutters class="pl-4 pt-2 pb-0">
<v-col cols="10" class="pa-0">
<pv-select
v-if="!isCameraNameEdit"
v-model="useStateStore().currentCameraUniqueName"
label="Camera"
:items="wrappedCameras"
@input="changeCurrentCameraUniqueName"
@update:modelValue="changeCurrentCameraUniqueName"
/>
<pv-input
v-else
@@ -270,7 +270,7 @@ const wrappedCameras = computed<SelectItem[]>(() =>
:disabled="checkCameraName(currentCameraName) !== true"
@click="() => saveCameraNameEdit(currentCameraName)"
/>
<pv-icon icon-name="mdi-cancel" color="red darken-2" @click="cancelCameraNameEdit" />
<pv-icon icon-name="mdi-cancel" color="red-darken-2" @click="cancelCameraNameEdit" />
</div>
<pv-icon
v-else
@@ -281,11 +281,11 @@ const wrappedCameras = computed<SelectItem[]>(() =>
/>
</v-col>
</v-row>
<v-row style="padding: 0 12px 0 30px">
<v-row no-gutters class="pl-4 pb-0 pt-0">
<v-col cols="10" class="pa-0">
<pv-select
v-if="!isPipelineNameEdit"
:value="useCameraSettingsStore().currentCameraSettings.currentPipelineIndex"
:model-value="useCameraSettingsStore().currentCameraSettings.currentPipelineIndex"
label="Pipeline"
tooltip="Each pipeline runs on a camera output and stores a unique set of processing settings"
:disabled="
@@ -294,7 +294,7 @@ const wrappedCameras = computed<SelectItem[]>(() =>
!useCameraSettingsStore().hasConnected
"
:items="pipelineNamesWrapper"
@input="(args) => useCameraSettingsStore().changeCurrentPipelineIndex(args, true)"
@update:modelValue="(args) => useCameraSettingsStore().changeCurrentPipelineIndex(args, true)"
/>
<pv-input
v-else
@@ -314,31 +314,36 @@ const wrappedCameras = computed<SelectItem[]>(() =>
:disabled="checkPipelineName(currentPipelineName) !== true"
@click="() => savePipelineNameEdit(currentPipelineName)"
/>
<pv-icon icon-name="mdi-cancel" color="red darken-2" @click="cancelPipelineNameEdit" />
<pv-icon icon-name="mdi-cancel" color="red-darken-2" @click="cancelPipelineNameEdit" />
</div>
<v-menu v-else-if="!useCameraSettingsStore().isDriverMode" offset-y nudge-bottom="7" auto>
<template #activator="{ on }">
<v-icon color="#c5c5c5" v-on="on" @click="cancelPipelineNameEdit"> mdi-menu </v-icon>
<v-menu v-else-if="!useCameraSettingsStore().isDriverMode" offset="7">
<template #activator="{ props }">
<v-icon color="#c5c5c5" v-bind="props" @click="cancelPipelineNameEdit"> mdi-menu </v-icon>
</template>
<v-list dark dense color="primary">
<v-list density="compact" color="primary">
<v-list-item @click="startPipelineNameEdit">
<v-list-item-title>
<pv-icon color="#c5c5c5" :right="true" icon-name="mdi-pencil" tooltip="Edit pipeline name" />
</v-list-item-title>
</v-list-item>
<v-list-item @click="duplicateCurrentPipeline">
<v-list-item-title>
<pv-icon color="#c5c5c5" :right="true" icon-name="mdi-content-copy" tooltip="Duplicate pipeline" />
</v-list-item-title>
</v-list-item>
<v-list-item @click="showCreatePipelineDialog">
<v-list-item-title>
<pv-icon color="#c5c5c5" :right="true" icon-name="mdi-plus" tooltip="Add new pipeline" />
<pv-icon color="green" :right="true" icon-name="mdi-plus" tooltip="Add new pipeline" />
</v-list-item-title>
</v-list-item>
<v-list-item @click="showPipelineDeletionConfirmationDialog = true">
<v-list-item-title>
<pv-icon color="red darken-2" :right="true" icon-name="mdi-delete" tooltip="Delete pipeline" />
</v-list-item-title>
</v-list-item>
<v-list-item @click="duplicateCurrentPipeline">
<v-list-item-title>
<pv-icon color="#c5c5c5" :right="true" icon-name="mdi-content-copy" tooltip="Duplicate pipeline" />
<pv-icon
color="red-darken-2"
:right="true"
icon-name="mdi-trash-can-outline"
tooltip="Delete pipeline"
/>
</v-list-item-title>
</v-list-item>
</v-list>
@@ -353,7 +358,7 @@ const wrappedCameras = computed<SelectItem[]>(() =>
/>
</v-col>
</v-row>
<v-row style="padding: 0 12px 24px 30px">
<v-row no-gutters class="pl-4 pt-0 pb-4">
<v-col cols="10" class="pa-0">
<pv-select
v-model="currentPipelineType"
@@ -365,78 +370,100 @@ const wrappedCameras = computed<SelectItem[]>(() =>
!useCameraSettingsStore().hasConnected
"
:items="pipelineTypesWrapper"
@input="showPipelineTypeChangeDialog = true"
@update:modelValue="showPipelineTypeChangeDialog = true"
/>
</v-col>
</v-row>
<v-dialog v-model="showPipelineCreationDialog" dark persistent width="500">
<v-card dark color="primary">
<v-card-title> Create New Pipeline </v-card-title>
<v-card-text>
<v-dialog v-model="showPipelineCreationDialog" persistent width="500">
<v-card color="surface">
<v-card-title class="pb-0"> Create New Pipeline </v-card-title>
<v-card-text class="pt-0 pb-0">
<pv-input
v-model="newPipelineName"
placeholder="Pipeline Name"
:label-cols="3"
:input-cols="12 - 3"
:label-cols="4"
:input-cols="12 - 4"
label="Pipeline Name"
:rules="[(v) => checkPipelineName(v)]"
/>
<pv-select
v-model="newPipelineType"
:select-cols="12 - 3"
:select-cols="12 - 4"
label="Tracking Type"
tooltip="Pipeline type, which changes the type of processing that will happen on input frames"
:items="validNewPipelineTypes"
/>
</v-card-text>
<v-divider />
<v-card-actions>
<v-spacer />
<v-card-actions class="pr-5 pt-10px pb-5">
<v-btn
color="#ffd843"
class="black--text"
color="buttonPassive"
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
@click="cancelPipelineCreation"
>
Cancel
</v-btn>
<v-btn
color="buttonActive"
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
:disabled="checkPipelineName(newPipelineName) !== true"
@click="createNewPipeline"
>
Save
Create
</v-btn>
<v-btn color="error" @click="cancelPipelineCreation"> Cancel </v-btn>
</v-card-actions>
</v-card>
</v-dialog>
<v-dialog v-model="showPipelineDeletionConfirmationDialog" dark width="500">
<v-card dark color="primary">
<v-card-title> Pipeline Deletion Confirmation </v-card-title>
<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 the pipeline
<b style="color: white; font-weight: bold">{{
useCameraSettingsStore().currentPipelineSettings.pipelineNickname
}}</b
>? This cannot be undone.
Are you sure you want to delete
<span style="color: white">"{{ useCameraSettingsStore().currentPipelineSettings.pipelineNickname }}"</span>?
This cannot be undone.
</v-card-text>
<v-divider />
<v-card-actions>
<v-spacer />
<v-btn color="error" @click="confirmDeleteCurrentPipeline"> Yes, I'm sure </v-btn>
<v-btn color="#ffd843" class="black--text" @click="showPipelineDeletionConfirmationDialog = false">
No, take me back
<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>
<v-dialog v-model="showPipelineTypeChangeDialog" persistent width="600">
<v-card color="primary" dark>
<v-card-title>Change Pipeline Type</v-card-title>
<v-card color="surface" dark>
<v-card-title class="pb-0">Change Pipeline Type</v-card-title>
<v-card-text>
Are you sure you want to change the current pipeline type? This will cause all the pipeline settings to be
overwritten and they will be lost. If this isn't what you want, duplicate this pipeline first or export
settings.
</v-card-text>
<v-divider />
<v-card-actions>
<v-spacer />
<v-btn color="error" @click="confirmChangePipelineType"> Yes, I'm sure </v-btn>
<v-btn color="#ffd843" class="black--text" @click="cancelChangePipelineType"> No, take me back </v-btn>
<v-card-actions class="pa-5 pt-0">
<v-btn
color="buttonPassive"
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
class="text-black"
@click="cancelChangePipelineType"
>
Cancel
</v-btn>
<v-btn
color="buttonActive"
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
@click="confirmChangePipelineType"
>
Confirm
</v-btn>
</v-card-actions>
</v-card>
</v-dialog>

View File

@@ -6,10 +6,7 @@ import { useSettingsStore } from "@/stores/settings/GeneralSettingsStore";
import { PipelineType } from "@/types/PipelineTypes";
import PhotonCameraStream from "@/components/app/photon-camera-stream.vue";
defineProps<{
// TODO fully update v-model usage in custom components on Vue3 update
value: number[];
}>();
const value = defineModel<number[]>();
const driverMode = computed<boolean>({
get: () => useCameraSettingsStore().isDriverMode,
@@ -42,34 +39,33 @@ const performanceRecommendation = computed<string>(() => {
</script>
<template>
<v-card color="primary" height="100%" class="d-flex flex-column" dark>
<v-card-title class="justify-space-between align-center pt-3 pb-3">
<v-card color="surface" height="100%" class="d-flex flex-column rounded-12" dark>
<v-card-title class="justify-space-between align-center pt-1 pb-1 d-flex">
<span>Cameras</span>
<v-chip
v-if="useCameraSettingsStore().currentCameraSettings.isConnected"
label
:color="fpsTooLow ? 'error' : 'transparent'"
:text-color="fpsTooLow ? '#C7EA46' : '#ff4d00'"
style="font-size: 1rem; padding: 0; margin: 0"
:color="fpsTooLow ? 'error' : 'primary'"
style="font-size: 1.1rem; padding: 0; margin: 0"
variant="text"
>
<span class="pr-1"
>Processing @ {{ Math.round(useStateStore().currentPipelineResults?.fps || 0) }}&nbsp;FPS &ndash;</span
<span class="pr-1">{{ Math.round(useStateStore().currentPipelineResults?.fps || 0) }}&nbsp;FPS &ndash;</span
><span>{{ performanceRecommendation }}</span>
</v-chip>
<v-chip v-else label color="transparent" text-color="red" style="font-size: 1rem; padding: 0; margin: 0">
<v-chip v-else label variant="text" color="red" style="font-size: 1rem; padding: 0; margin: 0">
<span class="pr-1"> Camera not connected </span>
</v-chip>
<v-switch
v-model="driverMode"
:disabled="useCameraSettingsStore().isCalibrationMode || useCameraSettingsStore().pipelineNames.length === 0"
label="Driver Mode"
color="accent"
color="primary"
hide-details="auto"
/>
</v-card-title>
<v-divider class="ml-3 mr-3" />
<v-row class="stream-viewer-container pa-3 align-center">
<v-col v-if="value.includes(0)" class="stream-view">
<v-col v-if="value?.includes(0)" class="stream-view">
<photon-camera-stream
id="input-camera-stream"
:camera-settings="useCameraSettingsStore().currentCameraSettings"
@@ -77,7 +73,7 @@ const performanceRecommendation = computed<string>(() => {
style="width: 100%; height: auto"
/>
</v-col>
<v-col v-if="value.includes(1)" class="stream-view">
<v-col v-if="value?.includes(1)" class="stream-view">
<photon-camera-stream
id="output-camera-stream"
:camera-settings="useCameraSettingsStore().currentCameraSettings"
@@ -90,9 +86,6 @@ const performanceRecommendation = computed<string>(() => {
</template>
<style scoped>
.v-input--switch {
margin-top: 0;
}
.stream-viewer-container {
display: flex;
justify-content: center;

View File

@@ -1,6 +1,6 @@
<script setup lang="ts">
import type { Component } from "vue";
import { computed, getCurrentInstance, onBeforeUpdate, ref } from "vue";
import { computed, ref } from "vue";
import { useCameraSettingsStore } from "@/stores/settings/CameraSettingsStore";
import { useStateStore } from "@/stores/StateStore";
import InputTab from "@/components/dashboard/tabs/InputTab.vue";
@@ -14,6 +14,10 @@ import TargetsTab from "@/components/dashboard/tabs/TargetsTab.vue";
import PnPTab from "@/components/dashboard/tabs/PnPTab.vue";
import Map3DTab from "@/components/dashboard/tabs/Map3DTab.vue";
import { WebsocketPipelineType } from "@/types/WebsocketDataTypes";
import { useDisplay } from "vuetify/lib/composables/display";
import { useTheme } from "vuetify";
const theme = useTheme();
interface ConfigOption {
tabName: string;
@@ -21,58 +25,25 @@ interface ConfigOption {
}
const allTabs = Object.freeze({
inputTab: {
tabName: "Input",
component: InputTab
},
thresholdTab: {
tabName: "Threshold",
component: ThresholdTab
},
contoursTab: {
tabName: "Contours",
component: ContoursTab
},
apriltagTab: {
tabName: "AprilTag",
component: AprilTagTab
},
arucoTab: {
tabName: "Aruco",
component: ArucoTab
},
objectDetectionTab: {
tabName: "Object Detection",
component: ObjectDetectionTab
},
outputTab: {
tabName: "Output",
component: OutputTab
},
targetsTab: {
tabName: "Targets",
component: TargetsTab
},
pnpTab: {
tabName: "PnP",
component: PnPTab
},
map3dTab: {
tabName: "3D",
component: Map3DTab
}
inputTab: { tabName: "Input", component: InputTab },
thresholdTab: { tabName: "Threshold", component: ThresholdTab },
contoursTab: { tabName: "Contours", component: ContoursTab },
apriltagTab: { tabName: "AprilTag", component: AprilTagTab },
arucoTab: { tabName: "Aruco", component: ArucoTab },
objectDetectionTab: { tabName: "Object Detection", component: ObjectDetectionTab },
outputTab: { tabName: "Output", component: OutputTab },
targetsTab: { tabName: "Targets", component: TargetsTab },
pnpTab: { tabName: "PnP", component: PnPTab },
map3dTab: { tabName: "3D", component: Map3DTab }
});
const selectedTabs = ref([0, 0, 0, 0]);
const getTabGroups = (): ConfigOption[][] => {
const smAndDown = getCurrentInstance()?.proxy.$vuetify.breakpoint.smAndDown || false;
const mdAndDown = getCurrentInstance()?.proxy.$vuetify.breakpoint.mdAndDown || false;
const lgAndDown = getCurrentInstance()?.proxy.$vuetify.breakpoint.lgAndDown || false;
const xl = getCurrentInstance()?.proxy.$vuetify.breakpoint.xl || false;
const { smAndDown, mdAndDown, lgAndDown, xl } = useDisplay();
if (smAndDown || useCameraSettingsStore().isDriverMode || (mdAndDown && !useStateStore().sidebarFolded)) {
const getTabGroups = (): ConfigOption[][] => {
if (smAndDown.value || useCameraSettingsStore().isDriverMode) {
return [Object.values(allTabs)];
} else if (mdAndDown || !useStateStore().sidebarFolded) {
} else if (mdAndDown.value || !useStateStore().sidebarFolded) {
return [
[
allTabs.inputTab,
@@ -85,7 +56,7 @@ const getTabGroups = (): ConfigOption[][] => {
],
[allTabs.targetsTab, allTabs.pnpTab, allTabs.map3dTab]
];
} else if (lgAndDown) {
} else if (lgAndDown.value) {
return [
[allTabs.inputTab],
[
@@ -98,7 +69,7 @@ const getTabGroups = (): ConfigOption[][] => {
],
[allTabs.targetsTab, allTabs.pnpTab, allTabs.map3dTab]
];
} else if (xl) {
} else if (xl.value) {
return [
[allTabs.inputTab],
[allTabs.thresholdTab],
@@ -135,45 +106,40 @@ const tabGroups = computed<ConfigOption[][]>(() => {
.filter((it) => it.length); // Remove empty tab groups
});
onBeforeUpdate(() => {
const onBeforeTabUpdate = () => {
// Force the current tab to the input tab on driver mode change
if (useCameraSettingsStore().isDriverMode) {
selectedTabs.value[0] = 0;
}
});
};
</script>
<template>
<v-row no-gutters class="tabGroups">
<template v-if="!useCameraSettingsStore().hasConnected">
<v-col cols="12">
<v-card color="error">
<v-card-title class="white--text">
Camera has not connected. Please check your connection and try again.
</v-card-title>
</v-card>
</v-col>
<v-alert
color="error"
density="compact"
text="Camera is not connected. Please check your connection and try again."
icon="mdi-alert-circle-outline"
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'tonal'"
/>
</template>
<template v-else>
<v-col
v-for="(tabGroupData, tabGroupIndex) in tabGroups"
:key="tabGroupIndex"
:cols="tabGroupIndex == 1 && useCameraSettingsStore().currentPipelineSettings.doMultiTarget ? 7 : ''"
:class="tabGroupIndex !== tabGroups.length - 1 && 'pr-3'"
@vue:before-update="onBeforeTabUpdate"
>
<v-card color="primary" height="100%" class="pr-4 pl-4">
<v-tabs
v-model="selectedTabs[tabGroupIndex]"
grow
background-color="primary"
dark
height="48"
slider-color="accent"
>
<v-card color="surface" height="100%" class="pr-5 pl-5 rounded-12">
<v-tabs v-model="selectedTabs[tabGroupIndex]" grow bg-color="surface" height="48" slider-color="buttonActive">
<v-tab v-for="(tabConfig, index) in tabGroupData" :key="index">
{{ tabConfig.tabName }}
</v-tab>
</v-tabs>
<div class="pl-2 pr-2 pt-3 pb-3">
<div class="pt-10px pb-10px">
<KeepAlive>
<Component :is="tabGroupData[selectedTabs[tabGroupIndex]].component" />
</KeepAlive>
@@ -185,6 +151,11 @@ onBeforeUpdate(() => {
</template>
<style>
.v-slide-group {
transition-duration: 0.28s;
transition-property: box-shadow, opacity, background;
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
}
.v-slide-group__next--disabled,
.v-slide-group__prev--disabled {
display: none !important;

View File

@@ -2,20 +2,12 @@
import { computed } from "vue";
import { useCameraSettingsStore } from "@/stores/settings/CameraSettingsStore";
import { useStateStore } from "@/stores/StateStore";
import { useTheme } from "vuetify";
import { PipelineType } from "@/types/PipelineTypes";
const props = defineProps<{
// TODO fully update v-model usage in custom components on Vue3 update
value: number[];
}>();
const theme = useTheme();
const emit = defineEmits<{
(e: "input", value: number[]): void;
}>();
const localValue = computed({
get: () => props.value,
set: (v) => emit("input", v)
});
const value = defineModel<number[]>();
const processingMode = computed<number>({
get: () => (useCameraSettingsStore().currentPipelineSettings.solvePNPEnabled ? 1 : 0),
@@ -30,25 +22,39 @@ const processingMode = computed<number>({
<template>
<v-card
:disabled="useCameraSettingsStore().isDriverMode || useStateStore().colorPickingMode"
class="mt-3"
color="primary"
style="height: 100%; display: flex; flex-direction: column"
class="mt-3 rounded-12"
color="surface"
style="flex-grow: 1; display: flex; flex-direction: column"
>
<v-row class="pa-3 pb-0 align-center">
<v-col class="pa-4">
<p style="color: white">Processing Mode</p>
<v-btn-toggle v-model="processingMode" mandatory dark class="fill">
<v-btn color="secondary" :disabled="!useCameraSettingsStore().hasConnected">
<v-icon left>mdi-square-outline</v-icon>
<v-btn-toggle v-model="processingMode" mandatory class="fill w-100">
<v-btn
color="buttonPassive"
:disabled="!useCameraSettingsStore().hasConnected"
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
class="w-50"
>
<template #prepend>
<v-icon size="large">mdi-square-outline</v-icon>
</template>
<span>2D</span>
</v-btn>
<v-btn
color="secondary"
color="buttonPassive"
:disabled="
!useCameraSettingsStore().hasConnected || !useCameraSettingsStore().isCurrentVideoFormatCalibrated
!useCameraSettingsStore().hasConnected ||
!useCameraSettingsStore().isCurrentVideoFormatCalibrated ||
useCameraSettingsStore().currentPipelineSettings.pipelineType == PipelineType.ObjectDetection ||
useCameraSettingsStore().currentPipelineSettings.pipelineType == PipelineType.ColoredShape
"
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
class="w-50"
>
<v-icon left>mdi-cube-outline</v-icon>
<template #prepend>
<v-icon size="large">mdi-cube-outline</v-icon>
</template>
<span>3D</span>
</v-btn>
</v-btn-toggle>
@@ -57,13 +63,21 @@ const processingMode = computed<number>({
<v-row class="pa-3 pt-0 align-center">
<v-col class="pa-4 pt-0">
<p style="color: white">Stream Display</p>
<v-btn-toggle v-model="localValue" :multiple="true" mandatory dark class="fill">
<v-btn color="secondary" class="fill">
<v-icon left class="mode-btn-icon">mdi-import</v-icon>
<v-btn-toggle v-model="value" :multiple="true" mandatory class="fill w-100">
<v-btn
color="buttonPassive"
class="fill w-50"
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
>
<v-icon start class="mode-btn-icon" size="large">mdi-import</v-icon>
<span class="mode-btn-label">Raw</span>
</v-btn>
<v-btn color="secondary" class="fill">
<v-icon left class="mode-btn-icon">mdi-export</v-icon>
<v-btn
color="buttonPassive"
class="fill w-50"
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
>
<v-icon start class="mode-btn-icon" size="large">mdi-export</v-icon>
<span class="mode-btn-label">Processed</span>
</v-btn>
</v-btn-toggle>
@@ -73,14 +87,8 @@ const processingMode = computed<number>({
</template>
<style scoped>
.v-btn-toggle.fill {
width: 100%;
height: 100%;
}
.v-btn-toggle.fill > .v-btn {
width: 50%;
height: 100%;
.v-btn--disabled {
background-color: #191919 !important;
}
th {

View File

@@ -3,22 +3,20 @@ import { PipelineType } from "@/types/PipelineTypes";
import PvSelect from "@/components/common/pv-select.vue";
import PvSlider from "@/components/common/pv-slider.vue";
import PvSwitch from "@/components/common/pv-switch.vue";
import { computed, getCurrentInstance } from "vue";
import { computed } from "vue";
import { useStateStore } from "@/stores/StateStore";
import type { ActivePipelineSettings } from "@/types/PipelineTypes";
import { useCameraSettingsStore } from "@/stores/settings/CameraSettingsStore";
import { useDisplay } from "vuetify";
// TODO fix pipeline typing in order to fix this, the store settings call should be able to infer that only valid pipeline type settings are exposed based on pre-checks for the entire config section
// Defer reference to store access method
const currentPipelineSettings = computed<ActivePipelineSettings>(
() => useCameraSettingsStore().currentPipelineSettings
);
const { mdAndDown } = useDisplay();
const interactiveCols = computed(() =>
(getCurrentInstance()?.proxy.$vuetify.breakpoint.mdAndDown || false) &&
(!useStateStore().sidebarFolded || useCameraSettingsStore().isDriverMode)
? 8
: 7
mdAndDown.value && (!useStateStore().sidebarFolded || useCameraSettingsStore().isDriverMode) ? 8 : 7
);
</script>
@@ -29,7 +27,7 @@ const interactiveCols = computed(() =>
label="Target family"
:items="['AprilTag 36h11 (6.5in)', 'AprilTag 16h5 (6in)']"
:select-cols="interactiveCols"
@input="(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ tagFamily: value }, false)"
@update:modelValue="(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ tagFamily: value }, false)"
/>
<pv-slider
v-model="currentPipelineSettings.decimate"
@@ -38,7 +36,7 @@ const interactiveCols = computed(() =>
tooltip="Increases FPS at the expense of range by reducing image resolution initially"
:min="1"
:max="8"
@input="(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ decimate: value }, false)"
@update:modelValue="(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ decimate: value }, false)"
/>
<pv-slider
v-model="currentPipelineSettings.blur"
@@ -48,7 +46,7 @@ const interactiveCols = computed(() =>
:min="0"
:max="5"
:step="0.1"
@input="(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ blur: value }, false)"
@update:modelValue="(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ blur: value }, false)"
/>
<pv-slider
v-model="currentPipelineSettings.threads"
@@ -57,7 +55,7 @@ const interactiveCols = computed(() =>
tooltip="Number of threads spawned by the AprilTag detector"
:min="1"
:max="8"
@input="(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ threads: value }, false)"
@update:modelValue="(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ threads: value }, false)"
/>
<pv-slider
v-model="currentPipelineSettings.decisionMargin"
@@ -66,7 +64,9 @@ const interactiveCols = computed(() =>
tooltip="Tags with a 'margin' (decoding quality score) less than this wil be rejected. Increase this to reduce the number of false positive detections"
:min="0"
:max="250"
@input="(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ decisionMargin: value }, false)"
@update:modelValue="
(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ decisionMargin: value }, false)
"
/>
<pv-slider
v-model="currentPipelineSettings.numIterations"
@@ -75,14 +75,18 @@ const interactiveCols = computed(() =>
tooltip="Number of iterations the pose estimation algorithm will run, 50-100 is a good starting point"
:min="0"
:max="500"
@input="(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ numIterations: value }, false)"
@update:modelValue="
(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ numIterations: value }, false)
"
/>
<pv-switch
v-model="currentPipelineSettings.refineEdges"
:switch-cols="interactiveCols"
label="Refine Edges"
tooltip="Further refines the AprilTag corner position initial estimate, suggested left on"
@input="(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ refineEdges: value }, false)"
@update:modelValue="
(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ refineEdges: value }, false)
"
/>
</div>
</template>

View File

@@ -5,20 +5,18 @@ import PvSlider from "@/components/common/pv-slider.vue";
import PvSwitch from "@/components/common/pv-switch.vue";
import PvRangeSlider from "@/components/common/pv-range-slider.vue";
import PvSelect from "@/components/common/pv-select.vue";
import { computed, getCurrentInstance } from "vue";
import { computed } from "vue";
import { useStateStore } from "@/stores/StateStore";
import { useDisplay } from "vuetify";
// TODO fix pipeline typing in order to fix this, the store settings call should be able to infer that only valid pipeline type settings are exposed based on pre-checks for the entire config section
// Defer reference to store access method
const currentPipelineSettings = computed<ActivePipelineSettings>(
() => useCameraSettingsStore().currentPipelineSettings
);
const { mdAndDown } = useDisplay();
const interactiveCols = computed(() =>
(getCurrentInstance()?.proxy.$vuetify.breakpoint.mdAndDown || false) &&
(!useStateStore().sidebarFolded || useCameraSettingsStore().isDriverMode)
? 8
: 7
mdAndDown.value && (!useStateStore().sidebarFolded || useCameraSettingsStore().isDriverMode) ? 8 : 7
);
</script>
@@ -29,7 +27,7 @@ const interactiveCols = computed(() =>
label="Target family"
:items="['AprilTag Family 36h11', 'AprilTag Family 16h5']"
:select-cols="interactiveCols"
@input="(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ tagFamily: value }, false)"
@update:modelValue="(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ tagFamily: value }, false)"
/>
<pv-range-slider
v-model="currentPipelineSettings.threshWinSizes"
@@ -39,7 +37,9 @@ const interactiveCols = computed(() =>
:max="255"
:slider-cols="interactiveCols"
:step="2"
@input="(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ threshWinSizes: value }, false)"
@update:modelValue="
(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ threshWinSizes: value }, false)
"
/>
<pv-slider
v-model="currentPipelineSettings.threshStepSize"
@@ -49,7 +49,9 @@ const interactiveCols = computed(() =>
:min="2"
:max="128"
:step="1"
@input="(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ threshStepSize: value }, false)"
@update:modelValue="
(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ threshStepSize: value }, false)
"
/>
<pv-slider
v-model="currentPipelineSettings.threshConstant"
@@ -59,21 +61,27 @@ const interactiveCols = computed(() =>
:min="0"
:max="128"
:step="1"
@input="(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ threshConstant: value }, false)"
@update:modelValue="
(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ threshConstant: value }, false)
"
/>
<pv-switch
v-model="currentPipelineSettings.useCornerRefinement"
label="Refine Corners"
tooltip="Further refine the initial corners with subpixel accuracy."
:switch-cols="interactiveCols"
@input="(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ useCornerRefinement: value }, false)"
@update:modelValue="
(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ useCornerRefinement: value }, false)
"
/>
<pv-switch
v-model="currentPipelineSettings.debugThreshold"
label="Debug Threshold"
tooltip="Display the first threshold step to the color stream."
:switch-cols="interactiveCols"
@input="(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ debugThreshold: value }, false)"
@update:modelValue="
(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ debugThreshold: value }, false)
"
/>
</div>
</template>

View File

@@ -4,8 +4,9 @@ import { type ActivePipelineSettings, PipelineType } from "@/types/PipelineTypes
import PvRangeSlider from "@/components/common/pv-range-slider.vue";
import PvSelect from "@/components/common/pv-select.vue";
import PvSlider from "@/components/common/pv-slider.vue";
import { computed, getCurrentInstance } from "vue";
import { computed } from "vue";
import { useStateStore } from "@/stores/StateStore";
import { useDisplay } from "vuetify";
// TODO fix pipeline typing in order to fix this, the store settings call should be able to infer that only valid pipeline type settings are exposed based on pre-checks for the entire config section
// Defer reference to store access method
@@ -48,12 +49,9 @@ const contourRadius = computed<[number, number]>({
}
}
});
const { mdAndDown } = useDisplay();
const interactiveCols = computed(() =>
(getCurrentInstance()?.proxy.$vuetify.breakpoint.mdAndDown || false) &&
(!useStateStore().sidebarFolded || useCameraSettingsStore().isDriverMode)
? 8
: 7
mdAndDown.value && (!useStateStore().sidebarFolded || useCameraSettingsStore().isDriverMode) ? 8 : 7
);
</script>
@@ -65,7 +63,7 @@ const interactiveCols = computed(() =>
tooltip="Used to determine how to calculate target landmarks, as well as aspect ratio"
:items="['Portrait', 'Landscape']"
:select-cols="interactiveCols"
@input="
@update:modelValue="
(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ contourTargetOrientation: value }, false)
"
/>
@@ -75,7 +73,9 @@ const interactiveCols = computed(() =>
tooltip="Chooses the sorting mode used to determine the 'best' targets to provide to user code"
:select-cols="interactiveCols"
:items="['Largest', 'Smallest', 'Highest', 'Lowest', 'Rightmost', 'Leftmost', 'Centermost']"
@input="(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ contourSortMode: value }, false)"
@update:modelValue="
(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ contourSortMode: value }, false)
"
/>
<pv-range-slider
v-model="contourArea"
@@ -84,7 +84,9 @@ const interactiveCols = computed(() =>
:max="100"
:slider-cols="interactiveCols"
:step="0.01"
@input="(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ contourArea: value }, false)"
@update:modelValue="
(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ contourArea: value }, false)
"
/>
<pv-range-slider
v-if="useCameraSettingsStore().currentPipelineType !== PipelineType.ColoredShape"
@@ -95,7 +97,9 @@ const interactiveCols = computed(() =>
:max="100"
:slider-cols="interactiveCols"
:step="0.1"
@input="(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ contourRatio: value }, false)"
@update:modelValue="
(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ contourRatio: value }, false)
"
/>
<pv-range-slider
v-if="useCameraSettingsStore().currentPipelineType === PipelineType.ColoredShape"
@@ -105,7 +109,9 @@ const interactiveCols = computed(() =>
:min="0"
:max="100"
:slider-cols="interactiveCols"
@input="(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ contourFullness: value }, false)"
@update:modelValue="
(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ contourFullness: value }, false)
"
/>
<pv-range-slider
v-if="currentPipelineSettings.pipelineType === PipelineType.ColoredShape"
@@ -115,7 +121,9 @@ const interactiveCols = computed(() =>
:min="0"
:max="4000"
:slider-cols="interactiveCols"
@input="(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ contourPerimeter: value }, false)"
@update:modelValue="
(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ contourPerimeter: value }, false)
"
/>
<pv-slider
v-model="useCameraSettingsStore().currentPipelineSettings.contourSpecklePercentage"
@@ -124,7 +132,7 @@ const interactiveCols = computed(() =>
:min="0"
:max="100"
:slider-cols="interactiveCols"
@input="
@update:modelValue="
(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ contourSpecklePercentage: value }, false)
"
/>
@@ -137,7 +145,9 @@ const interactiveCols = computed(() =>
:max="4"
:step="0.1"
:slider-cols="interactiveCols"
@input="(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ contourFilterRangeX: value }, false)"
@update:modelValue="
(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ contourFilterRangeX: value }, false)
"
/>
<pv-slider
v-model="currentPipelineSettings.contourFilterRangeY"
@@ -147,7 +157,9 @@ const interactiveCols = computed(() =>
:max="4"
:step="0.1"
:slider-cols="interactiveCols"
@input="(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ contourFilterRangeY: value }, false)"
@update:modelValue="
(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ contourFilterRangeY: value }, false)
"
/>
<pv-select
v-model="useCameraSettingsStore().currentPipelineSettings.contourGroupingMode"
@@ -155,7 +167,9 @@ const interactiveCols = computed(() =>
tooltip="Whether or not every two targets are paired with each other (good for e.g. 2019 targets)"
:select-cols="interactiveCols"
:items="['Single', 'Dual', 'Two or More']"
@input="(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ contourGroupingMode: value }, false)"
@update:modelValue="
(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ contourGroupingMode: value }, false)
"
/>
<pv-select
v-model="useCameraSettingsStore().currentPipelineSettings.contourIntersection"
@@ -164,7 +178,9 @@ const interactiveCols = computed(() =>
:select-cols="interactiveCols"
:items="['None', 'Up', 'Down', 'Left', 'Right']"
:disabled="useCameraSettingsStore().currentPipelineSettings.contourGroupingMode === 0"
@input="(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ contourIntersection: value }, false)"
@update:modelValue="
(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ contourIntersection: value }, false)
"
/>
</template>
<template v-else-if="currentPipelineSettings.pipelineType === PipelineType.ColoredShape">
@@ -174,7 +190,9 @@ const interactiveCols = computed(() =>
tooltip="The shape of targets to look for"
:select-cols="interactiveCols"
:items="['Circle', 'Polygon', 'Triangle', 'Quadrilateral']"
@input="(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ contourShape: value }, false)"
@update:modelValue="
(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ contourShape: value }, false)
"
/>
<pv-slider
v-if="currentPipelineSettings.contourShape >= 1"
@@ -185,7 +203,9 @@ const interactiveCols = computed(() =>
:min="0"
:max="100"
:slider-cols="interactiveCols"
@input="(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ accuracyPercentage: value }, false)"
@update:modelValue="
(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ accuracyPercentage: value }, false)
"
/>
<pv-slider
v-if="currentPipelineSettings.contourShape === 0"
@@ -196,7 +216,7 @@ const interactiveCols = computed(() =>
:min="1"
:max="100"
:slider-cols="interactiveCols"
@input="
@update:modelValue="
(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ circleDetectThreshold: value }, false)
"
/>
@@ -208,7 +228,9 @@ const interactiveCols = computed(() =>
:min="1"
:max="100"
:slider-cols="interactiveCols"
@input="(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ maxCannyThresh: value }, false)"
@update:modelValue="
(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ maxCannyThresh: value }, false)
"
/>
<pv-slider
v-if="currentPipelineSettings.contourShape === 0"
@@ -218,7 +240,9 @@ const interactiveCols = computed(() =>
:min="1"
:max="100"
:slider-cols="interactiveCols"
@input="(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ circleAccuracy: value }, false)"
@update:modelValue="
(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ circleAccuracy: value }, false)
"
/>
<pv-range-slider
v-if="currentPipelineSettings.contourShape === 0"
@@ -228,7 +252,9 @@ const interactiveCols = computed(() =>
:min="0"
:max="100"
:slider-cols="interactiveCols"
@input="(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ contourRadius: value }, false)"
@update:modelValue="
(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ contourRadius: value }, false)
"
/>
</template>
</div>

View File

@@ -3,10 +3,11 @@ import PvSlider from "@/components/common/pv-slider.vue";
import { useCameraSettingsStore } from "@/stores/settings/CameraSettingsStore";
import PvSwitch from "@/components/common/pv-switch.vue";
import PvSelect from "@/components/common/pv-select.vue";
import { computed, getCurrentInstance } from "vue";
import { computed } from "vue";
import { useSettingsStore } from "@/stores/settings/GeneralSettingsStore";
import { useStateStore } from "@/stores/StateStore";
import { getResolutionString } from "@/lib/PhotonUtils";
import { useDisplay } from "vuetify";
// Due to something with libcamera or something else IDK much about, the 90° rotations need to be disabled if the libcamera drivers are being used.
const cameraRotations = computed(() =>
@@ -62,12 +63,10 @@ const handleStreamResolutionChange = (value: number) => {
false
);
};
const { mdAndDown } = useDisplay();
const interactiveCols = computed(() =>
(getCurrentInstance()?.proxy.$vuetify.breakpoint.mdAndDown || false) &&
(!useStateStore().sidebarFolded || useCameraSettingsStore().isDriverMode)
? 8
: 7
mdAndDown.value && (!useStateStore().sidebarFolded || useCameraSettingsStore().isDriverMode) ? 8 : 7
);
</script>
@@ -75,11 +74,12 @@ const interactiveCols = computed(() =>
<div>
<pv-switch
v-model="useCameraSettingsStore().currentPipelineSettings.cameraAutoExposure"
class="pt-2"
label="Auto Exposure"
:switch-cols="interactiveCols"
tooltip="Enables or Disables camera automatic adjustment for current lighting conditions"
@input="(args) => useCameraSettingsStore().changeCurrentPipelineSetting({ cameraAutoExposure: args }, false)"
@update:modelValue="
(args) => useCameraSettingsStore().changeCurrentPipelineSetting({ cameraAutoExposure: args }, false)
"
/>
<pv-slider
v-model="useCameraSettingsStore().currentPipelineSettings.cameraExposureRaw"
@@ -90,7 +90,9 @@ const interactiveCols = computed(() =>
:max="useCameraSettingsStore().maxExposureRaw"
:slider-cols="interactiveCols"
:step="1"
@input="(args) => useCameraSettingsStore().changeCurrentPipelineSetting({ cameraExposureRaw: args }, false)"
@update:modelValue="
(args) => useCameraSettingsStore().changeCurrentPipelineSetting({ cameraExposureRaw: args }, false)
"
/>
<pv-slider
v-model="useCameraSettingsStore().currentPipelineSettings.cameraBrightness"
@@ -98,7 +100,9 @@ const interactiveCols = computed(() =>
:min="0"
:max="100"
:slider-cols="interactiveCols"
@input="(args) => useCameraSettingsStore().changeCurrentPipelineSetting({ cameraBrightness: args }, false)"
@update:modelValue="
(args) => useCameraSettingsStore().changeCurrentPipelineSetting({ cameraBrightness: args }, false)
"
/>
<pv-slider
v-if="useCameraSettingsStore().currentPipelineSettings.cameraGain >= 0"
@@ -108,7 +112,7 @@ const interactiveCols = computed(() =>
:min="0"
:max="100"
:slider-cols="interactiveCols"
@input="(args) => useCameraSettingsStore().changeCurrentPipelineSetting({ cameraGain: args }, false)"
@update:modelValue="(args) => useCameraSettingsStore().changeCurrentPipelineSetting({ cameraGain: args }, false)"
/>
<pv-slider
v-if="useCameraSettingsStore().currentPipelineSettings.cameraRedGain !== -1"
@@ -118,7 +122,9 @@ const interactiveCols = computed(() =>
:max="100"
:slider-cols="interactiveCols"
tooltip="Controls red automatic white balance gain, which affects how the camera captures colors in different conditions"
@input="(args) => useCameraSettingsStore().changeCurrentPipelineSetting({ cameraRedGain: args }, false)"
@update:modelValue="
(args) => useCameraSettingsStore().changeCurrentPipelineSetting({ cameraRedGain: args }, false)
"
/>
<pv-slider
v-if="useCameraSettingsStore().currentPipelineSettings.cameraBlueGain !== -1"
@@ -128,14 +134,18 @@ const interactiveCols = computed(() =>
:max="100"
:slider-cols="interactiveCols"
tooltip="Controls blue automatic white balance gain, which affects how the camera captures colors in different conditions"
@input="(args) => useCameraSettingsStore().changeCurrentPipelineSetting({ cameraBlueGain: args }, false)"
@update:modelValue="
(args) => useCameraSettingsStore().changeCurrentPipelineSetting({ cameraBlueGain: args }, false)
"
/>
<pv-switch
v-model="useCameraSettingsStore().currentPipelineSettings.cameraAutoWhiteBalance"
label="Auto White Balance"
:switch-cols="interactiveCols"
tooltip="Enables or Disables camera automatic adjustment for current lighting conditions"
@input="(args) => useCameraSettingsStore().changeCurrentPipelineSetting({ cameraAutoWhiteBalance: args }, false)"
@update:modelValue="
(args) => useCameraSettingsStore().changeCurrentPipelineSetting({ cameraAutoWhiteBalance: args }, false)
"
/>
<pv-slider
v-model="useCameraSettingsStore().currentPipelineSettings.cameraWhiteBalanceTemp"
@@ -144,7 +154,9 @@ const interactiveCols = computed(() =>
:min="useCameraSettingsStore().minWhiteBalanceTemp"
:max="useCameraSettingsStore().maxWhiteBalanceTemp"
:slider-cols="interactiveCols"
@input="(args) => useCameraSettingsStore().changeCurrentPipelineSetting({ cameraWhiteBalanceTemp: args }, false)"
@update:modelValue="
(args) => useCameraSettingsStore().changeCurrentPipelineSetting({ cameraWhiteBalanceTemp: args }, false)
"
/>
<pv-select
v-model="useCameraSettingsStore().currentPipelineSettings.inputImageRotationMode"
@@ -152,7 +164,9 @@ const interactiveCols = computed(() =>
tooltip="Rotates the camera stream. Rotation not available when camera has been calibrated."
:items="cameraRotations"
:select-cols="interactiveCols"
@input="(args) => useCameraSettingsStore().changeCurrentPipelineSetting({ inputImageRotationMode: args }, false)"
@update:modelValue="
(args) => useCameraSettingsStore().changeCurrentPipelineSetting({ inputImageRotationMode: args }, false)
"
/>
<pv-select
v-model="useCameraSettingsStore().currentPipelineSettings.cameraVideoModeIndex"
@@ -160,7 +174,7 @@ const interactiveCols = computed(() =>
tooltip="Resolution and FPS the camera should directly capture at"
:items="cameraResolutions"
:select-cols="interactiveCols"
@input="(args) => handleResolutionChange(args)"
@update:modelValue="(args) => handleResolutionChange(args)"
/>
<pv-select
v-model="useCameraSettingsStore().currentPipelineSettings.streamingFrameDivisor"
@@ -168,7 +182,7 @@ const interactiveCols = computed(() =>
tooltip="Resolution to which camera frames are downscaled for streaming to the dashboard"
:items="streamResolutions"
:select-cols="interactiveCols"
@input="(args) => handleStreamResolutionChange(args)"
@update:modelValue="(args) => handleStreamResolutionChange(args)"
/>
<pv-switch
v-if="useCameraSettingsStore().isDriverMode"
@@ -176,7 +190,7 @@ const interactiveCols = computed(() =>
label="Crosshair"
:switch-cols="interactiveCols"
tooltip="Enables or disables a crosshair overlay on the camera stream"
@input="(args) => useCameraSettingsStore().changeCurrentPipelineSetting({ crosshair: args }, false)"
@update:modelValue="(args) => useCameraSettingsStore().changeCurrentPipelineSetting({ crosshair: args }, false)"
/>
</div>
</template>

View File

@@ -11,12 +11,17 @@ const trackedTargets = computed<PhotonTarget[]>(() => useStateStore().currentPip
<div>
<v-row style="width: 100%">
<v-col>
<span class="white--text">Target Visualization</span>
<span class="text-white">Target Visualization</span>
</v-col>
</v-row>
<v-row style="width: 100%">
<v-col style="display: flex; align-items: center; justify-content: center">
<photon3d-visualizer :targets="trackedTargets" />
<Suspense>
<!-- Allows us to import three js when it's actually needed -->
<photon3d-visualizer :targets="trackedTargets" />
<template #fallback> Loading... </template>
</Suspense>
</v-col>
</v-row>
</div>

View File

@@ -2,9 +2,13 @@
import { useCameraSettingsStore } from "@/stores/settings/CameraSettingsStore";
import { type ObjectDetectionPipelineSettings, PipelineType } from "@/types/PipelineTypes";
import PvSlider from "@/components/common/pv-slider.vue";
import { computed, getCurrentInstance } from "vue";
import PvSelect from "@/components/common/pv-select.vue";
import PvRangeSlider from "@/components/common/pv-range-slider.vue";
import { computed } from "vue";
import { useStateStore } from "@/stores/StateStore";
import { useSettingsStore } from "@/stores/settings/GeneralSettingsStore";
import { useDisplay } from "vuetify";
import type { ObjectDetectionModelProperties } from "@/types/SettingTypes";
// TODO fix pipeline typing in order to fix this, the store settings call should be able to infer that only valid pipeline type settings are exposed based on pre-checks for the entire config section
// Defer reference to store access method
@@ -22,23 +26,38 @@ const contourRatio = computed<[number, number]>({
set: (v) => (useCameraSettingsStore().currentPipelineSettings.contourRatio = v)
});
const { mdAndDown } = useDisplay();
const interactiveCols = computed(() =>
(getCurrentInstance()?.proxy.$vuetify.breakpoint.mdAndDown || false) &&
(!useStateStore().sidebarFolded || useCameraSettingsStore().isDriverMode)
? 9
: 8
mdAndDown.value && (!useStateStore().sidebarFolded || useCameraSettingsStore().isDriverMode) ? 9 : 8
);
// Filters out models that are not supported by the current backend, and returns a flattened list.
const supportedModels = computed(() => {
const supportedModels = computed<ObjectDetectionModelProperties[]>(() => {
const { availableModels, supportedBackends } = useSettingsStore().general;
return supportedBackends.flatMap((backend) => availableModels[backend] || []);
const isSupported = (model: ObjectDetectionModelProperties) => {
// Check if model's family is in the list of supported backends
return supportedBackends.some((backend: string) => backend.toLowerCase() === model.family.toLowerCase());
};
// Filter models where the family is supported and flatten the list
return availableModels.filter(isSupported);
});
const selectedModel = computed({
get: () => supportedModels.value.indexOf(currentPipelineSettings.value.model),
get: () => {
const currentModel = currentPipelineSettings.value.model;
if (!currentModel) return undefined;
const index = supportedModels.value.findIndex((model) => model.modelPath === currentModel.modelPath);
return index === -1 ? undefined : index;
},
set: (v) => {
useCameraSettingsStore().changeCurrentPipelineSetting({ model: supportedModels.value[v] }, false);
if (v !== undefined && v >= 0 && v < supportedModels.value.length) {
const newModel = supportedModels.value[v];
useCameraSettingsStore().changeCurrentPipelineSetting({ model: newModel }, true);
}
}
});
</script>
@@ -50,8 +69,9 @@ const selectedModel = computed({
label="Model"
tooltip="The model used to detect objects in the camera feed"
:select-cols="interactiveCols"
:items="supportedModels"
:items="supportedModels.map((model) => model.nickname)"
/>
<pv-slider
v-model="currentPipelineSettings.confidence"
class="pt-2"
@@ -61,7 +81,20 @@ const selectedModel = computed({
:min="0"
:max="1"
:step="0.01"
@input="(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ confidence: value }, false)"
@update:modelValue="
(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ confidence: value }, false)
"
/>
<pv-slider
v-model="currentPipelineSettings.nms"
class="pt-2"
:slider-cols="interactiveCols"
label="NMS Threshold"
tooltip="The Non-Maximum Suppression threshold used to filter out overlapping detections. Higher values mean more detections are allowed through, but may result in false positives."
:min="0"
:max="1"
:step="0.01"
@update:modelValue="(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ nms: value }, false)"
/>
<pv-range-slider
v-model="contourArea"
@@ -70,7 +103,9 @@ const selectedModel = computed({
:max="100"
:slider-cols="interactiveCols"
:step="0.01"
@input="(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ contourArea: value }, false)"
@update:modelValue="
(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ contourArea: value }, false)
"
/>
<pv-range-slider
v-model="contourRatio"
@@ -80,7 +115,9 @@ const selectedModel = computed({
:max="100"
:slider-cols="interactiveCols"
:step="0.01"
@input="(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ contourRatio: value }, false)"
@update:modelValue="
(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ contourRatio: value }, false)
"
/>
<pv-select
v-model="useCameraSettingsStore().currentPipelineSettings.contourTargetOrientation"
@@ -88,8 +125,12 @@ const selectedModel = computed({
tooltip="Used to determine how to calculate target landmarks, as well as aspect ratio"
:items="['Portrait', 'Landscape']"
:select-cols="interactiveCols"
@input="
(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ contourTargetOrientation: value }, false)
@update:modelValue="
(value) =>
useCameraSettingsStore().changeCurrentPipelineSetting(
{ contourTargetOrientation: typeof value === 'string' ? Number(value) : value },
false
)
"
/>
<pv-select
@@ -98,7 +139,13 @@ const selectedModel = computed({
tooltip="Chooses the sorting mode used to determine the 'best' targets to provide to user code"
:select-cols="interactiveCols"
:items="['Largest', 'Smallest', 'Highest', 'Lowest', 'Rightmost', 'Leftmost', 'Centermost']"
@input="(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ contourSortMode: value }, false)"
@update:modelValue="
(value) =>
useCameraSettingsStore().changeCurrentPipelineSetting(
{ contourSortMode: typeof value === 'string' ? Number(value) : value },
false
)
"
/>
</div>
</template>

View File

@@ -3,9 +3,13 @@ import PvSelect from "@/components/common/pv-select.vue";
import { useCameraSettingsStore } from "@/stores/settings/CameraSettingsStore";
import { type ActivePipelineSettings, PipelineType, RobotOffsetPointMode } from "@/types/PipelineTypes";
import PvSwitch from "@/components/common/pv-switch.vue";
import { computed, getCurrentInstance } from "vue";
import { computed } from "vue";
import { RobotOffsetType } from "@/types/SettingTypes";
import { useStateStore } from "@/stores/StateStore";
import { useDisplay } from "vuetify";
import { useTheme } from "vuetify";
const theme = useTheme();
const isTagPipeline = computed(
() =>
@@ -45,12 +49,10 @@ const offsetPoints = computed<MetricItem[]>(() => {
const currentPipelineSettings = computed<ActivePipelineSettings>(
() => useCameraSettingsStore().currentPipelineSettings
);
const { mdAndDown } = useDisplay();
const interactiveCols = computed(() =>
(getCurrentInstance()?.proxy.$vuetify.breakpoint.mdAndDown || false) &&
(!useStateStore().sidebarFolded || useCameraSettingsStore().isDriverMode)
? 8
: 7
mdAndDown.value && (!useStateStore().sidebarFolded || useCameraSettingsStore().isDriverMode) ? 8 : 7
);
</script>
@@ -62,7 +64,7 @@ const interactiveCols = computed(() =>
tooltip="If enabled, up to five targets will be displayed and sent via PhotonLib, instead of just one"
:disabled="isTagPipeline"
:switch-cols="interactiveCols"
@input="
@update:modelValue="
(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ outputShowMultipleTargets: value }, false)
"
/>
@@ -78,7 +80,9 @@ const interactiveCols = computed(() =>
tooltip="If enabled, all visible fiducial targets will be used to provide a single pose estimate from their combined model."
:switch-cols="interactiveCols"
:disabled="!isTagPipeline"
@input="(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ doMultiTarget: value }, false)"
@update:modelValue="
(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ doMultiTarget: value }, false)
"
/>
<pv-switch
v-if="
@@ -92,7 +96,9 @@ const interactiveCols = computed(() =>
tooltip="If disabled, visible fiducial targets used for multi-target estimation will not also be used for single-target estimation."
:switch-cols="interactiveCols"
:disabled="!isTagPipeline || !currentPipelineSettings.doMultiTarget"
@input="(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ doSingleTargetAlways: value }, false)"
@update:modelValue="
(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ doSingleTargetAlways: value }, false)
"
/>
<pv-select
v-model="useCameraSettingsStore().currentPipelineSettings.contourTargetOffsetPointEdge"
@@ -100,7 +106,7 @@ const interactiveCols = computed(() =>
tooltip="Changes where the 'center' of the target is (used for calculating e.g. pitch and yaw)"
:items="['Center', 'Top', 'Bottom', 'Left', 'Right']"
:select-cols="interactiveCols"
@input="
@update:modelValue="
(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ contourTargetOffsetPointEdge: value }, false)
"
/>
@@ -111,7 +117,7 @@ const interactiveCols = computed(() =>
tooltip="Used to determine how to calculate target landmarks (e.g. the top, left, or bottom of the target)"
:items="['Portrait', 'Landscape']"
:select-cols="interactiveCols"
@input="
@update:modelValue="
(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ contourTargetOrientation: value }, false)
"
/>
@@ -121,22 +127,28 @@ const interactiveCols = computed(() =>
tooltip="Used to add an arbitrary offset to the location of the targeting crosshair"
:items="['None', 'Single Point', 'Dual Point']"
:select-cols="interactiveCols"
@input="(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ offsetRobotOffsetMode: value }, false)"
@update:modelValue="
(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ offsetRobotOffsetMode: value }, false)
"
/>
<table
v-if="useCameraSettingsStore().currentPipelineSettings.offsetRobotOffsetMode !== RobotOffsetPointMode.None"
class="metrics-table mt-3 mb-3"
>
<tr>
<th v-for="(item, itemIndex) in offsetPoints" :key="itemIndex" class="metric-item metric-item-title">
{{ item.header }}
</th>
</tr>
<tr>
<td v-for="(item, itemIndex) in offsetPoints" :key="itemIndex" class="metric-item">
{{ item.value }}
</td>
</tr>
<thead>
<tr>
<th v-for="(item, itemIndex) in offsetPoints" :key="itemIndex" class="metric-item metric-item-title">
{{ item.header }}
</th>
</tr>
</thead>
<tbody>
<tr>
<td v-for="(item, itemIndex) in offsetPoints" :key="itemIndex" class="metric-item">
{{ item.value }}
</td>
</tr>
</tbody>
</table>
<div
v-if="useCameraSettingsStore().currentPipelineSettings.offsetRobotOffsetMode !== RobotOffsetPointMode.None"
@@ -148,10 +160,11 @@ const interactiveCols = computed(() =>
>
<v-col cols="6" class="pl-0">
<v-btn
small
size="small"
block
color="accent"
class="black--text"
color="primary"
class="text-black"
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
@click="useCameraSettingsStore().takeRobotOffsetPoint(RobotOffsetType.Single)"
>
Take Point
@@ -159,9 +172,10 @@ const interactiveCols = computed(() =>
</v-col>
<v-col cols="6" class="pr-0">
<v-btn
small
size="small"
block
color="yellow darken-3"
color="error"
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
@click="useCameraSettingsStore().takeRobotOffsetPoint(RobotOffsetType.Clear)"
>
Clear All Points
@@ -174,10 +188,11 @@ const interactiveCols = computed(() =>
>
<v-col cols="6" lg="4" class="pl-0 pr-2">
<v-btn
small
size="small"
block
color="accent"
class="black--text"
color="primary"
class="text-black"
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
@click="useCameraSettingsStore().takeRobotOffsetPoint(RobotOffsetType.DualFirst)"
>
Take First Point
@@ -185,10 +200,11 @@ const interactiveCols = computed(() =>
</v-col>
<v-col cols="6" lg="4" class="pl-2 pr-0 pr-lg-2">
<v-btn
small
size="small"
block
color="accent"
class="black--text"
color="primary"
class="text-black"
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
@click="useCameraSettingsStore().takeRobotOffsetPoint(RobotOffsetType.DualSecond)"
>
Take Second Point
@@ -196,9 +212,10 @@ const interactiveCols = computed(() =>
</v-col>
<v-col cols="12" lg="4" class="pl-0 pl-lg-2 pr-0">
<v-btn
small
size="small"
block
color="yellow darken-3"
color="error"
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
@click="useCameraSettingsStore().takeRobotOffsetPoint(RobotOffsetType.Clear)"
>
Clear All Points
@@ -229,6 +246,6 @@ const interactiveCols = computed(() =>
.metric-item-title {
font-size: 18px;
text-decoration: underline;
text-decoration-color: #ffd843;
text-decoration-color: rgb(var(--v-theme-primary));
}
</style>

View File

@@ -3,14 +3,13 @@ import PvSelect from "@/components/common/pv-select.vue";
import { useCameraSettingsStore } from "@/stores/settings/CameraSettingsStore";
import { TargetModel } from "@/types/PipelineTypes";
import PvSlider from "@/components/common/pv-slider.vue";
import { computed, getCurrentInstance } from "vue";
import { computed } from "vue";
import { useStateStore } from "@/stores/StateStore";
import { useDisplay } from "vuetify";
const { mdAndDown } = useDisplay();
const interactiveCols = computed(() =>
(getCurrentInstance()?.proxy.$vuetify.breakpoint.mdAndDown || false) &&
(!useStateStore().sidebarFolded || useCameraSettingsStore().isDriverMode)
? 9
: 8
mdAndDown.value && (!useStateStore().sidebarFolded || useCameraSettingsStore().isDriverMode) ? 9 : 8
);
</script>
@@ -30,7 +29,9 @@ const interactiveCols = computed(() =>
{ name: '2025 Algae (16.25in)', value: TargetModel.ReefscapeAlgae }
]"
:select-cols="interactiveCols"
@input="(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ targetModel: value }, false)"
@update:modelValue="
(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ targetModel: value }, false)
"
/>
<pv-slider
v-model="useCameraSettingsStore().currentPipelineSettings.cornerDetectionAccuracyPercentage"
@@ -39,7 +40,7 @@ const interactiveCols = computed(() =>
label="Contour simplification Percentage"
:min="0"
:max="100"
@input="
@update:modelValue="
(value) =>
useCameraSettingsStore().changeCurrentPipelineSetting({ cornerDetectionAccuracyPercentage: value }, false)
"

View File

@@ -4,6 +4,9 @@ import { type ActivePipelineSettings, PipelineType } from "@/types/PipelineTypes
import { useStateStore } from "@/stores/StateStore";
import { angleModulus, toDeg } from "@/lib/MathUtils";
import { computed } from "vue";
import { useTheme } from "vuetify";
const theme = useTheme();
// TODO fix pipeline typing in order to fix this, the store settings call should be able to infer that only valid pipeline type settings are exposed based on pre-checks for the entire config section
// Defer reference to store access method
@@ -34,8 +37,8 @@ const resetCurrentBuffer = () => {
<template>
<div>
<v-row align="start" class="pb-4">
<v-simple-table dense class="pt-2 pb-12">
<v-row class="pb-4">
<v-table density="compact" class="pt-2 pb-12 pl-3 pr-3">
<template #default>
<thead>
<tr>
@@ -44,24 +47,24 @@ const resetCurrentBuffer = () => {
currentPipelineSettings.pipelineType === PipelineType.AprilTag ||
currentPipelineSettings.pipelineType === PipelineType.Aruco
"
class="text-center white--text"
class="text-center text-white"
>
Fiducial ID
</th>
<template v-if="currentPipelineSettings.pipelineType === PipelineType.ObjectDetection">
<th class="text-center white--text">Class</th>
<th class="text-center white--text">Confidence</th>
<th class="text-center text-white">Class</th>
<th class="text-center text-white">Confidence</th>
</template>
<template v-if="!useCameraSettingsStore().currentPipelineSettings.solvePNPEnabled">
<th class="text-center white--text">Pitch &theta;&deg;</th>
<th class="text-center white--text">Yaw &theta;&deg;</th>
<th class="text-center white--text">Skew &theta;&deg;</th>
<th class="text-center white--text">Area %</th>
<th class="text-center text-white">Pitch &theta;&deg;</th>
<th class="text-center text-white">Yaw &theta;&deg;</th>
<th class="text-center text-white">Skew &theta;&deg;</th>
<th class="text-center text-white">Area %</th>
</template>
<template v-else>
<th class="text-center white--text">X meters</th>
<th class="text-center white--text">Y meters</th>
<th class="text-center white--text">Z Angle &theta;&deg;</th>
<th class="text-center text-white">X meters</th>
<th class="text-center text-white">Y meters</th>
<th class="text-center text-white">Z Angle &theta;&deg;</th>
</template>
<template
v-if="
@@ -70,7 +73,7 @@ const resetCurrentBuffer = () => {
useCameraSettingsStore().currentPipelineSettings.solvePNPEnabled
"
>
<th class="text-center white--text">Ambiguity Ratio</th>
<th class="text-center text-white">Ambiguity Ratio</th>
</template>
</tr>
</thead>
@@ -78,7 +81,7 @@ const resetCurrentBuffer = () => {
<tr
v-for="(target, index) in useStateStore().currentPipelineResults?.targets"
:key="index"
class="white--text"
class="text-white"
>
<td
v-if="
@@ -91,13 +94,13 @@ const resetCurrentBuffer = () => {
</td>
<td
v-if="currentPipelineSettings.pipelineType === PipelineType.ObjectDetection"
class="text-center white--text"
class="text-center text-white"
>
{{ useStateStore().currentPipelineResults?.classNames[target.classId] }}
</td>
<td
v-if="currentPipelineSettings.pipelineType === PipelineType.ObjectDetection"
class="text-center white--text"
class="text-center text-white"
>
{{ target.confidence.toFixed(2) }}
</td>
@@ -105,7 +108,7 @@ const resetCurrentBuffer = () => {
<td class="text-center">{{ target.pitch.toFixed(2) }}&deg;</td>
<td class="text-center">{{ target.yaw.toFixed(2) }}&deg;</td>
<td class="text-center">{{ target.skew.toFixed(2) }}&deg;</td>
<td class="text-center">{{ target.area.toFixed(2) }}&deg;</td>
<td class="text-center">{{ target.area.toFixed(2) }}%</td>
</template>
<template v-else>
<td class="text-center">{{ target.pose?.x.toFixed(3) }}&nbsp;m</td>
@@ -126,7 +129,7 @@ const resetCurrentBuffer = () => {
</tr>
</tbody>
</template>
</v-simple-table>
</v-table>
</v-row>
<v-container
v-if="
@@ -136,122 +139,128 @@ const resetCurrentBuffer = () => {
useCameraSettingsStore().isCurrentVideoFormatCalibrated &&
useCameraSettingsStore().currentPipelineSettings.solvePNPEnabled
"
class="pl-3 pr-3"
>
<v-row class="pb-4 white--text">
<v-row class="pb-4 text-white">
<v-card-subtitle class="ma-0 pa-0 pb-4" style="font-size: 16px"
>Multi-tag pose, field-to-camera</v-card-subtitle
>
<v-simple-table dense>
<v-table density="compact">
<template #default>
<thead>
<tr class="white--text">
<th class="text-center white--text">X meters</th>
<th class="text-center white--text">Y meters</th>
<th class="text-center white--text">Z meters</th>
<th class="text-center white--text">X Angle &theta;&deg;</th>
<th class="text-center white--text">Y Angle &theta;&deg;</th>
<th class="text-center white--text">Z Angle &theta;&deg;</th>
<th class="text-center white--text">Tags</th>
<tr class="text-white">
<th class="text-center text-white">X meters</th>
<th class="text-center text-white">Y meters</th>
<th class="text-center text-white">Z meters</th>
<th class="text-center text-white">X Angle &theta;&deg;</th>
<th class="text-center text-white">Y Angle &theta;&deg;</th>
<th class="text-center text-white">Z Angle &theta;&deg;</th>
<th class="text-center text-white">Tags</th>
</tr>
</thead>
<tbody v-show="useStateStore().currentPipelineResults?.multitagResult">
<tr>
<td class="text-center white--text">
<td class="text-center text-white">
{{ useStateStore().currentPipelineResults?.multitagResult?.bestTransform.x.toFixed(3) }}&nbsp;m
</td>
<td class="text-center white--text">
<td class="text-center text-white">
{{ useStateStore().currentPipelineResults?.multitagResult?.bestTransform.y.toFixed(3) }}&nbsp;m
</td>
<td class="text-center white--text">
<td class="text-center text-white">
{{ useStateStore().currentPipelineResults?.multitagResult?.bestTransform.z.toFixed(3) }}&nbsp;m
</td>
<td class="text-center white--text">
<td class="text-center text-white">
{{
toDeg(useStateStore().currentPipelineResults?.multitagResult?.bestTransform.angle_x || 0).toFixed(
2
)
}}&deg;
</td>
<td class="text-center white--text">
<td class="text-center text-white">
{{
toDeg(useStateStore().currentPipelineResults?.multitagResult?.bestTransform.angle_y || 0).toFixed(
2
)
}}&deg;
</td>
<td class="text-center white--text">
<td class="text-center text-white">
{{
toDeg(useStateStore().currentPipelineResults?.multitagResult?.bestTransform.angle_z || 0).toFixed(
2
)
}}&deg;
</td>
<td class="text-center white--text">
<td class="text-center text-white">
{{ useStateStore().currentPipelineResults?.multitagResult?.fiducialIDsUsed }}
</td>
</tr>
</tbody>
</template>
</v-simple-table>
</v-table>
</v-row>
<v-row class="pb-4 white--text" style="display: flex; flex-direction: column">
<v-row class="pb-4 text-white" style="display: flex; flex-direction: column">
<v-card-subtitle class="ma-0 pa-0 pb-4 pr-4" style="font-size: 16px"
>Multi-tag pose standard deviation over the last
{{ useStateStore().currentMultitagBuffer?.length || "NaN" }}/100 samples
</v-card-subtitle>
<v-btn color="secondary" class="mb-4 mt-1" style="width: min-content" depressed @click="resetCurrentBuffer"
<v-btn
color="buttonActive"
class="mb-4 mt-1"
style="width: min-content"
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
@click="resetCurrentBuffer"
>Reset Samples</v-btn
>
<v-simple-table dense>
<v-table density="compact">
<template #default>
<thead>
<tr>
<th class="text-center white--text">X meters</th>
<th class="text-center white--text">Y meters</th>
<th class="text-center white--text">Z meters</th>
<th class="text-center white--text">X Angle &theta;&deg;</th>
<th class="text-center white--text">Y Angle &theta;&deg;</th>
<th class="text-center white--text">Z Angle &theta;&deg;</th>
<th class="text-center text-white">X meters</th>
<th class="text-center text-white">Y meters</th>
<th class="text-center text-white">Z meters</th>
<th class="text-center text-white">X Angle &theta;&deg;</th>
<th class="text-center text-white">Y Angle &theta;&deg;</th>
<th class="text-center text-white">Z Angle &theta;&deg;</th>
</tr>
</thead>
<tbody v-show="useStateStore().currentPipelineResults?.multitagResult">
<tr>
<td class="text-center white--text">
<td class="text-center text-white">
{{
calculateStdDev(useStateStore().currentMultitagBuffer?.map((v) => v.bestTransform.x) || []).toFixed(
5
)
}}&nbsp;m
</td>
<td class="text-center white--text">
<td class="text-center text-white">
{{
calculateStdDev(useStateStore().currentMultitagBuffer?.map((v) => v.bestTransform.y) || []).toFixed(
5
)
}}&nbsp;m
</td>
<td class="text-center white--text">
<td class="text-center text-white">
{{
calculateStdDev(useStateStore().currentMultitagBuffer?.map((v) => v.bestTransform.z) || []).toFixed(
5
)
}}&nbsp;m
</td>
<td class="text-center white--text">
<td class="text-center text-white">
{{
calculateStdDev(
useStateStore().currentMultitagBuffer?.map((v) => toDeg(v.bestTransform.angle_x)) || []
).toFixed(5)
}}&deg;
</td>
<td class="text-center white--text">
<td class="text-center text-white">
{{
calculateStdDev(
useStateStore().currentMultitagBuffer?.map((v) => toDeg(v.bestTransform.angle_y)) || []
).toFixed(5)
}}&deg;
</td>
<td class="text-center white--text">
<td class="text-center text-white">
{{
calculateStdDev(
useStateStore().currentMultitagBuffer?.map((v) => toDeg(v.bestTransform.angle_z)) || []
@@ -261,15 +270,18 @@ const resetCurrentBuffer = () => {
</tr>
</tbody>
</template>
</v-simple-table>
</v-table>
</v-row>
</v-container>
</div>
</template>
<style scoped lang="scss">
.v-data-table {
background-color: #006492 !important;
th {
padding-left: 8px !important;
padding-right: 8px !important;
}
.v-table {
width: 100%;
font-size: 1rem !important;
@@ -282,13 +294,9 @@ const resetCurrentBuffer = () => {
}
}
tbody {
:hover {
td {
background-color: #005281 !important;
}
}
tr {
td {
padding: 0 !important;
font-size: 1rem !important;
color: white !important;
}
@@ -307,7 +315,7 @@ const resetCurrentBuffer = () => {
}
::-webkit-scrollbar-thumb {
background-color: #ffd843;
background-color: rgb(var(--v-theme-accent));
border-radius: 10px;
}
}

View File

@@ -1,10 +1,14 @@
<script setup lang="ts">
import { useCameraSettingsStore } from "@/stores/settings/CameraSettingsStore";
import { computed, getCurrentInstance, onBeforeUnmount, onMounted } from "vue";
import { computed, onBeforeUnmount, onMounted } from "vue";
import PvRangeSlider from "@/components/common/pv-range-slider.vue";
import PvSwitch from "@/components/common/pv-switch.vue";
import { useStateStore } from "@/stores/StateStore";
import { ColorPicker, type HSV } from "@/lib/ColorPicker";
import { useDisplay } from "vuetify";
import { useTheme } from "vuetify";
const theme = useTheme();
const averageHue = computed<number>(() => {
const isHueInverted = useCameraSettingsStore().currentPipelineSettings.hueInverted;
@@ -123,12 +127,10 @@ onBeforeUnmount(() => {
cameraStream.removeEventListener("click", handleStreamClick);
});
const { mdAndDown } = useDisplay();
const interactiveCols = computed(() =>
(getCurrentInstance()?.proxy.$vuetify.breakpoint.mdAndDown || false) &&
(!useStateStore().sidebarFolded || useCameraSettingsStore().isDriverMode)
? 9
: 8
mdAndDown.value && (!useStateStore().sidebarFolded || useCameraSettingsStore().isDriverMode) ? 9 : 8
);
</script>
@@ -144,7 +146,7 @@ const interactiveCols = computed(() =>
:max="180"
:slider-cols="interactiveCols"
:inverted="useCameraSettingsStore().currentPipelineSettings.hueInverted"
@input="(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ hsvHue: value }, false)"
@update:modelValue="(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ hsvHue: value }, false)"
/>
<pv-range-slider
id="sat-slider"
@@ -155,7 +157,9 @@ const interactiveCols = computed(() =>
:min="0"
:max="255"
:slider-cols="interactiveCols"
@input="(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ hsvSaturation: value }, false)"
@update:modelValue="
(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ hsvSaturation: value }, false)
"
/>
<pv-range-slider
id="value-slider"
@@ -166,53 +170,73 @@ const interactiveCols = computed(() =>
:min="0"
:max="255"
:slider-cols="interactiveCols"
@input="(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ hsvValue: value }, false)"
@update:modelValue="(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ hsvValue: value }, false)"
/>
<pv-switch
v-model="useCameraSettingsStore().currentPipelineSettings.hueInverted"
label="Invert Hue"
:switch-cols="interactiveCols"
tooltip="Selects the hue range outside of the hue slider bounds instead of inside"
@input="(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ hueInverted: value }, false)"
@update:modelValue="
(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ hueInverted: value }, false)
"
/>
<div>
<div class="white--text pt-3">Color Picker</div>
<div class="text-white pt-3">Color Picker</div>
<div class="d-flex pt-3">
<template v-if="!useStateStore().colorPickingMode">
<v-col cols="4" class="pl-0 pr-2">
<v-btn
small
size="small"
block
color="accent"
class="black--text"
color="primary"
class="text-black"
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
@click="enableColorPicking(useCameraSettingsStore().currentPipelineSettings.hueInverted ? 2 : 3)"
>
<v-icon left> mdi-minus </v-icon>
<v-icon start size="large"> mdi-minus </v-icon>
Shrink Range
</v-btn>
</v-col>
<v-col cols="4" class="pl-0 pr-0">
<v-btn color="accent" class="black--text" small block @click="enableColorPicking(1)">
<v-icon left> mdi-plus-minus </v-icon>
<v-btn
color="primary"
class="text-black"
size="small"
block
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
@click="enableColorPicking(1)"
>
<v-icon start size="large"> mdi-plus-minus </v-icon>
{{ useCameraSettingsStore().currentPipelineSettings.hueInverted ? "Exclude" : "Set to" }} Average
</v-btn>
</v-col>
<v-col cols="4" class="pl-2 pr-0">
<v-btn
small
size="small"
block
color="accent"
class="black--text"
color="primary"
class="text-black"
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
@click="enableColorPicking(useCameraSettingsStore().currentPipelineSettings.hueInverted ? 3 : 2)"
>
<v-icon left> mdi-plus </v-icon>
<v-icon start size="large"> mdi-plus </v-icon>
Expand Range
</v-btn>
</v-col>
</template>
<template v-else>
<v-card-text class="pa-0 pt-3 pb-3">
<v-btn block color="accent" class="black--text" small @click="disableColorPicking"> Cancel </v-btn>
<v-btn
block
color="primary"
class="text-black"
size="small"
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
@click="disableColorPicking"
>
Cancel
</v-btn>
</v-card-text>
</template>
</div>
@@ -224,32 +248,32 @@ const interactiveCols = computed(() =>
.threshold-modifiers {
--averageHue: 0;
}
#hue-slider >>> .v-slider {
#hue-slider:deep(.v-slider__container) {
background: linear-gradient(to right, #f00 0%, #ff0 17%, #0f0 33%, #0ff 50%, #00f 67%, #f0f 83%, #f00 100%);
border-radius: 10px;
/* prettier-ignore */
box-shadow: 0 0 5px #333, inset 0 0 3px #333;
}
#sat-slider >>> .v-slider {
#sat-slider:deep(.v-slider__container) {
background: linear-gradient(to right, #fff 0%, hsl(var(--averageHue), 100%, 50%) 100%);
border-radius: 10px;
/* prettier-ignore */
box-shadow: 0 0 5px #333, inset 0 0 3px #333;
}
#value-slider >>> .v-slider {
#value-slider:deep(.v-slider__container) {
background: linear-gradient(to right, #000 0%, hsl(var(--averageHue), 100%, 50%) 100%);
border-radius: 10px;
/* prettier-ignore */
box-shadow: 0 0 5px #333, inset 0 0 3px #333;
}
>>> .v-slider__thumb {
:deep(.v-slider__thumb) {
outline: black solid thin;
}
.normal-slider >>> .v-slider__track-fill {
.normal-slider:deep(.v-slider__track-fill) {
outline: black solid thin;
}
.inverted-slider >>> .v-slider__track-background {
.inverted-slider:deep(.v-slider__track-background) {
outline: black solid thin;
}
</style>

View File

@@ -1,30 +1,26 @@
<script setup lang="ts">
import { useSettingsStore } from "@/stores/settings/GeneralSettingsStore";
import { Euler, Quaternion as ThreeQuat } from "three";
import type { Quaternion } from "@/types/PhotonTrackingTypes";
import { toDeg } from "@/lib/MathUtils";
const { Euler, Quaternion: ThreeQuat } = await import("three");
const quaternionToEuler = (rot_quat: Quaternion): { x: number; y: number; z: number } => {
const quat = new ThreeQuat(rot_quat.X, rot_quat.Y, rot_quat.Z, rot_quat.W);
const euler = new Euler().setFromQuaternion(quat, "ZYX");
return {
x: toDeg(euler.x),
y: toDeg(euler.y),
z: toDeg(euler.z)
};
return { x: toDeg(euler.x), y: toDeg(euler.y), z: toDeg(euler.z) };
};
</script>
<template>
<v-card dark style="background-color: #006492">
<v-card-title class="pa-6">AprilTag Field Layout</v-card-title>
<v-card-text class="pa-6 pt-0">
<v-card color="surface" class="rounded-12">
<v-card-title>AprilTag Field Layout</v-card-title>
<v-card-text class="pt-0">
<p>Field width: {{ useSettingsStore().currentFieldLayout.field.width.toFixed(2) }} meters</p>
<p>Field length: {{ useSettingsStore().currentFieldLayout.field.length.toFixed(2) }} meters</p>
<!-- Simple table height must be set here and in the CSS for the fixed-header to work -->
<v-simple-table fixed-header height="100%" dense dark>
<v-table fixed-header height="100%" density="compact">
<template #default>
<thead style="font-size: 1.25rem">
<tr>
@@ -47,21 +43,19 @@ const quaternionToEuler = (rot_quat: Quaternion): { x: number; y: number; z: num
</tr>
</tbody>
</template>
</v-simple-table>
</v-table>
</v-card-text>
</v-card>
</template>
<style scoped lang="scss">
.v-data-table {
.v-table {
width: 100%;
height: 100%;
text-align: center;
background-color: #006492 !important;
th,
td {
background-color: #006492 !important;
font-size: 1rem !important;
color: white !important;
}
@@ -70,10 +64,6 @@ const quaternionToEuler = (rot_quat: Quaternion): { x: number; y: number; z: num
font-family: monospace !important;
}
tbody :hover td {
background-color: #005281 !important;
}
::-webkit-scrollbar {
width: 0;
height: 0.55em;
@@ -86,7 +76,7 @@ const quaternionToEuler = (rot_quat: Quaternion): { x: number; y: number; z: num
}
::-webkit-scrollbar-thumb {
background-color: #ffd843;
background-color: rgb(var(--v-theme-accent));
border-radius: 10px;
}
}

View File

@@ -2,16 +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 { useTheme } from "vuetify";
const theme = useTheme();
const restartProgram = () => {
axios
.post("/utils/restartProgram")
.then(() => {
useStateStore().showSnackbarMessage({
message: "Successfully sent program restart request",
color: "success"
});
useStateStore().showSnackbarMessage({ message: "Successfully sent program restart request", color: "success" });
})
.catch((error) => {
// This endpoint always return 204 regardless of outcome
@@ -97,10 +98,7 @@ const handleOfflineUpdate = () => {
}
})
.then((response) => {
useStateStore().showSnackbarMessage({
message: response.data.text || response.data,
color: "success"
});
useStateStore().showSnackbarMessage({ message: response.data.text || response.data, color: "success" });
})
.catch((error) => {
if (error.response) {
@@ -140,10 +138,10 @@ enum ImportType {
ApriltagFieldLayout
}
const showImportDialog = ref(false);
const importType = ref<ImportType | number>(-1);
const importType = ref<ImportType | undefined>(undefined);
const importFile = ref<File | null>(null);
const handleSettingsImport = () => {
if (importType.value === -1 || importFile.value === null) return;
if (importType.value === undefined || importFile.value === null) return;
const formData = new FormData();
formData.append("data", importFile.value);
@@ -169,14 +167,9 @@ const handleSettingsImport = () => {
}
axios
.post(`/settings${settingsEndpoint}`, formData, {
headers: { "Content-Type": "multipart/form-data" }
})
.post(`/settings${settingsEndpoint}`, formData, { headers: { "Content-Type": "multipart/form-data" } })
.then((response) => {
useStateStore().showSnackbarMessage({
message: response.data.text || response.data,
color: "success"
});
useStateStore().showSnackbarMessage({ message: response.data.text || response.data, color: "success" });
})
.catch((error) => {
if (error.response) {
@@ -198,7 +191,7 @@ const handleSettingsImport = () => {
});
showImportDialog.value = false;
importType.value = -1;
importType.value = undefined;
importFile.value = null;
};
@@ -217,7 +210,7 @@ const nukePhotonConfigDirectory = () => {
.catch((error) => {
if (error.response) {
useStateStore().showSnackbarMessage({
message: "The backend is unable to fulfil the request to reset the device.",
message: "The backend is unable to fulfill the request to reset the device.",
color: "error"
});
} else if (error.request) {
@@ -237,52 +230,67 @@ const nukePhotonConfigDirectory = () => {
</script>
<template>
<v-card dark class="mb-3" style="background-color: #006492">
<v-card-title class="pa-6">Device Control</v-card-title>
<div class="pa-6 pt-0">
<v-card class="mb-3 rounded-12" color="surface">
<v-card-title>Device Control</v-card-title>
<div class="pa-5 pt-0">
<v-row>
<v-col cols="12" lg="4" md="6">
<v-btn color="error" @click="restartProgram">
<v-icon left class="open-icon"> mdi-restart </v-icon>
<v-btn
color="buttonActive"
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
@click="restartProgram"
>
<v-icon start class="open-icon" size="large"> mdi-restart </v-icon>
<span class="open-label">Restart PhotonVision</span>
</v-btn>
</v-col>
<v-col cols="12" lg="4" md="6">
<v-btn color="error" @click="restartDevice">
<v-icon left class="open-icon"> mdi-restart-alert </v-icon>
<v-btn
color="buttonActive"
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
@click="restartDevice"
>
<v-icon start class="open-icon" size="large"> mdi-restart-alert </v-icon>
<span class="open-label">Restart Device</span>
</v-btn>
</v-col>
<v-col cols="12" lg="4">
<v-btn color="secondary" @click="openOfflineUpdatePrompt">
<v-icon left class="open-icon"> mdi-upload </v-icon>
<v-btn
color="buttonPassive"
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
@click="openOfflineUpdatePrompt"
>
<v-icon start class="open-icon" size="large"> mdi-upload </v-icon>
<span class="open-label">Offline Update</span>
</v-btn>
<input ref="offlineUpdate" type="file" accept=".jar" style="display: none" @change="handleOfflineUpdate" />
</v-col>
</v-row>
<v-divider class="mt-3 pb-3" />
<v-row>
<v-col cols="12" sm="6">
<v-btn color="secondary" @click="() => (showImportDialog = true)">
<v-icon left class="open-icon"> mdi-import </v-icon>
<v-btn
color="buttonPassive"
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
@click="() => (showImportDialog = true)"
>
<v-icon start class="open-icon" size="large"> mdi-import </v-icon>
<span class="open-label">Import Settings</span>
</v-btn>
<v-dialog
v-model="showImportDialog"
width="600"
@input="
@update:modelValue="
() => {
importType = -1;
importType = undefined;
importFile = null;
}
"
>
<v-card color="primary" dark>
<v-card-title>Import Settings</v-card-title>
<v-card color="surface" dark>
<v-card-title class="pb-0">Import Settings</v-card-title>
<v-card-text>
Upload and apply previously saved or exported PhotonVision settings to this device
<v-row class="mt-6 ml-4">
<div class="pa-5 pb-0">
<pv-select
v-model="importType"
label="Type"
@@ -297,32 +305,35 @@ const nukePhotonConfigDirectory = () => {
:select-cols="10"
style="width: 100%"
/>
</v-row>
<v-row class="mt-6 ml-4 mr-8">
<v-file-input
v-model="importFile"
:disabled="importType === -1"
:error-messages="importType === -1 ? 'Settings type not selected' : ''"
class="pb-5"
variant="underlined"
:disabled="importType === undefined"
:error-messages="importType === undefined ? 'Settings type not selected' : ''"
:accept="importType === ImportType.AllSettings ? '.zip' : '.json'"
/>
</v-row>
<v-row
class="mt-12 ml-8 mr-8 mb-1"
style="display: flex; align-items: center; justify-content: center"
align="center"
>
<v-btn color="secondary" :disabled="importFile === null" @click="handleSettingsImport">
<v-icon left class="open-icon"> mdi-import </v-icon>
<v-btn
color="primary"
:disabled="importFile === null"
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
@click="handleSettingsImport"
>
<v-icon start class="open-icon"> mdi-import </v-icon>
<span class="open-label">Import Settings</span>
</v-btn>
</v-row>
</div>
</v-card-text>
</v-card>
</v-dialog>
</v-col>
<v-col cols="12" sm="6">
<v-btn color="secondary" @click="openExportSettingsPrompt">
<v-icon left class="open-icon"> mdi-export </v-icon>
<v-btn
color="buttonPassive"
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
@click="openExportSettingsPrompt"
>
<v-icon start class="open-icon" size="large"> mdi-export </v-icon>
<span class="open-label">Export Settings</span>
</v-btn>
<a
@@ -334,8 +345,12 @@ const nukePhotonConfigDirectory = () => {
/>
</v-col>
<v-col cols="12" sm="6">
<v-btn color="secondary" @click="openExportLogsPrompt">
<v-icon left class="open-icon"> mdi-download </v-icon>
<v-btn
color="buttonPassive"
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
@click="openExportLogsPrompt"
>
<v-icon start class="open-icon" size="large"> mdi-download </v-icon>
<span class="open-label">Download logs</span>
<!-- Special hidden link that gets 'clicked' when the user exports journalctl logs -->
@@ -349,46 +364,51 @@ const nukePhotonConfigDirectory = () => {
</v-btn>
</v-col>
<v-col cols="12" sm="6">
<v-btn color="secondary" @click="useStateStore().showLogModal = true">
<v-icon left class="open-icon"> mdi-eye </v-icon>
<span class="open-label">View program logs</span>
<v-btn
color="buttonPassive"
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
@click="useStateStore().showLogModal = true"
>
<v-icon start class="open-icon" size="large"> mdi-eye </v-icon>
<span class="open-label">View logs</span>
</v-btn>
</v-col>
</v-row>
<v-divider class="mt-3 pb-3" />
<v-row>
<v-col cols="12">
<v-btn color="error" @click="() => (showFactoryReset = true)">
<v-icon left class="open-icon"> mdi-skull-crossbones </v-icon>
<span class="open-icon">
{{
$vuetify.breakpoint.mdAndUp
? "Factory Reset PhotonVision and delete EVERYTHING"
: "Factory Reset PhotonVision"
}}
</span>
<v-btn
color="error"
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
@click="() => (showFactoryReset = true)"
>
<v-icon start class="open-icon" size="large"> mdi-trash-can-outline </v-icon>
<span class="open-icon"> Factory Reset PhotonVision </span>
</v-btn>
</v-col>
</v-row>
</div>
<v-dialog v-model="showFactoryReset" width="800" dark>
<v-card dark color="primary" class="pa-3" flat>
<v-card-title style="justify-content: center" class="pb-6">
<v-card color="surface" flat>
<v-card-title style="display: flex; justify-content: center">
<span class="open-label">
<v-icon right color="error" class="open-icon ma-1">mdi-nuke</v-icon>
<v-icon end color="red" class="open-icon ma-1" size="large">mdi-alert-outline</v-icon>
Factory Reset PhotonVision
<v-icon right color="error" class="open-icon ma-1">mdi-nuke</v-icon>
<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-3">
<v-row class="align-center white--text">
<v-card-text class="pt-0 pb-10px">
<v-row class="align-center text-white">
<v-col cols="12" md="6">
<span class="mt-3"> This will delete ALL OF YOUR SETTINGS and restart PhotonVision. </span>
<span> This will delete ALL OF YOUR SETTINGS and restart PhotonVision. </span>
</v-col>
<v-col cols="12" md="6">
<v-btn color="secondary" style="float: right" @click="openExportSettingsPrompt">
<v-icon left class="open-icon"> mdi-export </v-icon>
<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"
@@ -401,7 +421,7 @@ const nukePhotonConfigDirectory = () => {
</v-col>
</v-row>
</v-card-text>
<v-card-text>
<v-card-text class="pt-0 pb-0">
<pv-input
v-model="yesDeleteMySettingsText"
:label="'Type &quot;' + expected + '&quot;:'"
@@ -409,17 +429,16 @@ const nukePhotonConfigDirectory = () => {
:input-cols="6"
/>
</v-card-text>
<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 left class="open-icon"> mdi-trash-can-outline </v-icon>
<v-icon start class="open-icon" size="large"> mdi-trash-can-outline </v-icon>
<span class="open-label">
{{
$vuetify.breakpoint.mdAndUp ? "Delete everything, I have backed up what I need" : "Delete Everything"
}}
{{ $vuetify.display.mdAndUp ? "Delete everything, I have backed up what I need" : "Delete Everything" }}
</span>
</v-btn>
</v-card-text>

View File

@@ -4,18 +4,16 @@ import { useSettingsStore } from "@/stores/settings/GeneralSettingsStore";
</script>
<template>
<v-card dark class="mb-3 pr-6 pb-3" style="background-color: #006492">
<v-card-title>LED Control</v-card-title>
<div class="ml-5">
<v-card class="mb-3 rounded-12" color="surface">
<v-card-title class="pb-10px">LED Control</v-card-title>
<v-card-text>
<pv-slider
v-model="useSettingsStore().lighting.brightness"
label="Brightness"
class="pt-2"
:slider-cols="12"
:min="0"
:max="100"
@input="(args) => useSettingsStore().changeLEDBrightness(args)"
@update:modelValue="(args) => useSettingsStore().changeLEDBrightness(args)"
/>
</div>
</v-card-text>
</v-card>
</template>

View File

@@ -10,77 +10,82 @@ interface MetricItem {
const generalMetrics = computed<MetricItem[]>(() => {
const stats = [
{
header: "Version",
value: useSettingsStore().general.version || "Unknown"
},
{
header: "Hardware Model",
value: useSettingsStore().general.hardwareModel || "Unknown"
},
{
header: "Platform",
value: useSettingsStore().general.hardwarePlatform || "Unknown"
},
{
header: "GPU Acceleration",
value: useSettingsStore().general.gpuAcceleration || "Unknown"
}
{ header: "Version", value: useSettingsStore().general.version || "Unknown" },
{ header: "Hardware Model", value: useSettingsStore().general.hardwareModel || "Unknown" },
{ header: "Platform", value: useSettingsStore().general.hardwarePlatform || "Unknown" },
{ header: "GPU Acceleration", value: useSettingsStore().general.gpuAcceleration || "Unknown" }
];
if (!useSettingsStore().network.networkingDisabled) {
stats.push({
header: "IP Address",
value: useSettingsStore().metrics.ipAddress || "Unknown"
});
stats.push({ header: "IP Address", value: useSettingsStore().metrics.ipAddress || "Unknown" });
}
return stats;
});
// @ts-expect-error This uses Intl.DurationFormat which is newly implemented and not available in TS.
const durationFormatter = new Intl.DurationFormat("en", { style: "narrow" });
const platformMetrics = computed<MetricItem[]>(() => {
const metrics = useSettingsStore().metrics;
const stats = [
{
header: "CPU Temp",
value: useSettingsStore().metrics.cpuTemp === undefined ? "Unknown" : `${useSettingsStore().metrics.cpuTemp}°C`
value: metrics.cpuTemp === undefined || metrics.cpuTemp == -1 ? "Unknown" : `${metrics.cpuTemp}°C`
},
{
header: "CPU Usage",
value: useSettingsStore().metrics.cpuUtil === undefined ? "Unknown" : `${useSettingsStore().metrics.cpuUtil}%`
value: metrics.cpuUtil === undefined ? "Unknown" : `${metrics.cpuUtil}%`
},
{
header: "CPU Memory Usage",
value:
useSettingsStore().metrics.ramUtil === undefined || useSettingsStore().metrics.cpuMem === undefined
? "Unknown"
: `${useSettingsStore().metrics.ramUtil || "Unknown"}MB of ${useSettingsStore().metrics.cpuMem}MB`
metrics.ramUtil && metrics.ramMem && metrics.ramUtil >= 0 && metrics.ramMem >= 0
? `${metrics.ramUtil}MB of ${metrics.ramMem}MB`
: "Unknown"
},
{
header: "GPU Memory Usage",
value:
useSettingsStore().metrics.gpuMemUtil === undefined || useSettingsStore().metrics.gpuMem === undefined
? "Unknown"
: `${useSettingsStore().metrics.gpuMemUtil}MB of ${useSettingsStore().metrics.gpuMem}MB`
},
{
header: "CPU Throttling",
value: useSettingsStore().metrics.cpuThr || "Unknown"
},
{
header: "CPU Uptime",
value: useSettingsStore().metrics.cpuUptime || "Unknown"
header: "Uptime",
value: (() => {
const seconds = metrics.uptime;
if (seconds === undefined) return "Unknown";
const days = Math.floor(seconds / 86400);
const hours = Math.floor((seconds % 86400) / 3600);
const minutes = Math.floor((seconds % 3600) / 60);
const secs = Math.floor(seconds % 60);
return durationFormatter.format({
days: days,
hours: hours,
minutes: minutes,
seconds: secs
});
})()
},
{
header: "Disk Usage",
value: useSettingsStore().metrics.diskUtilPct || "Unknown"
value: metrics.diskUtilPct === undefined ? "Unknown" : `${metrics.diskUtilPct}%`
}
];
if (useSettingsStore().metrics.npuUsage) {
if (metrics.npuUsage && metrics.npuUsage.length > 0) {
stats.push({
header: "NPU Usage",
value: useSettingsStore().metrics.npuUsage || "Unknown"
value: metrics.npuUsage?.map((usage, index) => `Core${index} ${usage}%`).join(", ") || "Unknown"
});
}
if (metrics.gpuMem && metrics.gpuMemUtil && metrics.gpuMem > 0 && metrics.gpuMemUtil > 0) {
stats.push({
header: "GPU Memory Usage",
value: `${metrics.gpuMemUtil}MB of ${metrics.gpuMem}MB`
});
}
if (metrics.cpuThr) {
stats.push({
header: "CPU Throttling",
value: metrics.cpuThr.toString()
});
}
@@ -120,17 +125,17 @@ onBeforeMount(() => {
</script>
<template>
<v-card dark class="mb-3" style="background-color: #006492">
<v-card-title class="pl-6" style="display: flex; justify-content: space-between">
<span class="pt-2 pb-2">Stats</span>
<v-btn text @click="fetchMetrics">
<v-icon left class="open-icon">mdi-reload</v-icon>
<v-card class="mb-3 rounded-12" color="surface">
<v-card-title style="display: flex; justify-content: space-between">
<span>Metrics</span>
<v-btn variant="text" @click="fetchMetrics">
<v-icon start class="open-icon" size="large">mdi-reload</v-icon>
Last Fetched: {{ metricsLastFetched }}
</v-btn>
</v-card-title>
<v-card-text class="pa-6 pt-0 pb-3">
<v-card-subtitle class="pa-0" style="font-size: 16px">General Metrics</v-card-subtitle>
<v-simple-table class="metrics-table mt-3">
<v-card-text class="pt-0 pb-3">
<v-card-subtitle class="pa-0" style="font-size: 16px">General</v-card-subtitle>
<v-table class="metrics-table mt-3">
<thead>
<tr>
<th
@@ -163,11 +168,11 @@ onBeforeMount(() => {
</td>
</tr>
</tbody>
</v-simple-table>
</v-table>
</v-card-text>
<v-card-text class="pa-6 pt-4">
<v-card-subtitle class="pa-0 pb-1" style="font-size: 16px">Hardware Metrics</v-card-subtitle>
<v-simple-table class="metrics-table mt-3">
<v-card-text class="pt-4">
<v-card-subtitle class="pa-0 pb-1" style="font-size: 16px">Hardware</v-card-subtitle>
<v-table class="metrics-table mt-3">
<thead>
<tr>
<th
@@ -201,7 +206,7 @@ onBeforeMount(() => {
</td>
</tr>
</tbody>
</v-simple-table>
</v-table>
</v-card-text>
</v-card>
</template>
@@ -212,46 +217,52 @@ onBeforeMount(() => {
text-align: center;
}
$stats-table-border: rgba(255, 255, 255, 0.5);
$stats-table-inner: rgba(255, 255, 255, 0.1);
.t {
border-top: 1px solid white;
border-right: 1px solid white;
border-top: 1px solid $stats-table-border;
border-right: 1px solid $stats-table-border;
border-bottom: 1px solid $stats-table-inner !important;
}
.b {
border-bottom: 1px solid white;
border-right: 1px solid white;
border-bottom: 1px solid $stats-table-border;
border-right: 1px solid $stats-table-border;
}
.tl {
border-top: 1px solid white;
border-left: 1px solid white;
border-right: 1px solid white;
border-top: 1px solid $stats-table-border;
border-left: 1px solid $stats-table-border;
border-right: 1px solid $stats-table-border;
border-bottom: 1px solid $stats-table-inner !important;
border-top-left-radius: 5px;
}
.tr {
border-top: 1px solid white;
border-right: 1px solid white;
border-top: 1px solid $stats-table-border;
border-right: 1px solid $stats-table-border;
border-bottom: 1px solid $stats-table-inner !important;
border-top-right-radius: 5px;
}
.bl {
border-bottom: 1px solid white;
border-left: 1px solid white;
border-right: 1px solid white;
border-bottom: 1px solid $stats-table-border;
border-left: 1px solid $stats-table-border;
border-right: 1px solid $stats-table-border;
border-bottom-left-radius: 5px;
}
.br {
border-bottom: 1px solid white;
border-right: 1px solid white;
border-bottom: 1px solid $stats-table-border;
border-right: 1px solid $stats-table-border;
border-bottom-right-radius: 5px;
}
.metric-item {
font-size: 16px !important;
padding: 1px 15px 1px 10px;
border-right: 1px solid;
border-right: 1px solid $stats-table-border;
font-weight: normal;
color: white !important;
text-align: center !important;
@@ -259,22 +270,9 @@ onBeforeMount(() => {
.metric-item-title {
font-size: 18px !important;
text-decoration: underline;
text-decoration-color: #ffd843;
}
.v-data-table {
thead,
tbody {
background-color: #006492;
}
:hover {
tbody > tr {
background-color: #005281 !important;
}
}
.v-table {
::-webkit-scrollbar {
width: 0;
height: 0.55em;
@@ -287,7 +285,7 @@ onBeforeMount(() => {
}
::-webkit-scrollbar-thumb {
background-color: #ffd843;
background-color: rgb(var(--v-theme-accent));
border-radius: 10px;
}
}

View File

@@ -7,6 +7,10 @@ import PvSwitch from "@/components/common/pv-switch.vue";
import PvSelect from "@/components/common/pv-select.vue";
import { type ConfigurableNetworkSettings, NetworkConnectionType } from "@/types/SettingTypes";
import { useStateStore } from "@/stores/StateStore";
import { useTheme } from "vuetify";
import { getThemeColor, setThemeColor, resetTheme } from "@/lib/ThemeManager";
const theme = useTheme();
// Copy object to remove reference to store
const tempSettingsStruct = ref<ConfigurableNetworkSettings>(Object.assign({}, useSettingsStore().network));
@@ -16,6 +20,19 @@ const resetTempSettingsStruct = () => {
const settingsValid = ref(true);
const showThemeConfig = ref(false);
const backgroundColor = ref("");
const primaryColor = ref("");
const secondaryColor = ref("");
const surfaceColor = ref("");
const loadCurrentColors = () => {
backgroundColor.value = getThemeColor(theme, "background");
primaryColor.value = getThemeColor(theme, "primary");
secondaryColor.value = getThemeColor(theme, "secondary");
surfaceColor.value = getThemeColor(theme, "surface");
};
const isValidNetworkTablesIP = (v: string | undefined): boolean => {
// Check if it is a valid team number between 1-99999 (5 digits)
const teamNumberRegex = /^[1-9][0-9]{0,4}$/;
@@ -83,16 +100,10 @@ const saveGeneralSettings = () => {
useSettingsStore()
.updateGeneralSettings(payload)
.then((response) => {
useStateStore().showSnackbarMessage({
message: response.data.text || response.data,
color: "success"
});
useStateStore().showSnackbarMessage({ message: response.data.text || response.data, color: "success" });
// Update the local settings cause the backend checked their validity. Assign is to deref value
useSettingsStore().network = {
...useSettingsStore().network,
...Object.assign({}, tempSettingsStruct.value)
};
useSettingsStore().network = { ...useSettingsStore().network, ...Object.assign({}, tempSettingsStruct.value) };
})
.catch((error) => {
resetTempSettingsStruct();
@@ -124,9 +135,14 @@ const saveGeneralSettings = () => {
});
};
const currentNetworkInterfaceIndex = computed<number>({
get: () => useSettingsStore().networkInterfaceNames.indexOf(useSettingsStore().network.networkManagerIface || ""),
set: (v) => (tempSettingsStruct.value.networkManagerIface = useSettingsStore().networkInterfaceNames[v])
const currentNetworkInterfaceIndex = computed<number | undefined>({
get: () => {
const index = useSettingsStore().networkInterfaceNames.indexOf(
useSettingsStore().network.networkManagerIface || ""
);
return index === -1 ? undefined : index;
},
set: (v) => v && (tempSettingsStruct.value.networkManagerIface = useSettingsStore().networkInterfaceNames[v])
});
watchEffect(() => {
@@ -136,11 +152,24 @@ watchEffect(() => {
</script>
<template>
<v-card dark class="mb-3" style="background-color: #006492">
<v-card-title class="pa-6">Global Settings</v-card-title>
<div class="pa-6 pt-0">
<v-divider class="pb-3" />
<v-card-title class="pl-0 pt-3 pb-3">Networking</v-card-title>
<v-card class="mb-3 rounded-12" color="surface">
<v-card-title style="display: flex; justify-content: space-between">
<span>Global Settings</span>
<v-btn
variant="text"
@click="
() => {
loadCurrentColors();
showThemeConfig = true;
}
"
>
<v-icon size="x-large">mdi-palette-outline</v-icon>
Theme
</v-btn>
</v-card-title>
<div class="pa-5 pt-0">
<v-card-title class="pl-0 pt-0 pb-10px">Networking</v-card-title>
<v-form ref="form" v-model="settingsValid">
<pv-input
v-model="tempSettingsStruct.ntServerAddress"
@@ -154,16 +183,15 @@ watchEffect(() => {
'The NetworkTables Server Address must be a valid Team Number, IP address, or Hostname'
]"
/>
<v-banner
<v-alert
v-if="!isValidNetworkTablesIP(tempSettingsStruct.ntServerAddress) && !tempSettingsStruct.runNTServer"
rounded
class="pt-3 pb-3"
color="error"
text-color="white"
style="margin: 10px 0"
density="compact"
text="The NetworkTables Server Address is not set or is invalid. NetworkTables is unable to connect."
icon="mdi-alert-circle-outline"
>
The NetworkTables Server Address is not set or is invalid. NetworkTables is unable to connect.
</v-banner>
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'tonal'"
/>
<pv-radio
v-show="!useSettingsStore().network.networkingDisabled"
v-model="tempSettingsStruct.connectionType"
@@ -202,8 +230,7 @@ watchEffect(() => {
useSettingsStore().network.networkingDisabled
"
/>
<v-divider class="mt-3 pb-3" />
<v-card-title class="pl-0 pt-3 pb-3">Advanced Networking</v-card-title>
<v-card-title class="pl-0 pt-3 pb-10px">Advanced Networking</v-card-title>
<pv-switch
v-show="!useSettingsStore().network.networkingDisabled"
v-model="tempSettingsStruct.shouldManage"
@@ -225,57 +252,54 @@ watchEffect(() => {
tooltip="Name of the interface PhotonVision should manage the IP address of"
:items="useSettingsStore().networkInterfaceNames"
/>
<v-banner
<v-alert
v-if="
!useSettingsStore().networkInterfaceNames.length &&
tempSettingsStruct.shouldManage &&
useSettingsStore().network.canManage &&
!useSettingsStore().network.networkingDisabled
"
rounded
class="pt-3 pb-3"
color="error"
text-color="white"
icon="mdi-information-outline"
>
Photon cannot detect any wired connections! Please send program logs to the developers for help.
</v-banner>
density="compact"
text="Cannot detect any wired connections! Send program logs to the developers for help."
icon="mdi-alert-circle-outline"
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'tonal'"
/>
<pv-switch
v-model="tempSettingsStruct.runNTServer"
label="Run NetworkTables Server (Debugging Only)"
tooltip="If enabled, this device will create a NT server. This is useful for home debugging, but should be disabled on-robot."
:label-cols="4"
/>
<v-banner
<v-alert
v-if="tempSettingsStruct.runNTServer"
rounded
color="error"
text-color="white"
color="buttonActive"
density="compact"
text="This mode is intended for debugging and should be off for proper usage. PhotonLib will NOT work!"
icon="mdi-information-outline"
>
This mode is intended for debugging; it should be off for proper usage. PhotonLib will NOT work!
</v-banner>
<v-divider class="mt-3 pb-3" />
<v-card-title class="pl-0 pt-3 pb-3">Miscellaneous</v-card-title>
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'tonal'"
/>
<v-card-title class="pl-0 pt-3 pb-10px">Miscellaneous</v-card-title>
<pv-switch
v-model="tempSettingsStruct.shouldPublishProto"
label="Also Publish Protobuf"
tooltip="If enabled, Photon will publish all pipeline results in both the Packet and Protobuf formats. This is useful for visualizing pipeline results from NT viewers such as glass and logging software such as AdvantageScope. Note: photon-lib will ignore this value and is not recommended on the field for performance."
:label-cols="4"
/>
<v-banner
<v-alert
v-if="tempSettingsStruct.shouldPublishProto"
rounded
color="error"
text-color="white"
color="buttonActive"
density="compact"
text="This mode is intended for debugging and may reduce performance; it should be off for field use."
icon="mdi-information-outline"
>
This mode is intended for debugging; it should be off for field use. You may notice a performance hit by using
this mode.
</v-banner>
<v-divider class="mt-3 mb-6" />
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'tonal'"
/>
</v-form>
<v-btn
color="accent"
color="primary"
class="mt-3"
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
style="color: black; width: 100%"
:disabled="!settingsValid || !settingsHaveChanged()"
@click="saveGeneralSettings"
@@ -283,11 +307,89 @@ watchEffect(() => {
Save
</v-btn>
</div>
<v-dialog v-model="showThemeConfig" width="800" dark>
<v-card color="surface" flat>
<v-card-title class="text-center">Theme Configuration</v-card-title>
<v-card-text class="pt-0 pb-10px">
<v-row>
<v-col class="text-center">
Background
<v-color-picker
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)"
></v-color-picker>
</v-col>
<v-col class="text-center">
Surface
<v-color-picker
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)"
></v-color-picker>
</v-col>
</v-row>
<v-row>
<v-col class="text-center">
Primary
<v-color-picker
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)"
></v-color-picker>
</v-col>
<v-col class="text-center">
Secondary
<v-color-picker
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)"
></v-color-picker>
</v-col>
</v-row>
</v-card-text>
<v-card-actions class="pa-5 pt-0">
<v-btn
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
color="buttonPassive"
class="text-black"
@click="showThemeConfig = false"
>
Close
</v-btn>
<v-btn
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
color="buttonActive"
class="text-black"
@click="
() => {
resetTheme(theme);
loadCurrentColors();
}
"
>
Reset Default
</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
</v-card>
</template>
<style>
.v-banner__wrapper {
padding: 6px !important;
.mt-10px {
margin-top: 10px !important;
}
</style>

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