Compare commits

..

56 Commits
2026 ... Dev

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
485 changed files with 7511 additions and 9298 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,22 +251,21 @@ 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
name: Build with Gradle
# - run: ./gradlew photon-lib:publish photon-targeting:publish
# name: Publish
# env:
# ARTIFACTORY_API_KEY: ${{ secrets.ARTIFACTORY_API_KEY }}
# if: github.event_name == 'push' && github.repository_owner == 'photonvision'
- run: ./gradlew photon-lib:publish photon-targeting:publish
name: Publish
env:
ARTIFACTORY_API_KEY: ${{ secrets.ARTIFACTORY_API_KEY }}
if: github.event_name == 'push' && github.repository_owner == 'photonvision' && (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"
@@ -289,14 +299,14 @@ jobs:
- name: Build PhotonLib
# We don't need to run tests, since we specify only non-native platforms
run: ./gradlew photon-targeting:build photon-lib:build ${{ matrix.build-options }} -x test
# - name: Publish
# 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'
- name: Publish
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' && (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,30 +632,32 @@ 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
- uses: pyTooling/Actions/releaser@r6
with:
token: ${{ secrets.GITHUB_TOKEN }}
tag: 'Dev'
rm: true
snapshots: false
files: |
**/*.xz
@@ -674,13 +665,13 @@ jobs:
**/*win*.jar
**/photonlib*.json
**/photonlib*.zip
if: startsWith(github.ref, 'refs/tags/v')
# - name: Create Vendor JSON Repo PR
# uses: wpilibsuite/vendor-json-repo/.github/actions/add_vendordep@HEAD
# with:
# repo: PhotonVision/vendor-json-repo
# token: ${{ secrets.VENDOR_JSON_REPO_PUSH_TOKEN }}
# vendordep_file: ${{ github.workspace }}/photonlib-${{ github.ref_name }}.json
# pr_title: Update photonlib to ${{ github.ref_name }}
# pr_branch: photonlib-${{ github.ref_name }}
# if: github.repository == 'PhotonVision/photonvision' && startsWith(github.ref, 'refs/tags/v')
if: github.event_name == 'push'
- name: Create Vendor JSON Repo PR
uses: wpilibsuite/vendor-json-repo/.github/actions/add_vendordep@HEAD
with:
repo: PhotonVision/vendor-json-repo
token: ${{ secrets.VENDOR_JSON_REPO_PUSH_TOKEN }}
vendordep_file: ${{ github.workspace }}/photonlib-${{ github.ref_name }}.json
pr_title: Update photonlib to ${{ github.ref_name }}
pr_branch: photonlib-${{ github.ref_name }}
if: github.repository == 'PhotonVision/photonvision' && startsWith(github.ref, 'refs/tags/v')

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,37 +77,38 @@ 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 .
# - name: Publish Docs To Development
# if: github.ref == 'refs/heads/main'
# uses: up9cloud/action-rsync@v1.4
# env:
# HOST: ${{ secrets.WEBMASTER_SSH_HOST }}
# USER: ${{ secrets.WEBMASTER_SSH_USERNAME }}
# KEY: ${{secrets.WEBMASTER_SSH_KEY}}
# TARGET: /var/www/html/photonvision-docs/development/
# - name: Publish Docs To Release
# if: startsWith(github.ref, 'refs/tags/v')
# uses: up9cloud/action-rsync@v1.4
# env:
# HOST: ${{ secrets.WEBMASTER_SSH_HOST }}
# USER: ${{ secrets.WEBMASTER_SSH_USERNAME }}
# KEY: ${{ secrets.WEBMASTER_SSH_KEY }}
# TARGET: /var/www/html/photonvision-docs/release/
- name: Publish Docs To Development
if: github.ref == 'refs/heads/main'
uses: up9cloud/action-rsync@v1.4
env:
HOST: ${{ secrets.WEBMASTER_SSH_HOST }}
USER: ${{ secrets.WEBMASTER_SSH_USERNAME }}
KEY: ${{secrets.WEBMASTER_SSH_KEY}}
TARGET: /var/www/html/photonvision-docs/development/
- name: Publish Docs To Release
if: startsWith(github.ref, 'refs/tags/v')
uses: up9cloud/action-rsync@v1.4
env:
HOST: ${{ secrets.WEBMASTER_SSH_HOST }}
USER: ${{ secrets.WEBMASTER_SSH_USERNAME }}
KEY: ${{ secrets.WEBMASTER_SSH_KEY }}
TARGET: /var/www/html/photonvision-docs/release/
publish_demo:
name: Publish PhotonClient Demo
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 .
- name: Publish demo
if: github.ref == 'refs/heads/main'
uses: up9cloud/action-rsync@v1.4
env:
HOST: ${{ secrets.WEBMASTER_SSH_HOST }}

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,26 +121,26 @@ jobs:
for folder in */;
do
echo $folder
./run.sh $folder
./test.sh $folder
done
# deploy:
# needs: [test-py, build-python-examples]
# runs-on: ubuntu-24.04
# # Only upload on tags
# if: startsWith(github.ref, 'refs/tags/v')
deploy:
needs: [test-py, build-python-examples]
runs-on: ubuntu-24.04
# Only upload on tags
if: startsWith(github.ref, 'refs/tags/v')
# steps:
# - name: Download artifacts
# uses: actions/download-artifact@v6
# with:
# name: dist
# path: dist/
steps:
- name: Download artifacts
uses: actions/download-artifact@v8
with:
name: dist
path: dist/
# - name: Publish package distributions to PyPI
# uses: pypa/gh-action-pypi-publish@release/v1
# with:
# packages-dir: ./dist/
- name: Publish package distributions to PyPI
uses: pypa/gh-action-pypi-publish@release/v1
with:
packages-dir: ./dist/
# permissions:
# id-token: write # IMPORTANT: this permission is mandatory for trusted publishing
permissions:
id-token: write # IMPORTANT: this permission is mandatory for trusted publishing

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

2
.gitignore vendored
View File

@@ -161,3 +161,5 @@ photon-client/playwright/.auth/
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

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

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

@@ -5,7 +5,7 @@ import skipFormattingConfig from "@vue/eslint-config-prettier/skip-formatting";
export default defineConfigWithVueTs(
pluginVue.configs["flat/recommended-error"],
vueTsConfigs.recommended,
vueTsConfigs.recommendedTypeChecked,
skipFormattingConfig,
{
ignores: ["**/dist/**", "playwright-report"]
@@ -42,10 +42,13 @@ export default defineConfigWithVueTs(
"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,9 +37,10 @@
"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",
@@ -45,7 +49,7 @@
"prettier": "^3.6.2",
"sass": "^1.89.2",
"typescript": "^5.8.3",
"vite": "^8.0.2",
"vite": "^8.0.10",
"vite-plugin-vuetify": "^2.1.1"
}
}

View File

@@ -52,14 +52,14 @@ importers:
specifier: ^1.56.1
version: 1.56.1
'@types/node':
specifier: ^22.15.14
version: 22.15.14
specifier: ^24.0.0
version: 24.12.2
'@types/three':
specifier: ^0.178.0
version: 0.178.1
'@vitejs/plugin-vue':
specifier: ^6.0.0
version: 6.0.0(vite@8.0.2(@types/node@22.15.14)(sass@1.89.2))(vue@3.5.13(typescript@5.8.3))
specifier: ^6.0.6
version: 6.0.6(vite@8.0.10(@types/node@24.12.2)(sass@1.89.2))(vue@3.5.13(typescript@5.8.3))
'@vue/eslint-config-prettier':
specifier: ^10.2.0
version: 10.2.0(eslint@9.31.0)(prettier@3.6.2)
@@ -85,11 +85,14 @@ importers:
specifier: ^5.8.3
version: 5.8.3
vite:
specifier: ^8.0.2
version: 8.0.2(@types/node@22.15.14)(sass@1.89.2)
specifier: ^8.0.10
version: 8.0.10(@types/node@24.12.2)(sass@1.89.2)
vite-plugin-vuetify:
specifier: ^2.1.1
version: 2.1.1(vite@8.0.2(@types/node@22.15.14)(sass@1.89.2))(vue@3.5.13(typescript@5.8.3))(vuetify@3.8.3)
version: 2.1.1(vite@8.0.10(@types/node@24.12.2)(sass@1.89.2))(vue@3.5.13(typescript@5.8.3))(vuetify@3.8.3)
vue-tsc:
specifier: ^3.2.5
version: 3.2.5(typescript@5.8.3)
packages:
@@ -117,14 +120,14 @@ packages:
'@dimforge/rapier3d-compat@0.12.0':
resolution: {integrity: sha512-uekIGetywIgopfD97oDL5PfeezkFpNhwlzlaEYNOA0N6ghdsOvh/HYjSMek5Q2O1PYvRSDFcqFVJl4r4ZBwOow==}
'@emnapi/core@1.9.1':
resolution: {integrity: sha512-mukuNALVsoix/w1BJwFzwXBN/dHeejQtuVzcDsfOEsdpCumXb/E9j8w11h5S54tT1xhifGfbbSm/ICrObRb3KA==}
'@emnapi/core@1.10.0':
resolution: {integrity: sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==}
'@emnapi/runtime@1.9.1':
resolution: {integrity: sha512-VYi5+ZVLhpgK4hQ0TAjiQiZ6ol0oe4mBx7mVv7IflsiEp0OWoVsp/+f9Vc1hOhE0TtkORVrI1GvzyreqpgWtkA==}
'@emnapi/runtime@1.10.0':
resolution: {integrity: sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==}
'@emnapi/wasi-threads@1.2.0':
resolution: {integrity: sha512-N10dEJNSsUx41Z6pZsXU8FjPjpBEplgH24sfkmITrBED1/U2Esum9F3lfLrMjKHHjmi557zQn7kR9R+XWXu5Rg==}
'@emnapi/wasi-threads@1.2.1':
resolution: {integrity: sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==}
'@eslint-community/eslint-utils@4.7.0':
resolution: {integrity: sha512-dyybb3AcajC7uha6CvhdVRJqaKyn7w2YKqKyAN37NKYgZT36w+iRb0Dymmc5qEJ549c/S31cMMSFd75bteCpCw==}
@@ -197,8 +200,11 @@ packages:
resolution: {integrity: sha512-JEW4DEtBzfe8HvUYecLU9e6+XJnKDlUAIve8FvPzF3Kzs6Xo/KuZkZJsDH0wJXl/qEZbeeE7edxDNY3kMs39hQ==}
engines: {node: '>= 18'}
'@napi-rs/wasm-runtime@1.1.1':
resolution: {integrity: sha512-p64ah1M1ld8xjWv3qbvFwHiFVWrq1yFvV4f7w+mzaqiR4IlSgkqhcRdHwsGgomwzBH51sRY4NEowLxnaBjcW/A==}
'@napi-rs/wasm-runtime@1.1.4':
resolution: {integrity: sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow==}
peerDependencies:
'@emnapi/core': ^1.7.1
'@emnapi/runtime': ^1.7.1
'@nodelib/fs.scandir@2.1.5':
resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==}
@@ -212,8 +218,8 @@ packages:
resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==}
engines: {node: '>= 8'}
'@oxc-project/types@0.122.0':
resolution: {integrity: sha512-oLAl5kBpV4w69UtFZ9xqcmTi+GENWOcPF7FCrczTiBbmC0ibXxCwyvZGbO39rCVEuLGAZM84DH0pUIyyv/YJzA==}
'@oxc-project/types@0.127.0':
resolution: {integrity: sha512-aIYXQBo4lCbO4z0R3FHeucQHpF46l2LbMdxRvqvuRuW2OxdnSkcng5B8+K12spgLDj93rtN3+J2Vac/TIO+ciQ==}
'@parcel/watcher-android-arm64@2.5.1':
resolution: {integrity: sha512-KF8+j9nNbUN8vzOFDpRMsaKBHZ/mcjEjMToVMJOhTozkDonQFFrRcfdLWn6yWKCmJKmdVxSgHiYvTCef4/qcBA==}
@@ -244,36 +250,42 @@ packages:
engines: {node: '>= 10.0.0'}
cpu: [arm]
os: [linux]
libc: [glibc]
'@parcel/watcher-linux-arm-musl@2.5.1':
resolution: {integrity: sha512-6E+m/Mm1t1yhB8X412stiKFG3XykmgdIOqhjWj+VL8oHkKABfu/gjFj8DvLrYVHSBNC+/u5PeNrujiSQ1zwd1Q==}
engines: {node: '>= 10.0.0'}
cpu: [arm]
os: [linux]
libc: [musl]
'@parcel/watcher-linux-arm64-glibc@2.5.1':
resolution: {integrity: sha512-LrGp+f02yU3BN9A+DGuY3v3bmnFUggAITBGriZHUREfNEzZh/GO06FF5u2kx8x+GBEUYfyTGamol4j3m9ANe8w==}
engines: {node: '>= 10.0.0'}
cpu: [arm64]
os: [linux]
libc: [glibc]
'@parcel/watcher-linux-arm64-musl@2.5.1':
resolution: {integrity: sha512-cFOjABi92pMYRXS7AcQv9/M1YuKRw8SZniCDw0ssQb/noPkRzA+HBDkwmyOJYp5wXcsTrhxO0zq1U11cK9jsFg==}
engines: {node: '>= 10.0.0'}
cpu: [arm64]
os: [linux]
libc: [musl]
'@parcel/watcher-linux-x64-glibc@2.5.1':
resolution: {integrity: sha512-GcESn8NZySmfwlTsIur+49yDqSny2IhPeZfXunQi48DMugKeZ7uy1FX83pO0X22sHntJ4Ub+9k34XQCX+oHt2A==}
engines: {node: '>= 10.0.0'}
cpu: [x64]
os: [linux]
libc: [glibc]
'@parcel/watcher-linux-x64-musl@2.5.1':
resolution: {integrity: sha512-n0E2EQbatQ3bXhcH2D1XIAANAcTZkQICBPVaxMeaCVBtOpBZpWJuf7LwyWPSBDITb7In8mqQgJ7gH8CILCURXg==}
engines: {node: '>= 10.0.0'}
cpu: [x64]
os: [linux]
libc: [musl]
'@parcel/watcher-win32-arm64@2.5.1':
resolution: {integrity: sha512-RFzklRvmc3PkjKjry3hLF9wD7ppR4AKcWNzH7kXR7GUe0Igb3Nz8fyPwtZCSquGrhU5HhUNDr/mKBqj7tqA2Vw==}
@@ -306,100 +318,106 @@ packages:
engines: {node: '>=18'}
hasBin: true
'@rolldown/binding-android-arm64@1.0.0-rc.11':
resolution: {integrity: sha512-SJ+/g+xNnOh6NqYxD0V3uVN4W3VfnrGsC9/hoglicgTNfABFG9JjISvkkU0dNY84MNHLWyOgxP9v9Y9pX4S7+A==}
'@rolldown/binding-android-arm64@1.0.0-rc.17':
resolution: {integrity: sha512-s70pVGhw4zqGeFnXWvAzJDlvxhlRollagdCCKRgOsgUOH3N1l0LIxf83AtGzmb5SiVM4Hjl5HyarMRfdfj3DaQ==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm64]
os: [android]
'@rolldown/binding-darwin-arm64@1.0.0-rc.11':
resolution: {integrity: sha512-7WQgR8SfOPwmDZGFkThUvsmd/nwAWv91oCO4I5LS7RKrssPZmOt7jONN0cW17ydGC1n/+puol1IpoieKqQidmg==}
'@rolldown/binding-darwin-arm64@1.0.0-rc.17':
resolution: {integrity: sha512-4ksWc9n0mhlZpZ9PMZgTGjeOPRu8MB1Z3Tz0Mo02eWfWCHMW1zN82Qz/pL/rC+yQa+8ZnutMF0JjJe7PjwasYw==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm64]
os: [darwin]
'@rolldown/binding-darwin-x64@1.0.0-rc.11':
resolution: {integrity: sha512-39Ks6UvIHq4rEogIfQBoBRusj0Q0nPVWIvqmwBLaT6aqQGIakHdESBVOPRRLacy4WwUPIx4ZKzfZ9PMW+IeyUQ==}
'@rolldown/binding-darwin-x64@1.0.0-rc.17':
resolution: {integrity: sha512-SUSDOI6WwUVNcWxd02QEBjLdY1VPHvlEkw6T/8nYG322iYWCTxRb1vzk4E+mWWYehTp7ERibq54LSJGjmouOsw==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [x64]
os: [darwin]
'@rolldown/binding-freebsd-x64@1.0.0-rc.11':
resolution: {integrity: sha512-jfsm0ZHfhiqrvWjJAmzsqiIFPz5e7mAoCOPBNTcNgkiid/LaFKiq92+0ojH+nmJmKYkre4t71BWXUZDNp7vsag==}
'@rolldown/binding-freebsd-x64@1.0.0-rc.17':
resolution: {integrity: sha512-hwnz3nw9dbJ05EDO/PvcjaaewqqDy7Y1rn1UO81l8iIK1GjenME75dl16ajbvSSMfv66WXSRCYKIqfgq2KCfxw==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [x64]
os: [freebsd]
'@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.11':
resolution: {integrity: sha512-zjQaUtSyq1nVe3nxmlSCuR96T1LPlpvmJ0SZy0WJFEsV4kFbXcq2u68L4E6O0XeFj4aex9bEauqjW8UQBeAvfQ==}
'@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.17':
resolution: {integrity: sha512-IS+W7epTcwANmFSQFrS1SivEXHtl1JtuQA9wlxrZTcNi6mx+FDOYrakGevvvTwgj2JvWiK8B29/qD9BELZPyXQ==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm]
os: [linux]
'@rolldown/binding-linux-arm64-gnu@1.0.0-rc.11':
resolution: {integrity: sha512-WMW1yE6IOnehTcFE9eipFkm3XN63zypWlrJQ2iF7NrQ9b2LDRjumFoOGJE8RJJTJCTBAdmLMnJ8uVitACUUo1Q==}
'@rolldown/binding-linux-arm64-gnu@1.0.0-rc.17':
resolution: {integrity: sha512-e6usGaHKW5BMNZOymS1UcEYGowQMWcgZ71Z17Sl/h2+ZziNJ1a9n3Zvcz6LdRyIW5572wBCTH/Z+bKuZouGk9Q==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm64]
os: [linux]
libc: [glibc]
'@rolldown/binding-linux-arm64-musl@1.0.0-rc.11':
resolution: {integrity: sha512-jfndI9tsfm4APzjNt6QdBkYwre5lRPUgHeDHoI7ydKUuJvz3lZeCfMsI56BZj+7BYqiKsJm7cfd/6KYV7ubrBg==}
'@rolldown/binding-linux-arm64-musl@1.0.0-rc.17':
resolution: {integrity: sha512-b/CgbwAJpmrRLp02RPfhbudf5tZnN9nsPWK82znefso832etkem8H7FSZwxrOI9djcdTP7U6YfNhbRnh7djErg==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm64]
os: [linux]
libc: [musl]
'@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.11':
resolution: {integrity: sha512-ZlFgw46NOAGMgcdvdYwAGu2Q+SLFA9LzbJLW+iyMOJyhj5wk6P3KEE9Gct4xWwSzFoPI7JCdYmYMzVtlgQ+zfw==}
'@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.17':
resolution: {integrity: sha512-4EII1iNGRUN5WwGbF/kOh/EIkoDN9HsupgLQoXfY+D1oyJm7/F4t5PYU5n8SWZgG0FEwakyM8pGgwcBYruGTlA==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [ppc64]
os: [linux]
libc: [glibc]
'@rolldown/binding-linux-s390x-gnu@1.0.0-rc.11':
resolution: {integrity: sha512-hIOYmuT6ofM4K04XAZd3OzMySEO4K0/nc9+jmNcxNAxRi6c5UWpqfw3KMFV4MVFWL+jQsSh+bGw2VqmaPMTLyw==}
'@rolldown/binding-linux-s390x-gnu@1.0.0-rc.17':
resolution: {integrity: sha512-AH8oq3XqQo4IibpVXvPeLDI5pzkpYn0WiZAfT05kFzoJ6tQNzwRdDYQ45M8I/gslbodRZwW8uxLhbSBbkv96rA==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [s390x]
os: [linux]
libc: [glibc]
'@rolldown/binding-linux-x64-gnu@1.0.0-rc.11':
resolution: {integrity: sha512-qXBQQO9OvkjjQPLdUVr7Nr2t3QTZI7s4KZtfw7HzBgjbmAPSFwSv4rmET9lLSgq3rH/ndA3ngv3Qb8l2njoPNA==}
'@rolldown/binding-linux-x64-gnu@1.0.0-rc.17':
resolution: {integrity: sha512-cLnjV3xfo7KslbU41Z7z8BH/E1y5mzUYzAqih1d1MDaIGZRCMqTijqLv76/P7fyHuvUcfGsIpqCdddbxLLK9rA==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [x64]
os: [linux]
libc: [glibc]
'@rolldown/binding-linux-x64-musl@1.0.0-rc.11':
resolution: {integrity: sha512-/tpFfoSTzUkH9LPY+cYbqZBDyyX62w5fICq9qzsHLL8uTI6BHip3Q9Uzft0wylk/i8OOwKik8OxW+QAhDmzwmg==}
'@rolldown/binding-linux-x64-musl@1.0.0-rc.17':
resolution: {integrity: sha512-0phclDw1spsL7dUB37sIARuis2tAgomCJXAHZlpt8PXZ4Ba0dRP1e+66lsRqrfhISeN9bEGNjQs+T/Fbd7oYGw==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [x64]
os: [linux]
libc: [musl]
'@rolldown/binding-openharmony-arm64@1.0.0-rc.11':
resolution: {integrity: sha512-mcp3Rio2w72IvdZG0oQ4bM2c2oumtwHfUfKncUM6zGgz0KgPz4YmDPQfnXEiY5t3+KD/i8HG2rOB/LxdmieK2g==}
'@rolldown/binding-openharmony-arm64@1.0.0-rc.17':
resolution: {integrity: sha512-0ag/hEgXOwgw4t8QyQvUCxvEg+V0KBcA6YuOx9g0r02MprutRF5dyljgm3EmR02O292UX7UeS6HzWHAl6KgyhA==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm64]
os: [openharmony]
'@rolldown/binding-wasm32-wasi@1.0.0-rc.11':
resolution: {integrity: sha512-LXk5Hii1Ph9asuGRjBuz8TUxdc1lWzB7nyfdoRgI0WGPZKmCxvlKk8KfYysqtr4MfGElu/f/pEQRh8fcEgkrWw==}
engines: {node: '>=14.0.0'}
'@rolldown/binding-wasm32-wasi@1.0.0-rc.17':
resolution: {integrity: sha512-LEXei6vo0E5wTGwpkJ4KoT3OZJRnglwldt5ziLzOlc6qqb55z4tWNq2A+PFqCJuvWWdP53CVhG1Z9NtToDPJrA==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [wasm32]
'@rolldown/binding-win32-arm64-msvc@1.0.0-rc.11':
resolution: {integrity: sha512-dDwf5otnx0XgRY1yqxOC4ITizcdzS/8cQ3goOWv3jFAo4F+xQYni+hnMuO6+LssHHdJW7+OCVL3CoU4ycnh35Q==}
'@rolldown/binding-win32-arm64-msvc@1.0.0-rc.17':
resolution: {integrity: sha512-gUmyzBl3SPMa6hrqFUth9sVfcLBlYsbMzBx5PlexMroZStgzGqlZ26pYG89rBb45Mnia+oil6YAIFeEWGWhoZA==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm64]
os: [win32]
'@rolldown/binding-win32-x64-msvc@1.0.0-rc.11':
resolution: {integrity: sha512-LN4/skhSggybX71ews7dAj6r2geaMJfm3kMbK2KhFMg9B10AZXnKoLCVVgzhMHL0S+aKtr4p8QbAW8k+w95bAA==}
'@rolldown/binding-win32-x64-msvc@1.0.0-rc.17':
resolution: {integrity: sha512-3hkiolcUAvPB9FLb3UZdfjVVNWherN1f/skkGWJP/fgSQhYUZpSIRr0/I8ZK9TkF3F7kxvJAk0+IcKvPHk9qQg==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [x64]
os: [win32]
'@rolldown/pluginutils@1.0.0-beta.19':
resolution: {integrity: sha512-3FL3mnMbPu0muGOCaKAhhFEYmqv9eTfPSJRJmANrCwtgK8VuxpsZDGK+m0LYAGoyO8+0j5uRe4PeyPDK1yA/hA==}
'@rolldown/pluginutils@1.0.0-rc.13':
resolution: {integrity: sha512-3ngTAv6F/Py35BsYbeeLeecvhMKdsKm4AoOETVhAA+Qc8nrA2I0kF7oa93mE9qnIurngOSpMnQ0x2nQY2FPviA==}
'@rolldown/pluginutils@1.0.0-rc.11':
resolution: {integrity: sha512-xQO9vbwBecJRv9EUcQ/y0dzSTJgA7Q6UVN7xp6B81+tBGSLVAK03yJ9NkJaUA7JFD91kbjxRSC/mDnmvXzbHoQ==}
'@rolldown/pluginutils@1.0.0-rc.17':
resolution: {integrity: sha512-n8iosDOt6Ig1UhJ2AYqoIhHWh/isz0xpicHTzpKBeotdVsTEcxsSA/i3EVM7gQAj0rU27OLAxCjzlj15IWY7bg==}
'@tweenjs/tween.js@23.1.3':
resolution: {integrity: sha512-vJmvvwFxYuGnF2axRtPYocag6Clbb5YS7kLL+SO/TeVFzHqDIWrNKYtcsPMibjDx9O+bu+psAy9NKfWklassUA==}
@@ -413,8 +431,8 @@ packages:
'@types/json-schema@7.0.15':
resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==}
'@types/node@22.15.14':
resolution: {integrity: sha512-BL1eyu/XWsFGTtDWOYULQEs4KR0qdtYfCxYAUYRoB7JP7h9ETYLgQTww6kH8Sj2C0pFGgrpM0XKv6/kbIzYJ1g==}
'@types/node@24.12.2':
resolution: {integrity: sha512-A1sre26ke7HDIuY/M23nd9gfB+nrmhtYyMINbjI1zHJxYteKR6qSMX56FsmjMcDb3SMcjJg5BiRRgOCC/yBD0g==}
'@types/raf@3.4.3':
resolution: {integrity: sha512-c4YAvMedbPZ5tEyxzQdMoOhhJ4RD3rngZIdwC2/qDN3d7JpEhB6fiBRKVY1lg5B7Wk+uPBjn5f39j1/2MY1oOw==}
@@ -478,13 +496,22 @@ packages:
resolution: {integrity: sha512-1rYQTCLFFzOI5Nl0c8LUpJT8HxpwVRn9E4CkMsYfuN6ctmQqExjSTzzSk0Tz2apmXy7WU6/6fyaZVVA/thPN+w==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
'@vitejs/plugin-vue@6.0.0':
resolution: {integrity: sha512-iAliE72WsdhjzTOp2DtvKThq1VBC4REhwRcaA+zPAAph6I+OQhUXv+Xu2KS7ElxYtb7Zc/3R30Hwv1DxEo7NXQ==}
'@vitejs/plugin-vue@6.0.6':
resolution: {integrity: sha512-u9HHgfrq3AjXlysn0eINFnWQOJQLO9WN6VprZ8FXl7A2bYisv3Hui9Ij+7QZ41F/WYWarHjwBbXtD7dKg3uxbg==}
engines: {node: ^20.19.0 || >=22.12.0}
peerDependencies:
vite: ^5.0.0 || ^6.0.0 || ^7.0.0
vite: ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0
vue: ^3.2.25
'@volar/language-core@2.4.28':
resolution: {integrity: sha512-w4qhIJ8ZSitgLAkVay6AbcnC7gP3glYM3fYwKV3srj8m494E3xtrCv6E+bWviiK/8hs6e6t1ij1s2Endql7vzQ==}
'@volar/source-map@2.4.28':
resolution: {integrity: sha512-yX2BDBqJkRXfKw8my8VarTyjv48QwxdJtvRgUpNE5erCsgEUdI2DsLbpa+rOQVAJYshY99szEcRDmyHbF10ggQ==}
'@volar/typescript@2.4.28':
resolution: {integrity: sha512-Ja6yvWrbis2QtN4ClAKreeUZPVYMARDYZl9LMEv1iQ1QdepB6wn0jTRxA9MftYmYa4DQ4k/DaSZpFPUfxl8giw==}
'@vue/compiler-core@3.5.13':
resolution: {integrity: sha512-oOdAkwqUfW1WqpwSYJce06wvt6HljgY3fGeM9NcVA1HaYOij3mZG9Rkysn0OHuyUAGMbEbARIpsG+LPVlBJ5/Q==}
@@ -526,6 +553,9 @@ packages:
typescript:
optional: true
'@vue/language-core@3.2.5':
resolution: {integrity: sha512-d3OIxN/+KRedeM5wQ6H6NIpwS3P5gC9nmyaHgBk+rO6dIsjY+tOh4UlPpiZbAh3YtLdCGEX4M16RmsBqPmJV+g==}
'@vue/reactivity@3.5.13':
resolution: {integrity: sha512-NaCwtw8o48B9I6L1zl2p41OHo/2Z4wqYGGIK1Khu5T7yxrn+ATOixn/Udn2m+6kZKB/J7cuT9DbWWhRxqixACg==}
@@ -581,6 +611,9 @@ packages:
ajv@6.12.6:
resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==}
alien-signals@3.1.2:
resolution: {integrity: sha512-d9dYqZTS90WLiU0I5c6DHj/HcKkF8ZyGN3G5x8wSbslulz70KOxaqCT0hQCo9KOyhVqzqGojvNdJXoTumZOtcw==}
ansi-styles@4.3.0:
resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==}
engines: {node: '>=8'}
@@ -1051,24 +1084,28 @@ packages:
engines: {node: '>= 12.0.0'}
cpu: [arm64]
os: [linux]
libc: [glibc]
lightningcss-linux-arm64-musl@1.32.0:
resolution: {integrity: sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==}
engines: {node: '>= 12.0.0'}
cpu: [arm64]
os: [linux]
libc: [musl]
lightningcss-linux-x64-gnu@1.32.0:
resolution: {integrity: sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==}
engines: {node: '>= 12.0.0'}
cpu: [x64]
os: [linux]
libc: [glibc]
lightningcss-linux-x64-musl@1.32.0:
resolution: {integrity: sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==}
engines: {node: '>= 12.0.0'}
cpu: [x64]
os: [linux]
libc: [musl]
lightningcss-win32-arm64-msvc@1.32.0:
resolution: {integrity: sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==}
@@ -1135,6 +1172,9 @@ packages:
ms@2.1.3:
resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
muggle-string@0.4.1:
resolution: {integrity: sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ==}
nanoid@3.3.11:
resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==}
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
@@ -1165,6 +1205,9 @@ packages:
resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==}
engines: {node: '>=6'}
path-browserify@1.0.1:
resolution: {integrity: sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==}
path-exists@4.0.0:
resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==}
engines: {node: '>=8'}
@@ -1213,12 +1256,12 @@ packages:
resolution: {integrity: sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==}
engines: {node: '>=4'}
postcss@8.5.3:
resolution: {integrity: sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A==}
postcss@8.5.13:
resolution: {integrity: sha512-qif0+jGGZoLWdHey3UFHHWP0H7Gbmsk8T5VEqyYFbWqPr1XqvLGBbk/sl8V5exGmcYJklJOhOQq1pV9IcsiFag==}
engines: {node: ^10 || ^12 || >=14}
postcss@8.5.8:
resolution: {integrity: sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==}
postcss@8.5.3:
resolution: {integrity: sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A==}
engines: {node: ^10 || ^12 || >=14}
prelude-ls@1.2.1:
@@ -1269,8 +1312,8 @@ packages:
resolution: {integrity: sha512-9aZLIrhRaD97sgVhtJOW6ckOEh6/GnvQtdVNfdZ6s67+3/XwLS9lBcQYzEEhYVeUowN7pRzMLsyGhK2i/xvWbw==}
engines: {node: '>= 0.8.15'}
rolldown@1.0.0-rc.11:
resolution: {integrity: sha512-NRjoKMusSjfRbSYiH3VSumlkgFe7kYAa3pzVOsVYVFY3zb5d7nS+a3KGQ7hJKXuYWbzJKPVQ9Wxq2UvyK+ENpw==}
rolldown@1.0.0-rc.17:
resolution: {integrity: sha512-ZrT53oAKrtA4+YtBWPQbtPOxIbVDbxT0orcYERKd63VJTF13zPcgXTvD4843L8pcsI7M6MErt8QtON6lrB9tyA==}
engines: {node: ^20.19.0 || >=22.12.0}
hasBin: true
@@ -1333,8 +1376,8 @@ packages:
three@0.178.0:
resolution: {integrity: sha512-ybFIB0+x8mz0wnZgSGy2MO/WCO6xZhQSZnmfytSPyNpM0sBafGRVhdaj+erYh5U+RhQOAg/eXqw5uVDiM2BjhQ==}
tinyglobby@0.2.15:
resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==}
tinyglobby@0.2.16:
resolution: {integrity: sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==}
engines: {node: '>=12.0.0'}
to-regex-range@5.0.1:
@@ -1369,8 +1412,8 @@ packages:
engines: {node: '>=14.17'}
hasBin: true
undici-types@6.21.0:
resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==}
undici-types@7.16.0:
resolution: {integrity: sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==}
upath@2.0.1:
resolution: {integrity: sha512-1uEe95xksV1O0CYKXo8vQvN1JEbtJp7lb7C5U9HMsIp6IVwntkH/oNUzyVNQSd4S1sYk2FpSSW44FqMc8qee5w==}
@@ -1393,14 +1436,14 @@ packages:
vue: ^3.0.0
vuetify: ^3.0.0
vite@8.0.2:
resolution: {integrity: sha512-1gFhNi+bHhRE/qKZOJXACm6tX4bA3Isy9KuKF15AgSRuRazNBOJfdDemPBU16/mpMxApDPrWvZ08DcLPEoRnuA==}
vite@8.0.10:
resolution: {integrity: sha512-rZuUu9j6J5uotLDs+cAA4O5H4K1SfPliUlQwqa6YEwSrWDZzP4rhm00oJR5snMewjxF5V/K3D4kctsUTsIU9Mw==}
engines: {node: ^20.19.0 || >=22.12.0}
hasBin: true
peerDependencies:
'@types/node': ^20.19.0 || >=22.12.0
'@vitejs/devtools': ^0.1.0
esbuild: ^0.27.0
esbuild: ^0.27.0 || ^0.28.0
jiti: '>=1.21.0'
less: ^4.0.0
sass: ^1.70.0
@@ -1436,6 +1479,9 @@ packages:
yaml:
optional: true
vscode-uri@3.1.0:
resolution: {integrity: sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ==}
vue-eslint-parser@10.1.3:
resolution: {integrity: sha512-dbCBnd2e02dYWsXoqX5yKUZlOt+ExIpq7hmHKPb5ZqKcjf++Eo0hMseFTZMLKThrUk61m+Uv6A2YSBve6ZvuDQ==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
@@ -1447,6 +1493,12 @@ packages:
peerDependencies:
vue: ^3.2.0
vue-tsc@3.2.5:
resolution: {integrity: sha512-/htfTCMluQ+P2FISGAooul8kO4JMheOTCbCy4M6dYnYYjqLe3BExZudAua6MSIKSFYQtFOYAll7XobYwcpokGA==}
hasBin: true
peerDependencies:
typescript: '>=5.0.0'
vue3-virtual-scroll-list@0.2.1:
resolution: {integrity: sha512-G4KxITUOy9D4ro15zOp40D6ogmMefzjIyMsBKqN3xGbV1P6dlKYMx+BBXCKm3Nr/6iipcUKM272Sh2AJRyWMyQ==}
peerDependencies:
@@ -1515,18 +1567,18 @@ snapshots:
'@dimforge/rapier3d-compat@0.12.0': {}
'@emnapi/core@1.9.1':
'@emnapi/core@1.10.0':
dependencies:
'@emnapi/wasi-threads': 1.2.0
'@emnapi/wasi-threads': 1.2.1
tslib: 2.8.1
optional: true
'@emnapi/runtime@1.9.1':
'@emnapi/runtime@1.10.0':
dependencies:
tslib: 2.8.1
optional: true
'@emnapi/wasi-threads@1.2.0':
'@emnapi/wasi-threads@1.2.1':
dependencies:
tslib: 2.8.1
optional: true
@@ -1596,10 +1648,10 @@ snapshots:
'@msgpack/msgpack@3.1.2': {}
'@napi-rs/wasm-runtime@1.1.1':
'@napi-rs/wasm-runtime@1.1.4(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)':
dependencies:
'@emnapi/core': 1.9.1
'@emnapi/runtime': 1.9.1
'@emnapi/core': 1.10.0
'@emnapi/runtime': 1.10.0
'@tybys/wasm-util': 0.10.1
optional: true
@@ -1615,7 +1667,7 @@ snapshots:
'@nodelib/fs.scandir': 2.1.5
fastq: 1.19.1
'@oxc-project/types@0.122.0': {}
'@oxc-project/types@0.127.0': {}
'@parcel/watcher-android-arm64@2.5.1':
optional: true
@@ -1684,56 +1736,58 @@ snapshots:
dependencies:
playwright: 1.56.1
'@rolldown/binding-android-arm64@1.0.0-rc.11':
'@rolldown/binding-android-arm64@1.0.0-rc.17':
optional: true
'@rolldown/binding-darwin-arm64@1.0.0-rc.11':
'@rolldown/binding-darwin-arm64@1.0.0-rc.17':
optional: true
'@rolldown/binding-darwin-x64@1.0.0-rc.11':
'@rolldown/binding-darwin-x64@1.0.0-rc.17':
optional: true
'@rolldown/binding-freebsd-x64@1.0.0-rc.11':
'@rolldown/binding-freebsd-x64@1.0.0-rc.17':
optional: true
'@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.11':
'@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.17':
optional: true
'@rolldown/binding-linux-arm64-gnu@1.0.0-rc.11':
'@rolldown/binding-linux-arm64-gnu@1.0.0-rc.17':
optional: true
'@rolldown/binding-linux-arm64-musl@1.0.0-rc.11':
'@rolldown/binding-linux-arm64-musl@1.0.0-rc.17':
optional: true
'@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.11':
'@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.17':
optional: true
'@rolldown/binding-linux-s390x-gnu@1.0.0-rc.11':
'@rolldown/binding-linux-s390x-gnu@1.0.0-rc.17':
optional: true
'@rolldown/binding-linux-x64-gnu@1.0.0-rc.11':
'@rolldown/binding-linux-x64-gnu@1.0.0-rc.17':
optional: true
'@rolldown/binding-linux-x64-musl@1.0.0-rc.11':
'@rolldown/binding-linux-x64-musl@1.0.0-rc.17':
optional: true
'@rolldown/binding-openharmony-arm64@1.0.0-rc.11':
'@rolldown/binding-openharmony-arm64@1.0.0-rc.17':
optional: true
'@rolldown/binding-wasm32-wasi@1.0.0-rc.11':
'@rolldown/binding-wasm32-wasi@1.0.0-rc.17':
dependencies:
'@napi-rs/wasm-runtime': 1.1.1
'@emnapi/core': 1.10.0
'@emnapi/runtime': 1.10.0
'@napi-rs/wasm-runtime': 1.1.4(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)
optional: true
'@rolldown/binding-win32-arm64-msvc@1.0.0-rc.11':
'@rolldown/binding-win32-arm64-msvc@1.0.0-rc.17':
optional: true
'@rolldown/binding-win32-x64-msvc@1.0.0-rc.11':
'@rolldown/binding-win32-x64-msvc@1.0.0-rc.17':
optional: true
'@rolldown/pluginutils@1.0.0-beta.19': {}
'@rolldown/pluginutils@1.0.0-rc.13': {}
'@rolldown/pluginutils@1.0.0-rc.11': {}
'@rolldown/pluginutils@1.0.0-rc.17': {}
'@tweenjs/tween.js@23.1.3': {}
@@ -1746,9 +1800,9 @@ snapshots:
'@types/json-schema@7.0.15': {}
'@types/node@22.15.14':
'@types/node@24.12.2':
dependencies:
undici-types: 6.21.0
undici-types: 7.16.0
'@types/raf@3.4.3':
optional: true
@@ -1847,12 +1901,24 @@ snapshots:
'@typescript-eslint/types': 8.32.0
eslint-visitor-keys: 4.2.0
'@vitejs/plugin-vue@6.0.0(vite@8.0.2(@types/node@22.15.14)(sass@1.89.2))(vue@3.5.13(typescript@5.8.3))':
'@vitejs/plugin-vue@6.0.6(vite@8.0.10(@types/node@24.12.2)(sass@1.89.2))(vue@3.5.13(typescript@5.8.3))':
dependencies:
'@rolldown/pluginutils': 1.0.0-beta.19
vite: 8.0.2(@types/node@22.15.14)(sass@1.89.2)
'@rolldown/pluginutils': 1.0.0-rc.13
vite: 8.0.10(@types/node@24.12.2)(sass@1.89.2)
vue: 3.5.13(typescript@5.8.3)
'@volar/language-core@2.4.28':
dependencies:
'@volar/source-map': 2.4.28
'@volar/source-map@2.4.28': {}
'@volar/typescript@2.4.28':
dependencies:
'@volar/language-core': 2.4.28
path-browserify: 1.0.1
vscode-uri: 3.1.0
'@vue/compiler-core@3.5.13':
dependencies:
'@babel/parser': 7.27.2
@@ -1925,6 +1991,16 @@ snapshots:
transitivePeerDependencies:
- supports-color
'@vue/language-core@3.2.5':
dependencies:
'@volar/language-core': 2.4.28
'@vue/compiler-dom': 3.5.13
'@vue/shared': 3.5.13
alien-signals: 3.1.2
muggle-string: 0.4.1
path-browserify: 1.0.1
picomatch: 4.0.4
'@vue/reactivity@3.5.13':
dependencies:
'@vue/shared': 3.5.13
@@ -1981,6 +2057,8 @@ snapshots:
json-schema-traverse: 0.4.1
uri-js: 4.4.1
alien-signals@3.1.2: {}
ansi-styles@4.3.0:
dependencies:
color-convert: 2.0.1
@@ -2501,6 +2579,8 @@ snapshots:
ms@2.1.3: {}
muggle-string@0.4.1: {}
nanoid@3.3.11: {}
natural-compare@1.4.0: {}
@@ -2533,6 +2613,8 @@ snapshots:
dependencies:
callsites: 3.1.0
path-browserify@1.0.1: {}
path-exists@4.0.0: {}
path-key@3.1.1: {}
@@ -2568,13 +2650,13 @@ snapshots:
cssesc: 3.0.0
util-deprecate: 1.0.2
postcss@8.5.3:
postcss@8.5.13:
dependencies:
nanoid: 3.3.11
picocolors: 1.1.1
source-map-js: 1.2.1
postcss@8.5.8:
postcss@8.5.3:
dependencies:
nanoid: 3.3.11
picocolors: 1.1.1
@@ -2613,26 +2695,26 @@ snapshots:
rgbcolor@1.0.1:
optional: true
rolldown@1.0.0-rc.11:
rolldown@1.0.0-rc.17:
dependencies:
'@oxc-project/types': 0.122.0
'@rolldown/pluginutils': 1.0.0-rc.11
'@oxc-project/types': 0.127.0
'@rolldown/pluginutils': 1.0.0-rc.17
optionalDependencies:
'@rolldown/binding-android-arm64': 1.0.0-rc.11
'@rolldown/binding-darwin-arm64': 1.0.0-rc.11
'@rolldown/binding-darwin-x64': 1.0.0-rc.11
'@rolldown/binding-freebsd-x64': 1.0.0-rc.11
'@rolldown/binding-linux-arm-gnueabihf': 1.0.0-rc.11
'@rolldown/binding-linux-arm64-gnu': 1.0.0-rc.11
'@rolldown/binding-linux-arm64-musl': 1.0.0-rc.11
'@rolldown/binding-linux-ppc64-gnu': 1.0.0-rc.11
'@rolldown/binding-linux-s390x-gnu': 1.0.0-rc.11
'@rolldown/binding-linux-x64-gnu': 1.0.0-rc.11
'@rolldown/binding-linux-x64-musl': 1.0.0-rc.11
'@rolldown/binding-openharmony-arm64': 1.0.0-rc.11
'@rolldown/binding-wasm32-wasi': 1.0.0-rc.11
'@rolldown/binding-win32-arm64-msvc': 1.0.0-rc.11
'@rolldown/binding-win32-x64-msvc': 1.0.0-rc.11
'@rolldown/binding-android-arm64': 1.0.0-rc.17
'@rolldown/binding-darwin-arm64': 1.0.0-rc.17
'@rolldown/binding-darwin-x64': 1.0.0-rc.17
'@rolldown/binding-freebsd-x64': 1.0.0-rc.17
'@rolldown/binding-linux-arm-gnueabihf': 1.0.0-rc.17
'@rolldown/binding-linux-arm64-gnu': 1.0.0-rc.17
'@rolldown/binding-linux-arm64-musl': 1.0.0-rc.17
'@rolldown/binding-linux-ppc64-gnu': 1.0.0-rc.17
'@rolldown/binding-linux-s390x-gnu': 1.0.0-rc.17
'@rolldown/binding-linux-x64-gnu': 1.0.0-rc.17
'@rolldown/binding-linux-x64-musl': 1.0.0-rc.17
'@rolldown/binding-openharmony-arm64': 1.0.0-rc.17
'@rolldown/binding-wasm32-wasi': 1.0.0-rc.17
'@rolldown/binding-win32-arm64-msvc': 1.0.0-rc.17
'@rolldown/binding-win32-x64-msvc': 1.0.0-rc.17
run-parallel@1.2.0:
dependencies:
@@ -2686,7 +2768,7 @@ snapshots:
three@0.178.0: {}
tinyglobby@0.2.15:
tinyglobby@0.2.16:
dependencies:
fdir: 6.5.0(picomatch@4.0.4)
picomatch: 4.0.4
@@ -2719,7 +2801,7 @@ snapshots:
typescript@5.8.3: {}
undici-types@6.21.0: {}
undici-types@7.16.0: {}
upath@2.0.1: {}
@@ -2734,29 +2816,31 @@ snapshots:
base64-arraybuffer: 1.0.2
optional: true
vite-plugin-vuetify@2.1.1(vite@8.0.2(@types/node@22.15.14)(sass@1.89.2))(vue@3.5.13(typescript@5.8.3))(vuetify@3.8.3):
vite-plugin-vuetify@2.1.1(vite@8.0.10(@types/node@24.12.2)(sass@1.89.2))(vue@3.5.13(typescript@5.8.3))(vuetify@3.8.3):
dependencies:
'@vuetify/loader-shared': 2.1.0(vue@3.5.13(typescript@5.8.3))(vuetify@3.8.3)
debug: 4.4.0
upath: 2.0.1
vite: 8.0.2(@types/node@22.15.14)(sass@1.89.2)
vite: 8.0.10(@types/node@24.12.2)(sass@1.89.2)
vue: 3.5.13(typescript@5.8.3)
vuetify: 3.8.3(typescript@5.8.3)(vite-plugin-vuetify@2.1.1)(vue@3.5.13(typescript@5.8.3))
transitivePeerDependencies:
- supports-color
vite@8.0.2(@types/node@22.15.14)(sass@1.89.2):
vite@8.0.10(@types/node@24.12.2)(sass@1.89.2):
dependencies:
lightningcss: 1.32.0
picomatch: 4.0.4
postcss: 8.5.8
rolldown: 1.0.0-rc.11
tinyglobby: 0.2.15
postcss: 8.5.13
rolldown: 1.0.0-rc.17
tinyglobby: 0.2.16
optionalDependencies:
'@types/node': 22.15.14
'@types/node': 24.12.2
fsevents: 2.3.3
sass: 1.89.2
vscode-uri@3.1.0: {}
vue-eslint-parser@10.1.3(eslint@9.31.0):
dependencies:
debug: 4.4.0
@@ -2775,6 +2859,12 @@ snapshots:
'@vue/devtools-api': 6.6.4
vue: 3.5.13(typescript@5.8.3)
vue-tsc@3.2.5(typescript@5.8.3):
dependencies:
'@volar/typescript': 2.4.28
'@vue/language-core': 3.2.5
typescript: 5.8.3
vue3-virtual-scroll-list@0.2.1(vue@3.5.13(typescript@5.8.3)):
dependencies:
vue: 3.5.13(typescript@5.8.3)
@@ -2794,7 +2884,7 @@ snapshots:
vue: 3.5.13(typescript@5.8.3)
optionalDependencies:
typescript: 5.8.3
vite-plugin-vuetify: 2.1.1(vite@8.0.2(@types/node@22.15.14)(sass@1.89.2))(vue@3.5.13(typescript@5.8.3))(vuetify@3.8.3)
vite-plugin-vuetify: 2.1.1(vite@8.0.10(@types/node@24.12.2)(sass@1.89.2))(vue@3.5.13(typescript@5.8.3))(vuetify@3.8.3)
which@2.0.2:
dependencies:

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 });
},
@@ -75,36 +76,6 @@ onBeforeMount(() => {
<photon-log-view />
<photon-error-snackbar />
</v-app>
<!-- Quarky overlay -->
<div class="quarky-overlay">
<div id="quarkyContainer" class="quarky-container" style="left: calc(100vw - 550px); top: calc(100vh - 550px)">
<img id="quarkyImage" src="" alt="Quarky" />
<div
id="quarkySpeechBubble"
style="
display: none;
position: absolute;
left: 50%;
top: 0;
transform: translateX(-50%);
min-width: 120px;
max-width: 320px;
padding: 16px 24px;
background: #fff;
color: #222;
border-radius: 16px;
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15);
font-size: 1.2em;
font-family: sans-serif;
opacity: 0;
transition: opacity 0.7s;
pointer-events: none;
z-index: 10;
"
></div>
</div>
</div>
</template>
<style lang="scss">
@@ -147,33 +118,4 @@ onBeforeMount(() => {
div.v-layout {
overflow: unset !important;
}
/* Overlay container for Quarky */
.quarky-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
pointer-events: none;
z-index: 1000;
}
/* Quarky animation container */
.quarky-container {
position: absolute;
width: 500px;
height: 500px;
background-color: transparent;
transition:
left 1s cubic-bezier(0.42, 0, 0.58, 1),
top 1s cubic-bezier(0.42, 0, 0.58, 1);
}
.quarky-container img {
width: 100%;
height: 100%;
object-fit: contain;
display: block;
}
</style>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 101 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 82 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 102 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 113 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 99 KiB

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

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

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

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

@@ -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);
@@ -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
@@ -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) => {
@@ -295,23 +338,23 @@ const setSelectedVideoFormat = (format: VideoFormat) => {
>
<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"
@@ -483,11 +544,22 @@ const setSelectedVideoFormat = (format: VideoFormat) => {
<v-chip
: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
@@ -532,16 +604,14 @@ const setSelectedVideoFormat = (format: VideoFormat) => {
size="small"
block
:variant="theme.global.current.value.dark ? 'outlined' : 'elevated'"
:color="useStateStore().calibrationData.hasEnoughImages ? 'buttonActive' : 'error'"
: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,28 +13,28 @@ 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);

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.current.value.dark ? 'outlined' : 'tonal'"
:variant="theme.global.current.value.dark ? 'outlined' : 'elevated'"
@click="fetchSnapshots"
>
<v-icon start class="open-icon" size="large"> mdi-folder </v-icon>

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,8 +66,8 @@ 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;
@@ -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 }

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

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

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

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) {
@@ -129,7 +140,7 @@ const onBeforeTabUpdate = () => {
<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

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

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();
}
};
@@ -503,7 +502,7 @@ watch(metricsHistorySnapshot, () => {
width="600"
@update:modelValue="
() => {
importType = undefined;
importType = ImportType.AllSettings;
importFile = null;
}
"
@@ -517,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%"
/>
@@ -558,7 +563,9 @@ watch(metricsHistorySnapshot, () => {
: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(() => {
@@ -256,7 +254,7 @@ watchEffect(() => {
/>
<pv-select
v-show="!useSettingsStore().network.networkingDisabled"
v-model="currentNetworkInterfaceIndex"
v-model="currentNetworkInterface"
label="NetworkManager interface"
:disabled="
!tempSettingsStruct.shouldManage ||

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";
@@ -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",
@@ -188,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({

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

@@ -1,452 +0,0 @@
import IDLEGIF from "@/assets/images/idle.gif";
import GROWGIF from "@/assets/images/grow.gif";
import BLINKGIF from "@/assets/images/blink.gif";
import WAVEGIF from "@/assets/images/wave.gif";
import SPEAKGIF from "@/assets/images/speak.gif";
import SHRINKGIF from "@/assets/images/shrink.gif";
import POINTGIF from "@/assets/images/point.gif";
import { useCameraSettingsStore } from "@/stores/settings/CameraSettingsStore";
import { useStateStore } from "@/stores/StateStore";
const ANIMATIONS = {
idle: IDLEGIF,
grow: GROWGIF,
blink: BLINKGIF,
wave: WAVEGIF,
speak: SPEAKGIF,
shrink: SHRINKGIF,
point: POINTGIF
};
// Extended animation sequence
const SEQUENCE = [
{ animation: "grow", loops: 1 },
{ animation: "idle", loops: 2 },
{ animation: "blink", loops: 1 },
{ animation: "idle", loops: 1 },
{ animation: "speak", loops: 1 },
{ animation: "idle", loops: 1 },
{ animation: "point", loops: 1 },
{ animation: "idle", loops: 1 },
{ animation: "wave", loops: 1 },
{ animation: "idle", loops: 1 }
];
let quarkyImage;
let quarkyContainer;
let speechBubble;
let currentSequenceIndex = 0;
let currentLoopCount = 0;
let isMovingDemo = false;
let hasPlayedGrow = false;
// Speech bubble text (configurable)
let quarkySpeechText = "Hello from Quarky!";
// Turbo-encabulator style nonsense phrases
const quarkyPhrases = [
"Reverse phase oscillation detected!",
"Initializing hyperflux capacitor...",
"Reticulating splines in progress.",
"Quantum entanglement buffer overflowzomg",
"Did you remember the turbo-encabulator?",
"Engaging magnetic flux inverter.",
"Calibrating photon resonance field.",
"Deploying recursive feedback loop.",
"wow.",
"I applaud your pseudo-random bitstream.",
"Rebooting quantum foam stabilizer.",
"Analyzing subspace harmonics.",
"Transmitting encrypted flux packets.",
"Verifying entropic phase alignment.",
"Reconfiguring nano-particle array.",
"pew pew. pew pew.",
"I can't parse the synthetic logic matrix.",
"Don't forget to make the holographic interface.",
"You should generate more stochastic resonance.",
"Greetings",
"You look like you need some help!",
"Set this slider to 25",
"Set this slider to 67. HAHA 67!!!!",
"That's a horrible choice!",
"Fun is a core value! Is that a fun choice?",
"If your grandma saw that choice, would she be proud?",
"Chute Door?",
"Yes, Chute Door!",
"That's a bold strategy, Cotton.",
"asdflkjaslkdflklnf2222",
"00110101? That's just gibberish!",
"Three is my favorite number too!",
"Robots should not quit, but yours did!",
"Dont forget to disable auto-exposure! Or enable it. I'm not sure. ",
"Have you glued your lenses to keep them in focus?",
"Dont put spaces in your camera names — it makes the robot very sad",
"Upgrade to Photon Pro for gtsam support 👍",
"Did you forget to take off the lense covers? Its dark in here…"
];
// State-specific humorous phrases
const cameraNeedsSetupPhrases = [
"These cameras are just standing there... menacingly",
"Are your cameras plugged in? Trick question -- they aren't!",
"Have you hot-glued your USB cameras?"
];
const backendNotConnectedPhrases = [
"Um, is this thing even on?",
"Anyone home? Bulldozer? Bulldozer?",
"Have you tried turning the NI™ RoboRIO™ off and on again?"
];
const ntDisconnectedPhrases = [
"NetworkTables? More like Network'(; DROP TABLE websockets;--",
"Robots shouldn't quit, but I sure can't talk to yours!",
"Are you an OM5P? Because I can't talk to you over the LAN!",
"I'm a sentient subatomic particle, not a networking engineer."
];
/**
* Get list of applicable phrase categories based on current UI state
*/
function getApplicablePhraseLists() {
const cameraStore = useCameraSettingsStore();
const stateStore = useStateStore();
// Build list of applicable phrase categories
const applicableLists = [quarkyPhrases];
// Add state-specific categories (additive, not replacing)
if (cameraStore?.needsCameraConfiguration) {
applicableLists.push(cameraNeedsSetupPhrases);
}
if (!stateStore?.backendConnected) {
applicableLists.push(backendNotConnectedPhrases);
}
if (!stateStore?.ntConnectionStatus?.connected) {
applicableLists.push(ntDisconnectedPhrases);
}
return applicableLists;
}
/**
* Pick a random phrase from applicable categories
*/
function pickRandomPhrase() {
const applicableLists = getApplicablePhraseLists();
const randomList = applicableLists[Math.floor(Math.random() * applicableLists.length)];
return randomList[Math.floor(Math.random() * randomList.length)];
}
/**
* Get the duration of an animation in milliseconds
*/
function getAnimationDuration(animation) {
if (!animation) return 500; // Default 0.5s for empty state
// Animation durations (in seconds) based on quarky_generator.py
const durations = {
idle: 0.5,
grow: 2.0,
blink: 0.3,
wave: 1.8,
speak: 2.0,
shrink: 2.0,
point: 1.5
};
return (durations[animation] || 1.0) * 1000; // Convert to ms
}
/**
* Play an animation
*/
function playAnimation(animation) {
if (!animation) {
quarkyImage.src = "";
quarkyImage.style.display = "none";
return;
}
quarkyImage.style.display = "block";
quarkyImage.src = ANIMATIONS[animation];
if (animation === "speak") {
// Pick random phrase from applicable categories
quarkySpeechText = pickRandomPhrase();
speechBubble.textContent = quarkySpeechText;
speechBubble.style.display = "block";
speechBubble.style.opacity = 1;
setTimeout(() => {
speechBubble.style.opacity = 0;
setTimeout(() => {
speechBubble.style.display = "none";
}, 700);
}, 3000);
}
}
// Mini Quarky management
let miniQuarkies = [];
const MAX_MINI_QUARKIES = 20;
const MINI_QUARKY_SIZE = 120;
const MINI_QUARKY_Z_INDEX = 999;
const MINI_QUARKY_VELOCITY_MULTIPLIER = 12;
const MINI_QUARKY_VELOCITY_CENTER = 0.5;
const DIRECTION_CHANGE_PROBABILITY = 0.02;
const DIRECTION_CHANGE_VELOCITY_MULTIPLIER = 4;
const MINI_QUARKY_ANIMATION_INTERVAL_MS = 50;
const MINI_QUARKY_SPAWN_BASE_DELAY_MS = 4000;
const MINI_QUARKY_SPAWN_DELAY_RANGE_MS = 8000;
// Random movement every few cycles
let mouseX = window.innerWidth / 2;
let mouseY = window.innerHeight / 2;
let mouseMoving = false;
let mouseMoveTimeout = null;
/**
* Clamp Quarky's position to stay within the viewport, accounting for the sidebar
*/
function clampQuarkyPosition(x, y) {
const rect = quarkyContainer.getBoundingClientRect();
// Get sidebar width (account for both expanded and compact modes)
const sidebar = document.querySelector(".v-navigation-drawer");
const sidebarWidth = sidebar ? sidebar.offsetWidth : 0;
// Clamp to viewport, starting after the sidebar
const clampedX = Math.max(sidebarWidth, Math.min(x, window.innerWidth - rect.width));
const clampedY = Math.max(0, Math.min(y, window.innerHeight - rect.height));
return { x: clampedX, y: clampedY };
}
function mouseMoveHandler(e) {
mouseX = e.clientX + window.scrollX;
mouseY = e.clientY + window.scrollY;
mouseMoving = true;
clearTimeout(mouseMoveTimeout);
// After 1.5s of no movement, return Quarky to home and resume nonsense
mouseMoveTimeout = setTimeout(() => {
mouseMoving = false;
quarkyContainer.style.left = "calc(100vw - 550px)";
quarkyContainer.style.top = "calc(100vh - 550px)";
isMovingDemo = false;
playNextAnimation();
}, 1500);
// Actively track mouse: update Quarky's position every mouse move (clamped to viewport)
const clamped = clampQuarkyPosition(mouseX, mouseY);
quarkyContainer.style.left = `${clamped.x}px`;
quarkyContainer.style.top = `${clamped.y}px`;
// Immediately trigger Quarky to point at cursor
if (!isMovingDemo) {
isMovingDemo = true;
playAnimation("point");
setTimeout(() => {
playAnimation("idle");
}, getAnimationDuration("point"));
}
}
/**
* Advance to the next animation in the sequence
*/
function playNextAnimation() {
if (isMovingDemo) return;
let currentStep = SEQUENCE[currentSequenceIndex];
// On loop, skip grow after first time
if (hasPlayedGrow && currentSequenceIndex === 0 && currentStep.animation === "grow") {
currentSequenceIndex = 1;
currentStep = SEQUENCE[currentSequenceIndex];
}
// If mouse is moving, don't do normal cycle
if (mouseMoving) {
// Quarky will point at cursor via mousemove handler
return;
}
// Show speech bubble before speak
if (currentStep.animation === "speak") {
quarkySpeechText = pickRandomPhrase();
speechBubble.textContent = quarkySpeechText;
speechBubble.style.display = "block";
speechBubble.style.opacity = 1;
setTimeout(() => {
speechBubble.style.opacity = 0;
setTimeout(() => {
speechBubble.style.display = "none";
}, 700);
}, 3000);
}
// Fade out speech bubble after speak (during idle)
if (SEQUENCE[currentSequenceIndex - 1]?.animation === "speak" && currentStep.animation === "idle") {
// Speech bubble already has its own timeout from above
}
// Always return to corner when idle
quarkyContainer.style.left = "calc(100vw - 550px)";
quarkyContainer.style.top = "calc(100vh - 550px)";
// Play the animation
playAnimation(currentStep.animation);
// Calculate duration and schedule next animation
const duration = getAnimationDuration(currentStep.animation);
setTimeout(() => {
currentLoopCount++;
// Check if we've completed all loops for this step
if (currentLoopCount >= currentStep.loops) {
// Move to next step
currentLoopCount = 0;
currentSequenceIndex++;
// Loop back to start if we've completed the sequence
if (currentSequenceIndex >= SEQUENCE.length) {
currentSequenceIndex = 0;
hasPlayedGrow = true;
}
}
// Play the next animation
playNextAnimation();
}, duration);
}
function clickToPoint(e) {
// Ignore clicks on the button
if (e.target.id === "moveDemoBtn") return;
isMovingDemo = true;
const clickX = e.clientX + window.scrollX;
const clickY = e.clientY + window.scrollY;
const clamped = clampQuarkyPosition(clickX, clickY);
quarkyContainer.style.left = `${clamped.x}px`;
quarkyContainer.style.top = `${clamped.y}px`;
setTimeout(() => {
playAnimation("point");
setTimeout(() => {
playAnimation("idle");
quarkyContainer.style.left = "calc(100vw - 550px)";
quarkyContainer.style.top = "calc(100vh - 550px)";
setTimeout(() => {
isMovingDemo = false;
playNextAnimation();
}, 1000);
}, getAnimationDuration("point"));
}, 1000);
}
/**
* Spawn a mini Quarky that moves randomly around the screen
*/
function spawnMiniQuarky() {
console.log("SPAWNING A QUARKY");
// If at max, don't spawn
if (miniQuarkies.length >= MAX_MINI_QUARKIES) {
return;
}
const miniContainer = document.createElement("div");
miniContainer.style.position = "fixed";
miniContainer.style.width = `${MINI_QUARKY_SIZE}px`;
miniContainer.style.height = `${MINI_QUARKY_SIZE}px`;
miniContainer.style.pointerEvents = "none";
miniContainer.style.zIndex = MINI_QUARKY_Z_INDEX.toString();
// Spawn from main quarky's actual position
const rect = quarkyContainer.getBoundingClientRect();
const startX = rect.left + window.scrollX + rect.width / 2;
const startY = rect.top + window.scrollY + rect.height / 2;
miniContainer.style.left = startX + "px";
miniContainer.style.top = startY + "px";
const miniImage = document.createElement("img");
miniImage.src = ANIMATIONS["idle"];
miniImage.style.width = "100%";
miniImage.style.height = "100%";
miniImage.style.objectFit = "contain";
miniContainer.appendChild(miniImage);
document.body.appendChild(miniContainer);
const miniQuarky = {
container: miniContainer,
image: miniImage,
x: startX,
y: startY,
vx: (Math.random() - MINI_QUARKY_VELOCITY_CENTER) * MINI_QUARKY_VELOCITY_MULTIPLIER,
vy: (Math.random() - MINI_QUARKY_VELOCITY_CENTER) * MINI_QUARKY_VELOCITY_MULTIPLIER,
animationInterval: null
};
miniQuarkies.push(miniQuarky);
// Start movement loop
miniQuarky.animationInterval = setInterval(() => {
miniQuarky.x += miniQuarky.vx;
miniQuarky.y += miniQuarky.vy;
// Bounce off edges
if (miniQuarky.x <= 0 || miniQuarky.x >= window.innerWidth - MINI_QUARKY_SIZE) {
miniQuarky.vx *= -1;
miniQuarky.x = Math.max(0, Math.min(miniQuarky.x, window.innerWidth - MINI_QUARKY_SIZE));
}
if (miniQuarky.y <= 0 || miniQuarky.y >= window.innerHeight - MINI_QUARKY_SIZE) {
miniQuarky.vy *= -1;
miniQuarky.y = Math.max(0, Math.min(miniQuarky.y, window.innerHeight - MINI_QUARKY_SIZE));
}
// Occasionally change direction randomly
if (Math.random() < DIRECTION_CHANGE_PROBABILITY) {
miniQuarky.vx = (Math.random() - MINI_QUARKY_VELOCITY_CENTER) * DIRECTION_CHANGE_VELOCITY_MULTIPLIER;
miniQuarky.vy = (Math.random() - MINI_QUARKY_VELOCITY_CENTER) * DIRECTION_CHANGE_VELOCITY_MULTIPLIER;
}
miniContainer.style.left = miniQuarky.x + "px";
miniContainer.style.top = miniQuarky.y + "px";
}, MINI_QUARKY_ANIMATION_INTERVAL_MS);
}
/**
* Clean up all mini Quarkies
*/
function cleanupMiniQuarkies() {
// eslint-disable-line @typescript-eslint/no-unused-vars
miniQuarkies.forEach((mini) => {
clearInterval(mini.animationInterval);
mini.container.remove();
});
miniQuarkies = [];
}
export default function setup() {
quarkyImage = document.getElementById("quarkyImage");
quarkyContainer = document.getElementById("quarkyContainer");
speechBubble = document.getElementById("quarkySpeechBubble");
// Start the animation sequence
playNextAnimation();
//Install mouse move handler
window.addEventListener("mousemove", mouseMoveHandler);
// Click-to-point feature installation
window.addEventListener("click", (e) => {
clickToPoint(e);
});
// Spawn mini Quarkies on a random timer
function scheduleNextMiniQuarkySpawn() {
const delayMs = MINI_QUARKY_SPAWN_BASE_DELAY_MS + Math.random() * MINI_QUARKY_SPAWN_DELAY_RANGE_MS;
setTimeout(() => {
spawnMiniQuarky();
scheduleNextMiniQuarkySpawn();
}, delayMs);
}
scheduleNextMiniQuarkySpawn();
}

View File

@@ -6,14 +6,12 @@ import router from "@/router";
import vuetify from "@/plugins/vuetify";
import axios from "axios";
import setup from "@/lib/quarky.js";
type PhotonClientRuntimeMode = "production" | "development" | "local-network-development";
const runtimeMode: PhotonClientRuntimeMode = process.env.NODE_ENV as PhotonClientRuntimeMode;
let backendHost: string;
let backendHostname: string;
switch (runtimeMode as PhotonClientRuntimeMode) {
switch (runtimeMode) {
case "development":
backendHost = `${location.hostname}:5800`;
backendHostname = location.hostname;
@@ -47,4 +45,3 @@ app.use(pinia);
app.use(vuetify);
app.use(router);
app.mount("#app");
setup();

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"
@@ -233,12 +202,7 @@ const getMatchedDevice = (info: PVCameraInfo | undefined): PVCameraInfo => {
color="buttonPassive"
style="width: 100%"
:variant="theme.global.current.value.dark ? 'outlined' : 'elevated'"
@click="
setCameraView(
module.matchedCameraInfo,
cameraConnected(cameraInfoFor(module.matchedCameraInfo).uniquePath)
)
"
@click="setCameraView(module.matchedCameraInfo, cameraConnected(module.matchedCameraInfo.uniquePath))"
>
<span>Details</span>
</v-btn>
@@ -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>
@@ -327,12 +291,7 @@ const getMatchedDevice = (info: PVCameraInfo | undefined): PVCameraInfo => {
color="buttonPassive"
style="width: 100%"
:variant="theme.global.current.value.dark ? 'outlined' : 'elevated'"
@click="
setCameraView(
module.matchedCameraInfo,
cameraConnected(cameraInfoFor(module.matchedCameraInfo).uniquePath)
)
"
@click="setCameraView(module.matchedCameraInfo, cameraConnected(module.matchedCameraInfo.uniquePath))"
>
<span>Details</span>
</v-btn>
@@ -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>
@@ -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

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);
@@ -111,7 +121,7 @@ const showCameraSetupDialog = ref(useCameraSettingsStore().needsCameraConfigurat
</span>
</v-alert>
<v-alert
v-if="fpsLimitWarningShown"
v-if="fpsLimitedCameras"
class="mb-3"
color="error"
density="compact"
@@ -119,10 +129,22 @@ const showCameraSetupDialog = ref(useCameraSettingsStore().needsCameraConfigurat
: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"

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

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

View File

@@ -17,10 +17,11 @@
package org.photonvision.common.configuration;
import com.fasterxml.jackson.core.JsonProcessingException;
import edu.wpi.first.apriltag.AprilTagFieldLayout;
import edu.wpi.first.apriltag.AprilTagFields;
import io.avaje.json.JsonException;
import io.avaje.jsonb.Jsonb;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.nio.file.Files;
@@ -36,10 +37,9 @@ import java.util.stream.Stream;
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.pipeline.CVPipelineSettings;
import org.photonvision.vision.pipeline.DriverModePipelineSettings;
import org.photonvision.vision.processes.VisionSource;
import org.wpilib.vision.apriltag.AprilTagFieldLayout;
import org.wpilib.vision.apriltag.AprilTagFields;
import org.zeroturnaround.zip.ZipUtil;
class LegacyConfigProvider extends ConfigProvider {
@@ -126,14 +126,13 @@ class LegacyConfigProvider extends ConfigProvider {
AprilTagFieldLayout atfl = null;
if (hardwareConfigFile.exists()) {
try {
hardwareConfig =
JacksonUtils.deserialize(hardwareConfigFile.toPath(), HardwareConfig.class);
try (var stream = new FileInputStream(hardwareConfigFile)) {
hardwareConfig = Jsonb.instance().type(HardwareConfig.class).fromJson(stream);
if (hardwareConfig == null) {
logger.error("Could not deserialize hardware config! Loading defaults");
hardwareConfig = new HardwareConfig();
}
} catch (IOException e) {
} catch (IOException | IllegalStateException | JsonException e) {
logger.error("Could not deserialize hardware config! Loading defaults");
hardwareConfig = new HardwareConfig();
}
@@ -143,14 +142,13 @@ class LegacyConfigProvider extends ConfigProvider {
}
if (hardwareSettingsFile.exists()) {
try {
hardwareSettings =
JacksonUtils.deserialize(hardwareSettingsFile.toPath(), HardwareSettings.class);
try (var stream = new FileInputStream(hardwareSettingsFile)) {
hardwareSettings = Jsonb.instance().type(HardwareSettings.class).fromJson(stream);
if (hardwareSettings == null) {
logger.error("Could not deserialize hardware settings! Loading defaults");
hardwareSettings = new HardwareSettings();
}
} catch (IOException e) {
} catch (IOException | IllegalStateException | JsonException e) {
logger.error("Could not deserialize hardware settings! Loading defaults");
hardwareSettings = new HardwareSettings();
}
@@ -160,13 +158,13 @@ class LegacyConfigProvider extends ConfigProvider {
}
if (networkConfigFile.exists()) {
try {
networkConfig = JacksonUtils.deserialize(networkConfigFile.toPath(), NetworkConfig.class);
try (var stream = new FileInputStream(networkConfigFile)) {
networkConfig = Jsonb.instance().type(NetworkConfig.class).fromJson(stream);
if (networkConfig == null) {
logger.error("Could not deserialize network config! Loading defaults");
networkConfig = new NetworkConfig();
}
} catch (IOException e) {
} catch (IOException | IllegalStateException | JsonException e) {
logger.error("Could not deserialize network config! Loading defaults");
networkConfig = new NetworkConfig();
}
@@ -184,13 +182,12 @@ class LegacyConfigProvider extends ConfigProvider {
}
if (apriltagFieldLayoutFile.exists()) {
try {
atfl =
JacksonUtils.deserialize(apriltagFieldLayoutFile.toPath(), AprilTagFieldLayout.class);
try (var stream = new FileInputStream(apriltagFieldLayoutFile)) {
atfl = Jsonb.instance().type(AprilTagFieldLayout.class).fromJson(stream);
if (atfl == null) {
logger.error("Could not deserialize apriltag field layout! (still null)");
}
} catch (IOException e) {
} catch (IOException | IllegalStateException | JsonException e) {
logger.error("Could not deserialize apriltag field layout!", e);
atfl = null; // not required, nice to be explicit
}
@@ -227,14 +224,14 @@ class LegacyConfigProvider extends ConfigProvider {
// Delete old configs
FileUtils.deleteDirectory(camerasFolder.toPath());
try {
JacksonUtils.serialize(networkConfigFile.toPath(), config.getNetworkConfig());
} catch (IOException e) {
try (var stream = new FileOutputStream(networkConfigFile)) {
Jsonb.instance().type(NetworkConfig.class).toJson(config.getNetworkConfig(), stream);
} catch (IOException | IllegalStateException | JsonException e) {
logger.error("Could not save network config!", e);
}
try {
JacksonUtils.serialize(hardwareSettingsFile.toPath(), config.getHardwareSettings());
} catch (IOException e) {
try (var stream = new FileOutputStream(hardwareSettingsFile)) {
Jsonb.instance().type(HardwareSettings.class).toJson(config.getHardwareSettings(), stream);
} catch (IOException | IllegalStateException | JsonException e) {
logger.error("Could not save hardware config!", e);
}
@@ -249,33 +246,11 @@ class LegacyConfigProvider extends ConfigProvider {
subdir.toFile().mkdirs();
}
try {
JacksonUtils.serialize(Path.of(subdir.toString(), "config.json"), camConfig);
} catch (IOException e) {
try (var stream = new FileOutputStream(Path.of(subdir.toString(), "config.json").toFile())) {
Jsonb.instance().type(CameraConfiguration.class).toJson(camConfig, stream);
} catch (IOException | IllegalStateException | JsonException e) {
logger.error("Could not save config.json for " + subdir, e);
}
try {
JacksonUtils.serialize(
Path.of(subdir.toString(), "drivermode.json"), camConfig.driveModeSettings);
} catch (IOException e) {
logger.error("Could not save drivermode.json for " + subdir, e);
}
for (var pipe : camConfig.pipelineSettings) {
var pipePath = Path.of(subdir.toString(), "pipelines", pipe.pipelineNickname + ".json");
if (!pipePath.getParent().toFile().exists()) {
// TODO: check for error
pipePath.getParent().toFile().mkdirs();
}
try {
JacksonUtils.serialize(pipePath, pipe);
} catch (IOException e) {
logger.error("Could not save " + pipe.pipelineNickname + ".json!", e);
}
}
}
logger.info("Settings saved!");
return false; // TODO, deal with this. Do I need to?
@@ -289,11 +264,9 @@ class LegacyConfigProvider extends ConfigProvider {
for (var subdir : subdirectories) {
var cameraConfigPath = Path.of(subdir.toString(), "config.json");
CameraConfiguration loadedConfig = null;
try {
loadedConfig =
JacksonUtils.deserialize(
cameraConfigPath.toAbsolutePath(), CameraConfiguration.class);
} catch (JsonProcessingException e) {
try (var stream = new FileInputStream(cameraConfigPath.toFile())) {
loadedConfig = Jsonb.instance().type(CameraConfiguration.class).fromJson(stream);
} catch (IllegalStateException | JsonException e) {
logger.error("Camera config deserialization failed!", e);
e.printStackTrace();
}
@@ -302,63 +275,6 @@ class LegacyConfigProvider extends ConfigProvider {
continue; // TODO how do we later try to load this camera if it gets reconnected?
}
// At this point we have only loaded the base stuff
// We still need to deserialize pipelines, as well as
// driver mode settings
var driverModeFile = Path.of(subdir.toString(), "drivermode.json");
DriverModePipelineSettings driverMode;
try {
driverMode =
JacksonUtils.deserialize(
driverModeFile.toAbsolutePath(), DriverModePipelineSettings.class);
} catch (JsonProcessingException e) {
logger.error("Could not deserialize drivermode.json! Loading defaults");
logger.debug(Arrays.toString(e.getStackTrace()));
driverMode = new DriverModePipelineSettings();
}
if (driverMode == null) {
logger.warn(
"Could not load camera " + subdir + "'s drivermode.json! Loading" + " default");
driverMode = new DriverModePipelineSettings();
}
// Load pipelines by mapping the files within the pipelines subdir
// to their deserialized equivalents
var pipelineSubdirectory = Path.of(subdir.toString(), "pipelines");
List<CVPipelineSettings> settings = Collections.emptyList();
if (pipelineSubdirectory.toFile().exists()) {
try (Stream<Path> subdirectoryFiles = Files.list(pipelineSubdirectory)) {
settings =
subdirectoryFiles
.filter(p -> p.toFile().isFile())
.map(
p -> {
var relativizedFilePath =
configDirectoryFile
.toPath()
.toAbsolutePath()
.relativize(p)
.toString();
try {
return JacksonUtils.deserialize(p, CVPipelineSettings.class);
} catch (JsonProcessingException e) {
logger.error("Exception while deserializing " + relativizedFilePath, e);
} catch (IOException e) {
logger.warn(
"Could not load pipeline at "
+ relativizedFilePath
+ "! Skipping...");
}
return null;
})
.filter(Objects::nonNull)
.toList();
}
}
loadedConfig.driveModeSettings = driverMode;
loadedConfig.addPipelineSettings(settings);
loadedConfigurations.put(subdir.toFile().getName(), loadedConfig);
}
} catch (IOException e) {

View File

@@ -17,16 +17,17 @@
package org.photonvision.common.configuration;
import com.fasterxml.jackson.annotation.JsonAlias;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonProperty;
import io.avaje.jsonb.Json;
import org.photonvision.common.hardware.Platform;
import org.photonvision.common.networking.NetworkMode;
@Json
public class NetworkConfig {
// Can be an integer team number, or an IP address
// MIGRATION: 2023
@Json.Alias("teamNumber")
public String ntServerAddress = "0";
public NetworkMode connectionType = NetworkMode.DHCP;
public String staticIp = "";
public String hostname = "photonvision";
@@ -34,8 +35,8 @@ public class NetworkConfig {
public boolean shouldManage;
public boolean shouldPublishProto = false;
@JsonIgnore public static final String NM_IFACE_STRING = "${interface}";
@JsonIgnore public static final String NM_IP_STRING = "${ipaddr}";
public static final String NM_IFACE_STRING = "${interface}";
public static final String NM_IP_STRING = "${ipaddr}";
public String networkManagerIface = "";
// TODO: remove these strings if no longer needed
@@ -50,19 +51,17 @@ public class NetworkConfig {
setShouldManage(deviceCanManageNetwork());
}
@JsonCreator
public NetworkConfig(
@JsonProperty("ntServerAddress") @JsonAlias({"ntServerAddress", "teamNumber"})
String ntServerAddress,
@JsonProperty("connectionType") NetworkMode connectionType,
@JsonProperty("staticIp") String staticIp,
@JsonProperty("hostname") String hostname,
@JsonProperty("runNTServer") boolean runNTServer,
@JsonProperty("shouldManage") boolean shouldManage,
@JsonProperty("shouldPublishProto") boolean shouldPublishProto,
@JsonProperty("networkManagerIface") String networkManagerIface,
@JsonProperty("setStaticCommand") String setStaticCommand,
@JsonProperty("setDHCPcommand") String setDHCPcommand) {
String ntServerAddress,
NetworkMode connectionType,
String staticIp,
String hostname,
boolean runNTServer,
boolean shouldManage,
boolean shouldPublishProto,
String networkManagerIface,
String setStaticCommand,
String setDHCPcommand) {
this.ntServerAddress = ntServerAddress;
this.connectionType = connectionType;
this.staticIp = staticIp;
@@ -89,12 +88,10 @@ public class NetworkConfig {
config.setDHCPcommand);
}
@JsonIgnore
public String getPhysicalInterfaceName() {
return this.networkManagerIface;
}
@JsonIgnore
public String getEscapedInterfaceName() {
return "\"" + networkManagerIface + "\"";
}
@@ -103,7 +100,6 @@ public class NetworkConfig {
this.shouldManage = shouldManage && this.deviceCanManageNetwork();
}
@JsonIgnore
protected boolean deviceCanManageNetwork() {
return Platform.isLinux();
}

View File

@@ -318,7 +318,7 @@ public class NeuralNetworkModelManager {
}
ModelProperties properties =
ConfigManager.getInstance().getConfig().neuralNetworkPropertyManager().getModel(path);
ConfigManager.getInstance().getConfig().getNeuralNetworkProperties().getModel(path);
if (properties == null) {
logger.warn(
@@ -332,7 +332,7 @@ public class NeuralNetworkModelManager {
// NeuralNetworkModelsSettings
ConfigManager.getInstance()
.getConfig()
.neuralNetworkPropertyManager()
.getNeuralNetworkProperties()
.addModelProperties(properties);
} catch (IllegalArgumentException | IOException e) {
logger.error("Failed to translate legacy model filename to properties: " + path, e);
@@ -486,7 +486,7 @@ public class NeuralNetworkModelManager {
.getConfig()
.setNeuralNetworkProperties(
supportedProperties.sum(
ConfigManager.getInstance().getConfig().neuralNetworkPropertyManager()));
ConfigManager.getInstance().getConfig().getNeuralNetworkProperties()));
}
public boolean clearModels() {
@@ -511,7 +511,7 @@ public class NeuralNetworkModelManager {
}
// Delete model info
return ConfigManager.getInstance().getConfig().neuralNetworkPropertyManager().clear();
return ConfigManager.getInstance().getConfig().getNeuralNetworkProperties().clear();
}
public File exportSingleModel(String modelPath) {
@@ -525,7 +525,7 @@ public class NeuralNetworkModelManager {
ModelProperties properties =
ConfigManager.getInstance()
.getConfig()
.neuralNetworkPropertyManager()
.getNeuralNetworkProperties()
.getModel(Path.of(modelPath));
String fileName = "";

View File

@@ -17,9 +17,10 @@
package org.photonvision.common.configuration;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonProperty;
import io.avaje.jsonb.Json;
import io.avaje.jsonb.JsonType;
import io.avaje.jsonb.Jsonb;
import io.avaje.jsonb.Types;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
@@ -27,27 +28,29 @@ import java.nio.file.Paths;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.photonvision.common.configuration.NeuralNetworkModelManager.Family;
import org.photonvision.common.configuration.NeuralNetworkModelManager.Version;
@Json
public class NeuralNetworkModelsSettings {
/*
* The properties of the model. This is used to determine which model to load.
* The only families currently supported are RKNN and Rubik (custom .tflite)
*/
@Json
public record ModelProperties(
@JsonProperty("modelPath") Path modelPath,
@JsonProperty("nickname") String nickname,
@JsonProperty("labels") List<String> labels,
@JsonProperty("resolutionWidth") int resolutionWidth,
@JsonProperty("resolutionHeight") int resolutionHeight,
@JsonProperty("family") Family family,
@JsonProperty("version") Version version) {
@JsonCreator
public ModelProperties {}
Path modelPath,
String nickname,
List<String> labels,
int resolutionWidth,
int resolutionHeight,
Family family,
Version version) {
ModelProperties(ModelProperties other) {
this(
other.modelPath,
@@ -59,13 +62,6 @@ public class NeuralNetworkModelsSettings {
other.version);
}
// In v2025.3.1, this was single string for the model path. but the first argument
// is now nickname
public ModelProperties(@JsonProperty("nickname") String filename)
throws IllegalArgumentException, IOException {
this(createFromFilename(filename));
}
// ============= Migration code from v2025.3.1 ===========
private static Pattern modelPattern =
@@ -160,25 +156,58 @@ public class NeuralNetworkModelsSettings {
// The path to the model is used as the key in the map because it is unique to
// the model, and should not change
@JsonProperty("modelPathToProperties")
@Json.Ignore
private HashMap<Path, ModelProperties> modelPathToProperties =
new HashMap<Path, ModelProperties>();
/**
* Constructor for the NeuralNetworkProperties class.
*
* <p>This object holds a LinkedList of {@link ModelProperties} objects
* <p>This object holds a HashMap of {@link ModelProperties} objects
*/
public NeuralNetworkModelsSettings() {}
/**
* Constructor for the NeuralNetworkProperties class.
*
* <p>This object holds a LinkedList of {@link ModelProperties} objects.
* <p>This object holds a HashMap of {@link ModelProperties} objects.
*
* @param modelPropertiesMap When the class is constructed, it will hold the provided map
*/
public NeuralNetworkModelsSettings(HashMap<Path, ModelProperties> modelPropertiesMap) {
modelPathToProperties = modelPropertiesMap;
}
/**
* Constructor for the NeuralNetworkProperties class.
*
* <p>This object holds a HashMap of {@link ModelProperties} objects.
*
* @param modelPropertiesList When the class is constructed, it will hold the provided list
*/
public NeuralNetworkModelsSettings(HashMap<Path, ModelProperties> modelPropertiesList) {}
@Json.Creator
public NeuralNetworkModelsSettings(
ModelProperties[] models, @Json.Unmapped Map<String, Object> unmapped) {
JsonType<Map<String, ModelProperties>> modelPropsMapJsonb =
Jsonb.instance().type(Types.mapOf(ModelProperties.class));
Stream<ModelProperties> modelPropsStream;
if (models != null) {
modelPropsStream = Arrays.stream(models);
} else if (unmapped.containsKey("modelPathToProperties")) {
// MIGRATION: 2026
modelPropsStream =
modelPropsMapJsonb.fromObject(unmapped.get("modelPathToProperties")).values().stream();
} else {
modelPropsStream = Stream.empty();
}
this(
modelPropsStream.collect(
Collectors.toMap(
(model) -> model.modelPath(),
(model) -> model,
(prev, next) -> next,
HashMap::new)));
}
@Override
public String toString() {
@@ -239,7 +268,7 @@ public class NeuralNetworkModelsSettings {
*
* @return A list of all models
*/
@JsonIgnore
@Json.Property("models")
public ModelProperties[] getModels() {
return modelPathToProperties.values().toArray(new ModelProperties[0]);
}

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