Compare commits

...

68 Commits

Author SHA1 Message Date
Alan Everett
c04c8d76ed Use Avaje Jsonb for Javalin JSON (#2512) 2026-06-09 17:20:21 -07:00
PJ Reiniger
7b30c9306e [python] Fixup python examples / build (#2509)
## Description

This fixes up and renables the python examples builds.

Main fixes
- Test shell script just straight up didn't do what it was intended to
do
- Replace analog imus (and half refactored sims) with OnboardIMU
- Fixes swerve velocities function signature
- "regenerates" the default robotpy test

## 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_, including events
that led to this PR
- [ ] 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 all settings going back to the previous seasons's last release
(seasons end after champs ends)
- [ ] 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
- [ ] If this PR adds a dependency, the license has been checked for
compatibility and steps taken to follow it

---------

Co-authored-by: Matt Morley <matthew.morley.ca@gmail.com>
Co-authored-by: David Vo <auscompgeek@users.noreply.github.com>
2026-05-27 14:29:26 -07:00
Matt Morley
ae22742f05 Publish photonlib on main and tags (#2510)
## Description

Previously, we only published if `github.ref == 'refs/heads/main'`. But
we want to publish PhotonLib on tags, too

## 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_, including events
that led to this PR
- [ ] 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 all settings going back to the previous seasons's last release
(seasons end after champs ends)
- [ ] 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
- [ ] If this PR adds a dependency, the license has been checked for
compatibility and steps taken to follow it

Co-authored-by: Matthew Morley <mmorley@blueorigin.com>
2026-05-27 14:22:10 -07:00
Sam Freund
860d6807a8 use wpilibYear not year (#2508) 2026-05-27 03:04:25 +00:00
Sam Freund
e9006a2803 Upgrade to wpilib alpha-6 (#2434)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Ryanforce08 <rradtke1208@gmail.com>
Co-authored-by: PJ Reiniger <pj.reiniger@gmail.com>
Co-authored-by: Jade Turner <spacey-sooty@proton.me>
Co-authored-by: Matt Morley <matthew.morley.ca@gmail.com>
2026-05-26 20:47:48 -05:00
Sam Freund
d3de87f72b change to a switch statement to avoid object comparison (#2507) 2026-05-26 02:39:38 +00:00
Sam Freund
be27bb9916 Fix calibration typo (#2471)
closes #2371
2026-05-24 14:30:31 -07:00
Alan Everett
0525e762b4 Switch from FasterXML Jackson to Avaje Jsonb (#2503)
## Description

WPILib switched from FasterXML Jackson to Avaje Jsonb for speed reasons
in https://github.com/wpilibsuite/allwpilib/pull/8721. This does the
same for PhotonVision. Some temporary Jackson adapters are present to
allow compatibility with alpha-4 ahead of updating Photon's WPILib
version. A few old backwards compatibility migrations were also dropped
if they were difficult to port to Avaje Jsonb or otherwise complicated
the code.

## 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_, including events
that led to this PR
- [ ] 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 all settings going back to the previous seasons's last release
(seasons end after champs ends)
- [ ] 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
- [ ] If this PR adds a dependency, the license has been checked for
compatibility and steps taken to follow it

---------

Co-authored-by: samfreund <samf.236@proton.me>
Co-authored-by: Matt Morley <matthew.morley.ca@gmail.com>
2026-05-24 17:05:10 +00:00
Sam Freund
4db3d7be57 Make playwright an archived upload 2026-05-21 07:16:41 -04:00
Alan Everett
02d0f2b047 Add support for Green/Yellow status LEDs, like is used on some Limelights (#2287) 2026-05-20 16:46:41 +00:00
Jade
7e9a67ec6a Raise minimum images for calibration to 100 (#2437) 2026-05-20 08:08:47 -07:00
Sam Freund
b78b0dad97 Fix setting stream res (#2500) 2026-05-19 20:43:06 -07:00
Alan Everett
c146d559d0 Don't crash if libcamera fails to load (#2498) 2026-05-19 20:42:09 -07:00
Charlotte Wilson
645ff41ce9 More strictly warn against NT api usage in docs (#2488) 2026-05-19 08:50:45 -07:00
Alan Everett
6d579edcd5 Send isEnabled to UI (#2499) 2026-05-19 08:22:53 -07:00
JosephTLockwood
6ef0f91600 fix(photon-lib): only report actual contributors in targetsUsed (#2493) 2026-05-16 08:26:39 -07:00
JosephTLockwood
a6288afcb4 fix(photon-lib): skip non-fiducial targets in C++ LOWEST_AMBIGUITY (#2494) 2026-05-16 08:26:15 -07:00
Matt Morley
996ca3649e Add disabled param for PhotonCamera (#2484) 2026-05-16 07:41:59 -07:00
David Vo
0e834f0851 Allow calibration dimensions in mm (#2479) 2026-05-11 02:47:29 +00:00
Gold856
c221aed0c2 Approve dependencies in pnpm and pin pnpm version in Gradle (#2482)
Fixes failure in #2481. The pnpm version was unpinned, so it quietly moved to pnpm 11, which introduced several breaking changes including one with approved builds (specifically, strictDepBuilds). Since we're on pnpm 11 anyways, use their new system for approving postinstalls for certain dependencies.
2026-05-11 01:18:15 +00:00
Sam Freund
363b8274d0 Merge branch 'main' into 2027 2026-05-05 10:55:54 -05:00
Sam Freund
2372e110f9 TypeCheck Frontend (#2394)
We recently had an error that would've been caught by type checking in the frontend (see #2393). This PR implements type checking so that future errors will be caught.

Additionally, this PR contains miscellaneous frontend cleanup that's tangentially related to type-checking.
2026-05-05 15:24:19 +00:00
Alan Everett
adfa39b6f4 Don't interact with Libcamera cameras until connected (#2447) 2026-05-04 07:24:05 -04:00
Sam Freund
94acd9d631 add youtube video (#2466) 2026-05-03 21:45:41 -07:00
Sam Freund
d587cd19bb Add presentation from CMP to website (#2453) 2026-05-03 18:16:32 -07:00
Sam Freund
3c017ab961 Check website format in CI (#2454)
Add a command to lint the website to package.json, and update the
workflow. Updates docs for linting as well.

---------

Co-authored-by: Jade Turner <spacey-sooty@proton.me>
2026-05-03 18:16:32 -07:00
Sam Freund
4c9b36aa5c Upgrade builds to node 24 (#2463) 2026-05-03 17:50:52 -07:00
Jesse Kane
de5fe76123 switched to shared cleaners in object detectors (#2462)
Co-authored-by: Matt Morley <matthew.morley.ca@gmail.com>
2026-05-03 19:01:13 -05:00
Sam Freund
b8e9e4f5c7 Build images for 2027 and bump libcamera (#2442) 2026-05-03 13:28:01 -05:00
Charlotte Wilson
dbf71029b7 Disallow low-resolution calibration (#2438) 2026-05-03 18:00:11 +00:00
Sam Freund
c195d7ccc0 Add YOLO-Pro conversion docs (#2450)
Co-authored-by: Matt Morley <matthew.morley.ca@gmail.com>
2026-05-03 12:31:05 -05:00
Sam Freund
f640c28ac1 Update macOS docs (#2459)
Closes #2451
2026-05-03 12:26:36 -05:00
Sam Freund
b2ee79164b Serve docs in demo (#2445) 2026-05-03 17:20:22 +00:00
Sam Freund
d8dddf379f Change ssh login info (#2458) 2026-05-03 10:02:20 -07:00
Charlotte Wilson
b99e85d0ba Add missing buildAndCopyUI instructions (#2457)
Adds ./gradlew buildAndCopyUI instruction to the documentation on building Photonvision, to clarify that this step must be ran to properly build the UI

Closes #2452
2026-05-02 19:19:32 +00:00
Sam Freund
439f5fcd91 Add presentation from CMP to website (#2453) 2026-05-02 06:47:32 +00:00
Sam Freund
dbbe006fd3 Check website format in CI (#2454)
Add a command to lint the website to package.json, and update the
workflow. Updates docs for linting as well.

---------

Co-authored-by: Jade Turner <spacey-sooty@proton.me>
2026-05-02 06:02:05 +00:00
Sam Freund
803bdb222c Upgrade to java 25 (#2441) 2026-04-23 00:10:38 -05:00
Sam Freund
d1a02a542c Bump some versions (#2433) 2026-04-13 13:40:45 -05:00
Sam Freund
e970446c4c [photonlib] Remove deprecated pose estimator methods (#2431)
We added new API methods in 2026, and deprecated our past methods. This PR removes the deprecated methods.
2026-04-13 12:04:58 -05:00
Sam Freund
68fc1e7129 Make 2027 build (#2422)
This PR updates everything for 2027. This includes removing GradleRIO, simplifying our wpilib version defintion, updating APIs, updating to Java 21, and more.

Note that photonlibpy is failing because robotpy has not been fully updated yet. Examples are omitted because they need to be updated for our new PhotonPoseEstimator API and still need some changes from WPILIB. photonlib windows build is failing because we're waiting for some upstream changes. Finally, images are failing since they don't have Java 21 yet.
2026-04-11 12:23:14 -05:00
amsam0
4412df1516 Use Platform.isAthena instead of manually checking for frcRunRobot.sh (#2426) 2026-04-11 12:23:14 -05:00
Sam Freund
7764ccc533 Upgrade actions to Node 24 and remove unneeded archival (#2416)
Co-authored-by: Gold856 <117957790+Gold856@users.noreply.github.com>
Co-authored-by: Jade Turner <spacey-sooty@proton.me>
Co-authored-by: Jordan McMichael <jlmcmchl@gmail.com>
2026-04-11 12:23:14 -05:00
samfreund
872b587bae image version 2026.1.4 2026-04-11 12:23:14 -05:00
Gold856
934eed21d2 Update to match new WPILib organization 2026-04-11 12:23:14 -05:00
Jordan McMichael
c34c854583 Upgrade to 2027 alpha 2 (#2010) 2026-04-11 12:23:14 -05:00
Gold856
83b4522bf3 Add DataLog to the list of libraries loaded 2026-04-11 12:23:14 -05:00
Jordan McMichael
d83d53650a [2027] Add systemcore as a photonlib build target (#1995)
## Description

Added systemcore to a couple of build files in order for `./gradlew
publishToMavenLocal` to generate systemcore-compatible dependencies.

Needed to support deploying photonlib to systemcore.

## 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: Gold856 <117957790+Gold856@users.noreply.github.com>
2026-04-11 12:23:14 -05:00
Gold856
0734d1b806 Disable example builds 2026-04-11 12:23:14 -05:00
Gold856
3ace6122b0 Upgrade Gradle, fix build, and format 2026-04-11 12:23:14 -05:00
Jade Turner
a5f9a0b673 Start on updating examples for 2027
Signed-off-by: Jade Turner <spacey-sooty@proton.me>
2026-04-11 12:23:14 -05:00
Gold856
ed1b31cb7f Upgrade to 2027 alpha 2026-04-11 12:23:14 -05:00
Gold856
68bdb38a3d Switch to Ubuntu 24.04 2026-04-11 12:23:14 -05:00
Alan Everett
a5be3d062c Set raw exposure before setting auto exposure (#2429)
On a Luma P1, autoexposure on first boot was getting overridden with the manual exposure setting. This was traced back to #1814, where the order of setting auto exposure and raw exposure was flipped. This flips it back.
2026-04-10 04:04:49 +00:00
Chris Gerth
3379a1a132 better merch (#2427) 2026-04-07 21:46:07 -05:00
Sam Freund
5f59e9ab22 bump rubik image (#2424) 2026-04-05 18:56:33 -07:00
Jade
515a1a3d78 Fix comments in OutputStream pipeline (#2415) 2026-03-28 15:13:47 -05:00
Sam Freund
03ffcb1215 Upgrade website dependencies (#2414) 2026-03-27 14:31:18 -04:00
Sam Freund
131098bfdd Add AE quirk to OV2311 (#2411) 2026-03-27 10:52:04 -05:00
Chris Gerth
b5277e5f4c Add Community Contribution Guidelines (#2405)
## Description

Leadership team identified gaps in how we introduce new developers to
the community.

Additional docs for onboarding new developers, helping clarify roles and
what counts as a "good" PR.

## 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_, including events
that led to this PR
- [ ] 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 all settings going back to the previous seasons's last release
(seasons end after champs ends)
- [ ] 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
- [ ] If this PR adds a dependency, the license has been checked for
compatibility and steps taken to follow it
2026-03-27 02:32:17 +00:00
Jesse Kane
52f1f7726f Fix strong reference in Cleaners (#2404) 2026-03-26 18:55:06 +00:00
Sam Freund
e19534da47 Remove old camera quirk aliases (#2412) 2026-03-26 14:22:58 -04:00
Gold856
488646e755 Upgrade to Vite 8 (#2408) 2026-03-26 04:39:38 +00:00
Jade
a8a0024321 Add nix files to .gitignore (#2409)
I assume based off discord comments a PR won't be accepted for a
shell.nix or equivalent flake solution so we should gitignore this so
people don't accidentally add them to PRs
2026-03-25 23:04:38 -05:00
Sam Freund
032deba775 refactor dark mode checks (#2407)
The current method for checking light vs. dark mode is to compare the
name of the theme against a hardcoded string. This PR uses a dark mode
boolean. This change is for verbosity and so that we're not reliant on
theme name. Additionally, we change some references to colors to the
global theme, instead of indexing the list of themes.
2026-03-24 17:49:56 -05:00
Gold856
7b240a027a Add more linting rules (#2406)
As a precursor to #2394, add a bunch of linting rules to try and catch
more mistakes/potential code errors/unnecessary code. Add a bunch of
rules from https://eslint.vuejs.org/rules/ in the "uncategorized"
section that seem useful to have.
2026-03-23 18:41:11 -04:00
Gold856
d4bfe643d5 Clean up C++ headers (#2402) 2026-03-19 06:10:04 +00:00
Stéphane Dalton
12446a6c44 Populate classId and confidence level for object detection in Java simulation (#2372)
## Description

compute confidence level based on target area in total image size and
populate classId and confidence level in Java (while building the
PhotonTrackedTarget)

## Changes
- Add new VisionTargetSim constructor for object detection
- If class ID specified but confidence = -1, estimate based on total
area

---------

Co-authored-by: Matt Morley <matthew.morley.ca@gmail.com>
2026-03-19 03:42:37 +00:00
496 changed files with 9047 additions and 10209 deletions

View File

@@ -10,7 +10,7 @@ concurrency:
cancel-in-progress: true
env:
IMAGE_VERSION: v2026.1.3
IMAGE_VERSION: v2027.0.0
jobs:
@@ -21,102 +21,116 @@ jobs:
- uses: actions/checkout@v6
- uses: gradle/actions/wrapper-validation@v5
build-examples:
# build-examples:
strategy:
fail-fast: false
matrix:
include:
- os: windows-2022
artifact-name: Win64
- os: macos-14
artifact-name: macOS
- os: ubuntu-24.04
artifact-name: Linux
# strategy:
# fail-fast: false
# matrix:
# include:
# - os: windows-2022
# artifact-name: Win64
# - os: macos-14
# artifact-name: macOS
# - os: ubuntu-24.04
# artifact-name: Linux
name: "Photonlib - Build Examples - ${{ matrix.os }}"
runs-on: ${{ matrix.os }}
needs: [build-photonlib-host, build-photonlib-docker]
# name: "Photonlib - Build Examples - ${{ matrix.os }}"
# runs-on: ${{ matrix.os }}
# needs: [build-photonlib-host, build-photonlib-docker]
steps:
- name: Checkout code
uses: actions/checkout@v6
with:
fetch-depth: 0
- name: Fetch tags
run: git fetch --tags --force
- name: Install Java 17
uses: actions/setup-java@v5
with:
java-version: 17
distribution: temurin
- name: Install RoboRIO Toolchain
run: ./gradlew installRoboRioToolchain
- name: Delete duplicate toolchains
run: |
find ~/.gradle/cache/ -name *roborio-academic* -exec rm -rf {} +
du -h . | sort -h
if: matrix.os == 'ubuntu-24.04'
# Download prebuilt photonlib artifacts
- uses: actions/download-artifact@v7
with:
name: maven-${{ matrix.artifact-name }}
- uses: actions/download-artifact@v7
with:
name: maven-Athena
- name: Move to maven local
run: |
mkdir -p ~/.m2/repository/
mv maven/org ~/.m2/repository/
- name: Copy vendordeps
shell: bash
run: |
for vendordep_folder in photonlib-*-examples/*/; do
# Remove trailing slash for cross-platform compatibility
vendordep_folder="${vendordep_folder%/}"
# steps:
# - name: Checkout code
# uses: actions/checkout@v6
# with:
# fetch-depth: 0
# - name: Fetch tags
# run: git fetch --tags --force
# - uses: actions/setup-java@v5
# with:
# java-version: 25
# distribution: temurin
# - name: Install SystemCore Toolchain
# run: ./gradlew installSystemCoreToolchain
# - name: Delete duplicate toolchains
# run: |
# find ~/.gradle/cache/ -name *bookworm* -exec rm -rf {} +
# du -h . | sort -h
# if: matrix.os == 'ubuntu-24.04'
# # Download prebuilt photonlib artifacts
# - uses: actions/download-artifact@v7
# with:
# name: maven-${{ matrix.artifact-name }}
# - uses: actions/download-artifact@v7
# with:
# name: maven-SystemCore
# - name: Move to maven local
# run: |
# mkdir -p ~/.m2/repository/
# mv maven/org ~/.m2/repository/
# - name: Copy vendordeps
# shell: bash
# run: |
# for vendordep_folder in photonlib-*-examples/*/; do
# # Remove trailing slash for cross-platform compatibility
# vendordep_folder="${vendordep_folder%/}"
# Filter for projects only
if [ -e "$vendordep_folder/build.gradle" ]; then
mkdir -p "$vendordep_folder/vendordeps/"
cp vendordeps/photonlib-json-1.0.json "$vendordep_folder/vendordeps/"
fi
done
- name: Build Java examples
working-directory: photonlib-java-examples
run: |
./gradlew build
./gradlew clean
- name: Build C++ examples
working-directory: photonlib-cpp-examples
run: |
./gradlew build
./gradlew clean
# # Filter for projects only
# if [ -e "$vendordep_folder/build.gradle" ]; then
# mkdir -p "$vendordep_folder/vendordeps/"
# cp vendordeps/photonlib-json-1.0.json "$vendordep_folder/vendordeps/"
# fi
# done
# - name: Build Java examples
# working-directory: photonlib-java-examples
# run: |
# ./gradlew build
# ./gradlew clean
# - name: Build C++ examples
# working-directory: photonlib-cpp-examples
# run: |
# ./gradlew build
# ./gradlew clean
playwright-tests:
name: "Playwright E2E tests"
typecheck-client:
name: "Typecheck Client"
runs-on: ubuntu-24.04
needs: [validation]
steps:
# Checkout code.
- name: Checkout code
uses: actions/checkout@v6
with:
fetch-depth: 0
- name: Fetch tags
run: git fetch --tags --force
- name: Install Java 17
uses: actions/setup-java@v5
with:
java-version: 17
distribution: temurin
- name: Install pnpm
uses: pnpm/action-setup@v4
uses: pnpm/action-setup@v5
with:
version: 10
- name: Setup Node.js
uses: actions/setup-node@v6
with:
node-version: 22
node-version: 24
cache: pnpm
cache-dependency-path: photon-client/pnpm-lock.yaml
- name: Typecheck Client
working-directory: photon-client
run: |
pnpm install --frozen-lockfile
pnpm type-check
playwright-tests:
name: "Playwright E2E tests"
runs-on: ubuntu-24.04
needs: [validation]
steps:
- name: Checkout code
uses: actions/checkout@v6
- uses: actions/setup-java@v5
with:
java-version: 25
distribution: temurin
- uses: pnpm/action-setup@v5
with:
version: 10
- uses: actions/setup-node@v6
with:
cache: pnpm
cache-dependency-path: photon-client/pnpm-lock.yaml
node-version: 24
- name: Setup tests
working-directory: photon-client
run: |
@@ -127,10 +141,10 @@ jobs:
- name: Run Playwright tests
working-directory: photon-client
run: pnpm test
- uses: actions/upload-artifact@v6
- uses: actions/upload-artifact@v7
if: ${{ !cancelled() }}
with:
name: playwright-report
name: "Playwright Report"
path: photon-client/playwright-report/
retention-days: 30
build-gradle:
@@ -138,26 +152,24 @@ jobs:
runs-on: ubuntu-24.04
needs: [validation]
steps:
# Checkout code.
- name: Checkout code
uses: actions/checkout@v6
with:
fetch-depth: 0
- name: Fetch tags
run: git fetch --tags --force
- name: Install Java 17
uses: actions/setup-java@v5
- uses: actions/setup-java@v5
with:
java-version: 17
java-version: 25
distribution: temurin
- name: Install pnpm
uses: pnpm/action-setup@v4
- uses: pnpm/action-setup@v5
with:
version: 10
- name: Setup Node.js
uses: actions/setup-node@v6
- uses: actions/setup-node@v6
with:
node-version: 22
cache: pnpm
cache-dependency-path: photon-client/pnpm-lock.yaml
node-version: 24
- name: Gradle Build
run: ./gradlew photon-targeting:build photon-core:build photon-server:build -x check
- name: Gradle Tests and Coverage
@@ -184,7 +196,7 @@ jobs:
working-directory: docs
run: |
make html
- uses: actions/upload-artifact@v6
- uses: actions/upload-artifact@v7
with:
name: built-docs
path: docs/build/html
@@ -198,10 +210,9 @@ jobs:
with:
fetch-depth: 0
- name: Install Java 17
uses: actions/setup-java@v5
- uses: actions/setup-java@v5
with:
java-version: 17
java-version: 25
distribution: temurin
# grab all tags
@@ -214,9 +225,9 @@ jobs:
mv photon-lib/build/generated/vendordeps/photonlib.json photon-lib/build/generated/vendordeps/photonlib-$(git describe --tags --match=v*).json
# Upload it here so it shows up in releases
- uses: actions/upload-artifact@v6
- uses: actions/upload-artifact@v7
with:
name: photonlib-vendor-json
archive: false
path: photon-lib/build/generated/vendordeps/photonlib-*.json
build-photonlib-host:
@@ -226,12 +237,12 @@ jobs:
fail-fast: false
matrix:
include:
- os: windows-2022
artifact-name: Win64
- os: macos-26
artifact-name: macOS
- os: ubuntu-24.04
artifact-name: Linux
- os: windows-2022
artifact-name: Win64
- os: macos-26
artifact-name: macOS
- os: ubuntu-24.04
artifact-name: Linux
name: "Photonlib - Build Host - ${{ matrix.artifact-name }}"
runs-on: ${{ matrix.os }}
@@ -240,10 +251,9 @@ jobs:
- uses: actions/checkout@v6
with:
fetch-depth: 0
- name: Install Java 17
uses: actions/setup-java@v5
- uses: actions/setup-java@v5
with:
java-version: 17
java-version: 25
distribution: temurin
- run: git fetch --tags --force
- run: ./gradlew photon-targeting:build photon-lib:build
@@ -252,10 +262,10 @@ jobs:
name: Publish
env:
ARTIFACTORY_API_KEY: ${{ secrets.ARTIFACTORY_API_KEY }}
if: github.event_name == 'push' && github.repository_owner == 'photonvision'
if: github.event_name == 'push' && github.repository_owner == 'photonvision' && (github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/v'))
# Copy artifacts to build/outputs/maven
- run: ./gradlew photon-lib:publish photon-targeting:publish -PcopyOfflineArtifacts
- uses: actions/upload-artifact@v6
- uses: actions/upload-artifact@v7
with:
name: maven-${{ matrix.artifact-name }}
path: build/outputs
@@ -265,13 +275,13 @@ jobs:
fail-fast: false
matrix:
include:
- container: wpilib/roborio-cross-ubuntu:2025-24.04
artifact-name: Athena
build-options: "-Ponlylinuxathena"
- container: wpilib/raspbian-cross-ubuntu:bullseye-22.04
- container: wpilib/systemcore-cross-ubuntu:2027-24.04
artifact-name: SystemCore
build-options: "-Ponlylinuxsystemcore"
- container: wpilib/raspbian-cross-ubuntu:2027-bookworm-24.04
artifact-name: Raspbian
build-options: "-Ponlylinuxarm32"
- container: wpilib/aarch64-cross-ubuntu:bullseye-22.04
- container: wpilib/aarch64-cross-ubuntu:2027-bookworm-24.04
artifact-name: Aarch64
build-options: "-Ponlylinuxarm64"
@@ -293,10 +303,10 @@ jobs:
run: ./gradlew photon-lib:publish photon-targeting:publish ${{ matrix.build-options }}
env:
ARTIFACTORY_API_KEY: ${{ secrets.ARTIFACTORY_API_KEY }}
if: github.event_name == 'push' && github.repository_owner == 'photonvision'
if: github.event_name == 'push' && github.repository_owner == 'photonvision' && (github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/v'))
# Copy artifacts to build/outputs/maven
- run: ./gradlew photon-lib:publish photon-targeting:publish -PcopyOfflineArtifacts ${{ matrix.build-options }}
- uses: actions/upload-artifact@v6
- uses: actions/upload-artifact@v7
with:
name: maven-${{ matrix.artifact-name }}
path: build/outputs
@@ -311,7 +321,7 @@ jobs:
fetch-depth: 0
- run: git fetch --tags --force
# download all maven-* artifacts to outputs/
- uses: actions/download-artifact@v7
- uses: actions/download-artifact@v8
with:
merge-multiple: true
path: output
@@ -321,7 +331,7 @@ jobs:
name: ZIP stuff up
working-directory: output
- run: ls output
- uses: actions/upload-artifact@v6
- uses: actions/upload-artifact@v7
with:
name: photonlib-offline
path: output/*.zip
@@ -335,7 +345,7 @@ jobs:
include:
- os: ubuntu-24.04
artifact-name: Linux
arch-override: linuxx64
arch-override: linuxx86-64
- os: ubuntu-24.04
artifact-name: LinuxArm64
arch-override: linuxarm64
@@ -347,25 +357,22 @@ jobs:
- uses: actions/checkout@v6
with:
fetch-depth: 0
- name: Install Java 17
uses: actions/setup-java@v5
- uses: actions/setup-java@v5
with:
java-version: 17
java-version: 25
distribution: temurin
- name: Install pnpm
uses: pnpm/action-setup@v4
- uses: pnpm/action-setup@v5
with:
version: 10
- name: Setup Node.js
uses: actions/setup-node@v6
- uses: actions/setup-node@v6
with:
node-version: 22
node-version: 24
cache: pnpm
cache-dependency-path: photon-client/pnpm-lock.yaml
- name: Install Arm64 Toolchain
run: ./gradlew installArm64Toolchain
if: ${{ (matrix.artifact-name) == 'LinuxArm64' }}
- uses: actions/download-artifact@v7
- uses: actions/download-artifact@v8
with:
name: built-docs
path: photon-server/src/main/resources/web/docs
@@ -373,11 +380,11 @@ jobs:
if: ${{ (matrix.arch-override != 'none') }}
- run: ./gradlew photon-server:shadowJar
if: ${{ (matrix.arch-override == 'none') }}
- uses: actions/upload-artifact@v6
- uses: actions/upload-artifact@v7
with:
name: jar-${{ matrix.artifact-name }}
archive: false
path: photon-server/build/libs
- uses: actions/upload-artifact@v6
- uses: actions/upload-artifact@v7
with:
name: photon-targeting_jar-${{ matrix.artifact-name }}
path: photon-targeting/build/libs
@@ -394,7 +401,7 @@ jobs:
arch-override: macarm64
- os: macos-latest
artifact-name: macOS
arch-override: macx64
arch-override: macx86-64
runs-on: ${{ matrix.os }}
name: "Build fat JAR - ${{ matrix.artifact-name }}"
@@ -409,8 +416,8 @@ jobs:
matrix:
include:
- os: windows-latest
artifact-name: Win64
arch-override: winx64
artifact-name: Win
arch-override: winx86-64
runs-on: ${{ matrix.os }}
name: "Build fat JAR - ${{ matrix.artifact-name }}"
@@ -425,24 +432,26 @@ jobs:
matrix:
include:
- os: ubuntu-24.04
artifact-name: jar-Linux
artifact-name: photonvision-*-linuxx86-64.jar
extraOpts: -Djdk.lang.Process.launchMechanism=vfork
- os: windows-latest
artifact-name: jar-Win64
artifact-name: photonvision-*-winx86-64.jar
- os: macos-latest
artifact-name: jar-macOS
artifact-name: photonvision-*-macarm64.jar
- os: ubuntu-24.04-arm
artifact-name: photonvision-*-linuxarm64.jar
runs-on: ${{ matrix.os }}
steps:
- name: Install Java 17
uses: actions/setup-java@v5
- uses: actions/setup-java@v5
with:
java-version: 17
java-version: 25
distribution: temurin
- uses: actions/download-artifact@v7
- uses: actions/download-artifact@v8
with:
name: ${{ matrix.artifact-name }}
pattern: ${{ matrix.artifact-name }}
# The jar is run twice to exercise different code paths.
- run: |
echo "=== First run ==="
@@ -479,85 +488,66 @@ jobs:
matrix:
include:
- os: ubuntu-24.04-arm
artifact-name: LinuxArm64
image_suffix: RaspberryPi
plat_override: LINUX_RASPBIAN64
image_url: https://github.com/PhotonVision/photon-image-modifier/releases/download/$IMAGE_VERSION/photonvision_raspi.img.xz
minimum_free_mb: 100
- os: ubuntu-24.04-arm
artifact-name: LinuxArm64
image_suffix: limelight2
plat_override: LINUX_RASPBIAN64
image_url: https://github.com/PhotonVision/photon-image-modifier/releases/download/$IMAGE_VERSION/photonvision_limelight.img.xz
minimum_free_mb: 100
- os: ubuntu-24.04-arm
artifact-name: LinuxArm64
image_suffix: limelight3
plat_override: LINUX_RASPBIAN64
image_url: https://github.com/PhotonVision/photon-image-modifier/releases/download/$IMAGE_VERSION/photonvision_limelight3.img.xz
minimum_free_mb: 100
- os: ubuntu-24.04-arm
artifact-name: LinuxArm64
image_suffix: limelight3G
plat_override: LINUX_RASPBIAN64
image_url: https://github.com/PhotonVision/photon-image-modifier/releases/download/$IMAGE_VERSION/photonvision_limelight3g.img.xz
minimum_free_mb: 100
- os: ubuntu-24.04-arm
artifact-name: LinuxArm64
image_suffix: limelight4
plat_override: LINUX_RASPBIAN64
image_url: https://github.com/PhotonVision/photon-image-modifier/releases/download/$IMAGE_VERSION/photonvision_limelight4.img.xz
minimum_free_mb: 100
- os: ubuntu-24.04-arm
artifact-name: LinuxArm64
image_suffix: luma_p1
plat_override: LINUX_RASPBIAN64
image_url: https://github.com/PhotonVision/photon-image-modifier/releases/download/$IMAGE_VERSION/photonvision_luma_p1.img.xz
minimum_free_mb: 100
- os: ubuntu-24.04-arm
artifact-name: LinuxArm64
image_suffix: orangepi5
plat_override: LINUX_RK3588_64
image_url: https://github.com/PhotonVision/photon-image-modifier/releases/download/$IMAGE_VERSION/photonvision_opi5.img.xz
minimum_free_mb: 1024
- os: ubuntu-24.04-arm
artifact-name: LinuxArm64
image_suffix: orangepi5b
plat_override: LINUX_RK3588_64
image_url: https://github.com/PhotonVision/photon-image-modifier/releases/download/$IMAGE_VERSION/photonvision_opi5b.img.xz
minimum_free_mb: 1024
- os: ubuntu-24.04-arm
artifact-name: LinuxArm64
image_suffix: orangepi5plus
plat_override: LINUX_RK3588_64
image_url: https://github.com/PhotonVision/photon-image-modifier/releases/download/$IMAGE_VERSION/photonvision_opi5plus.img.xz
minimum_free_mb: 1024
- os: ubuntu-24.04-arm
artifact-name: LinuxArm64
image_suffix: orangepi5pro
plat_override: LINUX_RK3588_64
image_url: https://github.com/PhotonVision/photon-image-modifier/releases/download/$IMAGE_VERSION/photonvision_opi5pro.img.xz
minimum_free_mb: 1024
- os: ubuntu-24.04-arm
artifact-name: LinuxArm64
image_suffix: orangepi5max
plat_override: LINUX_RK3588_64
image_url: https://github.com/PhotonVision/photon-image-modifier/releases/download/$IMAGE_VERSION/photonvision_opi5max.img.xz
minimum_free_mb: 1024
- os: ubuntu-24.04-arm
artifact-name: LinuxArm64
image_suffix: rock5c
plat_override: LINUX_RK3588_64
image_url: https://github.com/PhotonVision/photon-image-modifier/releases/download/$IMAGE_VERSION/photonvision_rock5c.img.xz
minimum_free_mb: 1024
- os: ubuntu-24.04-arm
artifact-name: LinuxArm64
image_suffix: orangepi6plus
plat_override: LINUX_AARCH64
image_url: https://github.com/PhotonVision/photon-image-modifier/releases/download/$IMAGE_VERSION/photonvision_opi6plus.img.xz
minimum_free_mb: 1024
- os: ubuntu-24.04-arm
artifact-name: LinuxArm64
image_suffix: rubikpi3
plat_override: LINUX_QCS6490
image_url: https://github.com/PhotonVision/photon-image-modifier/releases/download/$IMAGE_VERSION/photonvision_rubikpi3.tar.xz
@@ -573,9 +563,9 @@ jobs:
uses: actions/checkout@v6
with:
fetch-depth: 0
- uses: actions/download-artifact@v7
- uses: actions/download-artifact@v8
with:
name: jar-${{ matrix.artifact-name }}
pattern: photonvision-*-linuxarm64.jar
- uses: photonvision/photon-image-runner@HEAD
name: Generate image
id: generate_image
@@ -611,10 +601,9 @@ jobs:
# Point smoketest to the old image
echo "smoketest_image_loc=${{ steps.generate_image.outputs.image }}" >> $GITHUB_ENV
- uses: actions/upload-artifact@v6
name: Upload image
- uses: actions/upload-artifact@v7
with:
name: image-${{ matrix.image_suffix }}
archive: false
path: photonvision*.xz
# This is done after uploading the image to avoid contaminating the image with logs, caches, etc.
@@ -643,25 +632,24 @@ jobs:
runs-on: ubuntu-24.04
steps:
# Download all fat JARs
- uses: actions/download-artifact@v7
- uses: actions/download-artifact@v8
with:
merge-multiple: true
pattern: jar-*
pattern: photonvision-*.jar
# Download offline photonlib
- uses: actions/download-artifact@v7
- uses: actions/download-artifact@v8
with:
merge-multiple: true
pattern: photonlib-offline
# Download vendor json
- uses: actions/download-artifact@v7
- uses: actions/download-artifact@v8
with:
merge-multiple: true
pattern: photonlib-vendor-json
pattern: photonlib-*.json
# Download all images
- uses: actions/download-artifact@v7
- uses: actions/download-artifact@v8
with:
merge-multiple: true
pattern: image-*
pattern: photonvision-*.xz
- run: find
# Push to dev release

View File

@@ -13,10 +13,9 @@ jobs:
steps:
- name: Checkout sources
uses: actions/checkout@v6
- name: Setup Java
uses: actions/setup-java@v5
- uses: actions/setup-java@v5
with:
distribution: 'temurin'
java-version: 17
java-version: 25
- name: Generate and submit dependency graph
uses: gradle/actions/dependency-submission@v5

View File

@@ -46,9 +46,9 @@ jobs:
- name: Generate diff
run: git diff HEAD > wpiformat-fixes.patch
if: ${{ failure() }}
- uses: actions/upload-artifact@v6
- uses: actions/upload-artifact@v7
with:
name: wpiformat fixes
archive: false
path: wpiformat-fixes.patch
if: ${{ failure() }}
javaformat:
@@ -61,7 +61,7 @@ jobs:
fetch-depth: 0
- uses: actions/setup-java@v5
with:
java-version: 17
java-version: 25
distribution: temurin
- run: |
set +e
@@ -81,13 +81,12 @@ jobs:
steps:
- uses: actions/checkout@v6
- name: Install pnpm
uses: pnpm/action-setup@v4
uses: pnpm/action-setup@v5
with:
version: 10
- name: Setup Node.js
uses: actions/setup-node@v6
- uses: actions/setup-node@v6
with:
node-version: 22
node-version: 24
cache: pnpm
cache-dependency-path: photon-client/pnpm-lock.yaml
- name: Install Dependencies

View File

@@ -30,21 +30,19 @@ jobs:
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v6
- name: Install pnpm
uses: pnpm/action-setup@v4
- uses: pnpm/action-setup@v5
with:
version: 10
- name: Setup Node.js
uses: actions/setup-node@v6
- uses: actions/setup-node@v6
with:
node-version: 22
node-version: 24
cache: pnpm
cache-dependency-path: photon-client/pnpm-lock.yaml
- name: Install Dependencies
run: pnpm i --frozen-lockfile
- name: Build Production Client
run: pnpm run build-demo
- uses: actions/upload-artifact@v6
- uses: actions/upload-artifact@v7
with:
name: built-demo
path: photon-client/dist/
@@ -60,16 +58,15 @@ jobs:
fetch-depth: 0
- name: Fetch tags
run: git fetch --tags --force
- name: Install Java 17
uses: actions/setup-java@v5
- uses: actions/setup-java@v5
with:
java-version: 17
java-version: 25
distribution: temurin
- name: Build javadocs/doxygen
run: |
chmod +x gradlew
./gradlew photon-docs:generateJavaDocs photon-docs:doxygen
- uses: actions/upload-artifact@v6
- uses: actions/upload-artifact@v7
with:
name: docs-java-cpp
path: photon-docs/build/docs
@@ -80,7 +77,7 @@ jobs:
runs-on: ubuntu-24.04
steps:
# Download docs artifact
- uses: actions/download-artifact@v7
- uses: actions/download-artifact@v8
with:
pattern: docs-*
- run: find .
@@ -106,7 +103,7 @@ jobs:
needs: [build_demo]
runs-on: ubuntu-24.04
steps:
- uses: actions/download-artifact@v7
- uses: actions/download-artifact@v8
with:
name: built-demo
- run: find .

View File

@@ -36,7 +36,7 @@ jobs:
run: python setup.py sdist bdist_wheel
- name: Upload artifacts
uses: actions/upload-artifact@v6
uses: actions/upload-artifact@v7
with:
name: dist
path: ./photon-lib/py/dist/
@@ -62,7 +62,7 @@ jobs:
pip install pytest mypy
- name: Download artifacts
uses: actions/download-artifact@v6
uses: actions/download-artifact@v8
with:
name: dist
path: dist/
@@ -100,9 +100,10 @@ jobs:
- name: Install dependencies
run: |
python -m pip install --upgrade pip
python -m pip install mypy
- name: Download artifacts
uses: actions/download-artifact@v6
uses: actions/download-artifact@v8
with:
name: dist
path: ./photon-lib/py/dist/
@@ -120,7 +121,7 @@ jobs:
for folder in */;
do
echo $folder
./run.sh $folder
./test.sh $folder
done
deploy:
@@ -131,7 +132,7 @@ jobs:
steps:
- name: Download artifacts
uses: actions/download-artifact@v6
uses: actions/download-artifact@v8
with:
name: dist
path: dist/

View File

@@ -11,13 +11,12 @@ jobs:
steps:
- uses: actions/checkout@v6
- name: Install pnpm
uses: pnpm/action-setup@v4
uses: pnpm/action-setup@v5
with:
version: 10
- name: Setup Node
uses: actions/setup-node@v6
- uses: actions/setup-node@v6
with:
node-version: 22
node-version: 24
cache: pnpm
cache-dependency-path: website/pnpm-lock.yaml
- name: Install packages
@@ -37,22 +36,27 @@ jobs:
format-check:
name: Check Formatting
defaults:
run:
working-directory: website
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- name: Install pnpm
uses: pnpm/action-setup@v4
- uses: pnpm/action-setup@v5
with:
version: 10
- name: Setup Node
uses: actions/setup-node@v6
- uses: actions/setup-node@v6
with:
node-version: 22
node-version: 24
cache: pnpm
cache-dependency-path: website/pnpm-lock.yaml
- name: Install Packages
run: pnpm i --frozen-lockfile
working-directory: website
- name: Run Formatting Check
run: pnpm prettier -c .
working-directory: website
- run: |
set +e
pnpm run format-ci
exit_code=$?
if test "$exit_code" -ne "0"; then
echo "::error ::Linting failed. See https://docs.photonvision.org/en/latest/docs/contributing/linting.html"
fi
exit $exit_code

7
.gitignore vendored
View File

@@ -156,3 +156,10 @@ photon-client/playwright-report/
photon-client/blob-report/
photon-client/playwright/.cache/
photon-client/playwright/.auth/
# Nix files
shell.nix
flake.nix
flake.lock
/workspace

View File

@@ -2,7 +2,7 @@ cppHeaderFileInclude {
\.h$
}
modifiableFileExclude {
generatedFileExclude {
photon-lib/py/photonlibpy/generated/
photon-targeting/src/generated/
photon-targeting/src/main/native/cpp/photon/constrained_solvepnp/generate/

View File

@@ -32,11 +32,11 @@ You can run one of the many built in examples straight from the command line, to
Note that these are case sensitive!
* `-PArchOverride=foobar`: builds for a target system other than your current architecture. [Valid overrides](https://github.com/wpilibsuite/wpilib-tool-plugin/blob/main/src/main/java/edu/wpi/first/tools/NativePlatforms.java) are:
* winx64
* winx86-64
* winarm64
* macx64
* macx86-64
* macarm64
* linuxx64
* linuxx86-64
* linuxarm64
* linuxathena
- `-PtgtIP`: Specifies where `./gradlew deploy` should try to copy the fat JAR to
@@ -45,7 +45,7 @@ Note that these are case sensitive!
- `-Pprofile`: enables JVM profiling
- `-PwithSanitizers`: On Linux, enables `-fsanitize=address,undefined,leak`
If you're cross-compiling, you'll need the WPILib toolchain installed. This must be done via Gradle: for example `./gradlew installArm64Toolchain` or `./gradlew installRoboRioToolchain`
If you're cross-compiling, you'll need the WPILib toolchain installed. This must be done via Gradle: for example `./gradlew installArm64Toolchain` or `./gradlew installSystemCoreToolchain`
## Out-of-Source Dependencies
@@ -67,7 +67,7 @@ PhotonVision was forked from [Chameleon Vision](https://github.com/Chameleon-Vis
* [EJML](https://github.com/lessthanoptimal/ejml)
* [Javalin](https://javalin.io/)
* [JSON](https://json.org)
* [FasterXML](https://github.com/FasterXML) - Specifically [jackson](https://github.com/FasterXML/jackson)
* [Avaje](https://avaje.io) - Specifically [jsonb](https://avaje.io/jsonb/)
* [MessagePack for Java](https://github.com/msgpack/msgpack-java)
* [OSHI](https://github.com/oshi/oshi)
* [QuickBuffers](https://github.com/HebiRobotics/QuickBuffers)

View File

@@ -1,16 +1,17 @@
import edu.wpi.first.toolchain.*
import org.wpilib.toolchain.*
plugins {
id "cpp"
id "com.diffplug.spotless" version "8.1.0"
id "edu.wpi.first.wpilib.repositories.WPILibRepositoriesPlugin" version "2020.2"
id "edu.wpi.first.GradleRIO" version "2026.2.1"
id 'org.photonvision.tools.WpilibTools' version '2.4.1-photon'
id 'com.google.protobuf' version '0.9.3' apply false
id 'edu.wpi.first.GradleJni' version '1.1.0'
id "org.wpilib.WPILibRepositoriesPlugin" version "2027.0.0"
id 'org.wpilib.NativeUtils' version '2027.7.1' apply false
id 'org.wpilib.DeployUtils' version '2027.1.0' apply false
id 'org.photonvision.tools.WpilibTools' version 'v5.0.1'
id 'com.google.protobuf' version '0.9.5' apply false
id 'org.wpilib.GradleJni' version '2027.0.0'
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 'com.gradleup.shadow' version '9.0.0' apply false
id "com.github.node-gradle.node" version "7.1.0" apply false
}
allprojects {
@@ -21,6 +22,7 @@ allprojects {
maven { url = "https://maven.photonvision.org/releases" }
maven { url = "https://maven.photonvision.org/snapshots" }
}
wpilibRepositories.use2027Repos()
wpilibRepositories.addAllReleaseRepositories(it)
wpilibRepositories.addAllDevelopmentRepositories(it)
}
@@ -28,20 +30,21 @@ allprojects {
ext.localMavenURL = file("$project.buildDir/outputs/maven")
ext.allOutputsFolder = file("$project.buildDir/outputs")
// Configure the version number.
apply from: "versioningHelper.gradle"
ext {
wpilibVersion = "2026.2.1"
wpimathVersion = wpilibVersion
openCVYear = "2025"
openCVversion = "4.10.0-3"
wpilibVersion = "2027.0.0-alpha-6"
openCVversion = "2027-4.13.0-3"
ejmlVersion = "0.43.1";
avajeJsonbVersion = "3.14";
msgpackVersion = "0.9.0";
quickbufVersion = "1.3.3";
jacocoVersion = "0.8.14";
javalinVersion = "6.7.0"
libcameraDriverVersion = "v2026.0.0"
rknnVersion = "dev-v2026.0.1-1-g89b2888"
rubikVersion = "dev-v2026.0.1-4-g13d6279"
frcYear = "2026"
mrcalVersion = "dev-v2026.0.0-1-g239d80e";
libcameraDriverVersion = "v2027.0.0-alpha-1"
rknnVersion = "dev-v2026.0.1-3-g14c3ecb"
tfliteVersion = "v2027.0.2-alpha-1"
wpilibYear = "2027_alpha5"
mrcalVersion = "v2027.0.2";
pubVersion = versionString
isDev = pubVersion.startsWith("dev")
@@ -62,7 +65,7 @@ spotless {
java {
target fileTree('.') {
include '**/*.java'
exclude '**/build/**', '**/build-*/**', '**/src/generated/**'
exclude '**/build/**', '**/build-*/**', '**/src/generated/**', "**/bin/generated-sources/**"
}
toggleOffOn()
googleJavaFormat()
@@ -94,7 +97,7 @@ spotless {
}
wrapper {
gradleVersion = '8.14.3'
gradleVersion = '9.4.0'
}
ext.getCurrentArch = {

View File

@@ -36,7 +36,6 @@ sphinx-autobuild==2024.10.3
sphinx-basic-ng==1.0.0b2
sphinx-notfound-page==1.1.0
sphinx-rtd-theme==3.0.2
sphinx-tabs==3.4.7
sphinx_design==0.6.1
sphinxcontrib-applehelp==2.0.0
sphinxcontrib-devhelp==2.0.0

View File

@@ -181,4 +181,4 @@ 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", "substitution"]
myst_enable_extensions = ["colon_fence", "substitution", "attrs_inline"]

View File

@@ -1,19 +1,17 @@
# NetworkTables API
## About
## Usage
:::{warning}
PhotonVision interfaces with PhotonLib, our vendor dependency, using NetworkTables. If you are running PhotonVision on a robot (ie. with a RoboRIO), you should **turn the NetworkTables server switch (in the settings tab) off** in order to get PhotonLib to work. Also ensure that you set your team number. **The NetworkTables server should only be enabled if you know what you're doing!**
PhotonVision's NetworkTables API is not designed for robot consumption, only internal at-a-glance debugging.**
:::
**Use PhotonLib instead, as the NetworkTables API is not supported for robot consumption.**
## API
:::{warning}
NetworkTables is not a supported setup/viable option when using PhotonVision as we only send one target at a time (this is problematic when using AprilTags, which will return data from multiple tags at once).
**We strongly recommend using PhotonLib instead, as the NetworkTables API will most likely be removed in 2027.**
:::
The tables below contain the the name of the key for each entry that PhotonVision sends over the network and a short description of the key. The entries should be extracted from a subtable with your camera's nickname (visible in the PhotonVision UI) under the main `photonvision` table.
### Getting Target Information
@@ -64,9 +62,3 @@ These entries are global, meaning that they should be called on the main `photon
| Key | Type | Description |
| --------- | ----- | -------------------------------------------------------- |
| `ledMode` | `int` | Sets the LED Mode (-1: default, 0: off, 1: on, 2: blink) |
:::{warning}
Setting the LED mode to -1 (default) when `multiple` cameras are connected may result in unexpected behavior. {ref}`This is a known limitation of PhotonVision. <docs/troubleshooting/common-errors:LED Control>`
Single camera operation should work without issue.
:::

View File

@@ -3,5 +3,8 @@
"ledPins" : [ 13, 18 ],
"ledsCanDim" : true,
"ledPWMFrequency" : 1000,
"statusLEDType": "GreenYellow",
"statusLEDPins": [ 5, 4 ],
"statusLEDActiveHigh": false,
"vendorFOV" : 75.76079874010732
}

View File

@@ -2,5 +2,8 @@
"deviceName" : "Limelight 2",
"ledPins" : [ 17, 18 ],
"ledsCanDim" : false,
"statusLEDType": "GreenYellow",
"statusLEDPins": [ 5, 4 ],
"statusLEDActiveHigh": false,
"vendorFOV" : 75.76079874010732
}

View File

@@ -25,7 +25,7 @@ Go to the [GitHub releases page](https://github.com/PhotonVision/photonvision/re
:::{note}
If your coprocessor has a 64 bit ARM based CPU architecture (OrangePi, Raspberry Pi, etc.), download the LinuxArm64.jar file.
If your coprocessor has an 64 bit x86 based CPU architecture (Mini PC, laptop, etc.), download the Linuxx64.jar file.
If your coprocessor has an 64 bit x86 based CPU architecture (Mini PC, laptop, etc.), download the Linuxx86-64.jar file.
:::
:::{warning}

View File

@@ -1,53 +1,9 @@
# Mac OS Installation
# MacOS Installation
:::{warning}
Due to current [cscore](https://github.com/wpilibsuite/allwpilib/tree/main/cscore) restrictions, the PhotonVision server backend may have issues running macOS.
:::
## Builds
:::{note}
You do not need to install PhotonVision on a Mac in order to access the webdashboard (assuming you are using an external coprocessor like a Raspberry Pi).
:::
Builds for macOS are currently generated in CI for testing compatibility. This allows us to ensure that macOS remains a viable development platform.
VERY Limited macOS support is available.
## Use as a coprocessor
## Installing Java
PhotonVision requires a JDK installed and on the system path. JDK 17 is needed (different versions will not work). You may already have this if you have installed WPILib 2026+. If not, [download and install it from here](https://adoptium.net/temurin/releases?version=17).
:::{warning}
Using a JDK other than JDK17 will cause issues when running PhotonVision and is not supported.
:::
## Downloading the Latest Stable Release of PhotonVision
Go to the [GitHub releases page](https://github.com/PhotonVision/photonvision/releases) and download the relevant .jar file for your coprocessor.
:::{note}
If you have an M Series Mac, download the macarm64.jar file.
If you have an Intel based Mac, download the macx64.jar file.
:::
:::{warning}
Be careful to pick the latest stable release. "Draft" or "Pre-Release" versions are not stable and often have bugs.
:::
## Running PhotonVision
To run PhotonVision, open a terminal window of your choice and run the following command:
```
$ java -jar /path/to/photonvision/photonvision-xxx.jar
```
:::{warning}
Due to current [cscore](https://github.com/wpilibsuite/allwpilib/tree/main/cscore) restrictions, the PhotonVision using test mode is all that is known to work currently.
:::
## Accessing the PhotonVision Interface
Once the Java backend is up and running, you can access the main vision interface by navigating to `localhost:5800` inside your browser.
:::{warning}
Due to current [cscore](https://github.com/wpilibsuite/allwpilib/tree/main/cscore) restrictions, it is unlikely any streams will open from real webcams.
:::
This is not a recommended path. MacOS is not officially supported, and your mileage may vary.

View File

@@ -16,7 +16,7 @@ PhotonVision requires a JDK installed and on the system path. **JDK 17 is needed
## Downloading the Latest Stable Release of PhotonVision
Go to the [GitHub releases page](https://github.com/PhotonVision/photonvision/releases) and download the winx64.jar file.
Go to the [GitHub releases page](https://github.com/PhotonVision/photonvision/releases) and download the winx86-64.jar file.
## Running PhotonVision

View File

@@ -23,7 +23,7 @@ Ensure that your camera is calibrated and 3D mode is enabled. Navigate to the Ou
By default, enabling multi-target will disable calculating camera-to-target transforms for each observed AprilTag target to increase performance; the X/Y/angle numbers shown in the target table of the UI are instead calculated using the tag's expected location (per the field layout JSON) and the field-to-camera transform calculated using MultiTag. If you additionally want the individual camera-to-target transform calculated using SolvePNP for each target, enable "Always Do Single-Target Estimation".
:::
This multi-target pose estimate can be accessed using PhotonLib. We suggest using {ref}`the PhotonPoseEstimator class <docs/programming/photonlib/robot-pose-estimator:AprilTags and PhotonPoseEstimator>` with the `MULTI_TAG_PNP_ON_COPROCESSOR` strategy to simplify code, but the transform can be directly accessed using `getMultiTagResult`/`MultiTagResult()`/`multitagResult` (Java/C++/Python).
This multi-target pose estimate can be accessed using PhotonLib. We suggest using {ref}`the PhotonPoseEstimator class <docs/programming/photonlib/robot-pose-estimator:AprilTags and PhotonPoseEstimator>` and calling `estimateCoprocMultiTagPose` (with an optional fallback like `estimateLowestAmbiguityPose`) to simplify code, but the transform can be directly accessed using `getMultiTagResult`/`MultiTagResult()`/`multitagResult` (Java/C++/Python).
```{eval-rst}
.. tab-set-code::
@@ -46,7 +46,7 @@ This multi-target pose estimate can be accessed using PhotonLib. We suggest usin
{
auto multiTagResult = result.MultiTagResult();
if (multiTagResult.has_value()) {
frc::Transform3d fieldToCamera = multiTagResult->estimatedPose.best;
wpi::math::Transform3d fieldToCamera = multiTagResult->estimatedPose.best;
}
}

View File

@@ -7,7 +7,7 @@ In order to detect AprilTags and use 3D mode, your camera must be calibrated at
To calibrate a camera, images of a ChArUco board (or chessboard) are taken. By comparing where the grid corners should be in object space (for example, a corner once every inch in an 8x6 grid) with where they appear in the camera image, we can find a least-squares estimate for intrinsic camera properties like focal lengths, center point, and distortion coefficients. For more on camera calibration, please review the [OpenCV documentation](https://docs.opencv.org/4.x/dc/dbb/tutorial_py_calibration.html).
:::{warning}
While any resolution can be calibrated, higher resolutions may be too performance-intensive for some coprocessors to handle. Therefore, we recommend experimenting to see what works best for your coprocessor.
PhotonVision supports many resolution configuration options, with only a minimum of 640x480 required. However, higher resolutions may be too performance-intensive for some coprocessors to handle. Therefore, we recommend experimenting to see what works best for your coprocessor.
:::
:::{note}

View File

@@ -8,11 +8,11 @@ This section contains the build instructions from the source code available at [
**Java Development Kit:**
This project requires Java Development Kit (JDK) 17 to be compiled. This is the same Java version that comes with WPILib for 2026+. **Windows Users must use the JDK that ships with WPILib.** For other platforms, you can follow the instructions to install JDK 17 for your platform [here](https://bell-sw.com/pages/downloads/#jdk-17-lts).
This project requires Java Development Kit (JDK) 25 to be compiled. This is the same Java version that comes with WPILib for 2027. **Windows Users must use the JDK that ships with WPILib.** For other platforms, you can follow the instructions to install JDK 25 for your platform [here](https://bell-sw.com/pages/downloads/#jdk-25-lts).
**Node JS:**
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/).
The UI is written in Node JS. To compile the UI, Node 24 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/).
**pnpm:**
@@ -47,6 +47,30 @@ In the photon-client directory:
pnpm install
```
### Building the UI
In order to properly build UI changes before running the project, run the following `gradlew` command in the project's root directory:
```{eval-rst}
.. tab-set::
.. tab-item:: Linux
:sync: linux
``./gradlew buildAndCopyUI``
.. tab-item:: macOS
:sync: macos
``./gradlew buildAndCopyUI``
.. tab-item:: Windows (cmd)
:sync: windows
``gradlew buildAndCopyUI``
```
### Using hot reload on the UI
In the photon-client directory:

View File

@@ -79,9 +79,9 @@ public class Robot extends TimedRobot {
camera.getAllUnreadResults();
}
var t1 = Timer.getFPGATimestamp();
var t1 = Timer.getMonotonicTimestamp();
light.set(true);
var t2 = Timer.getFPGATimestamp();
var t2 = Timer.getMonotonicTimestamp();
for (int i = 0; i < 100; i++) {

View File

@@ -0,0 +1,68 @@
# Welcome!
First and foremost, welcome to PhotonVision Development! We're pumped that you're interested to jump in, and help out!
Like most things in FIRST, PhotonVision is reliant on the hard work of dedicated volunteers. It doesn't exist without you. You're joining in with a strong tradition of open-source software development.
This page talks a bit about how we develop PhotonVision, as a community. It applies to all repos and community aspects for PhotonVision.
## Getting Started
The very first thing we'd recommend - get your computer set up to be able to build and run PhotonVision. [Docs for that are here](building-photon.md).
The two best ways to figure out what to do first:
1. Take a look at [the main PhotonVision Repo's Issues](https://github.com/PhotonVision/photonvision/issues) - especially those marked `bug` or `good first issue`!
2. Connect on [the Discord Server](https://discord.gg/wYxTwym) - Introduce yourself, and talk about what you're interested in!
From there - assign yourself to an issue if you intend to work it. Create a Fork in github, and make and test your changes. Once passing builds, meeting your expectations, and passing CI, open a pull request back to the main repo.
## Submitting A PR
Pull Requests are the mechanism used to ensure we only merge high-quality, reviewed, concrete, and organized changes to the codebase.
Things that peer reviewers will look for include:
* All CI checks are passing on the server.
* Documentation - does the PR match the issue? Is the description detailed?
* Cohesiveness - does the PR express a singular, related set of changes?
* Architecture - is the code consistent with other code around it? Is it maintainable for the long term?
* Testing - have unit tests been added as appropriate? Has the change been tested on real hardware?
Work as you can to clean up any changes requested. Once all changes are addressed, the contain should get merged, and included in the next release! Horary!
## General Developer Interaction
The main guiding principle: remember that we're all volunteers. While promptness is always appreciated (and occasionally required as release deadlines approach), it's important to remember each individual is only contributing when their personal schedule allows. Expect delays, prefer asynchronous communication, and be polite with reminders.
While most of the community members are either FIRST robotics mentors or students, the PhotonVision development team is primarily focused on delivering high quality software. Mentorship can and does occur, but is not the primary goal. Members are expected to do their learning fairly independently.
Seek to build trust in the quality of your work. Think carefully on your opinions before asking others to think about them too.
Bias toward action, and productionizable code. Limit the number of active PR's to help keep focus.
Finally, be sure to embody the ethos of Gracious Professionalism in all your actions, on all platforms in the project. See more in our [code of conduct](https://github.com/PhotonVision/.github/blob/738dfcb792fdbfc2e8408c0135e389179fc483c0/codeofcoduct.md)
### AI Usage
Coding assistants driven by Large Language Models ("AI") are extremely powerful tools, and have been used on more than one occasion to accelerate development.
PhotonVision still maintains a fundamental philosophy that the human submitting the pull request is responsible for the code and its behavior, regardless of the tools used to create it.
These tools can also generate a large volume of code changes, very rapidly. The above guidelines on PR quality still apply - large, undirected, or overly-scoped PR's are likely to be ignored, regardless of tooling used to generate them.
### Violations
We're thankful that we've rarely experienced major issues in our community. In all cases, the project leads shall have the final decision making authority when dealing with violations of these guidelines.
## Yearly Development Cycle
PhotonVision's Development Cycle follows the same general flow as the FRC Build Season. Larger experiments get done over the summer, fall focuses on testing and "production-readiness", build season focuses on keeping all teams running smoothly.
The actual priorities also shift as developers have more or time to commit, or express interest in specific direction.
PR's are absolutely always welcome. Just note depending on the scope and size, they may not be reviewed or merged immediately.
## Project Governance
This project is jointly lead by Matt and Banks, who may be contacted on Discord. They serve as a "Benevolent leader for life" role, coordinating and approving architecture as needed, and curating the team of developers who have Pull Request review and merge responsibilities.

View File

@@ -1,6 +1,7 @@
# Contributing to PhotonVision Projects
```{toctree}
guidelines
building-photon
building-docs
linting

View File

@@ -34,10 +34,16 @@ To install *doc8*, the python tool we use to lint our documentation, run `pipx i
To lint the documentation, run `doc8 docs` from the root level of the docs.
## Website
### Formatting the website
To format the website, run `pnpm -C website run format`.
## Alias
The following [alias](https://www.computerworld.com/article/1373210/how-to-use-aliases-in-linux-shell-commands.html) can be added to your shell config, which will allow you to lint the entirety of the PhotonVision project by running `pvLint`. The alias will work on Linux, macOS, Git Bash on Windows, and WSL.
```sh
alias pvLint='wpiformat -v && ./gradlew spotlessApply && pnpm -C photon-client lint && pnpm -C photon-client format && doc8 docs'
alias pvLint='wpiformat -v && ./gradlew spotlessApply && pnpm -C photon-client lint && pnpm -C photon-client format && doc8 docs && pnpm -C website format'
```

View File

@@ -19,14 +19,18 @@ When running on Linux, PhotonVision can use [diozero](https://www.diozero.com) t
"ledsCanDim" : true,
"ledBrightnessRange" : [ 0, 100 ],
"ledPWMFrequency" : 0,
"statusRGBPins" : [ ],
"statusRGBActiveHigh" : false,
"statusLEDType": "RGB",
"statusLEDPins" : [ ],
"statusLEDActiveHigh" : false,
}
```
:::{note}
No hardware boards with status RGB LED pins or non-dimming LED's have been tested yet. Please reach out to the development team if these features are desired, they can assist with configuration and testing.
:::
There are currently two types of status LEDs supported:
* `RGB` (default): A singular LED mixing separate red, green, and blue inputs
* `GreenYellow`: A pair of independent green and yellow LEDs
For an explanation of the colors used for status LEDs, see {ref}`Status LEDs<docs/troubleshooting/status-leds:Status LEDs>`
### GPIO Pinout
@@ -134,8 +138,9 @@ Here is a complete example `hardwareConfig.json`:
"ledsCanDim" : true,
"ledBrightnessRange" : [ 0, 100 ],
"ledPWMFrequency" : 0,
"statusRGBPins" : [ ],
"statusRGBActiveHigh" : false,
"statusLEDType": "RGB",
"statusLEDPins" : [ ],
"statusLEDActiveHigh" : false,
"getGPIOCommand" : "getGPIO {p}",
"setGPIOCommand" : "setGPIO {p} {s}",
"setPWMCommand" : "setPWM {p} {v}",

View File

@@ -10,7 +10,7 @@ In order to use PhotonVision, you need a coprocessor and a camera. This page dis
### Minimum System Requirements
- Ubuntu 22.04 LTS or Windows 10/11
- Ubuntu 24.04 LTS or Windows 10/11
- We don't recommend using Windows for anything except testing out the system on a local machine.
- CPU: ARM Cortex-A53 (the CPU on Raspberry Pi 3) or better
- At least 8GB of storage

View File

@@ -8,6 +8,8 @@ PhotonVision runs object detection on the Orange Pi 5 by use of the RKNN model a
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.
If you have a YOLO-Pro model from Edge Impulse, it can be converted using [this notebook](https://github.com/ramalamadingdong/yolo-pro-to-yolo11/tree/main).
## Converting Custom Models
:::{warning}

View File

@@ -8,6 +8,8 @@ PhotonVision runs object detection on the Rubik Pi 3 by use of [TensorflowLite](
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.
If you have a YOLO-Pro model from Edge Impulse, it can be converted using [this notebook](https://github.com/ramalamadingdong/yolo-pro-to-yolo11/tree/main).
## Converting Custom Models
:::{warning}

View File

@@ -60,7 +60,7 @@ You can also get the pipeline latency from a pipeline result using the `getLaten
.. code-block:: c++
// Get the pipeline latency.
units::second_t latency = result.GetLatency();
wpi::units::second_t latency = result.GetLatency();
.. code-block:: python

View File

@@ -107,7 +107,7 @@ You can get a list of tracked targets using the `getTargets()`/`GetTargets()` (J
.. code-block:: c++
// Get a list of currently tracked targets.
wpi::ArrayRef<photonlib::PhotonTrackedTarget> targets = result.GetTargets();
std::span<photonlib::PhotonTrackedTarget> targets = result.GetTargets();
.. code-block:: python
@@ -166,8 +166,8 @@ You can get the {ref}`best target <docs/reflectiveAndShape/contour-filtering:Con
double pitch = target.GetPitch();
double area = target.GetArea();
double skew = target.GetSkew();
frc::Transform2d pose = target.GetCameraToTarget();
wpi::SmallVector<std::pair<double, double>, 4> corners = target.GetCorners();
wpi::math::Transform2d pose = target.GetCameraToTarget();
wpi::util::SmallVector<std::pair<double, double>, 4> corners = target.GetCorners();
.. code-block:: python
@@ -206,8 +206,8 @@ All of the data above (**except skew**) is available when using AprilTags.
// Get information from target.
int targetID = target.GetFiducialId();
double poseAmbiguity = target.GetPoseAmbiguity();
frc::Transform3d bestCameraToTarget = target.getBestCameraToTarget();
frc::Transform3d alternateCameraToTarget = target.getAlternateCameraToTarget();
wpi::math::Transform3d bestCameraToTarget = target.getBestCameraToTarget();
wpi::math::Transform3d alternateCameraToTarget = target.getAlternateCameraToTarget();
.. code-block:: python

View File

@@ -38,8 +38,8 @@ You can get your robot's `Pose2D` on the field using various camera data, target
.. 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);
wpi::math::Pose2d robotPose = photonlib::EstimateFieldToRobot(
kCameraHeight, kTargetHeight, kCameraPitch, kTargetPitch, wpi::math::Rotation2d(wpi::units::degree_t(-target.GetYaw())), wpi::math::Rotation2d(wpi::units::degree_t(gyro.GetRotation2d)), targetPose, cameraToRobot);
.. code-block:: python
@@ -106,8 +106,8 @@ You can get a [translation](https://docs.wpilib.org/en/latest/docs/software/adva
.. 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())));
wpi::math::Translation2d translation = photonlib::PhotonUtils::EstimateCameraToTargetTranslation(
distance, wpi::math::Rotation2d(wpi::units::degree_t(-target.GetYaw())));
.. code-block:: python

View File

@@ -81,7 +81,7 @@ If you would like to access your Ethernet-connected vision device from a compute
.. code-block:: c++
wpi::PortForwarder::GetInstance().Add(5800, "photonvision.local", 5800);
wpi::net::PortForwarder::GetInstance().Add(5800, "photonvision.local", 5800);
.. code-block:: python

View File

@@ -95,6 +95,27 @@ These `TargetModel` are paired with a target pose to create a `VisionTargetSim`.
The pose of a `VisionTargetSim` object can be updated to simulate moving targets. Note, however, that this will break latency simulation for that target.
:::
To use simulated object detection, you must provide an objDetClassId (zero-indexed class ID) and confidence value. When you set objDetConf to -1, the simulation computes confidence based on the area of the target in the camera's field of view. To simulate a object detection model with one class (fuel, index 0) and specify confidence, you'd write:
```{eval-rst}
.. tab-set-code::
.. code-block:: java
// arbitrary position on field
final var targetPose = new Pose3d(new Translation3d(2, 0, 0), new Rotation3d());
// Class id, zero-indexed
final int classId = 0;
// Confidence, between 0 and 1.
final float conf = 0.67f;
// 6 inch diameter ball
final TargetModel ballModel = new TargetModel(Units.inchesToMeters(6));
final var ballTargetSim = new VisionTargetSim(targetPose, ballModel, classId, conf);
// Add this vision target to the vision system simulation to make it visible
visionSim.addVisionTargets(visionTarget);
```
For convenience, an `AprilTagFieldLayout` can also be added to automatically create a target for each of its AprilTags.
```{eval-rst}

View File

@@ -0,0 +1,10 @@
<svg xmlns="http://www.w3.org/2000/svg" id="led" width="30" height="30" viewBox="0 0 100 100" style="color-scheme: dark light">
<circle cx="50" cy="50" r="20" fill="dimgrey"/>
<circle cx="50" cy="50" r="20" stroke="black" stroke-width="5" fill="currentColor"/>
<line x1="5" y1="50" x2="20" y2="50" stroke="currentColor" stroke-width="5" stroke-linecap="round"/>
<line x1="27.5" y1="11.03" x2="35" y2="24.02" stroke="currentColor" stroke-width="5" stroke-linecap="round"/>
<line x1="72.5" y1="11.03" x2="65" y2="24.02" stroke="currentColor" stroke-width="5" stroke-linecap="round"/>
<line x1="80" y1="50" x2="95" y2="50" stroke="currentColor" stroke-width="5" stroke-linecap="round"/>
<line x1="72.5" y1="88.97" x2="65" y2="75.98" stroke="currentColor" stroke-width="5" stroke-linecap="round"/>
<line x1="27.5" y1="88.97" x2="35" y2="75.98" stroke="currentColor" stroke-width="5" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 927 B

View File

@@ -5,6 +5,7 @@
common-errors
logging
status-leds
camera-troubleshooting
networking-troubleshooting
unix-commands

View File

@@ -0,0 +1,101 @@
---
myst:
substitutions:
led_loader: |
```{image} images/led.svg
:height: 0
```
led: |
```{raw} html
<svg class="led" height="30" width="30">
<use href="../../_images/led.svg#led"/>
</svg>
```
---
<!-- markdownlint-disable-next-line MD033 MD041 -->
<style>
svg.led {
--off-color: transparent;
color: var(--on-color);
}
@keyframes led-blink {
66% {
color: var(--off-color);
}
}
:not(.solid) > svg.led {
animation: led-blink 0.45s steps(1) infinite;
}
@keyframes led-even-blink {
50% {
color: var(--off-color);
}
}
:not(.solid).fast > svg.led {
animation-name: led-even-blink;
animation-duration: 150ms;
}
:not(.solid).error > svg.led {
animation-name: led-even-blink;
animation-duration: 0.90s;
}
.green > svg.led {
--on-color: limegreen;
}
.blue > svg.led {
--on-color: blue;
}
.yellow > svg.led {
--on-color: yellow;
}
.red > svg.led {
--on-color: red;
}
.anti-yellow > svg.led {
--on-color: transparent;
--off-color: yellow;
}
.off > svg.led {
color: var(--off-color);
}
</style>
# Status LEDs
PhotonVision has support for multiple kinds of status LEDs. Make sure you reference the correct table for the type present on your hardware.
## RGB LED
Color | Flashing | Preview | Status
--------|----------|:-------------------------:|-----------------------------------------------
Green | Yes | [{{ led }}]{.green} | Running normally, no targets visible
Blue | No | [{{ led }}]{.solid .blue} | Running normally, targets visible
Yellow | Yes | [{{ led }}]{.yellow} | NT Disconnected, no targets visible
Blue | Yes | [{{ led }}]{.blue} | NT Disconnected, targets visible
Red | Yes | [{{ led }}]{.red} | Initializing or faulted, not running
Off | No | [{{ led }}]{.off} | No power or initialization fault, not running
## Green and Yellow LEDs
Used on Limelight 1, 2, 2+, 3, 3G, and 3A
Green and Yellow LED patterns may be active at the same time
Color | Pattern | Preview | Status
--------|----------------|:-----------------------------------------------------------:|-------------------------------------------------
Green | Slow Flashing | [{{ led }}]{.green} [{{ led }}]{.off} | No targets visible
Green | Quick Flashing | [{{ led }}]{.fast .green} [{{ led }}]{.off} | Targets visible
Yellow | Flashing | [{{ led }}]{.off} [{{ led }}]{.yellow} | NT Disconnected
Yellow | Solid | [{{ led }}]{.off} [{{ led }}]{.solid .yellow} | NT Connected
Both | Alternating | [{{ led }}]{.green .error} [{{ led }}]{.anti-yellow .error} | Initializing or faulted, not running
Both | Off | [{{ led }}]{.off} [{{ led }}]{.off} | No power or initialization fault, not running
{{ led_loader }}

View File

@@ -13,10 +13,10 @@ You may see a warning similar to `The authenticity of host 'xxx' can't be establ
Example:
```
ssh pi@hostname
ssh photon@hostname
```
For PhotonVision, the username will be `pi` and the password will be `raspberry`.
For PhotonVision images, the username will be `photon` and the password will be `vision`.
### ip

View File

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

View File

@@ -1 +1,3 @@
/// <reference types="vite/client" />
declare module "vue3-virtual-scroll-list";

View File

@@ -4,8 +4,8 @@ import { defineConfigWithVueTs, vueTsConfigs } from "@vue/eslint-config-typescri
import skipFormattingConfig from "@vue/eslint-config-prettier/skip-formatting";
export default defineConfigWithVueTs(
pluginVue.configs["flat/recommended"],
vueTsConfigs.recommended,
pluginVue.configs["flat/recommended-error"],
vueTsConfigs.recommendedTypeChecked,
skipFormattingConfig,
{
ignores: ["**/dist/**", "playwright-report"]
@@ -26,12 +26,29 @@ export default defineConfigWithVueTs(
semi: ["error", "always"],
"eol-last": "error",
eqeqeq: "error",
"no-useless-concat": "error",
"object-curly-spacing": ["error", "always"],
"quote-props": ["error", "as-needed"],
"no-case-declarations": "off",
"vue/eqeqeq": "error",
"vue/no-useless-concat": "error",
"vue/no-constant-condition": "error",
"vue/no-empty-pattern": "error",
"vue/no-undef-directives": "error",
"vue/no-undef-properties": "error",
"vue/no-unused-properties": "error",
"vue/no-unused-refs": "error",
"vue/no-use-v-else-with-v-for": "error",
"vue/no-useless-mustaches": "error",
"vue/no-useless-v-bind": "error",
"vue/prefer-use-template-ref": "error",
"vue/require-default-prop": "off",
"vue/require-typed-ref": "error",
"vue/v-for-delimiter-style": "error",
"vue/v-on-event-hyphenation": "off",
"@typescript-eslint/no-explicit-any": "off",
"@typescript-eslint/no-empty-object-type": "error",
"@typescript-eslint/no-explicit-any": "error",
"vue/valid-v-slot": ["error", { allowModifiers: true }]
}
}

View File

@@ -3,6 +3,9 @@
"version": "0.0.0",
"private": true,
"type": "module",
"engines": {
"node": "24.x"
},
"scripts": {
"dev": "vite",
"preview": "vite preview --port 4173",
@@ -14,8 +17,8 @@
"format-ci": "prettier --check src/",
"test": "playwright test",
"test-ui": "playwright test --ui",
"test-setup": "playwright install --with-deps"
"test-setup": "playwright install --with-deps",
"type-check": "vue-tsc --noEmit"
},
"dependencies": {
"@fontsource/prompt": "^5.2.6",
@@ -34,18 +37,19 @@
"devDependencies": {
"@eslint/js": "^9.31.0",
"@playwright/test": "^1.56.1",
"@types/node": "^22.15.14",
"@types/node": "^24.0.0",
"@types/three": "^0.178.0",
"@vitejs/plugin-vue": "^6.0.0",
"@vitejs/plugin-vue": "^6.0.6",
"vue-tsc": "^3.2.5",
"@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",
"eslint-plugin-vue": "^10.7.0",
"prettier": "^3.6.2",
"sass": "^1.89.2",
"typescript": "^5.8.3",
"vite": "^7.0.5",
"vite": "^8.0.10",
"vite-plugin-vuetify": "^2.1.1"
}
}

View File

@@ -74,7 +74,7 @@ export default defineConfig({
/* Run your local dev server before starting the tests */
webServer: {
command: process.platform == "win32" ? "" : "./" + "gradlew run",
command: process.platform === "win32" ? "" : "./gradlew run",
url: "http://localhost:5800",
timeout: 300 * 1000,
reuseExistingServer: !process.env.CI,

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,3 @@
allowBuilds:
'@parcel/watcher': true
core-js: true

View File

@@ -11,9 +11,10 @@ import { useTheme } from "vuetify";
import { restoreThemeConfig } from "@/lib/ThemeManager";
const is_demo = import.meta.env.MODE === "demo";
const backendHost = inject<string>("backendHost");
if (!is_demo) {
const websocket = new AutoReconnectingWebsocket(
`ws://${inject("backendHost")}/websocket_data`,
`ws://${backendHost}/websocket_data`,
() => {
useStateStore().$patch({ backendConnected: true });
},

View File

@@ -3,7 +3,7 @@ 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 type { TrackballControls } from "three/examples/jsm/controls/TrackballControls.js";
import { onBeforeUnmount, onMounted, watchEffect } from "vue";
const {
ArrowHelper,
@@ -20,7 +20,7 @@ const {
Scene,
WebGLRenderer
} = await import("three");
const { TrackballControls } = await import("three/examples/jsm/controls/TrackballControls");
const { TrackballControls } = await import("three/examples/jsm/controls/TrackballControls.js");
import { useCameraSettingsStore } from "@/stores/settings/CameraSettingsStore";
import { createPerspectiveCamera } from "@/lib/ThreeUtils";
@@ -50,8 +50,8 @@ const drawTargets = async (targets: PhotonTarget[]) => {
return;
}
if (theme.global.name.value === "LightTheme") scene.background = new Color(0xa9a9a9);
else scene.background = new Color(0x000000);
if (theme.global.current.value.dark) scene.background = new Color(0x000000);
else scene.background = new Color(0xa9a9a9);
scene.remove(...previousTargets);
previousTargets = [];
@@ -158,8 +158,8 @@ onMounted(async () => {
if (!canvas) return;
renderer = new WebGLRenderer({ canvas: canvas });
if (theme.global.name.value === "LightTheme") scene.background = new Color(0xa9a9a9);
else scene.background = new Color(0x000000);
if (theme.global.current.value.dark) scene.background = new Color(0x000000);
else scene.background = new Color(0xa9a9a9);
onWindowResize();
window.addEventListener("resize", onWindowResize);
@@ -213,14 +213,14 @@ onMounted(async () => {
renderer.render(scene, camera);
};
drawTargets(props.targets);
await drawTargets(props.targets);
animate();
});
onBeforeUnmount(() => {
window.removeEventListener("resize", onWindowResize);
});
watchEffect(() => {
drawTargets(props.targets);
void drawTargets(props.targets);
});
</script>
@@ -234,7 +234,7 @@ watchEffect(() => {
<v-btn
style="width: 100%"
color="buttonActive"
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
:variant="theme.global.current.value.dark ? 'outlined' : 'elevated'"
@click="resetCamFirstPerson"
>
First Person
@@ -244,7 +244,7 @@ watchEffect(() => {
<v-btn
style="width: 100%"
color="buttonActive"
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
:variant="theme.global.current.value.dark ? 'outlined' : 'elevated'"
@click="resetCamThirdPerson"
>
Third Person

View File

@@ -1,5 +1,13 @@
<script setup lang="ts">
import { onBeforeUnmount, onMounted, ref, watch, watchEffect, type Ref } from "vue";
import type {
Scene as SceneType,
PerspectiveCamera as PerspectiveCameraType,
WebGLRenderer as WebGLRendererType,
Group as GroupType,
Object3D
} from "three";
import type { TrackballControls as TrackballControlsType } from "three/examples/jsm/controls/TrackballControls.js";
const {
AmbientLight,
AxesHelper,
@@ -16,7 +24,7 @@ const {
SphereGeometry,
WebGLRenderer
} = await import("three");
const { TrackballControls } = await import("three/examples/jsm/controls/TrackballControls");
const { TrackballControls } = await import("three/examples/jsm/controls/TrackballControls.js");
import type { BoardObservation, CameraCalibrationResult } from "@/types/SettingTypes";
import axios from "axios";
import { useCameraSettingsStore } from "@/stores/settings/CameraSettingsStore";
@@ -31,12 +39,12 @@ const props = defineProps<{
title: string;
}>();
let scene: Scene | undefined;
let camera: PerspectiveCamera | undefined;
let renderer: WebGLRenderer | undefined;
let controls: TrackballControls | undefined;
let scene: SceneType | undefined;
let camera: PerspectiveCameraType | undefined;
let renderer: WebGLRendererType | undefined;
let controls: TrackballControlsType | undefined;
const createChessboard = (obs: BoardObservation, cal: CameraCalibrationResult): Group => {
const createChessboard = (obs: BoardObservation, cal: CameraCalibrationResult): GroupType => {
const group = new Group();
if (obs.locationInImageSpace.length === 0) return group;
@@ -194,9 +202,6 @@ const resetCamThirdPerson = () => {
let animationFrameId: number | null = null;
onMounted(async () => {
// Grab data first off
fetchCalibrationData();
scene = new Scene();
camera = new PerspectiveCamera(75, 800 / 800, 0.1, 1000);
@@ -208,8 +213,8 @@ onMounted(async () => {
const ambientLight = new AmbientLight(0xffffff, 0.6);
scene.add(ambientLight);
if (theme.global.name.value === "LightTheme") scene.background = new Color(0xa9a9a9);
else scene.background = new Color(0x000000);
if (theme.global.current.value.dark) scene.background = new Color(0x000000);
else scene.background = new Color(0xa9a9a9);
// Initialize a stable aspect ratio so subsequent resize events derive
// height from width, avoiding layout feedback during continuous resizing
@@ -256,6 +261,10 @@ onMounted(async () => {
controls.update();
// Fetch calibration only after the scene is ready so the initial draw
// can happen immediately when the data arrives.
await fetchCalibrationData();
const animate = () => {
if (!scene || !camera || !renderer || !controls) {
return;
@@ -318,7 +327,7 @@ if (import.meta.hot) {
}
watchEffect(() => {
drawCalibration(calibrationData.value);
void drawCalibration(calibrationData.value);
});
watch(
@@ -328,9 +337,9 @@ watch(
props.resolution.height,
useCameraSettingsStore().getCalibrationCoeffs(props.resolution)
],
() => {
async () => {
console.log("Camera or resolution changed, refetching calibration");
fetchCalibrationData();
await fetchCalibrationData();
}
);
</script>
@@ -347,7 +356,7 @@ watch(
<v-btn
style="width: 100%"
color="buttonActive"
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
:variant="theme.global.current.value.dark ? 'outlined' : 'elevated'"
@click="resetCamFirstPerson"
>
First Person
@@ -357,7 +366,7 @@ watch(
<v-btn
style="width: 100%"
color="buttonActive"
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
:variant="theme.global.current.value.dark ? 'outlined' : 'elevated'"
@click="resetCamThirdPerson"
>
Third Person

View File

@@ -1,5 +1,5 @@
<script setup lang="ts">
import { computed, inject, ref, onBeforeUnmount } from "vue";
import { computed, inject, onBeforeUnmount, useTemplateRef } from "vue";
import { useStateStore } from "@/stores/StateStore";
import { useCameraSettingsStore } from "@/stores/settings/CameraSettingsStore";
import type { StyleValue } from "vue";
@@ -13,6 +13,7 @@ const props = defineProps<{
cameraSettings: UiCameraConfiguration;
}>();
const backendHostname = inject<string>("backendHostname");
const emptyStreamSrc = "//:0";
const streamSrc = computed<string>(() => {
const port = props.cameraSettings.stream[props.streamType === "Raw" ? "inputPort" : "outputPort"];
@@ -21,7 +22,7 @@ const streamSrc = computed<string>(() => {
return emptyStreamSrc;
}
return `http://${inject("backendHostname")}:${port}/stream.mjpg`;
return `http://${backendHostname}:${port}/stream.mjpg`;
});
const streamDesc = computed<string>(() => `${props.streamType} Stream View`);
const streamStyle = computed<StyleValue>(() => {
@@ -50,7 +51,7 @@ const containerStyle = computed<StyleValue>(() => {
});
const overlayStyle = computed<StyleValue>(() => {
if (useStateStore().colorPickingMode || streamSrc.value == emptyStreamSrc) {
if (useStateStore().colorPickingMode || streamSrc.value === emptyStreamSrc) {
return { display: "none" };
} else {
return {};
@@ -67,26 +68,26 @@ const handleCaptureClick = () => {
const handlePopoutClick = () => {
window.open(streamSrc.value);
};
const handleFullscreenRequest = () => {
const handleFullscreenRequest = async () => {
const stream = document.getElementById(props.id);
if (!stream) return;
stream.requestFullscreen();
await stream.requestFullscreen();
};
const mjpgStream: any = ref(null);
const mjpgStream = useTemplateRef("mjpgStream");
const handleStreamError = () => {
if (streamSrc.value && streamSrc.value !== emptyStreamSrc) {
console.error("Error loading stream:", streamSrc.value, " Trying again.");
setTimeout(() => {
mjpgStream.value.src = streamSrc.value;
mjpgStream.value!.src = streamSrc.value;
}, 100);
}
};
onBeforeUnmount(() => {
if (!mjpgStream.value) return;
mjpgStream.value["src"] = emptyStreamSrc;
mjpgStream.value.src = emptyStreamSrc;
});
</script>

View File

@@ -14,7 +14,7 @@ import { useStateStore } from "@/stores/StateStore";
{{ useStateStore().snackbarData.message }}
</p>
<v-progress-linear
v-if="useStateStore().snackbarData.progressBar != -1"
v-if="useStateStore().snackbarData.progressBar !== -1"
v-model="useStateStore().snackbarData.progressBar"
height="15"
:color="useStateStore().snackbarData.progressBarColor"

View File

@@ -1,5 +1,5 @@
<script setup lang="ts">
import { computed, inject, ref, watch } from "vue";
import { computed, inject, ref, useTemplateRef, watch } from "vue";
import { LogLevel, type LogMessage } from "@/types/SettingTypes";
import { useStateStore } from "@/stores/StateStore";
import LogEntry from "@/components/app/photon-log-entry.vue";
@@ -10,10 +10,10 @@ const backendHost = inject<string>("backendHost");
const searchQuery = ref("");
const timeInput = ref<string>();
const autoScroll = ref(true);
const logList = ref();
const logList = useTemplateRef<InstanceType<typeof VirtualList>>("logList"); // this needs to be typed in the definition since vue has trouble inferring it
const logKeeps = ref(40);
const exportLogFile = ref();
const selectedLogLevels = ref({
const exportLogFile = useTemplateRef("exportLogFile");
const selectedLogLevels = ref<Record<number, boolean>>({
[LogLevel.ERROR]: true,
[LogLevel.WARN]: true,
[LogLevel.INFO]: true,
@@ -48,7 +48,7 @@ watch(logs, () => {
);
autoScroll.value = bottomOffset < 50;
if (autoScroll.value) logList.value.scrollToBottom();
if (autoScroll.value) logList.value?.scrollToBottom();
});
const getLogLevelFromIndex = (index: number): string => {
@@ -56,7 +56,7 @@ const getLogLevelFromIndex = (index: number): string => {
};
const handleLogExport = () => {
exportLogFile.value.click();
exportLogFile.value?.click();
};
const handleLogClear = () => {

View File

@@ -38,7 +38,7 @@ const renderCompact = computed<boolean>(() => compact.value || !mdAndUp.value);
<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" prepend-icon="mdi-camera">
<v-list-item link to="/cameras" prepend-icon="mdi-camera">
<v-list-item-title>Camera</v-list-item-title>
</v-list-item>
<v-list-item
@@ -73,7 +73,7 @@ const renderCompact = computed<boolean>(() => compact.value || !mdAndUp.value);
</v-list-item>
<v-list-item
link
:prepend-icon="theme.global.name.value === 'LightTheme' ? 'mdi-white-balance-sunny' : 'mdi-weather-night'"
:prepend-icon="theme.global.current.value.dark ? 'mdi-weather-night' : 'mdi-white-balance-sunny'"
@click="() => toggleTheme(theme)"
>
<v-list-item-title>Theme</v-list-item-title>

View File

@@ -20,6 +20,7 @@ const PromptRegular = import("@/assets/fonts/PromptRegular");
const jspdf = import("jspdf");
const theme = useTheme();
const MM_PER_INCH = 25.4;
const settingsValid = ref(true);
@@ -28,7 +29,7 @@ const getUniqueVideoFormatsByResolution = (): VideoFormat[] => {
if (useCameraSettingsStore().currentCameraSettings.validVideoFormats.length === 0) return uniqueResolutions;
useCameraSettingsStore().currentCameraSettings.validVideoFormats.forEach((format) => {
const index = uniqueResolutions.findIndex((v) => resolutionsAreEqual(v.resolution, format.resolution));
const contains = index != -1;
const contains = index !== -1;
let skip = false;
if (contains && format.fps > uniqueResolutions[index].fps) {
uniqueResolutions.splice(index, 1);
@@ -38,6 +39,11 @@ const getUniqueVideoFormatsByResolution = (): VideoFormat[] => {
if (!skip) {
const calib = useCameraSettingsStore().getCalibrationCoeffs(format.resolution);
// minPixelCount is the multiplied area of a 640x480 (the minimum for proper calibration) resolution
const minPixelCount = 640 * 480;
const resArea = format.resolution.width * format.resolution.height;
if (calib !== undefined) {
// Mean overall reprojection error
// Calculated as average of each observation's mean error
@@ -60,7 +66,10 @@ const getUniqueVideoFormatsByResolution = (): VideoFormat[] => {
) *
(180 / Math.PI);
}
uniqueResolutions.push(format);
if (resArea >= minPixelCount) {
uniqueResolutions.push(format);
}
}
});
uniqueResolutions.sort(
@@ -81,18 +90,26 @@ const calibrationDivisors = computed(() =>
})
);
const uniqueVideoResolutionString = ref("");
const uniqueVideoResolutionIndex = ref(getUniqueVideoResolutionStrings()?.[0]?.value);
// 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 currentFormatIndex = useCameraSettingsStore().currentVideoFormat.index ?? 0;
// Checks if the current resolution is present in the list of valid formats, if not defaults to the last index (which is usually the highest resolution)
const currentIndex =
getUniqueVideoResolutionStrings()
.map((x) => x.name)
.find((n) => n === names[currentFormatIndex]) !== undefined
? currentFormatIndex
: names.length - 1;
useStateStore().calibrationData.videoFormatIndex = currentIndex;
uniqueVideoResolutionIndex.value = currentIndex;
});
const dimensionUnit = ref<"in" | "mm">("in");
const squareSizeIn = ref(1);
const markerSizeIn = ref(0.75);
const patternWidth = ref(8);
@@ -102,6 +119,28 @@ const useOldPattern = ref(false);
const tagFamily = ref<CalibrationTagFamilies>(CalibrationTagFamilies.Dict_4X4_1000);
const requestedVideoFormatIndex = ref(0);
const convertInchesToDisplay = (valueInInches: number) =>
dimensionUnit.value === "mm" ? valueInInches * MM_PER_INCH : valueInInches;
const convertDisplayToInches = (displayValue: number) =>
dimensionUnit.value === "mm" ? displayValue / MM_PER_INCH : displayValue;
const squareSize = computed({
get: () => convertInchesToDisplay(squareSizeIn.value),
set(value) {
squareSizeIn.value = convertDisplayToInches(value);
}
});
const markerSize = computed({
get: () => convertInchesToDisplay(markerSizeIn.value),
set(value) {
markerSizeIn.value = convertDisplayToInches(value);
}
});
const dimensionStep = computed(() => (dimensionUnit.value === "mm" ? 0.1 : 0.01));
// Emperical testing - with stack size limit of 1MB, we can handle at -least- 700k points
const tooManyPoints = computed(
() => useStateStore().calibrationData.imageCount * patternWidth.value * patternHeight.value > 700000
@@ -132,7 +171,7 @@ const downloadCalibBoard = async () => {
const yPos = chessboardStartY + squareY * squareSizeIn.value;
// Only draw the odd squares to create the chessboard pattern
if (squareY % 2 != squareX % 2) {
if (squareY % 2 !== squareX % 2) {
doc.rect(xPos, yPos, squareSizeIn.value, squareSizeIn.value, "F");
}
}
@@ -176,7 +215,7 @@ const downloadCalibBoard = async () => {
};
const isCalibrating = computed(
() => useCameraSettingsStore().currentCameraSettings.currentPipelineIndex === WebsocketPipelineType.Calib3d
() => useCameraSettingsStore().currentCameraSettings.currentPipelineIndex === WebsocketPipelineType.Calib3d.valueOf()
);
const startCalibration = () => {
@@ -203,7 +242,7 @@ const endCalibration = () => {
calibSuccess.value = undefined;
calibEndpointFail.value = false;
if (!useStateStore().calibrationData.hasEnoughImages) {
if (!hasEnoughImages.value) {
calibCanceled.value = true;
}
@@ -231,6 +270,10 @@ const endCalibration = () => {
const drawAllSnapshots = ref(true);
const bypassVal = ref(false);
const minCount = computed(() => (bypassVal.value ? 10 : 100));
const hasEnoughImages = computed(() => useStateStore().calibrationData.imageCount >= minCount.value);
const showCalDialog = ref(false);
const selectedVideoFormat = ref<VideoFormat | undefined>(undefined);
const setSelectedVideoFormat = (format: VideoFormat) => {
@@ -293,25 +336,25 @@ const setSelectedVideoFormat = (format: VideoFormat) => {
<v-card-subtitle v-if="!isCalibrating" class="pl-0 pb-3 pt-4 opacity-100"
>Configure New Calibration</v-card-subtitle
>
<v-form ref="form" v-model="settingsValid">
<v-form v-model="settingsValid">
<pv-select
v-model="uniqueVideoResolutionString"
v-model="uniqueVideoResolutionIndex"
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()"
@update:model-value="
useStateStore().calibrationData.videoFormatIndex =
getUniqueVideoResolutionStrings().find((v) => v.value === $event)?.value || 0
"
@update:model-value="(value) => (useStateStore().calibrationData.videoFormatIndex = value)"
/>
<pv-select
v-model="boardType"
label="Board Type"
tooltip="Calibration board pattern to use"
:select-cols="8"
:items="['Chessboard', 'ChArUco']"
:items="[
{ value: CalibrationBoardTypes.Charuco, name: 'ChArUco' },
{ value: CalibrationBoardTypes.Chessboard, name: 'Chessboard' }
]"
:disabled="isCalibrating"
/>
<v-alert
@@ -341,25 +384,43 @@ const setSelectedVideoFormat = (format: VideoFormat) => {
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']"
:items="[
{ value: CalibrationTagFamilies.Dict_4X4_1000, name: 'Dict_4X4_1000' },
{ value: CalibrationTagFamilies.Dict_5X5_1000, name: 'Dict_5X5_1000' },
{ value: CalibrationTagFamilies.Dict_6X6_1000, name: 'Dict_6X6_1000' },
{ value: CalibrationTagFamilies.Dict_7X7_1000, name: 'Dict_7X7_1000' }
]"
:disabled="isCalibrating"
/>
<pv-select
v-model="dimensionUnit"
label="Dimension Unit"
tooltip="Units used for pattern spacing and marker size inputs"
:select-cols="8"
:items="[
{ value: 'in', name: 'Inches' },
{ value: 'mm', name: 'Millimeters' }
]"
:disabled="isCalibrating"
/>
<pv-number-input
v-model="squareSizeIn"
label="Pattern Spacing (in)"
tooltip="Spacing between pattern features in inches"
v-model="squareSize"
:label="`Pattern Spacing (${dimensionUnit})`"
:tooltip="`Spacing between pattern features in ${dimensionUnit === 'mm' ? 'millimeters' : 'inches'}`"
:disabled="isCalibrating"
:rules="[(v) => v > 0 || 'Size must be positive']"
:label-cols="4"
:step="dimensionStep"
/>
<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"
v-model="markerSize"
:label="`Marker Size (${dimensionUnit})`"
:tooltip="`Size of the tag markers in ${dimensionUnit === 'mm' ? 'millimeters' : 'inches'}; must be smaller than pattern spacing`"
:disabled="isCalibrating"
:rules="[(v) => v > 0 || 'Size must be positive']"
:label-cols="4"
:step="dimensionStep"
/>
<pv-number-input
v-model="patternWidth"
@@ -470,7 +531,7 @@ const setSelectedVideoFormat = (format: VideoFormat) => {
closable
density="compact"
class="mb-5"
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'tonal'"
:variant="theme.global.current.value.dark ? 'tonal' : 'elevated'"
:color="useSettingsStore().general.mrCalWorking ? 'buttonPassive' : 'error'"
:icon="useSettingsStore().general.mrCalWorking ? 'mdi-check' : 'mdi-close'"
:text="
@@ -481,20 +542,31 @@ const setSelectedVideoFormat = (format: VideoFormat) => {
/>
<div v-if="isCalibrating" class="d-flex justify-center align-center pb-5">
<v-chip
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'tonal'"
:variant="theme.global.current.value.dark ? 'tonal' : 'elevated'"
label
:color="useStateStore().calibrationData.hasEnoughImages ? 'buttonPassive' : 'light-grey'"
:color="hasEnoughImages ? 'buttonPassive' : 'light-grey'"
>
Snapshots: {{ useStateStore().calibrationData.imageCount }} of at least
{{ useStateStore().calibrationData.minimumImageCount }}
{{ minCount }}
</v-chip>
<v-spacer />
<pv-switch
v-model="bypassVal"
color="error"
hide-details
class="ml-4"
label="Bypass minimum"
:label-cols="6"
:switch-cols="6"
tooltip="Bypass the minimum recommended amount of snapshots for a calibration. Should only be used for dev work or temporary tests not competitions. Still requires 10 images to calibrate."
/>
</div>
<div>
<v-btn
color="buttonPassive"
size="small"
block
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
:variant="theme.global.current.value.dark ? 'outlined' : 'elevated'"
:disabled="!settingsValid"
@click="downloadCalibBoard"
>
@@ -509,7 +581,7 @@ const setSelectedVideoFormat = (format: VideoFormat) => {
density="compact"
text="Too many corners. Finish calibration now!"
icon="mdi-alert-circle-outline"
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'tonal'"
:variant="theme.global.current.value.dark ? 'tonal' : 'elevated'"
/>
<div class="d-flex pt-5">
<v-col cols="6" class="pa-0 pr-2">
@@ -517,7 +589,7 @@ const setSelectedVideoFormat = (format: VideoFormat) => {
size="small"
block
color="buttonActive"
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
:variant="theme.global.current.value.dark ? 'outlined' : 'elevated'"
:disabled="!settingsValid || tooManyPoints"
@click="isCalibrating ? useCameraSettingsStore().takeCalibrationSnapshot() : startCalibration()"
>
@@ -531,17 +603,15 @@ const setSelectedVideoFormat = (format: VideoFormat) => {
<v-btn
size="small"
block
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
:color="useStateStore().calibrationData.hasEnoughImages ? 'buttonActive' : 'error'"
:variant="theme.global.current.value.dark ? 'outlined' : 'elevated'"
:color="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" }}
{{ hasEnoughImages ? "mdi-flag-checkered" : "mdi-flag-off-outline" }}
</v-icon>
<span class="calib-btn-label">{{
useStateStore().calibrationData.hasEnoughImages ? "Finish Calibration" : "Cancel Calibration"
}}</span>
<span class="calib-btn-label">{{ hasEnoughImages ? "Finish Calibration" : "Cancel Calibration" }}</span>
</v-btn>
</v-col>
</div>

View File

@@ -3,7 +3,7 @@ import PhotonCalibrationVisualizer from "@/components/app/photon-calibration-vis
import type { CameraCalibrationResult, VideoFormat } from "@/types/SettingTypes";
import { useCameraSettingsStore } from "@/stores/settings/CameraSettingsStore";
import { useStateStore } from "@/stores/StateStore";
import { computed, inject, ref } from "vue";
import { computed, inject, ref, useTemplateRef } from "vue";
import { axiosPost, getResolutionString, parseJsonFile } from "@/lib/PhotonUtils";
import { useTheme } from "vuetify";
import PvDeleteModal from "@/components/common/pv-delete-modal.vue";
@@ -13,35 +13,35 @@ const props = defineProps<{
videoFormat: VideoFormat;
}>();
const confirmRemoveDialog = ref({ show: false, vf: props.videoFormat as VideoFormat });
const confirmRemoveDialog = ref({ show: false, vf: props.videoFormat });
const removeCalibration = (vf: VideoFormat) => {
axiosPost("/calibration/remove", "delete a camera calibration", {
const removeCalibration = async (vf: VideoFormat) => {
await axiosPost("/calibration/remove", "delete a camera calibration", {
cameraUniqueName: useCameraSettingsStore().currentCameraSettings.uniqueName,
width: vf.resolution.width,
height: vf.resolution.height
});
};
const exportCalibration = ref();
const exportCalibration = useTemplateRef("exportCalibration");
const openExportCalibrationPrompt = () => {
exportCalibration.value.click();
exportCalibration.value?.click();
};
const importCalibrationFromPhotonJson = ref();
const importCalibrationFromPhotonJson = useTemplateRef("importCalibrationFromPhotonJson");
const openUploadPhotonCalibJsonPrompt = () => {
importCalibrationFromPhotonJson.value.click();
importCalibrationFromPhotonJson.value?.click();
};
const importCalibration = async () => {
const files = importCalibrationFromPhotonJson.value.files;
if (files.length === 0) return;
const files = importCalibrationFromPhotonJson.value?.files;
if (!files?.length) return;
const uploadedJson = files[0];
const data = await parseJsonFile<CameraCalibrationResult>(uploadedJson);
if (
data.resolution.height != props.videoFormat.resolution.height ||
data.resolution.width != props.videoFormat.resolution.width
data.resolution.height !== props.videoFormat.resolution.height ||
data.resolution.width !== props.videoFormat.resolution.width
) {
useStateStore().showSnackbarMessage({
color: "error",
@@ -121,7 +121,7 @@ const viewingImg = ref(0);
<v-btn
color="buttonPassive"
style="width: 100%"
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
:variant="theme.global.current.value.dark ? 'outlined' : 'elevated'"
@click="openUploadPhotonCalibJsonPrompt"
>
<v-icon start size="large"> mdi-import</v-icon>
@@ -140,7 +140,7 @@ const viewingImg = ref(0);
color="buttonPassive"
:disabled="!currentCalibrationCoeffs"
style="width: 100%"
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
:variant="theme.global.current.value.dark ? 'outlined' : 'elevated'"
@click="openExportCalibrationPrompt"
>
<v-icon start size="large">mdi-export</v-icon>
@@ -318,7 +318,7 @@ const viewingImg = ref(0);
color="primary"
text="The selected video format has not been calibrated."
icon="mdi-alert-circle-outline"
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'tonal'"
:variant="theme.global.current.value.dark ? 'tonal' : 'elevated'"
/>
</div>
<Suspense v-else-if="tab === 'details'">
@@ -341,7 +341,7 @@ const viewingImg = ref(0);
<pv-delete-modal
v-model="confirmRemoveDialog.show"
:width="500"
:title="'Delete Calibration'"
title="Delete Calibration"
:description="`Are you sure you want to delete the calibration for '${confirmRemoveDialog.vf.resolution.width}x${confirmRemoveDialog.vf.resolution.height}'? This action cannot be undone.`"
:on-confirm="() => removeCalibration(confirmRemoveDialog.vf)"
/>

View File

@@ -53,7 +53,7 @@ const fetchSnapshots = () => {
.get("/utils/getImageSnapshots")
.then((response) => {
imgData.value = response.data.map(
(snapshotData: { snapshotName: string; cameraUniqueName: string; snapshotData: string }, index) => {
(snapshotData: { snapshotName: string; cameraUniqueName: string; snapshotData: string }, index: number) => {
const metadata = getSnapshotMetadataFromName(snapshotData.snapshotName);
return {
@@ -99,7 +99,7 @@ const expanded = ref([]);
<v-card-text class="pt-0">
<v-btn
color="buttonPassive"
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
:variant="theme.global.current.value.dark ? 'outlined' : 'elevated'"
@click="fetchSnapshots"
>
<v-icon start class="open-icon" size="large"> mdi-folder </v-icon>
@@ -115,7 +115,7 @@ const expanded = ref([]);
density="compact"
text="There are currently no saved snapshots."
icon="mdi-information-outline"
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'tonal'"
:variant="theme.global.current.value.dark ? 'tonal' : 'elevated'"
/>
</v-card-text>
<v-card-text v-else class="pt-0">
@@ -125,7 +125,7 @@ const expanded = ref([]);
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'"
:variant="theme.global.current.value.dark ? 'tonal' : 'elevated'"
/>
<v-data-table
v-model:expanded="expanded"

View File

@@ -9,6 +9,7 @@ import { computed, ref, watchEffect } from "vue";
import { type CameraSettingsChangeRequest, ValidQuirks } from "@/types/SettingTypes";
import { useTheme } from "vuetify";
import { axiosPost } from "@/lib/PhotonUtils";
import { WebsocketPipelineType } from "@/types/WebsocketDataTypes";
const theme = useTheme();
@@ -20,7 +21,7 @@ const focusMode = computed<boolean>({
get: () => useCameraSettingsStore().isFocusMode,
set: (v) =>
useCameraSettingsStore().changeCurrentPipelineIndex(
v ? -3 : useCameraSettingsStore().currentCameraSettings.lastPipelineIndex || 0,
v ? WebsocketPipelineType.FocusCamera : useCameraSettingsStore().currentCameraSettings.lastPipelineIndex || 0,
true
)
});
@@ -65,11 +66,11 @@ const settingsHaveChanged = (): boolean => {
const a = tempSettingsStruct.value;
const b = useCameraSettingsStore().currentCameraSettings;
for (const q in ValidQuirks) {
if (a.quirksToChange[q] != b.cameraQuirks.quirks[q]) return true;
for (const quirk of Object.values(ValidQuirks)) {
if (a.quirksToChange[quirk] !== b.cameraQuirks.quirks[quirk]) return true;
}
return a.fov != b.fov.value;
return a.fov !== b.fov.value;
};
const resetTempSettingsStruct = () => {
@@ -120,12 +121,12 @@ watchEffect(() => {
});
const showDeleteCamera = ref(false);
const deleteThisCamera = () => {
axiosPost("/utils/nukeOneCamera", "delete this camera", {
const deleteThisCamera = async () => {
await axiosPost("/utils/nukeOneCamera", "delete this camera", {
cameraUniqueName: useStateStore().currentCameraUniqueName
});
};
const wrappedCameras = computed<SelectItem[]>(() =>
const wrappedCameras = computed<SelectItem<string>[]>(() =>
Object.keys(useCameraSettingsStore().cameras).map((cameraUniqueName) => ({
name: useCameraSettingsStore().cameras[cameraUniqueName].nickname,
value: cameraUniqueName
@@ -159,7 +160,7 @@ const wrappedCameras = computed<SelectItem[]>(() =>
v-model="arducamSelectWrapper"
label="Arducam Model"
:items="[
{ name: 'None', value: 0, disabled: true },
{ name: 'None', value: 0 },
{ name: 'OV9281', value: 1 },
{ name: 'OV2311', value: 2 },
{ name: 'OV9782', value: 3 }
@@ -179,7 +180,7 @@ const wrappedCameras = computed<SelectItem[]>(() =>
size="small"
color="buttonActive"
:disabled="!settingsHaveChanged()"
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
:variant="theme.global.current.value.dark ? 'outlined' : 'elevated'"
@click="saveCameraSettings"
>
<v-icon start size="large"> mdi-content-save </v-icon>
@@ -191,7 +192,7 @@ const wrappedCameras = computed<SelectItem[]>(() =>
block
size="small"
color="error"
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
:variant="theme.global.current.value.dark ? 'outlined' : 'elevated'"
@click="() => (showDeleteCamera = true)"
>
<v-icon start size="large"> mdi-trash-can-outline </v-icon>

View File

@@ -6,6 +6,7 @@ import { PipelineType } from "@/types/PipelineTypes";
import { useStateStore } from "@/stores/StateStore";
import { useSettingsStore } from "@/stores/settings/GeneralSettingsStore";
import { useTheme } from "vuetify";
import { WebsocketPipelineType } from "@/types/WebsocketDataTypes";
const theme = useTheme();
@@ -15,7 +16,7 @@ const driverMode = computed<boolean>({
get: () => useCameraSettingsStore().isDriverMode,
set: (v) =>
useCameraSettingsStore().changeCurrentPipelineIndex(
v ? -1 : useCameraSettingsStore().currentCameraSettings.lastPipelineIndex || 0,
v ? WebsocketPipelineType.DriverMode : useCameraSettingsStore().currentCameraSettings.lastPipelineIndex || 0,
true
)
});
@@ -103,7 +104,7 @@ const fpsTooLow = computed<boolean>(() => {
<v-btn
color="buttonPassive"
class="fill"
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
:variant="theme.global.current.value.dark ? 'outlined' : 'elevated'"
:disabled="
useCameraSettingsStore().isDriverMode ||
useCameraSettingsStore().isCalibrationMode ||
@@ -116,7 +117,7 @@ const fpsTooLow = computed<boolean>(() => {
<v-btn
color="buttonPassive"
class="fill"
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
:variant="theme.global.current.value.dark ? 'outlined' : 'elevated'"
:disabled="
useCameraSettingsStore().isDriverMode ||
useCameraSettingsStore().isCalibrationMode ||

View File

@@ -1,69 +1,51 @@
<script setup lang="ts">
import { PVCameraInfo } from "@/types/SettingTypes";
import type { PVCameraInfo } from "@/types/SettingTypes";
const { camera } = defineProps({
camera: {
type: PVCameraInfo,
required: true
}
});
const cameraInfoFor: any = (camera: PVCameraInfo) => {
if (camera.PVUsbCameraInfo) {
return camera.PVUsbCameraInfo;
}
if (camera.PVCSICameraInfo) {
return camera.PVCSICameraInfo;
}
if (camera.PVFileCameraInfo) {
return camera.PVFileCameraInfo;
}
return {};
};
const { camera } = defineProps<{ camera: PVCameraInfo }>();
</script>
<template>
<div>
<v-table density="compact" :style="{ backgroundColor: 'var(--v-primary-base)' }">
<tbody>
<tr v-if="cameraInfoFor(camera).dev !== undefined && cameraInfoFor(camera).dev !== null">
<tr v-if="'dev' in camera && camera.dev !== null">
<td>Device Number:</td>
<td>{{ cameraInfoFor(camera).dev }}</td>
<td>{{ camera.dev }}</td>
</tr>
<tr v-if="cameraInfoFor(camera).name !== undefined && cameraInfoFor(camera).name !== null">
<tr v-if="'name' in camera && camera.name !== null">
<td>Name:</td>
<td>{{ cameraInfoFor(camera).name }}</td>
<td>{{ camera.name }}</td>
</tr>
<tr>
<td>Type:</td>
<td v-if="camera.PVUsbCameraInfo" class="mb-3">USB Camera</td>
<td v-else-if="camera.PVCSICameraInfo" class="mb-3">CSI Camera</td>
<td v-else-if="camera.PVFileCameraInfo" class="mb-3">File Camera</td>
<td v-if="camera.type === 'PVUsbCameraInfo'" class="mb-3">USB Camera</td>
<td v-else-if="camera.type === 'PVCSICameraInfo'" class="mb-3">CSI Camera</td>
<td v-else-if="camera.type === 'PVFileCameraInfo'" class="mb-3">File Camera</td>
<td v-else>Unidentified Camera Type</td>
</tr>
<tr v-if="cameraInfoFor(camera).baseName !== undefined && cameraInfoFor(camera).baseName !== null">
<tr v-if="'baseName' in camera && camera.baseName !== null">
<td>Base Name:</td>
<td>{{ cameraInfoFor(camera).baseName }}</td>
<td>{{ camera.baseName }}</td>
</tr>
<tr v-if="cameraInfoFor(camera).vendorId !== undefined && cameraInfoFor(camera).vendorId !== null">
<tr v-if="'vendorId' in camera && camera.vendorId !== null">
<td>Vendor ID:</td>
<td>{{ cameraInfoFor(camera).vendorId }}</td>
<td>{{ camera.vendorId }}</td>
</tr>
<tr v-if="cameraInfoFor(camera).productId !== undefined && cameraInfoFor(camera).productId !== null">
<tr v-if="'productId' in camera && camera.productId !== null">
<td>Product ID:</td>
<td>{{ cameraInfoFor(camera).productId }}</td>
<td>{{ camera.productId }}</td>
</tr>
<tr v-if="cameraInfoFor(camera).path !== undefined && cameraInfoFor(camera).path !== null">
<tr v-if="'path' in camera && camera.path !== null">
<td>Path:</td>
<td style="word-break: break-all">{{ cameraInfoFor(camera).path }}</td>
<td style="word-break: break-all">{{ camera.path }}</td>
</tr>
<tr v-if="cameraInfoFor(camera).uniquePath !== undefined && cameraInfoFor(camera).uniquePath !== null">
<tr v-if="'uniquePath' in camera && camera.uniquePath !== null">
<td>Unique Path:</td>
<td style="word-break: break-all">{{ cameraInfoFor(camera).uniquePath }}</td>
<td style="word-break: break-all">{{ camera.uniquePath }}</td>
</tr>
<tr v-if="cameraInfoFor(camera).otherPaths !== undefined && cameraInfoFor(camera).otherPaths !== null">
<tr v-if="'otherPaths' in camera && camera.otherPaths !== null">
<td>Other Paths:</td>
<td>{{ cameraInfoFor(camera).otherPaths }}</td>
<td>{{ camera.otherPaths }}</td>
</tr>
</tbody>
</v-table>

View File

@@ -1,5 +1,5 @@
<script setup lang="ts">
import { PVCameraInfo } from "@/types/SettingTypes";
import type { PVCameraInfo } from "@/types/SettingTypes";
function isEqual<T>(a: T, b: T): boolean {
if (a === b) {
@@ -15,29 +15,7 @@ function isEqual<T>(a: T, b: T): boolean {
);
}
const { saved, current } = defineProps({
saved: {
type: PVCameraInfo,
required: true
},
current: {
type: PVCameraInfo,
required: true
}
});
const cameraInfoFor = (camera: PVCameraInfo): any => {
if (camera.PVUsbCameraInfo) {
return camera.PVUsbCameraInfo;
}
if (camera.PVCSICameraInfo) {
return camera.PVCSICameraInfo;
}
if (camera.PVFileCameraInfo) {
return camera.PVFileCameraInfo;
}
return {};
};
const { saved, current } = defineProps<{ saved: PVCameraInfo; current: PVCameraInfo }>();
</script>
<template>
@@ -50,79 +28,70 @@ const cameraInfoFor = (camera: PVCameraInfo): any => {
<th>Current</th>
</tr>
<tr
v-if="cameraInfoFor(saved).dev !== undefined && cameraInfoFor(saved).dev !== null"
:class="cameraInfoFor(saved).dev !== cameraInfoFor(current).dev ? 'mismatch' : ''"
v-if="'dev' in saved && 'dev' in current && saved.dev !== null"
:class="saved.dev !== current.dev ? 'mismatch' : ''"
>
<td>Device Number:</td>
<td>{{ cameraInfoFor(saved).dev }}</td>
<td>{{ cameraInfoFor(current).dev }}</td>
<td>{{ saved.dev }}</td>
<td>{{ current.dev }}</td>
</tr>
<tr
v-if="cameraInfoFor(saved).name !== undefined && cameraInfoFor(saved).name !== null"
:class="cameraInfoFor(saved).name !== cameraInfoFor(current).name ? 'mismatch' : ''"
>
<tr v-if="saved.name !== null" :class="saved.name !== current.name ? 'mismatch' : ''">
<td>Name:</td>
<td>{{ cameraInfoFor(saved).name }}</td>
<td>{{ cameraInfoFor(current).name }}</td>
<td>{{ saved.name }}</td>
<td>{{ current.name }}</td>
</tr>
<tr
v-if="cameraInfoFor(saved).baseName !== undefined && cameraInfoFor(saved).baseName !== null"
:class="cameraInfoFor(saved).baseName !== cameraInfoFor(current).baseName ? 'mismatch' : ''"
v-if="'baseName' in saved && 'baseName' in current && saved.baseName !== null"
:class="saved.baseName !== current.baseName ? 'mismatch' : ''"
>
<td>Base Name:</td>
<td>{{ cameraInfoFor(saved).baseName }}</td>
<td>{{ cameraInfoFor(current).baseName }}</td>
<td>{{ saved.baseName }}</td>
<td>{{ current.baseName }}</td>
</tr>
<tr>
<td>Type:</td>
<td v-if="saved.PVUsbCameraInfo" class="mb-3">USB Camera</td>
<td v-else-if="saved.PVCSICameraInfo" class="mb-3">CSI Camera</td>
<td v-else-if="saved.PVFileCameraInfo" class="mb-3">File Camera</td>
<td v-if="saved.type === 'PVUsbCameraInfo'" class="mb-3">USB Camera</td>
<td v-else-if="saved.type === 'PVCSICameraInfo'" class="mb-3">CSI Camera</td>
<td v-else-if="saved.type === 'PVFileCameraInfo'" class="mb-3">File Camera</td>
<td v-else>Unidentified Camera Type</td>
<td v-if="current.PVUsbCameraInfo" class="mb-3">USB Camera</td>
<td v-else-if="current.PVCSICameraInfo" class="mb-3">CSI Camera</td>
<td v-else-if="current.PVFileCameraInfo" class="mb-3">File Camera</td>
<td v-if="current.type === 'PVUsbCameraInfo'" class="mb-3">USB Camera</td>
<td v-else-if="current.type === 'PVCSICameraInfo'" class="mb-3">CSI Camera</td>
<td v-else-if="current.type === 'PVFileCameraInfo'" class="mb-3">File Camera</td>
<td v-else>Unidentified Camera Type</td>
</tr>
<tr
v-if="cameraInfoFor(saved).vendorId !== undefined && cameraInfoFor(saved).vendorId !== null"
:class="cameraInfoFor(saved).vendorId !== cameraInfoFor(current).vendorId ? 'mismatch' : ''"
v-if="'vendorId' in saved && 'vendorId' in current && saved.vendorId !== null"
:class="saved.vendorId !== current.vendorId ? 'mismatch' : ''"
>
<td>Vendor ID:</td>
<td>{{ cameraInfoFor(saved).vendorId }}</td>
<td>{{ cameraInfoFor(current).vendorId }}</td>
<td>{{ saved.vendorId }}</td>
<td>{{ current.vendorId }}</td>
</tr>
<tr
v-if="cameraInfoFor(saved).productId !== undefined && cameraInfoFor(saved).productId !== null"
:class="cameraInfoFor(saved).productId !== cameraInfoFor(current).productId ? 'mismatch' : ''"
v-if="'productId' in saved && 'productId' in current && saved.productId !== null"
:class="saved.productId !== current.productId ? 'mismatch' : ''"
>
<td>Product ID:</td>
<td>{{ cameraInfoFor(saved).productId }}</td>
<td>{{ cameraInfoFor(current).productId }}</td>
<td>{{ saved.productId }}</td>
<td>{{ current.productId }}</td>
</tr>
<tr
v-if="cameraInfoFor(saved).path !== undefined && cameraInfoFor(saved).path !== null"
:class="cameraInfoFor(saved).path !== cameraInfoFor(current).path ? 'mismatch' : ''"
>
<tr v-if="saved.path !== null" :class="saved.path !== current.path ? 'mismatch' : ''">
<td>Path:</td>
<td style="word-break: break-all">{{ cameraInfoFor(saved).path }}</td>
<td style="word-break: break-all">{{ cameraInfoFor(current).path }}</td>
<td style="word-break: break-all">{{ saved.path }}</td>
<td style="word-break: break-all">{{ current.path }}</td>
</tr>
<tr
v-if="cameraInfoFor(saved).uniquePath !== undefined && cameraInfoFor(saved).uniquePath !== null"
:class="cameraInfoFor(saved).uniquePath !== cameraInfoFor(current).uniquePath ? 'mismatch' : ''"
>
<tr v-if="saved.uniquePath !== null" :class="saved.uniquePath !== current.uniquePath ? 'mismatch' : ''">
<td>Unique Path:</td>
<td style="word-break: break-all">{{ cameraInfoFor(saved).uniquePath }}</td>
<td style="word-break: break-all">{{ cameraInfoFor(current).uniquePath }}</td>
<td style="word-break: break-all">{{ saved.uniquePath }}</td>
<td style="word-break: break-all">{{ current.uniquePath }}</td>
</tr>
<tr
v-if="cameraInfoFor(saved).otherPaths !== undefined && cameraInfoFor(saved).otherPaths !== null"
:class="isEqual(cameraInfoFor(saved).otherPaths, cameraInfoFor(current).otherPaths) ? '' : 'mismatch'"
v-if="'otherPaths' in saved && 'otherPaths' in current && saved.otherPaths !== null"
:class="isEqual(saved.otherPaths, current.otherPaths) ? '' : 'mismatch'"
>
<td>Other Paths:</td>
<td>{{ cameraInfoFor(saved).otherPaths }}</td>
<td>{{ cameraInfoFor(current).otherPaths }}</td>
<td>{{ saved.otherPaths }}</td>
<td>{{ current.otherPaths }}</td>
</tr>
</tbody>
</v-table>

View File

@@ -49,7 +49,7 @@ const confirmationText = ref("");
color="buttonActive"
style="float: right"
width="100%"
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
:variant="theme.global.current.value.dark ? 'outlined' : 'elevated'"
@click="onBackup"
>
<v-icon start class="open-icon" size="large"> mdi-export </v-icon>
@@ -65,7 +65,7 @@ const confirmationText = ref("");
? confirmationText.toLowerCase() !== expectedConfirmationText.toLowerCase()
: false
"
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
:variant="theme.global.current.value.dark ? 'outlined' : 'elevated'"
@click="
onConfirm();
confirmationText = '';

View File

@@ -25,7 +25,7 @@ const emit = defineEmits<{
(e: "onEscape"): void;
}>();
const handleKeydown = ({ key }) => {
const handleKeydown = ({ key }: KeyboardEvent) => {
switch (key) {
case "Enter":
// Explicitly check that all rule props return true

View File

@@ -1,13 +1,15 @@
<script setup lang="ts">
<script setup lang="ts" generic="T extends string | number">
import { computed } from "vue";
import TooltippedLabel from "@/components/common/pv-tooltipped-label.vue";
export interface SelectItem {
export interface SelectItem<TValue extends string | number> {
name: string | number;
value: string | number;
value: TValue;
disabled?: boolean;
}
const value = defineModel<string | number | undefined>({ required: true });
type SelectItems = SelectItem<T>[] | ReadonlyArray<T>;
const value = defineModel<T>({ required: true });
const props = withDefaults(
defineProps<{
@@ -15,7 +17,7 @@ const props = withDefaults(
tooltip?: string;
selectCols?: number;
disabled?: boolean;
items: string[] | number[] | SelectItem[];
items: SelectItems;
}>(),
{
selectCols: 9,
@@ -23,18 +25,20 @@ const props = withDefaults(
}
);
const areSelectItems = (items: SelectItems): items is SelectItem<T>[] => typeof items[0] === "object";
// Computed in case items changes
const items = computed<SelectItem[]>(() => {
const items = computed<SelectItem<T>[]>(() => {
// Trivial case for empty list; we have no data
if (!props.items.length) {
return [];
}
// Check if the prop exists on the object to infer object type
if ((props.items[0] as SelectItem).name) {
return props.items as SelectItem[];
if (areSelectItems(props.items)) {
return props.items;
}
return props.items.map((v, i) => ({ name: v, value: i }));
return props.items.map((item) => ({ name: item, value: item }));
});
</script>
@@ -49,7 +53,7 @@ const items = computed<SelectItem[]>(() => {
:items="items"
item-title="name"
item-value="value"
item-props.disabled="disabled"
item-props
:disabled="disabled"
hide-details="auto"
variant="underlined"

View File

@@ -18,11 +18,11 @@ const props = withDefaults(
const emit = defineEmits<{ (e: "update:modelValue", value: number): void }>();
// Debounce function
function debounce(func: (...args: any[]) => void, wait: number) {
function debounce(func: (...args: number[]) => void, wait: number) {
let timeout: ReturnType<typeof setTimeout>;
return function (...args: any[]) {
return function (...args: number[]) {
clearTimeout(timeout);
timeout = setTimeout(() => func.apply(this, args), wait);
timeout = setTimeout(() => func(...args), wait);
};
}

View File

@@ -13,28 +13,6 @@ import PvDeleteModal from "@/components/common/pv-delete-modal.vue";
const theme = useTheme();
const changeCurrentCameraUniqueName = (cameraUniqueName: string) => {
useCameraSettingsStore().setCurrentCameraUniqueName(cameraUniqueName, true);
switch (useCameraSettingsStore().cameras[cameraUniqueName].pipelineSettings.pipelineType) {
case PipelineType.Reflective:
pipelineType.value = WebsocketPipelineType.Reflective;
break;
case PipelineType.ColoredShape:
pipelineType.value = WebsocketPipelineType.ColoredShape;
break;
case PipelineType.AprilTag:
pipelineType.value = WebsocketPipelineType.AprilTag;
break;
case PipelineType.Aruco:
pipelineType.value = WebsocketPipelineType.Aruco;
break;
case PipelineType.ObjectDetection:
pipelineType.value = WebsocketPipelineType.ObjectDetection;
break;
}
};
// Common RegEx used for naming both pipelines and cameras
const nameChangeRegex = /^[A-Za-z0-9_ \-)(]*[A-Za-z0-9][A-Za-z0-9_ \-)(.]*$/;
@@ -87,17 +65,17 @@ const cancelCameraNameEdit = () => {
};
// Pipeline Name Edit
const pipelineNamesWrapper = computed<SelectItem[]>(() => {
const pipelineNamesWrapper = computed(() => {
const pipelineNames = useCameraSettingsStore().pipelineNames.map((name, index) => ({ name: name, value: index }));
if (useCameraSettingsStore().isDriverMode) {
pipelineNames.push({ name: "Driver Mode", value: WebsocketPipelineType.DriverMode });
pipelineNames.push({ name: "Driver Mode", value: WebsocketPipelineType.DriverMode.valueOf() });
}
if (useCameraSettingsStore().isFocusMode) {
pipelineNames.push({ name: "Focus Mode", value: WebsocketPipelineType.FocusCamera });
pipelineNames.push({ name: "Focus Mode", value: WebsocketPipelineType.FocusCamera.valueOf() });
}
if (useCameraSettingsStore().isCalibrationMode) {
pipelineNames.push({ name: "3D Calibration Mode", value: WebsocketPipelineType.Calib3d });
pipelineNames.push({ name: "3D Calibration Mode", value: WebsocketPipelineType.Calib3d.valueOf() });
}
return pipelineNames;
@@ -240,7 +218,7 @@ useCameraSettingsStore().$subscribe((mutation, state) => {
break;
}
});
const wrappedCameras = computed<SelectItem[]>(() =>
const wrappedCameras = computed<SelectItem<string>[]>(() =>
Object.keys(useCameraSettingsStore().cameras).map((cameraUniqueName) => ({
name: useCameraSettingsStore().cameras[cameraUniqueName].nickname,
value: cameraUniqueName
@@ -257,7 +235,7 @@ const wrappedCameras = computed<SelectItem[]>(() =>
v-model="useStateStore().currentCameraUniqueName"
label="Camera"
:items="wrappedCameras"
@update:modelValue="changeCurrentCameraUniqueName"
@update:modelValue="pipelineType = useCameraSettingsStore().currentWebsocketPipelineType"
/>
<pv-input
v-else
@@ -407,14 +385,14 @@ const wrappedCameras = computed<SelectItem[]>(() =>
<v-card-actions class="pr-5 pt-10px pb-5">
<v-btn
color="buttonPassive"
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
:variant="theme.global.current.value.dark ? 'outlined' : 'elevated'"
@click="cancelPipelineCreation"
>
Cancel
</v-btn>
<v-btn
color="buttonActive"
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
:variant="theme.global.current.value.dark ? 'outlined' : 'elevated'"
:disabled="checkPipelineName(newPipelineName) !== true"
@click="createNewPipeline"
>
@@ -441,7 +419,7 @@ const wrappedCameras = computed<SelectItem[]>(() =>
<v-card-actions class="pa-5 pt-0">
<v-btn
color="buttonPassive"
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
:variant="theme.global.current.value.dark ? 'outlined' : 'elevated'"
class="text-black"
@click="cancelChangePipelineType"
>
@@ -449,7 +427,7 @@ const wrappedCameras = computed<SelectItem[]>(() =>
</v-btn>
<v-btn
color="buttonActive"
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
:variant="theme.global.current.value.dark ? 'outlined' : 'elevated'"
@click="confirmChangePipelineType"
>
Confirm

View File

@@ -13,9 +13,9 @@ import OutputTab from "@/components/dashboard/tabs/OutputTab.vue";
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 { PipelineType } from "@/types/PipelineTypes";
import { WebsocketPipelineType } from "@/types/WebsocketDataTypes";
import { useDisplay } from "vuetify/lib/composables/display";
import { useTheme } from "vuetify";
import { useDisplay, useTheme } from "vuetify";
const theme = useTheme();
@@ -106,6 +106,17 @@ const tabGroups = computed<ConfigOption[][]>(() => {
.filter((it) => it.length); // Remove empty tab groups
});
// This boolean is used to satisfy type-checking requirements.
const shouldUseWideSecondTabGroup = computed(() => {
const currentPipelineSettings = useCameraSettingsStore().currentPipelineSettings;
return (
(currentPipelineSettings.pipelineType === PipelineType.AprilTag ||
currentPipelineSettings.pipelineType === PipelineType.Aruco) &&
currentPipelineSettings.doMultiTarget
);
});
const onBeforeTabUpdate = () => {
// Force the current tab to the input tab on driver mode change
if (useCameraSettingsStore().isDriverMode) {
@@ -122,14 +133,14 @@ const onBeforeTabUpdate = () => {
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'"
:variant="theme.global.current.value.dark ? 'tonal' : 'elevated'"
/>
</template>
<template v-else>
<v-col
v-for="(tabGroupData, tabGroupIndex) in tabGroups"
:key="tabGroupIndex"
:cols="tabGroupIndex == 1 && useCameraSettingsStore().currentPipelineSettings.doMultiTarget ? 7 : ''"
:cols="tabGroupIndex === 1 && shouldUseWideSecondTabGroup ? 7 : ''"
:class="tabGroupIndex !== tabGroups.length - 1 && 'pr-3'"
@vue:before-update="onBeforeTabUpdate"
>

View File

@@ -35,7 +35,7 @@ const processingMode = computed<number>({
<v-btn
color="buttonPassive"
:disabled="!useCameraSettingsStore().hasConnected"
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
:variant="theme.global.current.value.dark ? 'outlined' : 'elevated'"
class="w-50"
>
<template #prepend>
@@ -48,10 +48,10 @@ const processingMode = computed<number>({
:disabled="
!useCameraSettingsStore().hasConnected ||
!useCameraSettingsStore().isCurrentVideoFormatCalibrated ||
useCameraSettingsStore().currentPipelineSettings.pipelineType == PipelineType.ObjectDetection ||
useCameraSettingsStore().currentPipelineSettings.pipelineType == PipelineType.ColoredShape
useCameraSettingsStore().currentPipelineSettings.pipelineType === PipelineType.ObjectDetection ||
useCameraSettingsStore().currentPipelineSettings.pipelineType === PipelineType.ColoredShape
"
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
:variant="theme.global.current.value.dark ? 'outlined' : 'elevated'"
class="w-50"
>
<template #prepend>
@@ -69,7 +69,7 @@ const processingMode = computed<number>({
<v-btn
color="buttonPassive"
class="fill w-50"
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
:variant="theme.global.current.value.dark ? 'outlined' : 'elevated'"
>
<v-icon start class="mode-btn-icon" size="large">mdi-import</v-icon>
<span class="mode-btn-label">Raw</span>
@@ -77,7 +77,7 @@ const processingMode = computed<number>({
<v-btn
color="buttonPassive"
class="fill w-50"
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
:variant="theme.global.current.value.dark ? 'outlined' : 'elevated'"
>
<v-icon start class="mode-btn-icon" size="large">mdi-export</v-icon>
<span class="mode-btn-label">Processed</span>

View File

@@ -1,18 +1,17 @@
<script setup lang="ts">
import { PipelineType } from "@/types/PipelineTypes";
import { PipelineType, type AprilTagPipelineSettings, AprilTagFamily } 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 } 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 currentPipelineSettings = computed<AprilTagPipelineSettings>(
() => useCameraSettingsStore().currentPipelineSettings as AprilTagPipelineSettings
);
const { mdAndDown } = useDisplay();
const interactiveCols = computed(() =>
@@ -25,7 +24,10 @@ const interactiveCols = computed(() =>
<pv-select
v-model="currentPipelineSettings.tagFamily"
label="Target family"
:items="['AprilTag 36h11 (6.5in)', 'AprilTag 16h5 (6in)']"
:items="[
{ value: AprilTagFamily.Family36h11, name: 'AprilTag 36h11 (6.5in)' },
{ value: AprilTagFamily.Family16h5, name: 'AprilTag 16h5 (6in)' }
]"
:select-cols="interactiveCols"
@update:modelValue="(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ tagFamily: value }, false)"
/>

View File

@@ -1,6 +1,6 @@
<script setup lang="ts">
import { useCameraSettingsStore } from "@/stores/settings/CameraSettingsStore";
import { PipelineType, type ActivePipelineSettings } from "@/types/PipelineTypes";
import { PipelineType, type ArucoPipelineSettings, AprilTagFamily } from "@/types/PipelineTypes";
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";
@@ -11,8 +11,8 @@ 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 currentPipelineSettings = computed<ArucoPipelineSettings>(
() => useCameraSettingsStore().currentPipelineSettings as ArucoPipelineSettings
);
const { mdAndDown } = useDisplay();
const interactiveCols = computed(() =>
@@ -25,7 +25,10 @@ const interactiveCols = computed(() =>
<pv-select
v-model="currentPipelineSettings.tagFamily"
label="Target family"
:items="['AprilTag Family 36h11', 'AprilTag Family 16h5']"
:items="[
{ value: AprilTagFamily.Family36h11, name: 'AprilTag 36h11 (6.5in)' },
{ value: AprilTagFamily.Family16h5, name: 'AprilTag 16h5 (6in)' }
]"
:select-cols="interactiveCols"
@update:modelValue="(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ tagFamily: value }, false)"
/>

View File

@@ -1,6 +1,14 @@
<script setup lang="ts">
import { useCameraSettingsStore } from "@/stores/settings/CameraSettingsStore";
import { type ActivePipelineSettings, PipelineType } from "@/types/PipelineTypes";
import {
type ActivePipelineSettings,
PipelineType,
ContourSortMode,
ContourTargetOrientation,
ContourGroupingMode,
ContourIntersection,
ContourShape
} 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";
@@ -61,7 +69,10 @@ const interactiveCols = computed(() =>
v-model="useCameraSettingsStore().currentPipelineSettings.contourTargetOrientation"
label="Target Orientation"
tooltip="Used to determine how to calculate target landmarks, as well as aspect ratio"
:items="['Portrait', 'Landscape']"
:items="[
{ value: ContourTargetOrientation.Portrait, name: 'Portrait' },
{ value: ContourTargetOrientation.Landscape, name: 'Landscape' }
]"
:select-cols="interactiveCols"
@update:modelValue="
(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ contourTargetOrientation: value }, false)
@@ -72,7 +83,15 @@ const interactiveCols = computed(() =>
label="Target Sort"
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']"
:items="[
{ value: ContourSortMode.Largest, name: 'Largest' },
{ value: ContourSortMode.Smallest, name: 'Smallest' },
{ value: ContourSortMode.Highest, name: 'Highest' },
{ value: ContourSortMode.Lowest, name: 'Lowest' },
{ value: ContourSortMode.Rightmost, name: 'Rightmost' },
{ value: ContourSortMode.Leftmost, name: 'Leftmost' },
{ value: ContourSortMode.Centermost, name: 'Centermost' }
]"
@update:modelValue="
(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ contourSortMode: value }, false)
"
@@ -166,7 +185,11 @@ const interactiveCols = computed(() =>
label="Target Grouping"
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']"
:items="[
{ value: ContourGroupingMode.Single, name: 'Single' },
{ value: ContourGroupingMode.Dual, name: 'Dual' },
{ value: ContourGroupingMode.TwoOrMore, name: 'Two or More' }
]"
@update:modelValue="
(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ contourGroupingMode: value }, false)
"
@@ -176,7 +199,13 @@ const interactiveCols = computed(() =>
label="Target Intersection"
tooltip="If target grouping is in dual mode it will use this dropdown to decide how targets are grouped with adjacent targets"
:select-cols="interactiveCols"
:items="['None', 'Up', 'Down', 'Left', 'Right']"
:items="[
{ value: ContourIntersection.None, name: 'None' },
{ value: ContourIntersection.Up, name: 'Up' },
{ value: ContourIntersection.Down, name: 'Down' },
{ value: ContourIntersection.Left, name: 'Left' },
{ value: ContourIntersection.Right, name: 'Right' }
]"
:disabled="useCameraSettingsStore().currentPipelineSettings.contourGroupingMode === 0"
@update:modelValue="
(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ contourIntersection: value }, false)
@@ -189,7 +218,12 @@ const interactiveCols = computed(() =>
label="Target Shape"
tooltip="The shape of targets to look for"
:select-cols="interactiveCols"
:items="['Circle', 'Polygon', 'Triangle', 'Quadrilateral']"
:items="[
{ value: ContourShape.Circle, name: 'Circle' },
{ value: ContourShape.Polygon, name: 'Polygon' },
{ value: ContourShape.Triangle, name: 'Triangle' },
{ value: ContourShape.Quadrilateral, name: 'Quadrilateral' }
]"
@update:modelValue="
(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ contourShape: value }, false)
"

View File

@@ -30,11 +30,11 @@ const getFilteredStreamDivisors = (): number[] => {
};
const getNumberOfSkippedDivisors = () => streamDivisors.length - getFilteredStreamDivisors().length;
const cameraResolutions = computed(() =>
useCameraSettingsStore().currentCameraSettings.validVideoFormats.map(
(f) => `${getResolutionString(f.resolution)} at ${f.fps} FPS, ${f.pixelFormat}`
)
);
const cameraResolutions = (): { name: string; value: number }[] =>
useCameraSettingsStore().currentCameraSettings.validVideoFormats.map<{ name: string; value: number }>((f) => ({
name: `${getResolutionString(f.resolution)} at ${f.fps} FPS, ${f.pixelFormat}`,
value: f.index || 0 // Index won't ever be undefined
}));
const handleResolutionChange = (value: number) => {
useCameraSettingsStore().changeCurrentPipelineSetting({ cameraVideoModeIndex: value }, false);
@@ -49,20 +49,23 @@ const handleResolutionChange = (value: number) => {
const streamResolutions = computed(() => {
const streamDivisors = getFilteredStreamDivisors();
const currentResolution = useCameraSettingsStore().currentVideoFormat.resolution;
return streamDivisors.map(
(x) =>
`${getResolutionString({
width: Math.floor(currentResolution.width / x),
height: Math.floor(currentResolution.height / x)
})}`
);
return streamDivisors.map((x, i) => ({
name: `${Math.floor(currentResolution.width / x)}x${Math.floor(currentResolution.height / x)}`,
value: i
}));
});
const currentStreamResolutionIndex = computed<number>({
get: () => {
const stored = useCameraSettingsStore().currentPipelineSettings.streamingFrameDivisor;
const skipped = getNumberOfSkippedDivisors();
return stored - skipped;
},
set: (index) => {
useCameraSettingsStore().changeCurrentPipelineSetting({
streamingFrameDivisor: index + getNumberOfSkippedDivisors()
});
}
});
const handleStreamResolutionChange = (value: number) => {
useCameraSettingsStore().changeCurrentPipelineSetting(
{ streamingFrameDivisor: value + getNumberOfSkippedDivisors() },
false
);
};
const { mdAndDown } = useDisplay();
const interactiveCols = computed(() =>
@@ -160,7 +163,7 @@ const interactiveCols = computed(() =>
/>
<pv-switch
v-model="useCameraSettingsStore().currentPipelineSettings.blockForFrames"
:disabled="!useCameraSettingsStore().currentCameraSettings.matchedCameraInfo.PVUsbCameraInfo"
:disabled="useCameraSettingsStore().currentCameraSettings.matchedCameraInfo.type !== 'PVUsbCameraInfo'"
label="Low Latency Mode"
:switch-cols="interactiveCols"
tooltip="When enabled, USB cameras wait for the next camera frame for lowest latency. When disabled, uses the most recent available frame for higher FPS."
@@ -182,17 +185,16 @@ const interactiveCols = computed(() =>
v-model="useCameraSettingsStore().currentPipelineSettings.cameraVideoModeIndex"
label="Resolution"
tooltip="Resolution and FPS the camera should directly capture at"
:items="cameraResolutions"
:items="cameraResolutions()"
:select-cols="interactiveCols"
@update:modelValue="(args) => handleResolutionChange(args)"
/>
<pv-select
v-model="useCameraSettingsStore().currentPipelineSettings.streamingFrameDivisor"
v-model="currentStreamResolutionIndex"
label="Stream Resolution"
tooltip="Resolution to which camera frames are downscaled for streaming to the dashboard"
:items="streamResolutions"
:select-cols="interactiveCols"
@update:modelValue="(args) => handleStreamResolutionChange(args)"
/>
<pv-switch
v-if="useCameraSettingsStore().isDriverMode"

View File

@@ -1,8 +1,13 @@
<script setup lang="ts">
import { useCameraSettingsStore } from "@/stores/settings/CameraSettingsStore";
import { type ObjectDetectionPipelineSettings, PipelineType } from "@/types/PipelineTypes";
import {
type ObjectDetectionPipelineSettings,
PipelineType,
ContourSortMode,
ContourTargetOrientation
} from "@/types/PipelineTypes";
import PvSlider from "@/components/common/pv-slider.vue";
import PvSelect from "@/components/common/pv-select.vue";
import PvSelect, { type SelectItem } from "@/components/common/pv-select.vue";
import PvRangeSlider from "@/components/common/pv-range-slider.vue";
import { computed } from "vue";
import { useStateStore } from "@/stores/StateStore";
@@ -44,19 +49,19 @@ const supportedModels = computed<ObjectDetectionModelProperties[]>(() => {
return availableModels.filter(isSupported);
});
const selectedModel = computed({
get: () => {
const currentModel = currentPipelineSettings.value.model;
if (!currentModel) return undefined;
const modelWrapper = computed<SelectItem<string>[]>(() =>
supportedModels.value.map((model) => ({
name: model.nickname,
value: model.modelPath
}))
);
const index = supportedModels.value.findIndex((model) => model.modelPath === currentModel.modelPath);
return index === -1 ? undefined : index;
},
set: (v) => {
if (v !== undefined && v >= 0 && v < supportedModels.value.length) {
const newModel = supportedModels.value[v];
useCameraSettingsStore().changeCurrentPipelineSetting({ model: newModel }, true);
const selectedModel = computed<string>({
get: () => currentPipelineSettings.value.model?.modelPath ?? "",
set: (value) => {
const model = supportedModels.value.find((supportedModel) => supportedModel.modelPath === value);
if (model) {
useCameraSettingsStore().changeCurrentPipelineSetting({ model }, true);
}
}
});
@@ -69,7 +74,7 @@ const selectedModel = computed({
label="Model"
tooltip="The model used to detect objects in the camera feed"
:select-cols="interactiveCols"
:items="supportedModels.map((model) => model.nickname)"
:items="modelWrapper"
/>
<pv-slider
@@ -123,14 +128,13 @@ const selectedModel = computed({
v-model="useCameraSettingsStore().currentPipelineSettings.contourTargetOrientation"
label="Target Orientation"
tooltip="Used to determine how to calculate target landmarks, as well as aspect ratio"
:items="['Portrait', 'Landscape']"
:items="[
{ value: ContourTargetOrientation.Portrait, name: 'Portrait' },
{ value: ContourTargetOrientation.Landscape, name: 'Landscape' }
]"
:select-cols="interactiveCols"
@update:modelValue="
(value) =>
useCameraSettingsStore().changeCurrentPipelineSetting(
{ contourTargetOrientation: typeof value === 'string' ? Number(value) : value },
false
)
(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ contourTargetOrientation: value }, false)
"
/>
<pv-select
@@ -138,13 +142,17 @@ const selectedModel = computed({
label="Target Sort"
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']"
:items="[
{ value: ContourSortMode.Largest, name: 'Largest' },
{ value: ContourSortMode.Smallest, name: 'Smallest' },
{ value: ContourSortMode.Highest, name: 'Highest' },
{ value: ContourSortMode.Lowest, name: 'Lowest' },
{ value: ContourSortMode.Rightmost, name: 'Rightmost' },
{ value: ContourSortMode.Leftmost, name: 'Leftmost' },
{ value: ContourSortMode.Centermost, name: 'Centermost' }
]"
@update:modelValue="
(value) =>
useCameraSettingsStore().changeCurrentPipelineSetting(
{ contourSortMode: typeof value === 'string' ? Number(value) : value },
false
)
(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ contourSortMode: value }, false)
"
/>
</div>

View File

@@ -1,7 +1,13 @@
<script setup lang="ts">
import PvSelect from "@/components/common/pv-select.vue";
import { useCameraSettingsStore } from "@/stores/settings/CameraSettingsStore";
import { type ActivePipelineSettings, PipelineType, RobotOffsetPointMode } from "@/types/PipelineTypes";
import {
type ActivePipelineSettings,
PipelineType,
RobotOffsetPointMode,
ContourTargetOrientation,
ContourTargetOffsetPointEdge
} from "@/types/PipelineTypes";
import PvSwitch from "@/components/common/pv-switch.vue";
import PvSlider from "@/components/common/pv-slider.vue";
import { computed } from "vue";
@@ -108,7 +114,13 @@ const interactiveCols = computed(() =>
v-model="useCameraSettingsStore().currentPipelineSettings.contourTargetOffsetPointEdge"
label="Target Offset Point"
tooltip="Changes where the 'center' of the target is (used for calculating e.g. pitch and yaw)"
:items="['Center', 'Top', 'Bottom', 'Left', 'Right']"
:items="[
{ value: ContourTargetOffsetPointEdge.Center, name: 'Center' },
{ value: ContourTargetOffsetPointEdge.Top, name: 'Top' },
{ value: ContourTargetOffsetPointEdge.Bottom, name: 'Bottom' },
{ value: ContourTargetOffsetPointEdge.Left, name: 'Left' },
{ value: ContourTargetOffsetPointEdge.Right, name: 'Right' }
]"
:select-cols="interactiveCols"
@update:modelValue="
(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ contourTargetOffsetPointEdge: value }, false)
@@ -119,7 +131,10 @@ const interactiveCols = computed(() =>
v-model="useCameraSettingsStore().currentPipelineSettings.contourTargetOrientation"
label="Target Orientation"
tooltip="Used to determine how to calculate target landmarks (e.g. the top, left, or bottom of the target)"
:items="['Portrait', 'Landscape']"
:items="[
{ value: ContourTargetOrientation.Portrait, name: 'Portrait' },
{ value: ContourTargetOrientation.Landscape, name: 'Landscape' }
]"
:select-cols="interactiveCols"
@update:modelValue="
(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ contourTargetOrientation: value }, false)
@@ -129,7 +144,11 @@ const interactiveCols = computed(() =>
v-model="useCameraSettingsStore().currentPipelineSettings.offsetRobotOffsetMode"
label="Robot Offset Mode"
tooltip="Used to add an arbitrary offset to the location of the targeting crosshair"
:items="['None', 'Single Point', 'Dual Point']"
:items="[
{ value: RobotOffsetPointMode.None, name: 'None' },
{ value: RobotOffsetPointMode.Single, name: 'Single Point' },
{ value: RobotOffsetPointMode.Dual, name: 'Dual Point' }
]"
:select-cols="interactiveCols"
@update:modelValue="
(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ offsetRobotOffsetMode: value }, false)
@@ -168,7 +187,7 @@ const interactiveCols = computed(() =>
block
color="primary"
class="text-black"
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
:variant="theme.global.current.value.dark ? 'outlined' : 'elevated'"
@click="useCameraSettingsStore().takeRobotOffsetPoint(RobotOffsetType.Single)"
>
Take Point
@@ -179,7 +198,7 @@ const interactiveCols = computed(() =>
size="small"
block
color="error"
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
:variant="theme.global.current.value.dark ? 'outlined' : 'elevated'"
@click="useCameraSettingsStore().takeRobotOffsetPoint(RobotOffsetType.Clear)"
>
Clear All Points
@@ -196,7 +215,7 @@ const interactiveCols = computed(() =>
block
color="primary"
class="text-black"
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
:variant="theme.global.current.value.dark ? 'outlined' : 'elevated'"
@click="useCameraSettingsStore().takeRobotOffsetPoint(RobotOffsetType.DualFirst)"
>
Take First Point
@@ -208,7 +227,7 @@ const interactiveCols = computed(() =>
block
color="primary"
class="text-black"
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
:variant="theme.global.current.value.dark ? 'outlined' : 'elevated'"
@click="useCameraSettingsStore().takeRobotOffsetPoint(RobotOffsetType.DualSecond)"
>
Take Second Point
@@ -219,7 +238,7 @@ const interactiveCols = computed(() =>
size="small"
block
color="error"
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
:variant="theme.global.current.value.dark ? 'outlined' : 'elevated'"
@click="useCameraSettingsStore().takeRobotOffsetPoint(RobotOffsetType.Clear)"
>
Clear All Points

View File

@@ -207,7 +207,7 @@ const resetCurrentBuffer = () => {
color="buttonActive"
class="mb-4 mt-1"
style="width: min-content"
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
:variant="theme.global.current.value.dark ? 'outlined' : 'elevated'"
@click="resetCurrentBuffer"
>Reset Samples</v-btn
>

View File

@@ -191,7 +191,7 @@ const interactiveCols = computed(() =>
block
color="primary"
class="text-black"
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
:variant="theme.global.current.value.dark ? 'outlined' : 'elevated'"
@click="enableColorPicking(useCameraSettingsStore().currentPipelineSettings.hueInverted ? 2 : 3)"
>
<v-icon start size="large"> mdi-minus </v-icon>
@@ -204,7 +204,7 @@ const interactiveCols = computed(() =>
class="text-black"
size="small"
block
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
:variant="theme.global.current.value.dark ? 'outlined' : 'elevated'"
@click="enableColorPicking(1)"
>
<v-icon start size="large"> mdi-plus-minus </v-icon>
@@ -217,7 +217,7 @@ const interactiveCols = computed(() =>
block
color="primary"
class="text-black"
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
:variant="theme.global.current.value.dark ? 'outlined' : 'elevated'"
@click="enableColorPicking(useCameraSettingsStore().currentPipelineSettings.hueInverted ? 3 : 2)"
>
<v-icon start size="large"> mdi-plus </v-icon>
@@ -232,7 +232,7 @@ const interactiveCols = computed(() =>
color="primary"
class="text-black"
size="small"
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
:variant="theme.global.current.value.dark ? 'outlined' : 'elevated'"
@click="disableColorPicking"
>
Cancel

View File

@@ -1,6 +1,5 @@
@ -0,0 +1,565 @@
<script setup lang="ts">
import { inject, computed, ref, watch } from "vue";
import { inject, computed, ref, watch, useTemplateRef } from "vue";
import { useStateStore } from "@/stores/StateStore";
import { useSettingsStore } from "@/stores/settings/GeneralSettingsStore";
import PvSelect from "@/components/common/pv-select.vue";
@@ -15,20 +14,20 @@ const theme = useTheme();
const restartProgram = async () => {
if (await axiosPost("/utils/restartProgram", "restart PhotonVision")) {
forceReloadPage();
await forceReloadPage();
}
};
const restartDevice = async () => {
if (await axiosPost("/utils/restartDevice", "restart the device")) {
forceReloadPage();
await forceReloadPage();
}
};
const address = inject<string>("backendHost");
const offlineUpdate = ref();
const offlineUpdate = useTemplateRef("offlineUpdate");
const openOfflineUpdatePrompt = () => {
offlineUpdate.value.click();
offlineUpdate.value?.click();
};
const offlineUpdateRegex = new RegExp("photonvision-((?:dev-)?v[\\w.-]+)-((?:linux|win|mac)\\w+)\\.jar");
@@ -37,8 +36,8 @@ const majorVersionRegex = new RegExp("(?:dev-)?(\\d+)\\.\\d+\\.\\d+");
const offlineUpdateDialog = ref({ show: false, confirmString: "" });
const handleOfflineUpdateRequest = async () => {
const files = offlineUpdate.value.files;
if (files.length === 0) return;
const files = offlineUpdate.value?.files;
if (!files?.length) return;
const match = files[0].name.match(offlineUpdateRegex);
if (!match) {
@@ -68,7 +67,7 @@ const handleOfflineUpdateRequest = async () => {
});
return;
} else if (versionMatch && !dev) {
handleOfflineUpdate(files[0]);
await handleOfflineUpdate(files[0]);
} else if (!versionMatch && !dev) {
offlineUpdateDialog.value = {
show: true,
@@ -99,7 +98,7 @@ const handleOfflineUpdate = async (file: File) => {
if (
await axiosPost("/utils/offlineUpdate", "upload new software", formData, {
headers: { "Content-Type": "multipart/form-data" },
onUploadProgress: ({ progress }) => {
onUploadProgress: ({ progress }: { progress?: number }) => {
const uploadPercentage = (progress || 0) * 100.0;
if (uploadPercentage < 99.5) {
useStateStore().showSnackbarMessage({
@@ -118,18 +117,18 @@ const handleOfflineUpdate = async (file: File) => {
color: "secondary",
timeout: -1
});
forceReloadPage();
await forceReloadPage();
}
};
const exportLogFile = ref();
const exportLogFile = useTemplateRef("exportLogFile");
const openExportLogsPrompt = () => {
exportLogFile.value.click();
exportLogFile.value?.click();
};
const exportSettings = ref();
const exportSettings = useTemplateRef("exportSettings");
const openExportSettingsPrompt = () => {
exportSettings.value.click();
exportSettings.value?.click();
};
enum ImportType {
@@ -141,10 +140,10 @@ enum ImportType {
}
const showImportDialog = ref(false);
const importType = ref<ImportType | undefined>(undefined);
const importType = ref<ImportType>(ImportType.AllSettings);
const importFile = ref<File | null>(null);
const handleSettingsImport = () => {
const handleSettingsImport = async () => {
if (importType.value === undefined || importFile.value === null) return;
const formData = new FormData();
formData.append("data", importFile.value);
@@ -167,18 +166,18 @@ const handleSettingsImport = () => {
settingsEndpoint = "";
break;
}
axiosPost(`/settings${settingsEndpoint}`, "import settings", formData, {
await axiosPost(`/settings${settingsEndpoint}`, "import settings", formData, {
headers: { "Content-Type": "multipart/form-data" }
});
showImportDialog.value = false;
importType.value = undefined;
importType.value = ImportType.AllSettings;
importFile.value = null;
};
const showFactoryReset = ref(false);
const nukePhotonConfigDirectory = async () => {
if (await axiosPost("/utils/nukeConfigDirectory", "delete the config directory")) {
forceReloadPage();
await forceReloadPage();
}
};
@@ -312,7 +311,7 @@ watch(metricsHistorySnapshot, () => {
<v-col>
<v-btn
color="buttonPassive"
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
:variant="theme.global.current.value.dark ? 'outlined' : 'elevated'"
@click="useStateStore().showLogModal = true"
>
<v-icon start class="open-icon" size="large"> mdi-eye </v-icon>
@@ -322,7 +321,7 @@ watch(metricsHistorySnapshot, () => {
<v-col>
<v-btn
color="buttonPassive"
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
:variant="theme.global.current.value.dark ? 'outlined' : 'elevated'"
@click="openExportLogsPrompt"
>
<v-icon start class="open-icon" size="large"> mdi-download </v-icon>
@@ -345,7 +344,7 @@ watch(metricsHistorySnapshot, () => {
<v-col>
<v-btn
color="buttonPassive"
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
:variant="theme.global.current.value.dark ? 'outlined' : 'elevated'"
@click="() => (showImportDialog = true)"
>
<v-icon start class="open-icon" size="large"> mdi-import </v-icon>
@@ -355,7 +354,7 @@ watch(metricsHistorySnapshot, () => {
<v-col>
<v-btn
color="buttonPassive"
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
:variant="theme.global.current.value.dark ? 'outlined' : 'elevated'"
@click="openExportSettingsPrompt"
>
<v-icon start class="open-icon" size="large"> mdi-export </v-icon>
@@ -369,7 +368,7 @@ watch(metricsHistorySnapshot, () => {
<v-col cols="12" sm="6"
><v-btn
color="buttonActive"
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
:variant="theme.global.current.value.dark ? 'outlined' : 'elevated'"
@click="restartProgram"
>
<v-icon start class="open-icon" size="large"> mdi-restart </v-icon>
@@ -379,7 +378,7 @@ watch(metricsHistorySnapshot, () => {
<v-col cols="12" sm="6">
<v-btn
color="buttonPassive"
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
:variant="theme.global.current.value.dark ? 'outlined' : 'elevated'"
@click="openOfflineUpdatePrompt"
>
<v-icon start class="open-icon" size="large"> mdi-upload </v-icon>
@@ -400,7 +399,7 @@ watch(metricsHistorySnapshot, () => {
<v-col cols="12" sm="6">
<v-btn
color="buttonActive"
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
:variant="theme.global.current.value.dark ? 'outlined' : 'elevated'"
@click="restartDevice"
>
<v-icon start class="open-icon" size="large"> mdi-restart-alert </v-icon>
@@ -410,7 +409,7 @@ watch(metricsHistorySnapshot, () => {
<v-col cols="12" sm="6">
<v-btn
color="error"
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
:variant="theme.global.current.value.dark ? 'outlined' : 'elevated'"
@click="() => (showFactoryReset = true)"
>
<v-icon start class="open-icon" size="large"> mdi-trash-can-outline </v-icon>
@@ -453,7 +452,7 @@ watch(metricsHistorySnapshot, () => {
<v-card-text class="pt-0 flex-0-0 pb-2">
<div class="d-flex justify-space-between pb-3 pt-3">
<span>CPU Temperature</span>
<span>{{ cpuTempData.at(-1)?.value == -1 ? "--- " : Math.round(cpuTempData.at(-1)?.value ?? 0) }}°C</span>
<span>{{ cpuTempData.at(-1)?.value === -1 ? "--- " : Math.round(cpuTempData.at(-1)?.value ?? 0) }}°C</span>
</div>
<Suspense>
<!-- Allows us to import echarts when it's actually needed -->
@@ -470,7 +469,10 @@ watch(metricsHistorySnapshot, () => {
tooltip="Measured rate for this coprocessor ONLY. This FMS limit is for ALL robot communication. If you are experiencing bandwidth issues while under this limit, check other sources."
/>
<span
>{{ networkUsageData.at(-1)?.value == -1 ? "---" : networkUsageData.at(-1)?.value.toFixed(3) }} Mb/s</span
>{{
networkUsageData.at(-1)?.value === -1 ? "---" : networkUsageData.at(-1)?.value.toFixed(3)
}}
Mb/s</span
>
</div>
<Suspense>
@@ -500,7 +502,7 @@ watch(metricsHistorySnapshot, () => {
width="600"
@update:modelValue="
() => {
importType = undefined;
importType = ImportType.AllSettings;
importFile = null;
}
"
@@ -514,7 +516,13 @@ watch(metricsHistorySnapshot, () => {
v-model="importType"
label="Type"
tooltip="Select the type of settings file you are trying to upload"
:items="['All Settings', 'Hardware Config', 'Hardware Settings', 'Network Config', 'Apriltag Layout']"
:items="[
{ value: ImportType.AllSettings, name: 'All Settings' },
{ value: ImportType.HardwareConfig, name: 'Hardware Config' },
{ value: ImportType.HardwareSettings, name: 'Hardware Settings' },
{ value: ImportType.NetworkConfig, name: 'Network Config' },
{ value: ImportType.ApriltagFieldLayout, name: 'AprilTag Field Layout' }
]"
:select-cols="10"
style="width: 100%"
/>
@@ -529,7 +537,7 @@ watch(metricsHistorySnapshot, () => {
<v-btn
color="primary"
:disabled="importFile === null"
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
:variant="theme.global.current.value.dark ? 'outlined' : 'elevated'"
@click="handleSettingsImport"
>
<v-icon start class="open-icon"> mdi-import </v-icon>
@@ -552,10 +560,12 @@ watch(metricsHistorySnapshot, () => {
<v-btn
color="buttonActive"
width="100%"
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
:variant="theme.global.current.value.dark ? 'outlined' : 'elevated'"
@click="
offlineUpdateDialog.show = false;
handleOfflineUpdate(offlineUpdate.files[0]);
if (offlineUpdate?.files?.length) {
handleOfflineUpdate(offlineUpdate.files[0]);
}
"
>
<v-icon start class="open-icon" size="large"> mdi-upload </v-icon>

View File

@@ -106,6 +106,7 @@ const saveGeneralSettings = async () => {
// Update the local settings cause the backend checked their validity. Assign is to deref value
useSettingsStore().network = { ...useSettingsStore().network, ...Object.assign({}, tempSettingsStruct.value) };
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} catch (error: any) {
resetTempSettingsStruct();
if (error.response) {
@@ -150,14 +151,11 @@ const saveGeneralSettings = async () => {
}
};
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])
const currentNetworkInterface = computed<string>({
get: () => useSettingsStore().network.networkManagerIface || "",
set: (v) => {
tempSettingsStruct.value.networkManagerIface = v;
}
});
watchEffect(() => {
@@ -185,7 +183,7 @@ watchEffect(() => {
</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">
<v-form v-model="settingsValid">
<pv-input
v-model="tempSettingsStruct.ntServerAddress"
label="Team Number/NetworkTables Server Address"
@@ -205,7 +203,7 @@ watchEffect(() => {
density="compact"
text="The NetworkTables Server Address is not set or is invalid. NetworkTables is unable to connect."
icon="mdi-alert-circle-outline"
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'tonal'"
:variant="theme.global.current.value.dark ? 'tonal' : 'elevated'"
/>
<pv-radio
v-show="!useSettingsStore().network.networkingDisabled"
@@ -256,7 +254,7 @@ watchEffect(() => {
/>
<pv-select
v-show="!useSettingsStore().network.networkingDisabled"
v-model="currentNetworkInterfaceIndex"
v-model="currentNetworkInterface"
label="NetworkManager interface"
:disabled="
!tempSettingsStruct.shouldManage ||
@@ -279,7 +277,7 @@ watchEffect(() => {
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'"
:variant="theme.global.current.value.dark ? 'tonal' : 'elevated'"
/>
<pv-switch
v-model="tempSettingsStruct.runNTServer"
@@ -293,7 +291,7 @@ watchEffect(() => {
density="compact"
text="This mode is intended for debugging and should be off for proper usage. PhotonLib will NOT work!"
icon="mdi-information-outline"
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'tonal'"
:variant="theme.global.current.value.dark ? 'tonal' : 'elevated'"
/>
<v-card-title class="pl-0 pt-3 pb-10px">Miscellaneous</v-card-title>
<pv-switch
@@ -308,13 +306,13 @@ watchEffect(() => {
density="compact"
text="This mode is intended for debugging and may reduce performance; it should be off for field use."
icon="mdi-information-outline"
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'tonal'"
:variant="theme.global.current.value.dark ? 'tonal' : 'elevated'"
/>
</v-form>
<v-btn
color="primary"
class="mt-3"
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
:variant="theme.global.current.value.dark ? 'outlined' : 'elevated'"
style="color: black; width: 100%"
:disabled="!settingsValid || !settingsHaveChanged()"
@click="saveGeneralSettings"
@@ -377,7 +375,7 @@ watchEffect(() => {
</v-card-text>
<v-card-actions class="pa-5 pt-0">
<v-btn
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
:variant="theme.global.current.value.dark ? 'outlined' : 'elevated'"
color="buttonPassive"
class="text-black"
@click="showThemeConfig = false"
@@ -385,7 +383,7 @@ watchEffect(() => {
Close
</v-btn>
<v-btn
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
:variant="theme.global.current.value.dark ? 'outlined' : 'elevated'"
color="buttonActive"
class="text-black"
@click="

View File

@@ -1,5 +1,5 @@
<script setup lang="ts">
import { onMounted, ref, onBeforeUnmount, watch } from "vue";
import { onMounted, onBeforeUnmount, watch, useTemplateRef } from "vue";
import { useTheme } from "vuetify";
// Color - original (adjusted)
@@ -8,14 +8,14 @@ import { useTheme } from "vuetify";
// green - 65, 181, 127 (r: 75, g: 209, b: 147)
// red - 238, 102, 102 (r: 238, g: 102, b: 102)
const colors = {
"blue-LightTheme": { r: 255, g: 216, b: 67 },
"blue-DarkTheme": { r: 92, g: 154, b: 255 },
"purple-LightTheme": { r: 255, g: 216, b: 67 },
"purple-DarkTheme": { r: 167, g: 104, b: 196 },
"red-LightTheme": { r: 255, g: 216, b: 67 },
"red-DarkTheme": { r: 238, g: 102, b: 102 },
"green-LightTheme": { r: 255, g: 216, b: 67 },
"green-DarkTheme": { r: 75, g: 209, b: 147 }
"blue-light": { r: 255, g: 216, b: 67 },
"blue-dark": { r: 92, g: 154, b: 255 },
"purple-light": { r: 255, g: 216, b: 67 },
"purple-dark": { r: 167, g: 104, b: 196 },
"red-light": { r: 255, g: 216, b: 67 },
"red-dark": { r: 238, g: 102, b: 102 },
"green-light": { r: 255, g: 216, b: 67 },
"green-dark": { r: 75, g: 209, b: 147 }
};
const DEFAULT_COLOR = "blue";
@@ -26,9 +26,13 @@ const typeLabels = {
};
const theme = useTheme();
const chartRef = ref(null);
const chartRef = useTemplateRef("chartRef");
let chart: echarts.ECharts | null = null;
interface TooltipSeriesParam {
value: [number, number];
}
const getOptions = (data: ChartData[] = []) => {
const now = Date.now();
return {
@@ -37,7 +41,7 @@ const getOptions = (data: ChartData[] = []) => {
},
tooltip: {
trigger: "axis",
formatter: (params: any) => {
formatter: (params: TooltipSeriesParam[]) => {
const p = params[0];
const append = typeLabels[props.type];
const fmsLimitLabel = "FMS Limit - 7.000 Mb/s";
@@ -53,9 +57,9 @@ const getOptions = (data: ChartData[] = []) => {
return `${tooltip}</div>`;
},
backgroundColor: theme.themes.value[theme.global.name.value].colors.background,
backgroundColor: theme.global.current.value.colors.background,
textStyle: {
color: theme.themes.value[theme.global.name.value].colors.onBackground
color: theme.global.current.value.colors.onBackground
},
axisPointer: {
animation: false
@@ -80,12 +84,12 @@ const getOptions = (data: ChartData[] = []) => {
min: now - 55 * 1000,
axisLine: {
lineStyle: {
color: theme.global.name.value === "LightTheme" ? "#aaa" : "#777"
color: theme.global.current.value.dark ? "#777" : "#aaa"
}
},
axisLabel: {
align: "left",
color: theme.global.name.value === "LightTheme" ? "#fff" : "#ddd",
color: theme.global.current.value.dark ? "#ddd" : "#fff",
formatter: (value: number) => {
const date = new Date(value);
return date.toLocaleTimeString([], {
@@ -102,12 +106,12 @@ const getOptions = (data: ChartData[] = []) => {
position: "right",
min:
props.min ??
function (value) {
function (value: { min: number; max: number }) {
return Math.max(0, (value.min - 10) | 0);
},
max:
props.max ??
function (value) {
function (value: { min: number; max: number }) {
return (value.max + 10) | 0;
},
splitNumber: 2,
@@ -118,7 +122,7 @@ const getOptions = (data: ChartData[] = []) => {
}
},
axisLabel: {
color: theme.global.name.value === "LightTheme" ? "#fff" : "#ddd"
color: theme.global.current.value.dark ? "#ddd" : "#fff"
}
},
series: getSeries(data),
@@ -127,7 +131,7 @@ const getOptions = (data: ChartData[] = []) => {
};
const getSeries = (data: ChartData[] = []) => {
const color = colors[`${props.color ?? DEFAULT_COLOR}-${theme.global.name.value}`];
const color = colors[`${props.color ?? DEFAULT_COLOR}-${theme.global.current.value.dark ? "dark" : "light"}`];
return [
{
type: "line",
@@ -149,10 +153,9 @@ const getSeries = (data: ChartData[] = []) => {
: null,
lineStyle: {
width: 1.5,
color:
theme.global.name.value === "LightTheme"
? theme.themes.value[theme.global.name.value].colors.primary
: `rgb(${color.r}, ${color.g}, ${color.b})`
color: theme.global.current.value.dark
? `rgb(${color.r}, ${color.g}, ${color.b})`
: theme.global.current.value.colors.primary
},
areaStyle: {
color: {
@@ -164,17 +167,15 @@ const getSeries = (data: ChartData[] = []) => {
colorStops: [
{
offset: 0,
color:
theme.global.name.value === "LightTheme"
? `${theme.themes.value[theme.global.name.value].colors.primary}40`
: `rgba(${color.r}, ${color.g}, ${color.b}, 0.15)`
color: theme.global.current.value.dark
? `rgba(${color.r}, ${color.g}, ${color.b}, 0.15)`
: `${theme.global.current.value.colors.primary}40`
},
{
offset: 1,
color:
theme.global.name.value === "LightTheme"
? `${theme.themes.value[theme.global.name.value].colors.primary}40`
: `rgba(${color.r}, ${color.g}, ${color.b}, 0.15)`
color: theme.global.current.value.dark
? `rgba(${color.r}, ${color.g}, ${color.b}, 0.15)`
: `${theme.global.current.value.colors.primary}40`
}
]
}
@@ -191,10 +192,10 @@ interface ChartData {
// Type options: "percentage", "temperature", "mb"
const props = defineProps<{
data: ChartData[];
type: string;
type: keyof typeof typeLabels;
min?: number;
max?: number;
color?: string;
color?: "red" | "green" | "blue" | "purple";
}>();
onMounted(async () => {

View File

@@ -1,5 +1,5 @@
<script setup lang="ts">
import { ref, computed, inject } from "vue";
import { ref, computed, inject, useTemplateRef } from "vue";
import { useStateStore } from "@/stores/StateStore";
import { useSettingsStore } from "@/stores/settings/GeneralSettingsStore";
import { type ObjectDetectionModelProperties } from "@/types/SettingTypes";
@@ -46,7 +46,7 @@ const handleImport = async () => {
if (
await axiosPost("/objectdetection/import", "import an object detection model", formData, {
headers: { "Content-Type": "multipart/form-data" },
onUploadProgress: ({ progress }) => {
onUploadProgress: ({ progress }: { progress?: number }) => {
const uploadPercentage = (progress || 0) * 100.0;
if (uploadPercentage < 99.5) {
useStateStore().showSnackbarMessage({
@@ -74,20 +74,20 @@ const handleImport = async () => {
importVersion.value = null;
};
const deleteModel = (model: ObjectDetectionModelProperties) => {
axiosPost("/objectdetection/delete", "delete an object detection model", {
const deleteModel = async (model: ObjectDetectionModelProperties) => {
await axiosPost("/objectdetection/delete", "delete an object detection model", {
modelPath: model.modelPath
});
};
const renameModel = (model: ObjectDetectionModelProperties, newName: string) => {
const renameModel = async (model: ObjectDetectionModelProperties, newName: string) => {
useStateStore().showSnackbarMessage({
message: "Renaming Object Detection Model...",
color: "secondary",
timeout: -1
});
axiosPost("/objectdetection/rename", "rename an object detection model", {
await axiosPost("/objectdetection/rename", "rename an object detection model", {
modelPath: model.modelPath,
newName: newName
});
@@ -97,7 +97,7 @@ const renameModel = (model: ObjectDetectionModelProperties, newName: string) =>
// Filters out models that are not supported by the current backend, and returns a flattened list.
const supportedModels = computed(() => {
const { availableModels, supportedBackends } = useSettingsStore().general;
const isSupported = (model: any) => {
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());
};
@@ -106,19 +106,19 @@ const supportedModels = computed(() => {
return availableModels.filter(isSupported);
});
const exportModels = ref();
const exportModels = useTemplateRef("exportModels");
const openExportPrompt = () => {
exportModels.value.click();
exportModels.value?.click();
};
const exportIndividualModel = ref();
const exportIndividualModel = useTemplateRef("exportIndividualModel");
const openExportIndividualModelPrompt = () => {
exportIndividualModel.value.click();
exportIndividualModel.value?.click();
};
const showNukeDialog = ref(false);
const nukeModels = () => {
axiosPost("/objectdetection/nuke", "clear and reset object detection models");
const nukeModels = async () => {
await axiosPost("/objectdetection/nuke", "clear and reset object detection models");
};
const showBulkImportDialog = ref(false);
@@ -132,7 +132,7 @@ const handleBulkImport = async () => {
if (
await axiosPost("/objectdetection/bulkimport", "import object detection models", formData, {
headers: { "Content-Type": "multipart/form-data" },
onUploadProgress: ({ progress }) => {
onUploadProgress: ({ progress }: { progress?: number }) => {
const uploadPercentage = (progress || 0) * 100.0;
if (uploadPercentage < 99.5) {
useStateStore().showSnackbarMessage({
@@ -166,7 +166,7 @@ const handleBulkImport = async () => {
<v-btn
color="buttonActive"
class="justify-center"
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
:variant="theme.global.current.value.dark ? 'outlined' : 'elevated'"
@click="() => (showImportDialog = true)"
>
<v-icon start class="open-icon"> mdi-import </v-icon>
@@ -245,7 +245,7 @@ const handleBulkImport = async () => {
importHeight === null ||
importVersion === null
"
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
:variant="theme.global.current.value.dark ? 'outlined' : 'elevated'"
@click="handleImport()"
>
<v-icon start class="open-icon" size="large"> mdi-import </v-icon>
@@ -260,7 +260,7 @@ const handleBulkImport = async () => {
<v-btn
color="buttonActive"
class="justify-center"
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
:variant="theme.global.current.value.dark ? 'outlined' : 'elevated'"
@click="() => (showBulkImportDialog = true)"
>
<v-icon start class="open-icon"> mdi-import </v-icon>
@@ -278,7 +278,7 @@ const handleBulkImport = async () => {
color="buttonActive"
width="100%"
:disabled="importFile === null"
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
:variant="theme.global.current.value.dark ? 'outlined' : 'elevated'"
@click="handleBulkImport()"
>
<v-icon start class="open-icon" size="large"> mdi-import </v-icon>
@@ -292,7 +292,7 @@ const handleBulkImport = async () => {
<v-col cols="12" sm="6">
<v-btn
color="buttonPassive"
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
:variant="theme.global.current.value.dark ? 'outlined' : 'elevated'"
@click="openExportPrompt"
>
<v-icon start class="open-icon"> mdi-export </v-icon>
@@ -309,7 +309,7 @@ const handleBulkImport = async () => {
<v-col cols="12" sm="6">
<v-btn
color="error"
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
:variant="theme.global.current.value.dark ? 'outlined' : 'elevated'"
@click="() => (showNukeDialog = true)"
>
<v-icon left class="open-icon"> mdi-trash </v-icon>
@@ -339,7 +339,7 @@ const handleBulkImport = async () => {
small
color="error"
title="Delete Model"
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
:variant="theme.global.current.value.dark ? 'outlined' : 'elevated'"
@click="() => (confirmDeleteDialog = { show: true, model })"
>
<v-icon size="large">mdi-trash-can-outline</v-icon>
@@ -351,7 +351,7 @@ const handleBulkImport = async () => {
small
color="buttonActive"
title="Rename Model"
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
:variant="theme.global.current.value.dark ? 'outlined' : 'elevated'"
@click="() => (showRenameDialog = { show: true, model, newName: '' })"
>
<v-icon size="large">mdi-pencil</v-icon>
@@ -362,7 +362,7 @@ const handleBulkImport = async () => {
icon
small
color="buttonPassive"
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
:variant="theme.global.current.value.dark ? 'outlined' : 'elevated'"
@click="() => (showInfo = { show: true, model })"
>
<v-icon size="large">mdi-information</v-icon>
@@ -391,13 +391,13 @@ const handleBulkImport = async () => {
</div>
<v-card-actions class="pt-5 pb-0 pr-0" style="justify-content: flex-end">
<v-btn
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
:variant="theme.global.current.value.dark ? 'outlined' : 'elevated'"
color="error"
@click="showRenameDialog.show = false"
>Cancel</v-btn
>
<v-btn
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
:variant="theme.global.current.value.dark ? 'outlined' : 'elevated'"
color="buttonActive"
@click="renameModel(showRenameDialog.model, showRenameDialog.newName)"
>Rename</v-btn
@@ -413,7 +413,7 @@ const handleBulkImport = async () => {
<v-btn
color="buttonPassive"
width="100%"
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
:variant="theme.global.current.value.dark ? 'outlined' : 'elevated'"
@click="openExportIndividualModelPrompt"
>
<v-icon left class="open-icon" size="large"> mdi-export </v-icon>
@@ -446,8 +446,8 @@ const handleBulkImport = async () => {
:on-backup="openExportPrompt"
:on-confirm="nukeModels"
title="Delete and Reset All Object Detection Models"
:description="'This will delete ALL object detection models and re-extract the default object detection models. This action cannot be undone.'"
:expected-confirmation-text="'Delete Models'"
description="This will delete ALL object detection models and re-extract the default object detection models. This action cannot be undone."
expected-confirmation-text="Delete Models"
delete-text="Delete all models"
/>
</v-card>

View File

@@ -40,18 +40,13 @@ export class AutoReconnectingWebsocket {
* Send data over the websocket. This is a no-op if the websocket is not in the OPEN state.
*
* @param data data to send
* @param encodeData whether or not to encode the data using msgpack (defaults to true)
* @see isConnected
*
*/
send(data, encodeData = true) {
send(data: unknown) {
// Only send data if the websocket is open
if (this.isConnected()) {
if (encodeData) {
this.websocket?.send(encode(data));
} else {
this.websocket?.send(data);
}
this.websocket?.send(encode(data));
}
}

View File

@@ -1,6 +1,6 @@
import { useStateStore } from "@/stores/StateStore";
import type { Resolution } from "@/types/SettingTypes";
import axios from "axios";
import axios, { type AxiosRequestConfig } from "axios";
export const resolutionsAreEqual = (a: Resolution, b: Resolution) => {
return a.height === b.height && a.width === b.width;
@@ -51,15 +51,16 @@ export const forceReloadPage = async () => {
export const getResolutionString = (resolution: Resolution): string => `${resolution.width}x${resolution.height}`;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export const parseJsonFile = async <T extends Record<string, any>>(file: File): Promise<T> => {
return new Promise((resolve, reject) => {
const fileReader = new FileReader();
fileReader.onload = (event) => {
const target: FileReader | null = event.target;
if (target === null) reject();
if (target === null) reject(new Error("FileReader event target is null"));
else resolve(JSON.parse(target.result as string) as T);
};
fileReader.onerror = (error) => reject(error);
fileReader.onerror = () => reject(new Error("Error reading file"));
fileReader.readAsText(file);
});
};
@@ -73,7 +74,13 @@ export const parseJsonFile = async <T extends Record<string, any>>(file: File):
* @param config Optional axios request configuration
* @returns A promise that resolves to true if the POST request is successful, or false if an error occurs.
*/
export const axiosPost = async (url: string, description: string, data?: any, config?: any): Promise<boolean> => {
export const axiosPost = async (
url: string,
description: string,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
data?: any,
config?: AxiosRequestConfig
): Promise<boolean> => {
try {
await axios.post(url, data, config);
useStateStore().showSnackbarMessage({
@@ -81,6 +88,7 @@ export const axiosPost = async (url: string, description: string, data?: any, co
color: "success"
});
return true;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} catch (error: any) {
if (error.response) {
useStateStore().showSnackbarMessage({

View File

@@ -2,7 +2,7 @@ import { type ThemeInstance } from "vuetify";
import { LightTheme, DarkTheme } from "@/plugins/vuetify";
export const resetTheme = (theme: ThemeInstance) => {
const themeType = theme.global.name.value === "LightTheme" ? "light" : "dark";
const themeType = theme.global.current.value.dark ? "dark" : "light";
localStorage.removeItem(`${themeType}-background`);
localStorage.removeItem(`${themeType}-primary`);
localStorage.removeItem(`${themeType}-secondary`);
@@ -12,13 +12,13 @@ export const resetTheme = (theme: ThemeInstance) => {
};
export const getThemeColor = (theme: ThemeInstance, color: string): string => {
const themeType = theme.global.name.value === "LightTheme" ? "light" : "dark";
const defaultTheme = theme.global.name.value === "LightTheme" ? LightTheme : DarkTheme;
const themeType = theme.global.current.value.dark ? "dark" : "light";
const defaultTheme = theme.global.current.value.dark ? DarkTheme : LightTheme;
return localStorage.getItem(`${themeType}-${color}`) ?? defaultTheme.colors![color]!;
};
export const setThemeColor = (theme: ThemeInstance, color: string, value: string | null) => {
const themeType = theme.global.name.value === "LightTheme" ? "light" : "dark";
const themeType = theme.global.current.value.dark ? "dark" : "light";
if (value) localStorage.setItem(`${themeType}-${color}`, value);
else localStorage.removeItem(`${themeType}-${color}`);
@@ -38,7 +38,7 @@ export const restoreThemeConfig = (theme: ThemeInstance) => {
if (storedTheme) theme.global.name.value = storedTheme;
// Restore custom theme colors
const themeType = theme.global.name.value === "LightTheme" ? "light" : "dark";
const themeType = theme.global.current.value.dark ? "dark" : "light";
const defaultTheme = theme.global.name.value === "LightTheme" ? LightTheme : DarkTheme;
const customBackground = localStorage.getItem(`${themeType}-background`);
@@ -47,7 +47,7 @@ export const restoreThemeConfig = (theme: ThemeInstance) => {
const customSurface = localStorage.getItem(`${themeType}-surface`);
theme.themes.value[theme.global.name.value].colors.background = customBackground ?? defaultTheme.colors!.background!;
theme.themes.value[theme.global.name.value].colors.sidebar = theme.themes.value[theme.global.name.value].dark
theme.themes.value[theme.global.name.value].colors.sidebar = theme.global.current.value.dark
? (customBackground ?? defaultTheme.colors!.sidebar!)
: (customSurface ?? defaultTheme.colors!.sidebar!);

View File

@@ -11,7 +11,7 @@ const runtimeMode: PhotonClientRuntimeMode = process.env.NODE_ENV as PhotonClien
let backendHost: string;
let backendHostname: string;
switch (runtimeMode as PhotonClientRuntimeMode) {
switch (runtimeMode) {
case "development":
backendHost = `${location.hostname}:5800`;
backendHostname = location.hostname;

View File

@@ -1,6 +1,6 @@
import "vuetify/styles";
import("@mdi/font/css/materialdesignicons.css");
import type { ThemeDefinition } from "vuetify/lib/composables/theme";
void import("@mdi/font/css/materialdesignicons.css");
import type { ThemeDefinition } from "vuetify";
import { createVuetify } from "vuetify";
const CommonColors = {

View File

@@ -31,7 +31,8 @@ interface StateStore {
currentCameraUniqueName: string;
networkUsageHistory: NetworkUsageEntry[];
backendResults: Record<number, PipelineResult>;
// Key is a string, although often used as an index, because we need to reference using the camera unique name at times.
backendResults: Record<string, PipelineResult>;
multitagResultBuffer: Record<string, MultitagResult[]>;
colorPickingMode: boolean;
@@ -39,8 +40,6 @@ interface StateStore {
calibrationData: {
imageCount: number;
videoFormatIndex: number;
minimumImageCount: number;
hasEnoughImages: boolean;
};
snackbarData: {
@@ -88,9 +87,7 @@ export const useStateStore = defineStore("state", {
calibrationData: {
imageCount: 0,
videoFormatIndex: 0,
minimumImageCount: 12,
hasEnoughImages: false
videoFormatIndex: 0
},
snackbarData: {
@@ -161,9 +158,7 @@ export const useStateStore = defineStore("state", {
updateCalibrationStateValuesFromWebsocket(data: WebsocketCalibrationData) {
this.calibrationData = {
imageCount: data.count,
videoFormatIndex: data.videoModeIndex,
minimumImageCount: data.minCount,
hasEnoughImages: data.hasEnough
videoFormatIndex: data.videoModeIndex
};
},
updateDiscoveredCameras(data: VsmState) {

View File

@@ -64,17 +64,14 @@ export const useCameraSettingsStore = defineStore("cameraSettings", {
pipelineNames(): string[] {
return this.currentCameraSettings.pipelineNicknames;
},
currentPipelineName(): string {
return this.pipelineNames[useStateStore().currentCameraUniqueName];
},
isDriverMode(): boolean {
return this.currentCameraSettings.currentPipelineIndex === WebsocketPipelineType.DriverMode;
return this.currentCameraSettings.currentPipelineIndex === WebsocketPipelineType.DriverMode.valueOf();
},
isCalibrationMode(): boolean {
return this.currentCameraSettings.currentPipelineIndex == WebsocketPipelineType.Calib3d;
return this.currentCameraSettings.currentPipelineIndex === WebsocketPipelineType.Calib3d.valueOf();
},
isFocusMode(): boolean {
return this.currentCameraSettings.currentPipelineIndex == WebsocketPipelineType.FocusCamera;
return this.currentCameraSettings.currentPipelineIndex === WebsocketPipelineType.FocusCamera.valueOf();
},
isCSICamera(): boolean {
return this.currentCameraSettings.isCSICamera;
@@ -94,6 +91,9 @@ export const useCameraSettingsStore = defineStore("cameraSettings", {
fpsLimit(): number {
return this.currentCameraSettings.fpsLimit;
},
isEnabled(): boolean {
return this.currentCameraSettings.isEnabled;
},
isConnected(): boolean {
return this.currentCameraSettings.isConnected;
},
@@ -116,23 +116,20 @@ export const useCameraSettingsStore = defineStore("cameraSettings", {
inputPort: d.inputStreamPort,
outputPort: d.outputStreamPort
},
validVideoFormats: Object.entries(d.videoFormatList)
.sort(([firstKey], [secondKey]) => parseInt(firstKey) - parseInt(secondKey))
// eslint-disable-next-line @typescript-eslint/no-unused-vars
.map<VideoFormat>(([k, v], i) => ({
resolution: {
width: v.width,
height: v.height
},
fps: v.fps,
pixelFormat: v.pixelFormat,
index: v.index || i,
diagonalFOV: v.diagonalFOV,
horizontalFOV: v.horizontalFOV,
verticalFOV: v.verticalFOV,
standardDeviation: v.standardDeviation,
mean: v.mean
})),
validVideoFormats: d.videoFormatList.map((v, i) => ({
resolution: {
width: v.width,
height: v.height
},
fps: v.fps,
pixelFormat: v.pixelFormat,
index: v.index || i,
diagonalFOV: v.diagonalFOV,
horizontalFOV: v.horizontalFOV,
verticalFOV: v.verticalFOV,
standardDeviation: v.standardDeviation,
mean: v.mean
})),
completeCalibrations: d.calibrations,
isCSICamera: d.isCSICamera,
minExposureRaw: d.minExposureRaw,
@@ -145,6 +142,7 @@ export const useCameraSettingsStore = defineStore("cameraSettings", {
maxWhiteBalanceTemp: d.maxWhiteBalanceTemp,
matchedCameraInfo: d.matchedCameraInfo,
fpsLimit: d.fpsLimit,
isEnabled: d.isEnabled,
isConnected: d.isConnected,
hasConnected: d.hasConnected,
mismatch: d.mismatch
@@ -196,7 +194,7 @@ export const useCameraSettingsStore = defineStore("cameraSettings", {
addNewPipeline: [newPipelineName, pipelineType],
cameraUniqueName: cameraUniqueName
};
useStateStore().websocket?.send(payload, true);
useStateStore().websocket?.send(payload);
},
/**
* Modify the settings of the currently selected pipeline of the provided camera.
@@ -220,15 +218,13 @@ export const useCameraSettingsStore = defineStore("cameraSettings", {
if (updateStore) {
this.changePipelineSettingsInStore(settings, cameraUniqueName);
}
useStateStore().websocket?.send(payload, true);
useStateStore().websocket?.send(payload);
},
changePipelineSettingsInStore(
settings: Partial<ActivePipelineSettings>,
cameraUniqueName: string = useStateStore().currentCameraUniqueName
) {
Object.entries(settings).forEach(([k, v]) => {
this.cameras[cameraUniqueName].pipelineSettings[k] = v;
});
Object.assign(this.cameras[cameraUniqueName].pipelineSettings, settings);
},
/**
* Change the nickname of the currently selected pipeline of the provided camera.
@@ -249,7 +245,7 @@ export const useCameraSettingsStore = defineStore("cameraSettings", {
if (updateStore) {
this.cameras[cameraUniqueName].pipelineSettings.pipelineNickname = newName;
}
useStateStore().websocket?.send(payload, true);
useStateStore().websocket?.send(payload);
},
/**
* Modify the Pipeline type of the currently selected pipeline of the provided camera. This overwrites the current pipeline's settings when the backend resets the current pipeline settings.
@@ -265,7 +261,7 @@ export const useCameraSettingsStore = defineStore("cameraSettings", {
pipelineType: type,
cameraUniqueName: cameraUniqueName
};
useStateStore().websocket?.send(payload, true);
useStateStore().websocket?.send(payload);
},
/**
* Change the index of the pipeline of the currently selected camera.
@@ -285,21 +281,22 @@ export const useCameraSettingsStore = defineStore("cameraSettings", {
};
if (updateStore) {
if (
this.cameras[cameraUniqueName].currentPipelineIndex !== -1 &&
this.cameras[cameraUniqueName].currentPipelineIndex !== -2
this.cameras[cameraUniqueName].currentPipelineIndex !== WebsocketPipelineType.DriverMode.valueOf() &&
this.cameras[cameraUniqueName].currentPipelineIndex !== WebsocketPipelineType.Calib3d.valueOf() &&
this.cameras[cameraUniqueName].currentPipelineIndex !== WebsocketPipelineType.FocusCamera.valueOf()
) {
this.cameras[cameraUniqueName].lastPipelineIndex = this.cameras[cameraUniqueName].currentPipelineIndex;
}
this.cameras[cameraUniqueName].currentPipelineIndex = index;
}
useStateStore().websocket?.send(payload, true);
useStateStore().websocket?.send(payload);
},
setDriverMode(isDriverMode: boolean, cameraUniqueName: string = useStateStore().currentCameraUniqueName) {
const payload = {
driverMode: isDriverMode,
cameraUniqueName: cameraUniqueName
};
useStateStore().websocket?.send(payload, true);
useStateStore().websocket?.send(payload);
},
/**
* Change the currently selected pipeline of the provided camera.
@@ -311,7 +308,7 @@ export const useCameraSettingsStore = defineStore("cameraSettings", {
deleteCurrentPipeline: {},
cameraUniqueName: cameraUniqueName
};
useStateStore().websocket?.send(payload, true);
useStateStore().websocket?.send(payload);
},
/**
* Duplicate the pipeline at the provided index.
@@ -324,7 +321,7 @@ export const useCameraSettingsStore = defineStore("cameraSettings", {
duplicatePipeline: pipelineIndex,
cameraUniqueName: cameraUniqueName
};
useStateStore().websocket?.send(payload, true);
useStateStore().websocket?.send(payload);
},
/**
* Change the currently set camera
@@ -339,7 +336,7 @@ export const useCameraSettingsStore = defineStore("cameraSettings", {
if (updateStore) {
useStateStore().currentCameraUniqueName = cameraUniqueName;
}
useStateStore().websocket?.send(payload, true);
useStateStore().websocket?.send(payload);
},
/**
* Change the nickname of the provided camera.
@@ -385,14 +382,12 @@ export const useCameraSettingsStore = defineStore("cameraSettings", {
const payload = {
startPnpCalibration: {
count: stateCalibData.imageCount,
minCount: stateCalibData.minimumImageCount,
hasEnough: stateCalibData.hasEnoughImages,
videoModeIndex: stateCalibData.videoFormatIndex,
...calibrationInitData
},
cameraUniqueName: cameraUniqueName
};
useStateStore().websocket?.send(payload, true);
useStateStore().websocket?.send(payload);
},
/**
* End the 3D calibration process for the provided camera.
@@ -424,7 +419,7 @@ export const useCameraSettingsStore = defineStore("cameraSettings", {
takeCalibrationSnapshot: true,
cameraUniqueName: cameraUniqueName
};
useStateStore().websocket?.send(payload, true);
useStateStore().websocket?.send(payload);
},
/**
* Save a snapshot of the input frame of the camera.
@@ -436,7 +431,7 @@ export const useCameraSettingsStore = defineStore("cameraSettings", {
saveInputSnapshot: true,
cameraUniqueName: cameraUniqueName
};
useStateStore().websocket?.send(payload, true);
useStateStore().websocket?.send(payload);
},
/**
* Save a snapshot of the output frame of the camera.
@@ -448,7 +443,7 @@ export const useCameraSettingsStore = defineStore("cameraSettings", {
saveOutputSnapshot: true,
cameraUniqueName: cameraUniqueName
};
useStateStore().websocket?.send(payload, true);
useStateStore().websocket?.send(payload);
},
/**
* Set the robot offset mode type.
@@ -461,7 +456,7 @@ export const useCameraSettingsStore = defineStore("cameraSettings", {
robotOffsetPoint: type,
cameraUniqueName: cameraUniqueName
};
useStateStore().websocket?.send(payload, true);
useStateStore().websocket?.send(payload);
},
getCalibrationCoeffs(
resolution: Resolution,

View File

@@ -10,6 +10,7 @@ import { NetworkConnectionType } from "@/types/SettingTypes";
import { useStateStore } from "@/stores/StateStore";
import axios from "axios";
import type { WebsocketSettingsUpdate } from "@/types/WebsocketDataTypes";
import type { AprilTagFieldLayout } from "@/types/PhotonTrackingTypes";
import { ref } from "vue";
interface GeneralSettingsStore {
@@ -17,7 +18,7 @@ interface GeneralSettingsStore {
network: NetworkSettings;
lighting: LightingSettings;
metrics: MetricData;
currentFieldLayout;
currentFieldLayout: AprilTagFieldLayout;
}
interface MetricsEntry {
@@ -184,7 +185,7 @@ export const useSettingsStore = defineStore("settings", {
const payload = {
enabledLEDPercentage: brightness
};
useStateStore().websocket?.send(payload, true);
useStateStore().websocket?.send(payload);
}
}
});

View File

@@ -5,6 +5,7 @@ import type { ObjectDetectionModelProperties } from "@/types/SettingTypes";
* The on-wire form of PipelineType.java (the enum is serialized with `ordinal()`)
*/
export enum PipelineType {
Calibration3d = 1,
DriverMode = 2,
Reflective = 3,
ColoredShape = 4,
@@ -35,18 +36,62 @@ export enum TargetModel {
ReefscapeAlgae = 7
}
export enum ContourSortMode {
Largest = 0,
Smallest = 1,
Highest = 2,
Lowest = 3,
Leftmost = 4,
Rightmost = 5,
Centermost = 6
}
export enum ContourTargetOrientation {
Portrait = 0,
Landscape = 1
}
export enum ContourGroupingMode {
Single = 0,
Dual = 1,
TwoOrMore = 2
}
export enum ContourIntersection {
None = 0,
Up = 1,
Down = 2,
Left = 3,
Right = 4
}
export enum ContourShape {
Circle = 0,
Polygon = 1,
Triangle = 2,
Quadrilateral = 3
}
export enum ContourTargetOffsetPointEdge {
Center = 0,
Top = 1,
Bottom = 2,
Left = 3,
Right = 4
}
export interface PipelineSettings {
offsetRobotOffsetMode: RobotOffsetPointMode;
streamingFrameDivisor: number;
offsetDualPointBArea: number;
contourGroupingMode: number;
contourGroupingMode: ContourGroupingMode;
hsvValue: WebsocketNumberPair | [number, number];
cameraGain: number;
cameraBlueGain: number;
cameraRedGain: number;
cornerDetectionSideCount: number;
contourRatio: WebsocketNumberPair | [number, number];
contourTargetOffsetPointEdge: number;
contourTargetOffsetPointEdge: ContourTargetOffsetPointEdge;
pipelineNickname: string;
inputImageRotationMode: number;
contourArea: WebsocketNumberPair | [number, number];
@@ -56,7 +101,7 @@ export interface PipelineSettings {
inputShouldShow: boolean;
cameraAutoExposure: boolean;
contourSpecklePercentage: number;
contourTargetOrientation: number;
contourTargetOrientation: ContourTargetOrientation;
targetModel: TargetModel;
cornerDetectionUseConvexHulls: boolean;
outputShouldShow: boolean;
@@ -67,7 +112,7 @@ export interface PipelineSettings {
ledMode: boolean;
hueInverted: boolean;
outputMaximumTargets: number;
contourSortMode: number;
contourSortMode: ContourSortMode;
cameraExposureRaw: number;
cameraMinExposureRaw: number;
cameraMaxExposureRaw: number;
@@ -80,11 +125,13 @@ export interface PipelineSettings {
cornerDetectionAccuracyPercentage: number;
hsvSaturation: WebsocketNumberPair | [number, number];
pipelineType: PipelineType;
contourIntersection: number;
contourIntersection: ContourIntersection;
cameraAutoWhiteBalance: boolean;
cameraWhiteBalanceTemp: number;
crosshair: boolean;
blockForFrames: boolean;
}
export type ConfigurablePipelineSettings = Partial<
@@ -113,13 +160,13 @@ export const DefaultPipelineSettings: Omit<
offsetRobotOffsetMode: RobotOffsetPointMode.None,
streamingFrameDivisor: 0,
offsetDualPointBArea: 0,
contourGroupingMode: 0,
contourGroupingMode: ContourGroupingMode.Single,
hsvValue: { first: 50, second: 255 },
cameraBlueGain: 20,
cameraRedGain: 11,
cornerDetectionSideCount: 4,
contourRatio: { first: 0, second: 20 },
contourTargetOffsetPointEdge: 0,
contourTargetOffsetPointEdge: ContourTargetOffsetPointEdge.Center,
pipelineNickname: "Placeholder Pipeline",
inputImageRotationMode: 0,
contourArea: { first: 0, second: 100 },
@@ -129,7 +176,7 @@ export const DefaultPipelineSettings: Omit<
inputShouldShow: false,
cameraAutoExposure: false,
contourSpecklePercentage: 5,
contourTargetOrientation: 1,
contourTargetOrientation: ContourTargetOrientation.Landscape,
cornerDetectionUseConvexHulls: true,
outputShouldShow: true,
outputShouldDraw: true,
@@ -138,7 +185,7 @@ export const DefaultPipelineSettings: Omit<
hsvHue: { first: 50, second: 180 },
hueInverted: false,
outputMaximumTargets: 20,
contourSortMode: 0,
contourSortMode: ContourSortMode.Largest,
offsetSinglePoint: { x: 0, y: 0 },
cameraBrightness: 50,
offsetDualPointAArea: 0,
@@ -147,11 +194,12 @@ export const DefaultPipelineSettings: Omit<
cornerDetectionStrategy: 0,
cornerDetectionAccuracyPercentage: 10,
hsvSaturation: { first: 50, second: 255 },
contourIntersection: 1,
contourIntersection: ContourIntersection.Up,
cameraAutoWhiteBalance: false,
cameraWhiteBalanceTemp: 4000,
cameraMinExposureRaw: 1,
cameraMaxExposureRaw: 2,
crosshair: true,
blockForFrames: true
};
@@ -184,7 +232,7 @@ export interface ColoredShapePipelineSettings extends PipelineSettings {
contourRadius: WebsocketNumberPair | [number, number];
circleDetectThreshold: number;
accuracyPercentage: number;
contourShape: number;
contourShape: ContourShape;
contourPerimeter: WebsocketNumberPair | [number, number];
minDist: number;
maxCannyThresh: number;
@@ -209,7 +257,7 @@ export const DefaultColoredShapePipelineSettings: ColoredShapePipelineSettings =
contourRadius: { first: 0, second: 100 },
circleDetectThreshold: 5,
accuracyPercentage: 10,
contourShape: 2,
contourShape: ContourShape.Triangle,
contourPerimeter: { first: 0, second: 1.7976931348623157e308 },
minDist: 20,
maxCannyThresh: 90
@@ -324,13 +372,14 @@ export const DefaultObjectDetectionPipelineSettings: ObjectDetectionPipelineSett
};
export interface Calibration3dPipelineSettings extends PipelineSettings {
pipelineType: PipelineType.Calibration3d;
drawAllSnapshots: boolean;
}
export type ConfigurableCalibration3dPipelineSettings = Partial<Omit<Calibration3dPipelineSettings, "pipelineType">> &
ConfigurablePipelineSettings;
export const DefaultCalibration3dPipelineSettings: Calibration3dPipelineSettings = {
...DefaultPipelineSettings,
pipelineType: PipelineType.ObjectDetection,
pipelineType: PipelineType.Calibration3d,
cameraGain: 20,
targetModel: TargetModel.InfiniteRechargeHighGoalOuter,
ledMode: true,

View File

@@ -75,46 +75,29 @@ export type ConfigurableNetworkSettings = Omit<
"canManage" | "networkInterfaceNames" | "networkingDisabled"
>;
export interface PVCameraInfoBase {
/*
Huge hack. In Jackson, this is set based on the underlying type -- this
then maps to one of the 3 subclasses here below. Not sure how to best deal with this.
*/
cameraTypename: "PVUsbCameraInfo" | "PVCSICameraInfo" | "PVFileCameraInfo";
interface PVCameraInfoBase {
type: "PVUsbCameraInfo" | "PVCSICameraInfo" | "PVFileCameraInfo";
path: string;
name: string;
uniquePath: string;
}
export interface PVUsbCameraInfo {
export interface PVUsbCameraInfo extends PVCameraInfoBase {
type: "PVUsbCameraInfo";
dev: number;
name: string;
otherPaths: string[];
path: string;
vendorId: number;
productId: number;
// In Java, PVCameraInfo provides a uniquePath property so we can have one Source of Truth here
uniquePath: string;
}
export interface PVCSICameraInfo {
export interface PVCSICameraInfo extends PVCameraInfoBase {
type: "PVCSICameraInfo";
baseName: string;
path: string;
// In Java, PVCameraInfo provides a uniquePath property so we can have one Source of Truth here
uniquePath: string;
}
export interface PVFileCameraInfo {
path: string;
name: string;
// In Java, PVCameraInfo provides a uniquePath property so we can have one Source of Truth here
uniquePath: string;
export interface PVFileCameraInfo extends PVCameraInfoBase {
type: "PVFileCameraInfo";
}
// This camera info will only ever hold one of its members - the others should be undefined.
export class PVCameraInfo {
PVUsbCameraInfo: PVUsbCameraInfo | undefined;
PVCSICameraInfo: PVCSICameraInfo | undefined;
PVFileCameraInfo: PVFileCameraInfo | undefined;
}
export type PVCameraInfo = PVUsbCameraInfo | PVCSICameraInfo | PVFileCameraInfo;
export interface VsmState {
disabledConfigs: WebsocketCameraSettingsUpdate[];
@@ -276,6 +259,7 @@ export interface UiCameraConfiguration {
maxWhiteBalanceTemp: number;
fpsLimit: number;
isEnabled: boolean;
matchedCameraInfo: PVCameraInfo;
isConnected: boolean;
@@ -438,15 +422,13 @@ export const PlaceholderCameraSettings: UiCameraConfiguration = reactive({
minWhiteBalanceTemp: 2000,
maxWhiteBalanceTemp: 10000,
matchedCameraInfo: {
PVFileCameraInfo: {
name: "Foobar",
path: "/dev/foobar",
uniquePath: "/dev/foobar2"
},
PVCSICameraInfo: undefined,
PVUsbCameraInfo: undefined
type: "PVFileCameraInfo",
name: "Foobar",
path: "/dev/foobar",
uniquePath: "/dev/foobar2"
},
fpsLimit: -1,
isEnabled: true,
isConnected: true,
hasConnected: true,
mismatch: false

View File

@@ -30,21 +30,18 @@ export interface WebsocketNumberPair {
second: number;
}
export type WebsocketVideoFormat = Record<
number,
{
fps: number;
height: number;
width: number;
pixelFormat: string;
index?: number;
diagonalFOV?: number;
horizontalFOV?: number;
verticalFOV?: number;
standardDeviation?: number;
mean?: number;
}
>;
export type WebsocketVideoFormat = {
fps: number;
height: number;
width: number;
pixelFormat: string;
index?: number;
diagonalFOV?: number;
horizontalFOV?: number;
verticalFOV?: number;
standardDeviation?: number;
mean?: number;
}[];
// Companion to UICameraConfiguration in Java
export interface WebsocketCameraSettingsUpdate {
@@ -68,6 +65,7 @@ export interface WebsocketCameraSettingsUpdate {
maxWhiteBalanceTemp: number;
matchedCameraInfo: PVCameraInfo;
fpsLimit: number;
isEnabled: boolean;
isConnected: boolean;
hasConnected: boolean;
mismatch: boolean;

View File

@@ -2,13 +2,7 @@
import { useCameraSettingsStore } from "@/stores/settings/CameraSettingsStore";
import { computed, inject, ref } from "vue";
import { useStateStore } from "@/stores/StateStore";
import {
PlaceholderCameraSettings,
PVCameraInfo,
type PVCSICameraInfo,
type PVFileCameraInfo,
type PVUsbCameraInfo
} from "@/types/SettingTypes";
import { PlaceholderCameraSettings, type PVCameraInfo } from "@/types/SettingTypes";
import { axiosPost, getResolutionString } from "@/lib/PhotonUtils";
import PhotonCameraStream from "@/components/app/photon-camera-stream.vue";
import PvDeleteModal from "@/components/common/pv-delete-modal.vue";
@@ -18,20 +12,22 @@ import { useTheme } from "vuetify";
const theme = useTheme();
const formatUrl = (port) => `http://${inject("backendHostname")}:${port}/stream.mjpg`;
const backendHostname = inject<string>("backendHostname");
const formatUrl = (port: number) => `http://${backendHostname}:${port}/stream.mjpg`;
const activatingModule = ref(false);
const activateModule = (moduleUniqueName: string) => {
const activateModule = async (moduleUniqueName: string) => {
if (activatingModule.value) return;
activatingModule.value = true;
axiosPost("/utils/activateMatchedCamera", "activate a matched camera", {
await axiosPost("/utils/activateMatchedCamera", "activate a matched camera", {
cameraUniqueName: moduleUniqueName
}).finally(() => (activatingModule.value = false));
});
activatingModule.value = false;
};
const assigningCamera = ref(false);
const assignCamera = (cameraInfo: PVCameraInfo) => {
const assignCamera = async (cameraInfo: PVCameraInfo) => {
if (assigningCamera.value) return;
assigningCamera.value = true;
@@ -39,48 +35,39 @@ const assignCamera = (cameraInfo: PVCameraInfo) => {
cameraInfo: cameraInfo
};
axiosPost("/utils/assignUnmatchedCamera", "assign an unmatched camera", payload).finally(
() => (assigningCamera.value = false)
);
await axiosPost("/utils/assignUnmatchedCamera", "assign an unmatched camera", payload);
assigningCamera.value = false;
};
const deactivatingModule = ref(false);
const deactivateModule = (cameraUniqueName: string) => {
const deactivateModule = async (cameraUniqueName: string) => {
if (deactivatingModule.value) return;
deactivatingModule.value = true;
axiosPost("/utils/unassignCamera", "unassign a camera", { cameraUniqueName: cameraUniqueName }).finally(
() => (deactivatingModule.value = false)
);
await axiosPost("/utils/unassignCamera", "unassign a camera", { cameraUniqueName: cameraUniqueName });
deactivatingModule.value = false;
};
const confirmDeleteDialog = ref({ show: false, nickname: "", cameraUniqueName: "" });
const deletingCamera = ref<string | null>(null);
const deleteThisCamera = (cameraUniqueName: string) => {
const deleteThisCamera = async (cameraUniqueName: string) => {
if (deletingCamera.value) return;
deletingCamera.value = cameraUniqueName;
axiosPost("/utils/nukeOneCamera", "delete a camera", { cameraUniqueName: cameraUniqueName }).finally(() => {
deletingCamera.value = null;
});
await axiosPost("/utils/nukeOneCamera", "delete a camera", { cameraUniqueName: cameraUniqueName });
deletingCamera.value = null;
};
const cameraConnected = (uniquePath: string): boolean => {
return (
useStateStore().vsmState.allConnectedCameras.find((it) => cameraInfoFor(it).uniquePath === uniquePath) !== undefined
);
const cameraConnected = (uniquePath: string | undefined): boolean => {
if (!uniquePath) return false;
return useStateStore().vsmState.allConnectedCameras.find((it) => it.uniquePath === uniquePath) !== undefined;
};
const unmatchedCameras = computed(() => {
const activeVmPaths = Object.values(useCameraSettingsStore().cameras).map(
(it) => cameraInfoFor(it.matchedCameraInfo).uniquePath
);
const disabledVmPaths = useStateStore().vsmState.disabledConfigs.map(
(it) => cameraInfoFor(it.matchedCameraInfo).uniquePath
);
const activeVmPaths = Object.values(useCameraSettingsStore().cameras).map((it) => it.matchedCameraInfo.uniquePath);
const disabledVmPaths = useStateStore().vsmState.disabledConfigs.map((it) => it.matchedCameraInfo.uniquePath);
return useStateStore().vsmState.allConnectedCameras.filter(
(it) =>
!activeVmPaths.includes(cameraInfoFor(it).uniquePath) && !disabledVmPaths.includes(cameraInfoFor(it).uniquePath)
(it) => !activeVmPaths.includes(it.uniquePath) && !disabledVmPaths.includes(it.uniquePath)
);
});
@@ -91,8 +78,8 @@ const activeVisionModules = computed(() =>
// Display connected cameras first
.sort(
(first, second) =>
(cameraConnected(cameraInfoFor(second.matchedCameraInfo).uniquePath) ? 1 : 0) -
(cameraConnected(cameraInfoFor(first.matchedCameraInfo).uniquePath) ? 1 : 0)
(cameraConnected(second.matchedCameraInfo.uniquePath) ? 1 : 0) -
(cameraConnected(first.matchedCameraInfo.uniquePath) ? 1 : 0)
)
);
@@ -105,41 +92,24 @@ const setCameraView = (camera: PVCameraInfo | null, isConnected: boolean | null)
viewingCamera.value = [camera, isConnected];
};
/**
* Get the connection-type-specific camera info from the given PVCameraInfo object.
*/
const cameraInfoFor = (camera: PVCameraInfo | null): PVUsbCameraInfo | PVCSICameraInfo | PVFileCameraInfo | any => {
if (!camera) return null;
if (camera.PVUsbCameraInfo) {
return camera.PVUsbCameraInfo;
}
if (camera.PVCSICameraInfo) {
return camera.PVCSICameraInfo;
}
if (camera.PVFileCameraInfo) {
return camera.PVFileCameraInfo;
}
return {};
};
/**
* Find the PVCameraInfo currently occupying the same uniquePath as the the given module
*/
const getMatchedDevice = (info: PVCameraInfo | undefined): PVCameraInfo => {
if (!info) {
return {
PVFileCameraInfo: undefined,
PVCSICameraInfo: undefined,
PVUsbCameraInfo: undefined
type: "PVFileCameraInfo",
path: "",
name: "",
uniquePath: ""
};
}
return (
useStateStore().vsmState.allConnectedCameras.find(
(it) => cameraInfoFor(it).uniquePath === cameraInfoFor(info).uniquePath
) || {
PVFileCameraInfo: undefined,
PVCSICameraInfo: undefined,
PVUsbCameraInfo: undefined
useStateStore().vsmState.allConnectedCameras.find((it) => it.uniquePath === info.uniquePath) || {
type: "PVFileCameraInfo",
path: "",
name: "",
uniquePath: ""
}
);
};
@@ -158,12 +128,11 @@ const getMatchedDevice = (info: PVCameraInfo | undefined): PVCameraInfo => {
class="pr-0"
>
<v-card color="surface" class="rounded-12">
<v-card-title>{{ cameraInfoFor(module.matchedCameraInfo).name }}</v-card-title>
<v-card-subtitle v-if="!cameraConnected(cameraInfoFor(module.matchedCameraInfo).uniquePath)"
<v-card-title>{{ module.matchedCameraInfo.name }}</v-card-title>
<v-card-subtitle v-if="!cameraConnected(module.matchedCameraInfo.uniquePath)"
>Status: <span class="inactive-status">Disconnected</span></v-card-subtitle
>
<v-card-subtitle
v-else-if="cameraConnected(cameraInfoFor(module.matchedCameraInfo).uniquePath) && !module.mismatch"
<v-card-subtitle v-else-if="cameraConnected(module.matchedCameraInfo.uniquePath) && !module.mismatch"
>Status: <span class="active-status">Active</span></v-card-subtitle
>
<v-card-subtitle v-else>Status: <span class="mismatch-status">Mismatch</span></v-card-subtitle>
@@ -172,7 +141,7 @@ const getMatchedDevice = (info: PVCameraInfo | undefined): PVCameraInfo => {
<tbody>
<tr
v-if="
cameraConnected(cameraInfoFor(module.matchedCameraInfo).uniquePath) &&
cameraConnected(module.matchedCameraInfo.uniquePath) &&
useStateStore().backendResults[module.uniqueName]
"
>
@@ -214,7 +183,7 @@ const getMatchedDevice = (info: PVCameraInfo | undefined): PVCameraInfo => {
</tbody>
</v-table>
<div
v-if="cameraConnected(cameraInfoFor(module.matchedCameraInfo).uniquePath)"
v-if="cameraConnected(module.matchedCameraInfo.uniquePath)"
:id="`stream-container-${index}`"
class="d-flex flex-column justify-center align-center mt-3"
style="height: 250px"
@@ -232,13 +201,8 @@ const getMatchedDevice = (info: PVCameraInfo | undefined): PVCameraInfo => {
<v-btn
color="buttonPassive"
style="width: 100%"
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
@click="
setCameraView(
module.matchedCameraInfo,
cameraConnected(cameraInfoFor(module.matchedCameraInfo).uniquePath)
)
"
:variant="theme.global.current.value.dark ? 'outlined' : 'elevated'"
@click="setCameraView(module.matchedCameraInfo, cameraConnected(module.matchedCameraInfo.uniquePath))"
>
<span>Details</span>
</v-btn>
@@ -248,7 +212,7 @@ const getMatchedDevice = (info: PVCameraInfo | undefined): PVCameraInfo => {
class="text-black"
color="buttonActive"
style="width: 100%"
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
:variant="theme.global.current.value.dark ? 'outlined' : 'elevated'"
:loading="deactivatingModule"
@click="deactivateModule(module.uniqueName)"
>
@@ -261,7 +225,7 @@ const getMatchedDevice = (info: PVCameraInfo | undefined): PVCameraInfo => {
color="error"
style="width: 100%"
:loading="module.uniqueName === deletingCamera"
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
:variant="theme.global.current.value.dark ? 'outlined' : 'elevated'"
@click="
() =>
(confirmDeleteDialog = {
@@ -315,7 +279,7 @@ const getMatchedDevice = (info: PVCameraInfo | undefined): PVCameraInfo => {
</tr>
<tr>
<td>Connected</td>
<td>{{ cameraConnected(cameraInfoFor(module.matchedCameraInfo).uniquePath) }}</td>
<td>{{ cameraConnected(module.matchedCameraInfo.uniquePath) }}</td>
</tr>
</tbody>
</v-table>
@@ -326,13 +290,8 @@ const getMatchedDevice = (info: PVCameraInfo | undefined): PVCameraInfo => {
<v-btn
color="buttonPassive"
style="width: 100%"
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
@click="
setCameraView(
module.matchedCameraInfo,
cameraConnected(cameraInfoFor(module.matchedCameraInfo).uniquePath)
)
"
:variant="theme.global.current.value.dark ? 'outlined' : 'elevated'"
@click="setCameraView(module.matchedCameraInfo, cameraConnected(module.matchedCameraInfo.uniquePath))"
>
<span>Details</span>
</v-btn>
@@ -342,7 +301,7 @@ const getMatchedDevice = (info: PVCameraInfo | undefined): PVCameraInfo => {
class="text-black"
color="buttonActive"
style="width: 100%"
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
:variant="theme.global.current.value.dark ? 'outlined' : 'elevated'"
:loading="activatingModule"
@click="activateModule(module.uniqueName)"
>
@@ -355,7 +314,7 @@ const getMatchedDevice = (info: PVCameraInfo | undefined): PVCameraInfo => {
color="error"
style="width: 100%"
:loading="module.uniqueName === deletingCamera"
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
:variant="theme.global.current.value.dark ? 'outlined' : 'elevated'"
@click="
() =>
(confirmDeleteDialog = {
@@ -377,15 +336,15 @@ const getMatchedDevice = (info: PVCameraInfo | undefined): PVCameraInfo => {
<v-col v-for="(camera, index) in unmatchedCameras" :key="index" cols="12" sm="6" lg="4" class="pr-0">
<v-card class="pr-0 rounded-12" color="surface">
<v-card-title>
<span v-if="camera.PVUsbCameraInfo">USB Camera:</span>
<span v-else-if="camera.PVCSICameraInfo">CSI Camera:</span>
<span v-else-if="camera.PVFileCameraInfo">File Camera:</span>
<span v-if="camera.type === 'PVUsbCameraInfo'">USB Camera:</span>
<span v-else-if="camera.type === 'PVCSICameraInfo'">CSI Camera:</span>
<span v-else-if="camera.type === 'PVFileCameraInfo'">File Camera:</span>
<span v-else>Unknown Camera:</span>
&nbsp;<span>{{ cameraInfoFor(camera)?.name ?? cameraInfoFor(camera)?.baseName }}</span>
&nbsp;<span>{{ camera.name }}</span>
</v-card-title>
<v-card-subtitle>Status: Unassigned</v-card-subtitle>
<v-card-text class="pt-3">
<span style="word-break: break-all">{{ cameraInfoFor(camera)?.path }}</span>
<span style="word-break: break-all">{{ camera?.path }}</span>
</v-card-text>
<v-card-text class="pt-0">
<v-row>
@@ -393,7 +352,7 @@ const getMatchedDevice = (info: PVCameraInfo | undefined): PVCameraInfo => {
<v-btn
color="buttonPassive"
style="width: 100%"
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
:variant="theme.global.current.value.dark ? 'outlined' : 'elevated'"
@click="setCameraView(camera, false)"
>
<span>Details</span>
@@ -405,7 +364,7 @@ const getMatchedDevice = (info: PVCameraInfo | undefined): PVCameraInfo => {
color="buttonActive"
style="width: 100%"
:loading="assigningCamera"
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
:variant="theme.global.current.value.dark ? 'outlined' : 'elevated'"
@click="assignCamera(camera)"
>
Activate
@@ -436,7 +395,7 @@ const getMatchedDevice = (info: PVCameraInfo | undefined): PVCameraInfo => {
<v-dialog v-model="viewingDetails" max-width="800">
<v-card v-if="viewingCamera[0] !== null" flat color="surface">
<v-card-title class="d-flex justify-space-between">
<span>{{ cameraInfoFor(viewingCamera[0])?.name ?? cameraInfoFor(viewingCamera[0])?.baseName }}</span>
<span>{{ viewingCamera[0].name }}</span>
<v-btn variant="text" @click="setCameraView(null, null)">
<v-icon size="x-large">mdi-close</v-icon>
</v-btn>
@@ -446,9 +405,7 @@ const getMatchedDevice = (info: PVCameraInfo | undefined): PVCameraInfo => {
</v-card-text>
<v-card-text
v-else-if="
activeVisionModules.find(
(it) => cameraInfoFor(it.matchedCameraInfo).uniquePath === cameraInfoFor(viewingCamera[0]).uniquePath
)?.mismatch
activeVisionModules.find((it) => it.matchedCameraInfo.uniquePath === viewingCamera[0]?.uniquePath)?.mismatch
"
>
<v-alert
@@ -457,7 +414,7 @@ const getMatchedDevice = (info: PVCameraInfo | undefined): PVCameraInfo => {
density="compact"
text="A different camera may have been connected to this device! Compare the following information carefully."
icon="mdi-information-outline"
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'tonal'"
:variant="theme.global.current.value.dark ? 'tonal' : 'elevated'"
/>
<PvCameraMatchCard :saved="viewingCamera[0]" :current="getMatchedDevice(viewingCamera[0])" />
</v-card-text>

View File

@@ -77,8 +77,18 @@ const conflictingCameraShown = computed<boolean>(() => {
return useSettingsStore().general.conflictingCameras.length > 0;
});
const fpsLimitWarningShown = computed<boolean>(() => {
return Object.values(useCameraSettingsStore().cameras).some((c) => c.fpsLimit > 0);
const fpsLimitedCameras = computed<string>(() => {
return Object.values(useCameraSettingsStore().cameras)
.filter((c) => c.fpsLimit > 0)
.map((c) => c.nickname)
.join(", ");
});
const disabledCameras = computed<string>(() => {
return Object.values(useCameraSettingsStore().cameras)
.filter((c) => !c.isEnabled)
.map((c) => c.nickname)
.join(", ");
});
const showCameraSetupDialog = ref(useCameraSettingsStore().needsCameraConfiguration);
@@ -92,7 +102,7 @@ const showCameraSetupDialog = ref(useCameraSettingsStore().needsCameraConfigurat
color="error"
density="compact"
icon="mdi-alert-circle-outline"
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'tonal'"
:variant="theme.global.current.value.dark ? 'tonal' : 'elevated'"
>
<span>
Arducam camera detected! Please configure the camera model in the <a href="#/cameras">Camera tab</a>!
@@ -104,32 +114,44 @@ const showCameraSetupDialog = ref(useCameraSettingsStore().needsCameraConfigurat
color="error"
density="compact"
icon="mdi-alert-circle-outline"
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'tonal'"
:variant="theme.global.current.value.dark ? 'tonal' : 'elevated'"
>
<span>
Conflicting hostname detected! Please change the hostname in the <a href="#/settings">Settings tab</a>!
</span>
</v-alert>
<v-alert
v-if="fpsLimitWarningShown"
v-if="fpsLimitedCameras"
class="mb-3"
color="error"
density="compact"
icon="mdi-alert-circle-outline"
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'tonal'"
:variant="theme.global.current.value.dark ? 'tonal' : 'elevated'"
>
<span
>One or more cameras have an FPS limit set! This may cause performance issues. Check your logs for more
>{{ fpsLimitedCameras }} have an FPS limit set! This may cause performance issues. Check your logs for more
information.
</span>
</v-alert>
<v-alert
v-if="disabledCameras"
class="mb-3"
color="error"
density="compact"
icon="mdi-alert-circle-outline"
:variant="theme.global.current.value.dark ? 'tonal' : 'elevated'"
>
<span
>{{ disabledCameras }} are disabled! This may cause performance issues. Check your logs for more information.
</span>
</v-alert>
<v-alert
v-if="conflictingCameraShown"
class="mb-3"
color="error"
density="compact"
icon="mdi-alert-circle-outline"
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'tonal'"
:variant="theme.global.current.value.dark ? 'tonal' : 'elevated'"
>
<span
>Conflicting camera name(s) detected! Please change the name(s) of

View File

@@ -1,5 +1,6 @@
<script setup lang="ts">
const devMode = process.env.NODE_ENV === "development";
const docsSrc = import.meta.env.MODE === "demo" ? "https://docs.photonvision.org" : "docs/index.html";
</script>
<template>
<div style="overflow: hidden; height: 100vh; width: 100%">
@@ -21,7 +22,7 @@ const devMode = process.env.NODE_ENV === "development";
</div>
<div v-else style="width: 100%; height: 100%">
<!--suppress HtmlUnknownTarget -->
<iframe src="docs/index.html" style="overflow: hidden; height: 100%; width: 100%; border: 0" />
<iframe :src="docsSrc" style="overflow: hidden; height: 100%; width: 100%; border: 0" />
</div>
</div>
</template>

View File

@@ -1,8 +0,0 @@
{
"extends": "@vue/tsconfig/tsconfig.json",
"include": ["vite.config.*"],
"compilerOptions": {
"composite": true,
"types": ["node"]
}
}

View File

@@ -1,9 +1,10 @@
{
"extends": "@vue/tsconfig/tsconfig.dom.json",
"include": ["env.d.ts", "src/**/*", "src/**/*.vue"],
"include": ["env.d.ts", "vite.config.ts", "playwright.config.ts", "src/**/*", "src/**/*.vue", "tests/**/*"],
"compilerOptions": {
"moduleResolution": "node",
"noImplicitAny": false,
"lib": ["ES2022", "DOM", "DOM.Iterable"],
"moduleResolution": "bundler",
"noImplicitAny": true,
"strict": true,
"removeComments": true,
"sourceMap": true,
@@ -14,9 +15,4 @@
"@/*": ["./src/*"]
}
},
"references": [
{
"path": "./tsconfig.config.json"
}
]
}

View File

@@ -25,7 +25,7 @@ export default defineConfig({
}
},
build: {
rollupOptions: {
rolldownOptions: {
external: ["html2canvas", "dompurify", "canvg"]
},
sourcemap: true

View File

@@ -6,7 +6,7 @@ ext.licenseFile = file("$rootDir/LICENSE")
ext.externalLicensesFolder = file("$rootDir/ExternalLicenses")
apply from: "${rootDir}/shared/common.gradle"
wpilibTools.deps.wpilibVersion = wpi.versions.wpilibVersion.get()
wpilibTools.deps.wpilibVersion = wpilibVersion
def nativeConfigName = 'wpilibNatives'
configurations {
@@ -23,11 +23,12 @@ dependencies {
wpilibNatives wpilibTools.deps.wpilib("wpimath")
wpilibNatives wpilibTools.deps.wpilib("wpinet")
wpilibNatives wpilibTools.deps.wpilib("wpiutil")
wpilibNatives wpilibTools.deps.wpilib("datalog")
wpilibNatives wpilibTools.deps.wpilib("ntcore")
wpilibNatives wpilibTools.deps.wpilib("cscore")
wpilibNatives wpilibTools.deps.wpilib("apriltag")
wpilibNatives wpilibTools.deps.wpilib("hal")
wpilibNatives wpilibTools.deps.wpilibOpenCv("frc" + openCVYear, wpi.versions.opencvVersion.get())
wpilibNatives wpilibTools.deps.wpilibOpenCv(openCVversion)
// These stay as implementation dependencies since they don't have native code that gets packaged
implementation 'org.zeroturnaround:zt-zip:1.14'
@@ -40,7 +41,7 @@ dependencies {
wpilibNatives("org.photonvision:rknn_jni-jni:$rknnVersion:$jniPlatform") {
transitive = false
}
wpilibNatives("org.photonvision:rubik_jni-jni:$rubikVersion:$jniPlatform") {
wpilibNatives("org.photonvision:tflite_jni-jni:$tfliteVersion:$jniPlatform") {
transitive = false
}
wpilibNatives("org.photonvision:photon-libcamera-gl-driver-jni:$libcameraDriverVersion:$jniPlatform") {
@@ -48,11 +49,17 @@ dependencies {
}
}
if (jniPlatform == "linuxx86-64") {
wpilibNatives("org.photonvision:tflite_jni-jni:$tfliteVersion:$jniPlatform") {
transitive = false
}
}
implementation("org.photonvision:rknn_jni-java:$rknnVersion") {
transitive = false
}
implementation("org.photonvision:rubik_jni-java:$rubikVersion") {
implementation("org.photonvision:tflite_jni-java:$tfliteVersion") {
transitive = false
}

View File

@@ -26,7 +26,7 @@ public class LoadJNI {
private static HashMap<JNITypes, Boolean> loadedMap = new HashMap<>();
public enum JNITypes {
RUBIK_DETECTOR("tensorflowlite", "tensorflowlite_c", "external_delegate", "rubik_jni"),
RUBIK_DETECTOR("tflite_jni"),
RKNN_DETECTOR("rga", "rknnrt", "rknn_jni"),
MRCAL("mrcal_jni"),
LIBCAMERA("photonlibcamera");

View File

@@ -17,10 +17,7 @@
package org.photonvision.common.configuration;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonProperty;
import edu.wpi.first.cscore.UsbCameraInfo;
import io.avaje.jsonb.Json;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
@@ -35,7 +32,9 @@ import org.photonvision.vision.camera.QuirkyCamera;
import org.photonvision.vision.pipeline.CVPipelineSettings;
import org.photonvision.vision.pipeline.DriverModePipelineSettings;
import org.photonvision.vision.processes.PipelineManager;
import org.wpilib.vision.camera.UsbCameraInfo;
@Json
public class CameraConfiguration {
private static final Logger logger = new Logger(CameraConfiguration.class, LogGroup.Camera);
@@ -62,11 +61,8 @@ public class CameraConfiguration {
public int streamIndex = 0; // 0 index means ports [1181, 1182], 1 means [1183, 1184], etc...
// Ignore the pipes, as we serialize them to their own column to hack around
// polymorphic lists
@JsonIgnore public List<CVPipelineSettings> pipelineSettings = new ArrayList<>();
public List<CVPipelineSettings> pipelineSettings = new ArrayList<>();
@JsonIgnore
public DriverModePipelineSettings driveModeSettings = new DriverModePipelineSettings();
public CameraConfiguration(PVCameraInfo cameraInfo, String uniqueName, String nickname) {
@@ -78,24 +74,22 @@ public class CameraConfiguration {
logger.debug("Creating USB camera configuration for " + this.toShortString());
}
// Shiny new constructor
@JsonCreator
// JSON Constructor (can't be marked with @Json.Creator due to public fields that aren't part of
// the parameters)
public CameraConfiguration(
@JsonProperty("uniqueName") String uniqueName,
@JsonProperty("matchedCameraInfo") PVCameraInfo matchedCameraInfo,
@JsonProperty("nickname") String nickname,
@JsonProperty("deactivated") boolean deactivated,
@JsonProperty("cameraQuirks") QuirkyCamera cameraQuirks,
@JsonProperty("FOV") double FOV,
@JsonProperty("calibrations") List<CameraCalibrationCoefficients> calibrations,
@JsonProperty("currentPipelineIndex") int currentPipelineIndex) {
String uniqueName,
PVCameraInfo matchedCameraInfo,
String nickname,
boolean deactivated,
QuirkyCamera cameraQuirks,
double FOV,
int currentPipelineIndex) {
this.uniqueName = uniqueName;
this.matchedCameraInfo = matchedCameraInfo;
this.nickname = nickname;
this.deactivated = deactivated;
this.cameraQuirks = cameraQuirks;
this.FOV = FOV;
this.calibrations = calibrations != null ? calibrations : new ArrayList<>();
this.currentPipelineIndex = currentPipelineIndex;
}
@@ -120,14 +114,14 @@ public class CameraConfiguration {
PVCameraInfo matchedCameraInfo;
/** Legacy constructor for compat with 2024.3.1 */
@JsonCreator
@Json.Creator
public LegacyCameraConfigStruct(
@JsonProperty("baseName") String baseName,
@JsonProperty("path") String path,
@JsonProperty("otherPaths") String[] otherPaths,
@JsonProperty("cameraType") CameraType cameraType,
@JsonProperty("usbVID") int usbVID,
@JsonProperty("usbPID") int usbPID) {
String baseName,
String path,
String[] otherPaths,
CameraType cameraType,
int usbVID,
int usbPID) {
if (cameraType == CameraType.UsbCamera) {
this.matchedCameraInfo =
PVCameraInfo.fromUsbCameraInfo(
@@ -171,16 +165,16 @@ public class CameraConfiguration {
}
/**
* Replace a calibration in our list with the same unrotatedImageSize with a new one, or add it if
* none exists yet. If we are replacing an existing calibration, the old one will be "released"
* and the underlying data matrices will become invalid.
* Replace a calibration in our list with the same resolution with a new one, or add it if none
* exists yet. If we are replacing an existing calibration, the old one will be "released" and the
* underlying data matrices will become invalid.
*
* @param calibration The calibration to add.
*/
public void addCalibration(CameraCalibrationCoefficients calibration) {
logger.info("adding calibration " + calibration.unrotatedImageSize);
logger.info("adding calibration " + calibration.resolution);
calibrations.stream()
.filter(it -> it.unrotatedImageSize.equals(calibration.unrotatedImageSize))
.filter(it -> it.resolution.equals(calibration.resolution))
.findAny()
.ifPresent(
(it) -> {
@@ -194,12 +188,12 @@ public class CameraConfiguration {
* Remove a calibration from our list. If found, the calibration will be "released". If not found,
* no-op.
*
* @param unrotatedImageSize The resolution to remove.
* @param resolution The resolution to remove.
*/
public void removeCalibration(Size unrotatedImageSize) {
logger.info("deleting calibration " + unrotatedImageSize);
public void removeCalibration(Size resolution) {
logger.info("deleting calibration " + resolution);
calibrations.stream()
.filter(it -> it.unrotatedImageSize.equals(unrotatedImageSize))
.filter(it -> it.resolution.equals(resolution))
.findAny()
.ifPresent(
(it) -> {
@@ -215,7 +209,6 @@ public class CameraConfiguration {
*
* <p>This represents our best guess at an immutable path to detect a camera at.
*/
@JsonIgnore
public String getDevicePath() {
return matchedCameraInfo.uniquePath();
}

View File

@@ -17,7 +17,10 @@
package org.photonvision.common.configuration;
import io.avaje.json.JsonException;
import io.avaje.jsonb.Jsonb;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
@@ -34,7 +37,6 @@ import org.opencv.core.Size;
import org.photonvision.common.logging.LogGroup;
import org.photonvision.common.logging.Logger;
import org.photonvision.common.util.file.FileUtils;
import org.photonvision.common.util.file.JacksonUtils;
import org.photonvision.vision.processes.VisionSource;
import org.zeroturnaround.zip.ZipUtil;
@@ -233,14 +235,15 @@ public class ConfigManager {
Path.of(getModelsDirectory().toString(), "photonvision-object-detection-models.json")
.toFile();
try {
JacksonUtils.serialize(
tempProperties.toPath(), this.getConfig().neuralNetworkPropertyManager());
Jsonb.instance()
.type(NeuralNetworkModelsSettings.class)
.toJson(this.getConfig().getNeuralNetworkProperties(), new FileWriter(tempProperties));
ZipUtil.pack(getModelsDirectory(), out);
// Now delete the tempProperties
if (tempProperties.exists()) {
Files.delete(tempProperties.toPath());
}
} catch (Exception e) {
} catch (IOException | IllegalStateException | JsonException e) {
e.printStackTrace();
}
return out;

View File

@@ -17,40 +17,50 @@
package org.photonvision.common.configuration;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import io.avaje.jsonb.Json;
import java.util.ArrayList;
import java.util.List;
import org.photonvision.common.hardware.statusLED.StatusLEDType;
@JsonIgnoreProperties(ignoreUnknown = true)
@Json
public class HardwareConfig {
public final String deviceName;
public String deviceName;
// LED control
public final ArrayList<Integer> ledPins;
public final boolean ledsCanDim;
public final ArrayList<Integer> ledBrightnessRange;
public final int ledPWMFrequency;
public final ArrayList<Integer> statusRGBPins;
public final boolean statusRGBActiveHigh;
public List<Integer> ledPins;
public boolean ledsCanDim;
public List<Integer> ledBrightnessRange;
public int ledPWMFrequency;
public StatusLEDType statusLEDType;
// MIGRATION: 2026
@Json.Alias("statusRGBPins")
public List<Integer> statusLEDPins;
// MIGRATION: 2026
@Json.Alias("statusRGBActiveHigh")
public boolean statusLEDActiveHigh;
// Custom GPIO
public final String getGPIOCommand;
public final String setGPIOCommand;
public final String setPWMCommand;
public final String setPWMFrequencyCommand;
public final String releaseGPIOCommand;
public String getGPIOCommand;
public String setGPIOCommand;
public String setPWMCommand;
public String setPWMFrequencyCommand;
public String releaseGPIOCommand;
// Device stuff
public final String restartHardwareCommand;
public final double vendorFOV; // -1 for unmanaged
public String restartHardwareCommand;
public double vendorFOV; // -1 for unmanaged
public HardwareConfig(
String deviceName,
ArrayList<Integer> ledPins,
List<Integer> ledPins,
boolean ledsCanDim,
ArrayList<Integer> ledBrightnessRange,
List<Integer> ledBrightnessRange,
int ledPwmFrequency,
ArrayList<Integer> statusRGBPins,
boolean statusRGBActiveHigh,
StatusLEDType statusLEDType,
List<Integer> statusLEDPins,
boolean statusLEDActiveHigh,
String getGPIOCommand,
String setGPIOCommand,
String setPWMCommand,
@@ -63,8 +73,9 @@ public class HardwareConfig {
this.ledsCanDim = ledsCanDim;
this.ledBrightnessRange = ledBrightnessRange;
this.ledPWMFrequency = ledPwmFrequency;
this.statusRGBPins = statusRGBPins;
this.statusRGBActiveHigh = statusRGBActiveHigh;
this.statusLEDType = statusLEDType;
this.statusLEDPins = statusLEDPins;
this.statusLEDActiveHigh = statusLEDActiveHigh;
this.getGPIOCommand = getGPIOCommand;
this.setGPIOCommand = setGPIOCommand;
this.setPWMCommand = setPWMCommand;
@@ -80,8 +91,9 @@ public class HardwareConfig {
ledsCanDim = false;
ledBrightnessRange = new ArrayList<>();
ledPWMFrequency = 0;
statusRGBPins = new ArrayList<>();
statusRGBActiveHigh = false;
statusLEDType = StatusLEDType.RGB;
statusLEDPins = new ArrayList<>();
statusLEDActiveHigh = false;
getGPIOCommand = "";
setGPIOCommand = "";
setPWMCommand = "";
@@ -121,10 +133,12 @@ public class HardwareConfig {
+ ledBrightnessRange
+ ", ledPWMFrequency="
+ ledPWMFrequency
+ ", statusRGBPins="
+ statusRGBPins
+ ", statusRGBActiveHigh"
+ statusRGBActiveHigh
+ ", statusLEDType="
+ statusLEDType
+ ", statusLEDPins="
+ statusLEDPins
+ ", statusLEDActiveHigh"
+ statusLEDActiveHigh
+ ", getGPIOCommand="
+ getGPIOCommand
+ ", setGPIOCommand="

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