Compare commits

..

5 Commits

Author SHA1 Message Date
Matt
6e7192671d Update main.yml 2021-09-24 16:38:44 -04:00
Matt
11b4504cd6 Update versioningHelper.gradle 2021-09-24 16:35:56 -04:00
Matt
2eb0cbe280 Update versioningHelper.gradle 2021-09-24 16:33:29 -04:00
Matt
1331faafb9 aa 2021-09-24 16:27:25 -04:00
Matt
2122bba268 Fix capitalization of "dev" 2021-09-24 16:24:33 -04:00
836 changed files with 35697 additions and 60346 deletions

37
.gitattributes vendored
View File

@@ -1,37 +0,0 @@
# Set default behavior to automatically normalize line endings (LF on check-in).
* text=auto
# Force batch scripts to always use CRLF line endings so that if a repo is accessed
# in Windows via a file share from Linux, the scripts will work.
*.{cmd,[cC][mM][dD]} text eol=crlf
*.{bat,[bB][aA][tT]} text eol=crlf
*.{ics,[iI][cC][sS]} text eol=crlf
# Force bash scripts to always use LF line endings so that if a repo is accessed
# in Unix via a file share from Windows, the scripts will work.
*.sh text eol=lf
# Ensure Spotless does not try to use CRLF line endings on Windows in the local repo.
*.gradle text eol=lf
*.java text eol=lf
*.json text eol=lf
*.md text eol=lf
*.xml text eol=lf
*.h text eol=lf
*.hpp text eol=lf
*.inc text eol=lf
*.inl text eol=lf
*.cpp text eol=lf
# Frontend Files
*.js text eol=lf
*.vue text eol=lf
# Denote all files that are truly binary and should not be modified.
*.png binary
*.jpg binary
*.jpeg binary
*.gif binary
*.so binary
*.dll binary
*.webp binary

1
.github/CODEOWNERS vendored
View File

@@ -1,2 +1,3 @@
# These owners will be the default owners for everything in the repo.
* @PhotonVision/program-devs

View File

@@ -1,31 +0,0 @@
---
name: Bug report
about: Create a report to help us improve
title: ''
labels: bug
assignees: ''
---
**Describe the bug**
A clear and concise description of what the bug is and what the expected behavior should have been.
**To Reproduce**
Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
**Screenshots / Videos**
If applicable, add screenshots to help explain your problem. Additionally, provide journalctl logs and settings zip export. If your issue is regarding the web dashboard, please provide screenshots and the output of the browser console.
**Platform:**
- Hardware Platform (ex. Raspberry Pi 4, Windows x64):
- Network Configuration (Connection between the Radio and any devices in between, such as a Network Switch):
- PhotonVision Version:
- Browser (with Version) (Chrome, Edge, Firefox, etc.):
- Camera(s) Used:
**Additional context**
Add any other context about the problem here.

View File

@@ -1,20 +0,0 @@
---
name: Feature request
about: Suggest an idea for this project
title: ''
labels: enhancement
assignees: ''
---
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.
**Additional context**
Add any other context or screenshots about the feature request here.

View File

@@ -1,81 +0,0 @@
name: Build
on:
push:
branches:
- master
tags:
- 'v*'
pull_request:
branches: [ master ]
jobs:
build-photonlib-host:
env:
MACOSX_DEPLOYMENT_TARGET: 12
strategy:
fail-fast: false
matrix:
include:
- os: windows-2022
artifact-name: Win64
architecture: x64
- os: macos-12
artifact-name: macOS
architecture: x64
- os: ubuntu-22.04
artifact-name: Linux
name: "Photonlib - Build Host - ${{ matrix.artifact-name }}"
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Install Java 17
uses: actions/setup-java@v4
with:
java-version: 17
distribution: temurin
- run: git fetch --tags --force
- run: |
chmod +x gradlew
./gradlew photon-lib:build --max-workers 1
- run: ./gradlew photon-lib:publish photon-targeting:publish
name: Publish
env:
ARTIFACTORY_API_KEY: ${{ secrets.ARTIFACTORY_API_KEY }}
if: github.event_name == 'push'
build-photonlib-docker:
strategy:
fail-fast: false
matrix:
include:
- container: wpilib/roborio-cross-ubuntu:2024-22.04
artifact-name: Athena
- container: wpilib/raspbian-cross-ubuntu:bullseye-22.04
artifact-name: Raspbian
- container: wpilib/aarch64-cross-ubuntu:bullseye-22.04
artifact-name: Aarch64
runs-on: ubuntu-22.04
container: ${{ matrix.container }}
name: "Photonlib - Build Docker - ${{ matrix.artifact-name }}"
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Config Git
run: |
git config --global --add safe.directory /__w/photonvision/photonvision
- name: Build PhotonLib
run: |
chmod +x gradlew
./gradlew photon-lib:build --max-workers 1
- name: Publish
run: |
chmod +x gradlew
./gradlew photon-lib:publish photon-targeting:publish
env:
ARTIFACTORY_API_KEY: ${{ secrets.ARTIFACTORY_API_KEY }}
if: github.event_name == 'push'

313
.github/workflows/main.yml vendored Normal file
View File

@@ -0,0 +1,313 @@
# This workflow builds the client (UI), the server, builds the JAR.
name: CI
# Controls when the action will run. Triggers the workflow on push or pull request
# events but only for the master branch
on:
push:
branches: [ master ]
tags:
- 'v*'
pull_request:
branches: [ master ]
jobs:
# This job builds the client (web view).
photonclient-build:
# Let all steps run within the photon-client dir.
defaults:
run:
working-directory: photon-client
# The type of runner that the job will run on.
runs-on: ubuntu-latest
# Grab the docker container.
container:
image: docker://node:10
steps:
# Checkout code.
- uses: actions/checkout@v1
# Setup Node.js
- name: Setup Node.js
uses: actions/setup-node@v1
with:
node-version: 10
# Run npm
- run: |
npm ci
npm run build --if-present
# Upload client artifact.
- uses: actions/upload-artifact@master
with:
name: built-client
path: photon-client/dist/
photon-build-all:
# The type of runner that the job will run on.
runs-on: ubuntu-latest
steps:
# Checkout code.
- name: Checkout code
uses: actions/checkout@v1
# Fetch tags.
- name: Fetch tags
run: git fetch --tags --force
# Install Java 11.
- name: Install Java 11
uses: actions/setup-java@v1
with:
java-version: 11
# Run Gradle build.
- name: Gradle Build
run: |
chmod +x gradlew
./gradlew build -x check --max-workers 1
# Run Gradle Tests.
- name: Gradle Tests
run: ./gradlew testHeadless -i --max-workers 1
# Generate Coverage Report.
- name: Gradle Coverage
run: ./gradlew jacocoTestReport --max-workers 1
# Publish Coverage Report.
- name: Publish Server Coverage Report
uses: codecov/codecov-action@v1
with:
file: ./photon-server/build/reports/jacoco/test/jacocoTestReport.xml
- name: Publish Core Coverage Report
uses: codecov/codecov-action@v1
with:
file: ./photon-core/build/reports/jacoco/test/jacocoTestReport.xml
photonserver-build-offline-docs:
runs-on: ubuntu-latest
steps:
# Checkout docs.
- uses: actions/checkout@v2
with:
repository: 'PhotonVision/photonvision-docs.git'
ref: master
# Install Python.
- uses: actions/setup-python@v2
with:
python-version: '3.6'
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install sphinx sphinx_rtd_theme sphinx-tabs sphinxext-opengraph doc8
pip install -r requirements.txt
- name: Check the docs
run: |
make linkcheck
make lint
- name: Build the docs
run: |
make html
# Upload docs artifact.
- uses: actions/upload-artifact@master
with:
name: built-docs
path: build/html
photonserver-check-lint:
# The type of runner that the job will run on.
runs-on: ubuntu-latest
steps:
# Checkout code.
- uses: actions/checkout@v1
# Install Java 11.
- uses: actions/setup-java@v1
with:
java-version: 11
# Check server code with Spotless.
- run: |
chmod +x gradlew
./gradlew spotlessCheck
photon-release:
if: startsWith(github.ref, 'refs/tags/v')
needs: [photon-build-package]
runs-on: ubuntu-latest
steps:
- uses: actions/download-artifact@v2
with:
name: jar
- uses: softprops/action-gh-release@v1
with:
files: '**/*'
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# Building photonlib
photonlib-build-host:
strategy:
fail-fast: false
matrix:
include:
- os: windows-latest
artifact-name: Win64
- os: macos-latest
artifact-name: macOS
- os: ubuntu-latest
artifact-name: Linux
runs-on: ${{ matrix.os }}
name: "Photonlib - Build - ${{ matrix.artifact-name }}"
steps:
- uses: actions/checkout@v2.3.4
with:
fetch-depth: 0
- uses: actions/setup-java@v1
with:
java-version: 11
- run: git fetch --tags --force
- run: |
chmod +x gradlew
./gradlew photon-lib:build --max-workers 1
- run: ./gradlew photon-lib:publish
name: Publish
env:
ARTIFACTORY_API_KEY: ${{ secrets.ARTIFACTORY_API_KEY }}
if: github.event_name == 'push'
photonlib-build-docker:
strategy:
fail-fast: false
matrix:
include:
- container: wpilib/roborio-cross-ubuntu:2021-18.04
artifact-name: Athena
- container: wpilib/raspbian-cross-ubuntu:10-18.04
artifact-name: Raspbian
- container: wpilib/aarch64-cross-ubuntu:bionic-18.04
artifact-name: Aarch64
runs-on: ubuntu-latest
container: ${{ matrix.container }}
name: "Photonlib - Build - ${{ matrix.artifact-name }}"
steps:
- uses: actions/checkout@v2.3.4
with:
fetch-depth: 0
- uses: actions/setup-java@v1
with:
java-version: 11
- run: |
git describe --tags --exclude="dev" --exclude="[dD]ev*"
- run: |
chmod +x gradlew
./gradlew photon-lib:build --max-workers 1
- run: |
chmod +x gradlew
./gradlew photon-lib:publish
env:
ARTIFACTORY_API_KEY: ${{ secrets.ARTIFACTORY_API_KEY }}
if: github.event_name == 'push'
photonlib-wpiformat:
name: "wpiformat"
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Fetch all history and metadata
run: |
git fetch --prune --unshallow
git checkout -b pr
git branch -f master origin/master
- name: Set up Python 3.8
uses: actions/setup-python@v2
with:
python-version: 3.8
- name: Install clang-format
run: sudo apt-get update -q && sudo apt-get install clang-format-10
- name: Install wpiformat
run: pip3 install wpiformat
- name: Run
run: wpiformat -clang 10 -f photon-lib
- name: Check Output
run: git --no-pager diff --exit-code HEAD
- name: Generate diff
run: git diff HEAD > wpiformat-fixes.patch
if: ${{ failure() }}
- uses: actions/upload-artifact@v2
with:
name: wpiformat fixes
path: wpiformat-fixes.patch
if: ${{ failure() }}
photon-build-package:
needs: [photonclient-build, photon-build-all, photonserver-build-offline-docs, photonlib-build-host, photonlib-build-docker]
# The type of runner that the job will run on.
runs-on: ubuntu-latest
steps:
# Checkout code.
- uses: actions/checkout@v1
# Install Java 11.
- uses: actions/setup-java@v1
with:
java-version: 11
# Clear any existing web resources.
- run: |
rm -rf photon-server/src/main/resources/web/*
mkdir -p photon-server/src/main/resources/web/docs
# Download client artifact to resources folder.
- uses: actions/download-artifact@v2
with:
name: built-client
path: photon-server/src/main/resources/web/
# Download docs artifact to resources folder.
- uses: actions/download-artifact@v2
with:
name: built-docs
path: photon-server/src/main/resources/web/docs
# Build fat jar.
- run: |
chmod +x gradlew
./gradlew photon-server:shadowJar --max-workers 1
# Upload final fat jar as artifact.
- uses: actions/upload-artifact@master
with:
name: jar
path: photon-server/build/libs
- uses: eine/tip@master
with:
token: ${{ secrets.GITHUB_TOKEN }}
tag: 'Dev'
rm: true
files: |
photon-server/build/libs/*.jar
if: github.event_name == 'push'

31
.gitignore vendored
View File

@@ -10,8 +10,6 @@ Python/app/handlers/__pycache__/
\.vscode/
/.vs
backend/settings/
/.vscode/
# Compiled class file
@@ -32,7 +30,6 @@ backend/settings/
*.nar
*.ear
*.zip
*.xz
*.tar.gz
*.rar
@@ -114,17 +111,10 @@ fabric.properties
**/.settings
**/.classpath
**/.project
**/settings
**/dependency-reduced-pom.xml
# photon-server/photon-vision.iml
# compile_commands
compile_commands.json
# clang configuration and clangd cache
.clang
.clangd/
.cache/
New client/photon-client/*
*.prefs
@@ -144,23 +134,4 @@ build/spotlessJava
build/*
build
photon-lib/src/main/java/org/photonvision/PhotonVersion.java
photon-lib/bin/main/images/*
/photonlib-java-examples/bin/
photon-lib/src/generate/native/include/PhotonVersion.h
.gitattributes
lib/*
photon-server/lib/libapriltag.so
photon-server/bin/main/nativelibraries/apriltag/*
photon-server/src/main/resources/nativelibraries/apriltag/*
photonlib-java-examples/*/vendordeps/*
photonlib-cpp-examples/*/vendordeps/*
*/networktables.json
*/networktables.json.bck
photonlib-cpp-examples/*/networktables.json.bck
photonlib-java-examples/*/networktables.json.bck
*.sqlite
photon-server/src/main/resources/web/index.html
venv

View File

@@ -14,3 +14,4 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/

View File

@@ -6,75 +6,18 @@ PhotonVision is the free, fast, and easy-to-use computer vision solution for the
A copy of the latest Raspberry Pi image is available [here](https://github.com/PhotonVision/photon-pi-gen/releases). A copy of the latest standalone JAR is available [here](https://github.com/PhotonVision/photonvision/releases). If you are a Gloworm user you can find the latest Gloworm image [here](https://github.com/gloworm-vision/pi-gen/releases).
If you are interested in contributing code or documentation to the project, please [read our getting started page for contributors](https://docs.photonvision.org/en/latest/docs/contributing/index.html) and **[join the Discord](https://discord.gg/wYxTwym) to introduce yourself!** We hope to provide a welcoming community to anyone who is interested in helping.
If you are interested in contributing code or documentation to the project, please [read our getting started page for contributors](https://docs.photonvision.org/en/latest/docs/other/contributing/index.html) and **[join the Discord](https://discord.gg/wYxTwym) to introduce yourself!** We hope to provide a welcoming community to anyone who is interested in helping.
## Authors
<a href="https://github.com/PhotonVision/photonvision/graphs/contributors">
<img src="https://contrib.rocks/image?repo=PhotonVision/photonvision" />
</a>
## Gradle Arguments
Note that these are case sensitive!
* `-PArchOverride=foobar`: builds for a target system other than your current architecture. Valid overrides are:
* linuxathena
* linuxarm32
* linuxarm64
* arm32
* arm64
* x86-64
* x86
- `-PtgtIp`: Specifies where `./gradlew deploy` should try to copy the fat JAR to
- `-Pprofile`: enables JVM profiling
## Building
Gradle is used for all C++ and Java code, and NPM is used for the web UI. Instructions to compile PhotonVision yourself can be found [in our docs](https://docs.photonvision.org/en/latest/docs/contributing/photonvision/build-instructions.html?highlight=npm%20install#compiling-instructions).
You can run one of the many built in examples straight from the command line, too! They contain a fully featured robot project, and some include simulation support. The projects can be found inside the `photonlib-java-examples` and `photonlib-cpp-examples` subdirectories, respectively. The projects currently available include:
- photonlib-java-examples:
- aimandrange:simulateJava
- aimattarget:simulateJava
- getinrange:simulateJava
- simaimandrange:simulateJava
- simposeest:simulateJava
- photonlib-cpp-examples:
- aimandrange:simulateNative
- getinrange:simulateNative
To run them, use the commands listed below. Photonlib must first be published to your local maven repository, then the `copyPhotonlib` task will copy the generated vendordep json file into each example. After that, the simulateJava/simulateNative task can be used like a normal robot project. Robot simulation with attached debugger is technically possible by using simulateExternalJava and modifying the launch script it exports, though unsupported.
```
~/photonvision$ ./gradlew publishToMavenLocal
~/photonvision$ cd photonlib-java-examples
~/photonvision/photonlib-java-examples$ ./gradlew copyPhotonlib
~/photonvision/photonlib-java-examples$ ./gradlew <example-name>:simulateJava
~/photonvision$ cd photonlib-cpp-examples
~/photonvision/photonlib-cpp-examples$ ./gradlew copyPhotonlib
~/photonvision/photonlib-cpp-examples$ ./gradlew <example-name>:simulateNative
```
## Out-of-Source Dependencies
PhotonVision uses the following additonal out-of-source repositories for building code.
- Base system images for Raspberry Pi & Orange Pi: https://github.com/PhotonVision/photon-image-modifier
- C++ driver for Raspberry Pi CSI cameras: https://github.com/PhotonVision/photon-libcamera-gl-driver
- JNI code for [mrcal](https://mrcal.secretsauce.net/): https://github.com/PhotonVision/mrcal-java
- Custom build of OpenCV with GStreamer/Protobuf/other custom flags: https://github.com/PhotonVision/thirdparty-opencv
- JNI code for aruco-nano: https://github.com/PhotonVision/aruconano-jni
A list of contributors is available in our documentation on ReadTheDocs.
## Acknowledgments
PhotonVision was forked from [Chameleon Vision](https://github.com/Chameleon-Vision/chameleon-vision/). Thank you to everyone who worked on the original project.
* [WPILib](https://github.com/wpilibsuite) - Specifically [cscore](https://github.com/wpilibsuite/allwpilib/tree/master/cscore), [CameraServer](https://github.com/wpilibsuite/allwpilib/tree/master/cameraserver), [NTCore](https://github.com/wpilibsuite/allwpilib/tree/master/ntcore), and [OpenCV](https://github.com/wpilibsuite/thirdparty-opencv).
* [WPILib](https://github.com/wpilibsuite) - Specifically [cscore](https://github.com/wpilibsuite/allwpilib/tree/master/cscore), [CameraServer](https://github.com/wpilibsuite/allwpilib/tree/master/cameraserver), [NTCore](https://github.com/wpilibsuite/allwpilib/tree/master/ntcore), and [OpenCV](https://github.com/wpilibsuite/thirdparty-opencv).
* [Apache Commons](https://commons.apache.org/) - Specifically [Commons Math](https://commons.apache.org/proper/commons-math/), and [Commons Lang](https://commons.apache.org/proper/commons-lang/)
@@ -84,26 +27,5 @@ PhotonVision was forked from [Chameleon Vision](https://github.com/Chameleon-Vis
* [FasterXML](https://github.com/FasterXML) - Specifically [jackson](https://github.com/FasterXML/jackson)
## License
## License
PhotonVision is licensed under the [GNU General Public License](https://www.gnu.org/licenses/gpl-3.0.html)
## Meeting Notes
Our meeting notes can be found in the wiki section of this repository.
* [2020 Meeting Notes](https://github.com/PhotonVision/photonvision/wiki/2020-Meeting-Notes)
* [2021 Meeting Notes](https://github.com/PhotonVision/photonvision/wiki/2021-Meeting-Notes)
## Additional packages
For now, using mrcal requires installing these additional packages on Linux systems:
```
sudo apt install libcholmod3 liblapack3 libsuitesparseconfig5
```
## Documentation
- Our main documentation page: [docs.photonvision.org](https://docs.photonvision.org)
- Photon UI demo: [demo.photonvision.org](https://demo.photonvision.org) (or [manual link](https://photonvision.github.io/photonvision/built-client/))
- Javadocs: [javadocs.photonvision.org](https://javadocs.photonvision.org) (or [manual link](https://photonvision.github.io/photonvision/built-docs/javadoc/))
- C++ Doxygen [cppdocs.photonvision.org](https://cppdocs.photonvision.org) (or [manual link](https://photonvision.github.io/photonvision/built-docs/doxygen/html/))

View File

@@ -1,21 +1,18 @@
import edu.wpi.first.toolchain.*
plugins {
id "com.diffplug.spotless" version "6.24.0"
id "edu.wpi.first.NativeUtils" version "2024.6.1" apply false
id "com.diffplug.gradle.spotless" version "3.28.0"
id "com.github.johnrengelman.shadow" version "5.2.0"
id "com.github.node-gradle.node" version "2.2.4" apply false
id "edu.wpi.first.GradleJni" version "0.10.1"
id "edu.wpi.first.GradleVsCode" version "0.12.0"
id "edu.wpi.first.NativeUtils" version "2021.1.1" apply false
id "edu.wpi.first.wpilib.repositories.WPILibRepositoriesPlugin" version "2020.2"
id "edu.wpi.first.GradleRIO" version "2024.2.1"
id 'edu.wpi.first.WpilibTools' version '1.3.0'
id 'com.google.protobuf' version '0.9.4' apply false
id "org.hidetake.ssh" version "2.10.1"
}
allprojects {
repositories {
mavenCentral()
mavenLocal()
maven { url = "https://maven.photonvision.org/releases" }
maven { url = "https://maven.photonvision.org/snapshots" }
maven { url = "https://jogamp.org/deployment/maven/" }
jcenter()
maven { url = "https://maven.photonvision.org/repository/internal/" }
}
wpilibRepositories.addAllReleaseRepositories(it)
wpilibRepositories.addAllDevelopmentRepositories(it)
@@ -25,87 +22,24 @@ allprojects {
apply from: "versioningHelper.gradle"
ext {
wpilibVersion = "2024.2.1"
wpimathVersion = wpilibVersion
openCVversion = "4.8.0-2"
wpilibVersion = "2021.3.1"
opencvVersion = "3.4.7-5"
joglVersion = "2.4.0-rc-20200307"
javalinVersion = "5.6.2"
photonGlDriverLibVersion = "dev-v2023.1.0-9-g75fc678"
rknnVersion = "dev-v2024.0.0-30-g001b5ec"
frcYear = "2024"
mrcalVersion = "dev-v2024.0.0-7-gc976aaa";
pubVersion = versionString
isDev = pubVersion.startsWith("dev")
// A list, for legacy reasons, with only the current platform contained
wpilibNativeName = wpilibTools.platformMapper.currentPlatform.platformName;
def nativeName = wpilibNativeName
if (wpilibNativeName == "linuxx64") nativeName = "linuxx86-64";
if (wpilibNativeName == "winx64") nativeName = "windowsx86-64";
if (wpilibNativeName == "macx64") nativeName = "osxx86-64";
if (wpilibNativeName == "macarm64") nativeName = "osxarm64";
jniPlatform = nativeName
println("Building for platform " + jniPlatform + " wpilib: " + wpilibNativeName)
println("Using Wpilib: " + wpilibVersion)
println("Using OpenCV: " + openCVversion)
photonMavenURL = 'https://maven.photonvision.org/' + (isDev ? 'snapshots' : 'releases');
println("Publishing Photonlib to " + photonMavenURL)
}
spotless {
java {
target fileTree('.') {
include '**/*.java'
exclude '**/build/**', '**/build-*/**', "photon-core\\src\\main\\java\\org\\photonvision\\PhotonVersion.java", "photon-lib\\src\\main\\java\\org\\photonvision\\PhotonVersion.java"
}
toggleOffOn()
googleJavaFormat()
paddedCell()
indentWithTabs(2)
indentWithSpaces(4)
removeUnusedImports()
trimTrailingWhitespace()
endWithNewline()
}
groovyGradle {
target fileTree('.') {
include '**/*.gradle'
exclude '**/build/**', '**/build-*/**'
}
greclipse()
indentWithSpaces(4)
trimTrailingWhitespace()
endWithNewline()
}
format 'xml', {
target fileTree('.') {
include '**/*.xml'
exclude '**/build/**', '**/build-*/**', "**/.idea/**"
}
eclipseWtp('xml')
trimTrailingWhitespace()
indentWithSpaces(2)
endWithNewline()
}
format 'misc', {
target fileTree('.') {
include '**/*.md', '**/.gitignore'
exclude '**/build/**', '**/build-*/**'
}
trimTrailingWhitespace()
indentWithSpaces(2)
endWithNewline()
java {
target "**/*.java"
licenseHeaderFile "$rootDir/LicenseHeader.txt"
targetExclude("photon-core/src/main/java/org/photonvision/PhotonVersion.java")
}
}
wrapper {
gradleVersion '8.4'
}
ext.getCurrentArch = {
return NativePlatforms.desktop
}

View File

@@ -2,4 +2,4 @@ coverage:
# Turning off commit status to prevent failed checks if coverage decreases
status:
project: no
patch: no
patch: no

View File

@@ -1,255 +0,0 @@
import argparse
import base64
from dataclasses import dataclass
import json
import os
from typing import Union
import cv2
import numpy as np
import mrcal
from wpimath.geometry import Quaternion as _Quat
@dataclass
class Size:
width: int
height: int
@dataclass
class JsonMatOfDoubles:
rows: int
cols: int
type: int
data: list[float]
@dataclass
class JsonMat:
rows: int
cols: int
type: int
data: str # Base64-encoded PNG data
@dataclass
class Point2:
x: float
y: float
@dataclass
class Translation3d:
x: float
y: float
z: float
@dataclass
class Quaternion:
X: float
Y: float
Z: float
W: float
@dataclass
class Rotation3d:
quaternion: Quaternion
@dataclass
class Pose3d:
translation: Translation3d
rotation: Rotation3d
@dataclass
class Point3:
x: float
y: float
z: float
@dataclass
class Observation:
# Expected feature 3d location in the camera frame
locationInObjectSpace: list[Point3]
# Observed location in pixel space
locationInImageSpace: list[Point2]
# (measured location in pixels) - (expected from FK)
reprojectionErrors: list[Point2]
# Solver optimized board poses
optimisedCameraToObject: Pose3d
# If we should use this observation when re-calculating camera calibration
includeObservationInCalibration: bool
snapshotName: str
# The actual image the snapshot is from
snapshotData: JsonMat
@dataclass
class CameraCalibration:
resolution: Size
cameraIntrinsics: JsonMatOfDoubles
distCoeffs: JsonMatOfDoubles
observations: list[Observation]
calobjectWarp: list[float]
calobjectSize: Size
calobjectSpacing: float
def __convert_cal_to_mrcal_cameramodel(
cal: CameraCalibration,
) -> mrcal.cameramodel | None:
if len(cal.distCoeffs.data) == 5:
model = "LENSMODEL_OPENCV5"
elif len(cal.distCoeffs.data) == 8:
model = "LENSMODEL_OPENCV8"
else:
print("Unknown camera model? giving up")
return None
def opencv_to_mrcal_intrinsics(ocv):
return [ocv[0], ocv[4], ocv[2], ocv[5]]
def pose_to_rt(pose: Pose3d):
r = _Quat(
w=pose.rotation.quaternion.W,
x=pose.rotation.quaternion.X,
y=pose.rotation.quaternion.Y,
z=pose.rotation.quaternion.Z,
).toRotationVector()
t = [
pose.translation.x,
pose.translation.y,
pose.translation.z,
]
return np.concatenate((r, t))
imagersize = (cal.resolution.width, cal.resolution.height)
# Always weight=1 for Photon data
WEIGHT = 1
observations_board = np.array(
[
# note that we expect row-major observations here. I think this holds
np.array(
list(map(lambda it: [it.x, it.y, WEIGHT], o.locationInImageSpace))
).reshape((cal.calobjectSize.width, cal.calobjectSize.height, 3))
for o in cal.observations
]
)
optimization_inputs = {
"intrinsics": np.array(
[
opencv_to_mrcal_intrinsics(cal.cameraIntrinsics.data)
+ cal.distCoeffs.data
],
dtype=np.float64,
),
"extrinsics_rt_fromref": np.zeros((0, 6), dtype=np.float64),
"frames_rt_toref": np.array(
[pose_to_rt(o.optimisedCameraToObject) for o in cal.observations]
),
"points": None,
"observations_board": observations_board,
"indices_frame_camintrinsics_camextrinsics": np.array(
[[i, 0, -1] for i in range(len(cal.observations))], dtype=np.int32
),
"observations_point": None,
"indices_point_camintrinsics_camextrinsics": None,
"lensmodel": model,
"imagersizes": np.array([imagersize], dtype=np.int32),
"calobject_warp": np.array(cal.calobjectWarp)
if len(cal.calobjectWarp) > 0
else None,
# We always do all the things
"do_optimize_intrinsics_core": True,
"do_optimize_intrinsics_distortions": True,
"do_optimize_extrinsics": True,
"do_optimize_frames": True,
"do_optimize_calobject_warp": len(cal.calobjectWarp) > 0,
"do_apply_outlier_rejection": True,
"do_apply_regularization": True,
"verbose": False,
"calibration_object_spacing": cal.calobjectSpacing,
"imagepaths": np.array([it.snapshotName for it in cal.observations]),
}
return mrcal.cameramodel(
optimization_inputs=optimization_inputs,
icam_intrinsics=0,
)
def convert_photon_to_mrcal(photon_cal_json_path: str, output_folder: str):
"""
Unpack a Photon calibration JSON (eg, photon_calibration_Microsoft_LifeCam_HD-3000_800x600.json) into
the output_folder directory with images and corners.vnl file for use with mrcal.
"""
with open(photon_cal_json_path, "r") as cal_json:
# Convert to nested objects instead of nameddicts on json-loads
class Generic:
@classmethod
def from_dict(cls, dict):
obj = cls()
obj.__dict__.update(dict)
return obj
camera_cal_data: CameraCalibration = json.loads(
cal_json.read(), object_hook=Generic.from_dict
)
# Create output_folder if not exists
if not os.path.exists(output_folder):
os.makedirs(output_folder)
# Decode each image and save it as a png
for obs in camera_cal_data.observations:
image = obs.snapshotData.data
decoded_data = base64.b64decode(image)
np_data = np.frombuffer(decoded_data, np.uint8)
img = cv2.imdecode(np_data, cv2.IMREAD_UNCHANGED)
cv2.imwrite(f"{output_folder}/{obs.snapshotName}", img)
# And create a VNL file for use with mrcal
with open(f"{output_folder}/corners.vnl", "w+") as vnl_file:
vnl_file.write("# filename x y level\n")
for obs in camera_cal_data.observations:
for corner in obs.locationInImageSpace:
# Always level zero
vnl_file.write(f"{obs.snapshotName} {corner.x} {corner.y} 0\n")
vnl_file.flush()
mrcal_model = __convert_cal_to_mrcal_cameramodel(camera_cal_data)
with open(f"{output_folder}/camera-0.cameramodel", "w+") as mrcal_file:
mrcal_model.write(
mrcal_file,
note="Generated from PhotonVision calibration file: "
+ photon_cal_json_path
+ "\nCalobject_warp (m): "
+ str(camera_cal_data.calobjectWarp),
)
def main():
parser = argparse.ArgumentParser(
description="Convert Photon calibration JSON for use with mrcal"
)
parser.add_argument("input", type=str, help="Path to Photon calibration JSON file")
parser.add_argument(
"output_folder", type=str, help="Output folder for mrcal VNL file + images"
)
args = parser.parse_args()
convert_photon_to_mrcal(args.input, args.output_folder)
if __name__ == "__main__":
main()

View File

@@ -1,287 +0,0 @@
// From allwpilib/docs. Licensed under the WPILib BSD License
plugins {
id 'java'
id "org.ysb33r.doxygen" version "0.7.0"
}
evaluationDependsOn ':photon-targeting'
evaluationDependsOn ':photon-core'
evaluationDependsOn ':photon-server'
evaluationDependsOn ':photon-lib'
def baseArtifactIdCpp = 'documentation'
def artifactGroupIdCpp = 'org.photonvision.wpilibc'
def zipBaseNameCpp = '_GROUP_org.photonvision_cpp_ID_documentation_CLS'
def baseArtifactIdJava = 'documentation'
def artifactGroupIdJava = 'org.photonvision.wpilibj'
def zipBaseNameJava = '_GROUP_org.photonvision_java_ID_documentation_CLS'
def outputsFolder = file("$project.buildDir/outputs")
def cppProjectZips = []
def cppIncludeRoots = []
cppProjectZips.add(project(':photon-lib').cppHeadersZip)
cppProjectZips.add(project(':photon-targeting').cppHeadersZip)
doxygen {
// Doxygen binaries are only provided for x86_64 platforms
// Other platforms will need to provide doxygen via their system
// See below maven and https://doxygen.nl/download.html for provided binaries
String arch = System.getProperty("os.arch");
if (arch.equals("x86_64") || arch.equals("amd64")) {
executables {
doxygen version : '1.9.4',
baseURI : 'https://frcmaven.wpi.edu/artifactory/generic-release-mirror/doxygen'
}
}
}
doxygen {
generate_html true
html_extra_stylesheet 'theme.css'
cppProjectZips.each {
dependsOn it
source it.source
it.ext.includeDirs.each {
cppIncludeRoots.add(it.absolutePath)
}
}
cppIncludeRoots << '../ntcore/build/generated/main/native/include/'
if (project.hasProperty('docWarningsAsErrors')) {
// Eigen
exclude 'Eigen/**'
exclude 'unsupported/**'
// LLVM
exclude 'wpi/AlignOf.h'
exclude 'wpi/Casting.h'
exclude 'wpi/Chrono.h'
exclude 'wpi/Compiler.h'
exclude 'wpi/ConvertUTF.h'
exclude 'wpi/DenseMap.h'
exclude 'wpi/DenseMapInfo.h'
exclude 'wpi/Endian.h'
exclude 'wpi/EpochTracker.h'
exclude 'wpi/Errc.h'
exclude 'wpi/Errno.h'
exclude 'wpi/ErrorHandling.h'
exclude 'wpi/bit.h'
exclude 'wpi/fs.h'
exclude 'wpi/FunctionExtras.h'
exclude 'wpi/function_ref.h'
exclude 'wpi/Hashing.h'
exclude 'wpi/iterator.h'
exclude 'wpi/iterator_range.h'
exclude 'wpi/ManagedStatic.h'
exclude 'wpi/MapVector.h'
exclude 'wpi/MathExtras.h'
exclude 'wpi/MemAlloc.h'
exclude 'wpi/PointerIntPair.h'
exclude 'wpi/PointerLikeTypeTraits.h'
exclude 'wpi/PointerUnion.h'
exclude 'wpi/raw_os_ostream.h'
exclude 'wpi/raw_ostream.h'
exclude 'wpi/SmallPtrSet.h'
exclude 'wpi/SmallSet.h'
exclude 'wpi/SmallString.h'
exclude 'wpi/SmallVector.h'
exclude 'wpi/StringExtras.h'
exclude 'wpi/StringMap.h'
exclude 'wpi/SwapByteOrder.h'
exclude 'wpi/type_traits.h'
exclude 'wpi/VersionTuple.h'
exclude 'wpi/WindowsError.h'
// fmtlib
exclude 'fmt/**'
// libuv
exclude 'uv.h'
exclude 'uv/**'
exclude 'wpinet/uv/**'
// json
exclude 'wpi/adl_serializer.h'
exclude 'wpi/byte_container_with_subtype.h'
exclude 'wpi/detail/**'
exclude 'wpi/json.h'
exclude 'wpi/json_fwd.h'
exclude 'wpi/ordered_map.h'
exclude 'wpi/thirdparty/**'
// memory
exclude 'wpi/memory/**'
// mpack
exclude 'wpi/mpack.h'
// units
exclude 'units/**'
}
//TODO: building memory docs causes search to break
exclude 'wpi/memory/**'
exclude '*.pb.h'
// Save space by excluding protobuf and eigen
exclude 'Eigen/**'
exclude 'google/protobuf/**'
aliases 'effects=\\par <i>Effects:</i>^^',
'notes=\\par <i>Notes:</i>^^',
'requires=\\par <i>Requires:</i>^^',
'requiredbe=\\par <i>Required Behavior:</i>^^',
'concept{2}=<a href=\"md_doc_concepts.html#\1\">\2</a>',
'defaultbe=\\par <i>Default Behavior:</i>^^'
case_sense_names false
extension_mapping 'inc=C++', 'no_extension=C++'
extract_all true
extract_static true
file_patterns '*'
full_path_names true
generate_html true
generate_latex false
generate_treeview true
html_extra_stylesheet 'theme.css'
html_timestamp true
javadoc_autobrief true
project_name 'PhotonVision C++'
project_logo '../wpiutil/src/main/native/resources/wpilib-128.png'
project_number pubVersion
quiet true
recursive true
strip_code_comments false
strip_from_inc_path cppIncludeRoots as String[]
strip_from_path cppIncludeRoots as String[]
use_mathjax true
warnings false
warn_if_incomplete_doc true
warn_if_undocumented false
warn_no_paramdoc true
//enable doxygen preprocessor expansion of WPI_DEPRECATED to fix MotorController docs
enable_preprocessing true
macro_expansion true
expand_only_predef true
predefined "WPI_DEPRECATED(x)=[[deprecated(x)]]\"\\\n" +
"\"__cplusplus\"\\\n" +
"\"HAL_ENUM(name)=enum name : int32_t"
if (project.hasProperty('docWarningsAsErrors')) {
warn_as_error 'FAIL_ON_WARNINGS'
}
}
tasks.register("zipCppDocs", Zip) {
archiveBaseName = zipBaseNameCpp
destinationDirectory = outputsFolder
dependsOn doxygen
from ("$buildDir/docs/doxygen/html")
into '/'
}
// Java
configurations {
javaSource {
transitive false
}
}
ext {
sharedCvConfigs = [:]
staticCvConfigs = [:]
useJava = true
useCpp = false
skipDev = true
useDocumentation = true
}
task generateJavaDocs(type: Javadoc) {
def exportedProjects = [
':photon-core',
':photon-server',
':photon-targeting',
':photon-lib'
]
source exportedProjects.collect { project(it).sourceSets.main.allJava }
classpath = files(exportedProjects.collect { project(it).sourceSets.main.compileClasspath })
dependsOn project(':photon-core').writeCurrentVersion
options.links("https://docs.oracle.com/en/java/javase/17/docs/api/")
options.addStringOption("tag", "pre:a:Pre-Condition")
options.addBooleanOption("Xdoclint:html,missing,reference,syntax", true)
options.addBooleanOption('html5', true)
failOnError = true
title = "PhotonVision $pubVersion"
ext.entryPoint = "$destinationDir/index.html"
if (JavaVersion.current().isJava8Compatible() && project.hasProperty('docWarningsAsErrors')) {
// Treat javadoc warnings as errors.
//
// The second argument '-quiet' is a hack. The one parameter
// addStringOption() doesn't work, so we add '-quiet', which is added
// anyway by gradle. See https://github.com/gradle/gradle/issues/2354.
//
// See JDK-8200363 (https://bugs.openjdk.java.net/browse/JDK-8200363)
// for information about the nonstandard -Xwerror option. JDK 15+ has
// -Werror.
options.addStringOption('Xwerror', '-quiet')
}
if (JavaVersion.current().isJava11Compatible()) {
if (!JavaVersion.current().isJava12Compatible()) {
options.addBooleanOption('-no-module-directories', true)
}
doLast {
// This is a work-around for https://bugs.openjdk.java.net/browse/JDK-8211194. Can be removed once that issue is fixed on JDK's side
// Since JDK 11, package-list is missing from javadoc output files and superseded by element-list file, but a lot of external tools still need it
// Here we generate this file manually
new File(destinationDir, 'package-list').text = new File(destinationDir, 'element-list').text
}
}
}
tasks.register("zipJavaDocs", Zip) {
archiveBaseName = zipBaseNameJava
destinationDirectory = outputsFolder
dependsOn generateJavaDocs
from ("$buildDir/docs/javadoc")
into '/'
}
tasks.register("zipDocs") {
dependsOn zipCppDocs
dependsOn zipJavaDocs
}
apply plugin: 'maven-publish'
publishing {
publications {
java(MavenPublication) {
artifact zipJavaDocs
artifactId = "${baseArtifactIdJava}"
groupId artifactGroupIdJava
version pubVersion
}
cpp(MavenPublication) {
artifact zipCppDocs
artifactId = "${baseArtifactIdCpp}"
groupId artifactGroupIdCpp
version pubVersion
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,8 +0,0 @@
# The --add-exports flags work around a bug with spotless and JDK 17
# https://github.com/diffplug/spotless/issues/834
org.gradle.jvmargs= \
--add-exports jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED \
--add-exports jdk.compiler/com.sun.tools.javac.file=ALL-UNNAMED \
--add-exports jdk.compiler/com.sun.tools.javac.parser=ALL-UNNAMED \
--add-exports jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED \
--add-exports jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED

Binary file not shown.

View File

@@ -1,7 +1,6 @@
distributionBase=GRADLE_USER_HOME
distributionPath=permwrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.4-bin.zip
networkTimeout=10000
validateDistributionUrl=true
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-6.7.1-bin.zip
distributionSha256Sum=3239b5ed86c3838a37d983ac100573f64c1f3fd8e1eb6c89fa5f9529b5ec091d
zipStoreBase=GRADLE_USER_HOME
zipStorePath=permwrapper/dists
zipStorePath=wrapper/dists

284
gradlew vendored
View File

@@ -1,7 +1,7 @@
#!/bin/sh
#!/usr/bin/env sh
#
# Copyright © 2015-2021 the original authors.
# Copyright 2015 the original author or authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
@@ -17,111 +17,78 @@
#
##############################################################################
#
# Gradle start up script for POSIX generated by Gradle.
#
# Important for running:
#
# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
# noncompliant, but you have some other compliant shell such as ksh or
# bash, then to run this script, type that shell name before the whole
# command line, like:
#
# ksh Gradle
#
# Busybox and similar reduced shells will NOT work, because this script
# requires all of these POSIX shell features:
# * functions;
# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
# * compound commands having a testable exit status, especially «case»;
# * various built-in commands including «command», «set», and «ulimit».
#
# Important for patching:
#
# (2) This script targets any POSIX shell, so it avoids extensions provided
# by Bash, Ksh, etc; in particular arrays are avoided.
#
# The "traditional" practice of packing multiple parameters into a
# space-separated string is a well documented source of bugs and security
# problems, so this is (mostly) avoided, by progressively accumulating
# options in "$@", and eventually passing that to Java.
#
# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
# see the in-line comments for details.
#
# There are tweaks for specific operating systems such as AIX, CygWin,
# Darwin, MinGW, and NonStop.
#
# (3) This script is generated from the Groovy template
# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# within the Gradle project.
#
# You can find Gradle at https://github.com/gradle/gradle/.
#
##
## Gradle start up script for UN*X
##
##############################################################################
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
app_path=$0
# Need this for daisy-chained symlinks.
while
APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
[ -h "$app_path" ]
do
ls=$( ls -ld "$app_path" )
link=${ls#*' -> '}
case $link in #(
/*) app_path=$link ;; #(
*) app_path=$APP_HOME$link ;;
esac
PRG="$0"
# Need this for relative symlinks.
while [ -h "$PRG" ] ; do
ls=`ls -ld "$PRG"`
link=`expr "$ls" : '.*-> \(.*\)$'`
if expr "$link" : '/.*' > /dev/null; then
PRG="$link"
else
PRG=`dirname "$PRG"`"/$link"
fi
done
SAVED="`pwd`"
cd "`dirname \"$PRG\"`/" >/dev/null
APP_HOME="`pwd -P`"
cd "$SAVED" >/dev/null
# This is normally unused
# shellcheck disable=SC2034
APP_BASE_NAME=${0##*/}
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit
APP_NAME="Gradle"
APP_BASE_NAME=`basename "$0"`
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD=maximum
MAX_FD="maximum"
warn () {
echo "$*"
} >&2
}
die () {
echo
echo "$*"
echo
exit 1
} >&2
}
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
nonstop=false
case "$( uname )" in #(
CYGWIN* ) cygwin=true ;; #(
Darwin* ) darwin=true ;; #(
MSYS* | MINGW* ) msys=true ;; #(
NONSTOP* ) nonstop=true ;;
case "`uname`" in
CYGWIN* )
cygwin=true
;;
Darwin* )
darwin=true
;;
MINGW* )
msys=true
;;
NONSTOP* )
nonstop=true
;;
esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD=$JAVA_HOME/jre/sh/java
JAVACMD="$JAVA_HOME/jre/sh/java"
else
JAVACMD=$JAVA_HOME/bin/java
JAVACMD="$JAVA_HOME/bin/java"
fi
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
@@ -130,120 +97,87 @@ Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
JAVACMD=java
if ! command -v java >/dev/null 2>&1
then
die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
JAVACMD="java"
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
fi
# Increase the maximum file descriptors if we can.
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
case $MAX_FD in #(
max*)
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC2039,SC3045
MAX_FD=$( ulimit -H -n ) ||
warn "Could not query maximum file descriptor limit"
esac
case $MAX_FD in #(
'' | soft) :;; #(
*)
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC2039,SC3045
ulimit -n "$MAX_FD" ||
warn "Could not set maximum file descriptor limit to $MAX_FD"
esac
if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
MAX_FD_LIMIT=`ulimit -H -n`
if [ $? -eq 0 ] ; then
if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
MAX_FD="$MAX_FD_LIMIT"
fi
ulimit -n $MAX_FD
if [ $? -ne 0 ] ; then
warn "Could not set maximum file descriptor limit: $MAX_FD"
fi
else
warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
fi
fi
# Collect all arguments for the java command, stacking in reverse order:
# * args from the command line
# * the main class name
# * -classpath
# * -D...appname settings
# * --module-path (only if needed)
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
# For Darwin, add options to specify how the application appears in the dock
if $darwin; then
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
fi
# For Cygwin or MSYS, switch paths to Windows format before running java
if "$cygwin" || "$msys" ; then
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
JAVACMD=`cygpath --unix "$JAVACMD"`
JAVACMD=$( cygpath --unix "$JAVACMD" )
# Now convert the arguments - kludge to limit ourselves to /bin/sh
for arg do
if
case $arg in #(
-*) false ;; # don't mess with options #(
/?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
[ -e "$t" ] ;; #(
*) false ;;
esac
then
arg=$( cygpath --path --ignore --mixed "$arg" )
fi
# Roll the args list around exactly as many times as the number of
# args, so each arg winds up back in the position where it started, but
# possibly modified.
#
# NB: a `for` loop captures its iteration list before it begins, so
# changing the positional parameters here affects neither the number of
# iterations, nor the values presented in `arg`.
shift # remove old arg
set -- "$@" "$arg" # push replacement arg
# We build the pattern for arguments to be converted via cygpath
ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
SEP=""
for dir in $ROOTDIRSRAW ; do
ROOTDIRS="$ROOTDIRS$SEP$dir"
SEP="|"
done
OURCYGPATTERN="(^($ROOTDIRS))"
# Add a user-defined pattern to the cygpath arguments
if [ "$GRADLE_CYGPATTERN" != "" ] ; then
OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
fi
# Now convert the arguments - kludge to limit ourselves to /bin/sh
i=0
for arg in "$@" ; do
CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
else
eval `echo args$i`="\"$arg\""
fi
i=`expr $i + 1`
done
case $i in
0) set -- ;;
1) set -- "$args0" ;;
2) set -- "$args0" "$args1" ;;
3) set -- "$args0" "$args1" "$args2" ;;
4) set -- "$args0" "$args1" "$args2" "$args3" ;;
5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
esac
fi
# Escape application args
save () {
for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
echo " "
}
APP_ARGS=`save "$@"`
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Collect all arguments for the java command:
# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
# and any embedded shellness will be escaped.
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
# treated as '${Hostname}' itself on the command line.
set -- \
"-Dorg.gradle.appname=$APP_BASE_NAME" \
-classpath "$CLASSPATH" \
org.gradle.wrapper.GradleWrapperMain \
"$@"
# Stop when "xargs" is not available.
if ! command -v xargs >/dev/null 2>&1
then
die "xargs is not available"
fi
# Use "xargs" to parse quoted args.
#
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
#
# In Bash we could simply go:
#
# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
# set -- "${ARGS[@]}" "$@"
#
# but POSIX shell has neither arrays nor command substitution, so instead we
# post-process each arg (as a line of input to sed) to backslash-escape any
# character that might be a shell metacharacter, then use eval to reverse
# that process (while maintaining the separation between arguments), and wrap
# the whole thing up as a single "set" statement.
#
# This will of course break if any of these variables contains a newline or
# an unmatched quote.
#
eval "set -- $(
printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
xargs -n1 |
sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
tr '\n' ' '
)" '"$@"'
# Collect all arguments for the java command, following the shell quoting and substitution rules
eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
exec "$JAVACMD" "$@"

38
gradlew.bat vendored
View File

@@ -14,7 +14,7 @@
@rem limitations under the License.
@rem
@if "%DEBUG%"=="" @echo off
@if "%DEBUG%" == "" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@@ -25,14 +25,10 @@
if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
if "%DIRNAME%"=="" set DIRNAME=.
@rem This is normally unused
if "%DIRNAME%" == "" set DIRNAME=.
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
@@ -41,7 +37,7 @@ if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if %ERRORLEVEL% equ 0 goto execute
if "%ERRORLEVEL%" == "0" goto init
echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
@@ -55,7 +51,7 @@ goto fail
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto execute
if exist "%JAVA_EXE%" goto init
echo.
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
@@ -65,26 +61,38 @@ echo location of your Java installation.
goto fail
:init
@rem Get command-line arguments, handling Windows variants
if not "%OS%" == "Windows_NT" goto win9xME_args
:win9xME_args
@rem Slurp the command line arguments.
set CMD_LINE_ARGS=
set _SKIP=2
:win9xME_args_slurp
if "x%~1" == "x" goto execute
set CMD_LINE_ARGS=%*
:execute
@rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
:end
@rem End local scope for the variables with windows NT shell
if %ERRORLEVEL% equ 0 goto mainEnd
if "%ERRORLEVEL%"=="0" goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
set EXIT_CODE=%ERRORLEVEL%
if %EXIT_CODE% equ 0 set EXIT_CODE=1
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
exit /b %EXIT_CODE%
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
exit /b 1
:mainEnd
if "%OS%"=="Windows_NT" endlocal

View File

@@ -0,0 +1,2 @@
> 1%
last 2 versions

View File

@@ -0,0 +1,17 @@
module.exports = {
root: true,
env: {
node: true
},
'extends': [
'plugin:vue/recommended',
'eslint:recommended'
],
rules: {
'no-console': 'off',
'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off'
},
parserOptions: {
parser: 'babel-eslint'
}
};

View File

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

View File

@@ -1,28 +1,21 @@
# Logs
logs
*.log
.DS_Store
node_modules
/dist
# local env files
.env.local
.env.*.local
# Log files
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
.DS_Store
dist
dist-ssr
*.local
/cypress/videos/
/cypress/screenshots/
# Editor directories and files
.vscode
!.vscode/extensions.json
.idea
.vscode
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
components.d.ts

View File

@@ -1 +0,0 @@
src/assets/fonts/PromptRegular.ts

View File

@@ -1,8 +0,0 @@
{
"$schema": "https://json.schemastore.org/prettierrc",
"semi": true,
"tabWidth": 2,
"singleQuote": false,
"printWidth": 120,
"trailingComma": "none"
}

37
photon-client/README.md Normal file
View File

@@ -0,0 +1,37 @@
# PhotonVision Client UI
## Install Node.js
Follow [this](https://nodejs.org/en/) link.
## Project setup
Run this one time, this command downloades the packages the UI uses and it might take a short while
```
npm install
```
### Compiles and hot-reloads for development
Run this every development session, this command auto-builds the UI after every change you make
```
npm run serve
```
### Compiles and minifies for production
```
npm run build
```
### Run your tests
```
npm run test
```
### Lints and fixes files
```
npm run lint
```
### Customize configuration
See Node.js' [Configuration Reference](https://cli.vuejs.org/config/).

View File

@@ -0,0 +1,5 @@
module.exports = {
presets: [
'@vue/app'
]
};

View File

@@ -1 +0,0 @@
/// <reference types="vite/client" />

View File

@@ -1,13 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" href="/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Photon Client</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>

File diff suppressed because it is too large Load Diff

View File

@@ -1,48 +1,40 @@
{
"name": "photonclient",
"version": "0.0.0",
"name": "photon-client",
"version": "3.0.0",
"private": true,
"scripts": {
"dev": "vite",
"build": "run-p build-only",
"preview": "vite preview --port 4173",
"build-only": "vite build",
"build-demo": "vite build --mode demo",
"lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix --ignore-path .gitignore",
"format": "prettier --write src/",
"lint-ci": "eslint . --max-warnings 0 --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix --ignore-path .gitignore",
"format-ci": "prettier --check src/"
"serve": "vue-cli-service serve",
"build": "vue-cli-service build",
"lint": "vue-cli-service lint"
},
"dependencies": {
"@fontsource/prompt": "^5.0.9",
"@mdi/font": "^7.4.47",
"@msgpack/msgpack": "^3.0.0-beta2",
"axios": "^1.6.3",
"jspdf": "^2.5.1",
"pinia": "^2.1.4",
"three": "^0.160.0",
"vue": "^2.7.14",
"vue-router": "^3.6.5",
"vuetify": "^2.7.1"
"@femessage/log-viewer": "^1.4.2",
"@vue/eslint-config-prettier": "^6.0.0",
"axios": "^0.19.2",
"core-js": "^2.6.11",
"downloadjs": "^1.4.7",
"material-design-icons-iconfont": "^5.0.1",
"msgpack5": "^4.2.1",
"vue": "^2.6.12",
"vue-axios": "^2.1.5",
"vue-native-websocket": "git+https://github.com/PhotonVision/vue-native-websocket.git#7a32791",
"vue-router": "^3.4.3",
"vuetify": "^2.3.10",
"vuex": "^3.5.1"
},
"devDependencies": {
"@rushstack/eslint-patch": "^1.3.2",
"@types/node": "^16.11.45",
"@types/three": "^0.160.0",
"@vitejs/plugin-vue2": "^2.3.1",
"@vue/eslint-config-prettier": "^9.0.0",
"@vue/eslint-config-typescript": "^12.0.0",
"@vue/tsconfig": "^0.5.1",
"deepmerge": "^4.3.1",
"eslint": "^8.56.0",
"eslint-plugin-vue": "^9.19.2",
"npm-run-all": "^4.1.5",
"prettier": "3.2.2",
"sass": "~1.32",
"sass-loader": "^13.3.2",
"terser": "^5.14.2",
"typescript": "^5.3.3",
"unplugin-vue-components": "^0.26.0",
"vite": "^4.5.1"
"@mdi/font": "^4.9.95",
"@vue/cli-plugin-babel": "^3.12.1",
"@vue/cli-plugin-eslint": "^4.5.4",
"@vue/cli-service": "^4.5.4",
"babel-eslint": "^10.1.0",
"eslint": "^5.16.0",
"eslint-plugin-vue": "^5.0.0",
"papaparse": "^5.3.0",
"sass": "^1.26.10",
"sass-loader": "^7.1.0",
"vue-cli-plugin-vuetify": "^0.6.3",
"vue-template-compiler": "^2.6.12",
"vuetify-loader": "^1.6.0"
}
}

View File

@@ -0,0 +1,5 @@
module.exports = {
plugins: {
autoprefixer: {}
}
};

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

View File

@@ -0,0 +1,20 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<link rel="icon" href="<%= BASE_URL %>favicon.png">
<title>PhotonVision</title>
</head>
<body>
<noscript>
<strong>We're sorry but PhotonVision doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
</noscript>
<div id="app"></div>
<!-- built files will be auto injected -->
</body>
</html>

View File

@@ -1,87 +1,346 @@
<script setup lang="ts">
import { useStateStore } from "@/stores/StateStore";
import { useSettingsStore } from "@/stores/settings/GeneralSettingsStore";
import { useCameraSettingsStore } from "@/stores/settings/CameraSettingsStore";
import { AutoReconnectingWebsocket } from "@/lib/AutoReconnectingWebsocket";
import { inject } from "vue";
import PhotonSidebar from "@/components/app/photon-sidebar.vue";
import PhotonLogView from "@/components/app/photon-log-view.vue";
import PhotonErrorSnackbar from "@/components/app/photon-error-snackbar.vue";
const is_demo = import.meta.env.MODE === "demo";
if (!is_demo) {
const websocket = new AutoReconnectingWebsocket(
`ws://${inject("backendHost")}/websocket_data`,
() => {
useStateStore().$patch({ backendConnected: true });
},
(data) => {
if (data.log !== undefined) {
useStateStore().addLogFromWebsocket(data.log);
}
if (data.settings !== undefined) {
useSettingsStore().updateGeneralSettingsFromWebsocket(data.settings);
}
if (data.cameraSettings !== undefined) {
useCameraSettingsStore().updateCameraSettingsFromWebsocket(data.cameraSettings);
}
if (data.ntConnectionInfo !== undefined) {
useStateStore().updateNTConnectionStatusFromWebsocket(data.ntConnectionInfo);
}
if (data.metrics !== undefined) {
useSettingsStore().updateMetricsFromWebsocket(data.metrics);
}
if (data.updatePipelineResult !== undefined) {
useStateStore().updateBackendResultsFromWebsocket(data.updatePipelineResult);
}
if (data.mutatePipelineSettings !== undefined && data.cameraIndex !== undefined) {
useCameraSettingsStore().changePipelineSettingsInStore(data.mutatePipelineSettings, data.cameraIndex);
}
if (data.calibrationData !== undefined) {
useStateStore().updateCalibrationStateValuesFromWebsocket(data.calibrationData);
}
},
() => {
useStateStore().$patch({ backendConnected: false });
}
);
useStateStore().$patch({ websocket: websocket });
}
</script>
<template>
<v-app>
<photon-sidebar />
<!-- Although most of the app runs with the "light" theme, the navigation drawer needs to have white text and icons so it uses the dark theme-->
<v-navigation-drawer
dark
app
permanent
:mini-variant="compact"
color="primary"
>
<v-list>
<!-- List item for the heading; note that there are some tricks in setting padding and image width make things look right -->
<v-list-item :class="compact ? 'pr-0 pl-0' : ''">
<v-list-item-icon class="mr-0">
<img
v-if="!compact"
class="logo"
src="./assets/logoLarge.png"
>
<img
v-else
class="logo"
src="./assets/logoSmall.png"
>
</v-list-item-icon>
</v-list-item>
<v-list-item
link
to="dashboard"
@click="rollbackPipelineIndex()"
>
<v-list-item-icon>
<v-icon>mdi-view-dashboard</v-icon>
</v-list-item-icon>
<v-list-item-content>
<v-list-item-title>Dashboard</v-list-item-title>
</v-list-item-content>
</v-list-item>
<v-list-item
ref="camerasTabOpener"
link
to="cameras"
@click="switchToDriverMode()"
>
<v-list-item-icon>
<v-icon>mdi-camera</v-icon>
</v-list-item-icon>
<v-list-item-content>
<v-list-item-title>Cameras</v-list-item-title>
</v-list-item-content>
</v-list-item>
<v-list-item
link
to="settings"
@click="switchToSettingsTab()"
>
<v-list-item-icon>
<v-icon>mdi-settings</v-icon>
</v-list-item-icon>
<v-list-item-content>
<v-list-item-title>Settings</v-list-item-title>
</v-list-item-content>
</v-list-item>
<v-list-item
link
to="docs"
>
<v-list-item-icon>
<v-icon>mdi-bookshelf</v-icon>
</v-list-item-icon>
<v-list-item-content>
<v-list-item-title>Documentation</v-list-item-title>
</v-list-item-content>
</v-list-item>
<v-list-item
v-if="this.$vuetify.breakpoint.mdAndUp"
link
@click.stop="toggleCompactMode"
>
<v-list-item-icon>
<v-icon v-if="compact">
mdi-chevron-right
</v-icon>
<v-icon v-else>
mdi-chevron-left
</v-icon>
</v-list-item-icon>
<v-list-item-content>
<v-list-item-title>Advanced Mode</v-list-item-title>
</v-list-item-content>
</v-list-item>
<v-list-item style="position: absolute; bottom: 0; left: 0;">
<v-list-item-icon>
<v-icon v-if="$store.state.backendConnected">
mdi-wifi
</v-icon>
<v-icon
v-else
class="pulse"
style="border-radius: 100%;"
>
mdi-wifi-off
</v-icon>
</v-list-item-icon>
<v-list-item-content>
<v-list-item-title>
{{ $store.state.backendConnected ? "Connected" : "Trying to connect..." }}
</v-list-item-title>
</v-list-item-content>
</v-list-item>
</v-list>
</v-navigation-drawer>
<v-main>
<v-container class="main-container" fluid fill-height>
<v-container
fluid
fill-height
>
<v-layout>
<v-flex>
<router-view />
<router-view @switch-to-cameras="switchToDriverMode" />
</v-flex>
</v-layout>
</v-container>
</v-main>
<photon-log-view />
<photon-error-snackbar />
<v-dialog
v-model="$store.state.logsOverlay"
width="1500"
dark
>
<logs />
</v-dialog>
</v-app>
</template>
<style lang="scss">
@import "vuetify/src/styles/settings/_variables";
<script>
import Logs from "./views/LogsView"
@media #{map-get($display-breakpoints, 'md-and-down')} {
html {
font-size: 14px !important;
}
}
export default {
name: 'App',
components: {
Logs
},
data: () => ({
// Used so that we can switch back to the previously selected pipeline after camera calibration
previouslySelectedIndices: [],
timer: undefined,
}),
computed: {
compact: {
get() {
if (this.$store.state.compactMode === undefined) {
return this.$vuetify.breakpoint.smAndDown;
} else {
return this.$store.state.compactMode || this.$vuetify.breakpoint.smAndDown;
}
},
set(value) {
// compactMode is the user's preference for compact mode; it overrides screen size
this.$store.commit("compactMode", value);
localStorage.setItem("compactMode", value);
},
},
},
created() {
document.addEventListener("keydown", e => {
switch (e.key) {
case "`":
this.$store.state.logsOverlay = !this.$store.state.logsOverlay;
break;
case "z":
if (e.ctrlKey && this.$store.getters.canUndo) {
this.$store.dispatch('undo', {vm: this});
}
break;
case "y":
if (e.ctrlKey && this.$store.getters.canRedo) {
this.$store.dispatch('redo', {vm: this});
}
break;
.main-container {
background-color: #232c37;
padding: 0 !important;
}
}
});
#title {
color: #ffd843;
}
this.$options.sockets.onmessage = (data) => {
try {
let message = this.$msgPack.decode(data.data);
for (let prop in message) {
if (message.hasOwnProperty(prop)) {
this.handleMessage(prop, message[prop]);
}
}
} catch (error) {
console.error('error: ' + JSON.stringify(data.data) + " , " + error);
}
};
this.$options.sockets.onopen = () => {
this.$store.state.backendConnected = true;
this.$store.state.connectedCallbacks.forEach(it => it())
};
let closed = () => {
this.$store.state.backendConnected = false;
};
this.$options.sockets.onclose = closed;
this.$options.sockets.onerror = closed;
this.$connect();
},
methods: {
handleMessage(key, value) {
if (key === "logMessage") {
this.logMessage(value["logMessage"], value["logLevel"]);
} else if(key === "log"){
this.logMessage(value["logMessage"]["logMessage"], value["logMessage"]["logLevel"]);
} else if (key === "updatePipelineResult") {
this.$store.commit('mutatePipelineResults', value)
} else if (this.$store.state.hasOwnProperty(key)) {
this.$store.commit(key, value);
} else if (this.$store.getters.currentPipelineSettings.hasOwnProperty(key)) {
this.$store.commit('mutatePipeline', {[key]: value});
} else if (this.$store.state.settings.hasOwnProperty(key)) {
this.$store.commit('mutateSettings', {[key]: value});
} else {
switch (key) {
default: {
console.error("Unknown message from backend: " + value);
}
}
}
},
toggleCompactMode() {
this.compact = !this.compact;
},
// eslint-disable-next-line no-unused-vars
logMessage(message, levelInt) {
this.$store.commit('logString', {
['level']: levelInt,
['message']: message
})
},
switchToDriverMode() {
if (!this.previouslySelectedIndices) this.previouslySelectedIndices = [];
for (const [i, cameraSettings] of this.$store.state.cameraSettings.entries()) {
this.previouslySelectedIndices[i] = cameraSettings.currentPipelineIndex;
this.handleInputWithIndex('currentPipeline', -1, i);
}
},
rollbackPipelineIndex()
{
if (this.previouslySelectedIndices !== null) {
for (const [i] of this.$store.state.cameraSettings.entries()) {
this.handleInputWithIndex('currentPipeline', this.previouslySelectedIndices[i] || 0, i);
}
}
this.previouslySelectedIndices = null;
}
,
switchToSettingsTab() {
this.axios.post('http://' + this.$address + '/api/sendMetrics', {})
}
}
};
</script>
<style lang="sass">
@import "./scss/variables.scss"
</style>
<style>
.pulse {
animation: pulse-animation 2s infinite;
}
@keyframes pulse-animation {
0% {
box-shadow: 0 0 0 0px rgba(0, 0, 0, 0.2);
background-color: rgba(0, 0, 0, 0.2);
}
100% {
box-shadow: 0 0 0 20px rgba(0, 0, 0, 0);
background-color: rgba(0, 0, 0, 0);
}
}
.logo {
width: 100%;
height: 70px;
object-fit: contain;
}
::-webkit-scrollbar {
width: 0.5em;
border-radius: 5px;
}
::-webkit-scrollbar-track {
-webkit-box-shadow: inset 0 0 6px rgba(0, 0, 0, 0.3);
border-radius: 10px;
}
::-webkit-scrollbar-thumb {
background-color: #ffd843;
border-radius: 10px;
}
.container {
background-color: #232c37;
padding: 0 !important;
}
#title {
color: #ffd843;
}
</style>
<style>
/* Hacks */
.v-divider {
border-color: white !important;
}
.v-input {
font-size: 1rem !important;
}
/* This is unfortunately the only way to override table background color */
.theme--dark.v-data-table > .v-data-table__wrapper > table > tbody > tr:hover:not(.v-data-table__expanded__content):not(.v-data-table__empty-wrapper) {
background: #005281 !important;
}
</style>
<style lang="scss">
@import '~vuetify/src/styles/settings/_variables';
@media #{map-get($display-breakpoints, 'md-and-down')} {
html {
font-size: 14px !important;
}
}
</style>

Binary file not shown.

After

Width:  |  Height:  |  Size: 61 KiB

View File

@@ -0,0 +1,68 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
version="1.1"
width="24"
height="24"
viewBox="0 0 24 24"
id="svg865"
sodipodi:docname="eyedropper.svg"
inkscape:version="0.92.4 (unknown)">
<metadata
id="metadata871">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
</cc:Work>
</rdf:RDF>
</metadata>
<defs
id="defs869" />
<sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="1916"
inkscape:window-height="1040"
id="namedview867"
showgrid="false"
inkscape:zoom="35.541667"
inkscape:cx="12.112544"
inkscape:cy="10.171169"
inkscape:window-x="0"
inkscape:window-y="1458"
inkscape:window-maximized="0"
inkscape:current-layer="svg865" />
<g
id="g905"
inkscape:export-xdpi="77.2733"
inkscape:export-ydpi="77.2733">
<path
inkscape:connector-curvature="0"
id="path863"
d="m 12.28,19.4725 -2.13,-2.13 1.42,-1.41 -7.71,-7.71 -1.86,-4.6 1.5,-1.5 4.6,1.86 7.71,7.71 1.41,-1.42 2.13,2.13 -7.07,7.07 m 8.72,-2.59 c 1.17,1.17 1.17,3.07 0,4.24 -1.17,1.17 -3.07,1.17 -4.24,0 l -1.92,-1.92 4.24,-4.24 1.92,1.92 m -14.03,-11.2 -2.47,-1.06 1.06,2.47 7.44,7.43 1.4,-1.4 z" />
<path
inkscape:export-ydpi="161.91951"
inkscape:export-xdpi="161.91951"
d="m 3.5996094,2.6132812 -1.109375,1.109375 1.7246094,4.2636719 7.6503902,7.6503909 a 0.41783994,0.41783994 0 0 1 0,0.591797 l -1.123046,1.115234 1.537109,1.539062 6.480469,-6.480468 -1.53711,-1.53711 -1.115234,1.123047 a 0.41783994,0.41783994 0 0 1 -0.591797,0 L 7.8652344,4.3378906 Z m 0.9101562,1.5917969 a 0.41783994,0.41783994 0 0 1 0.1542969,0.033203 L 7.1347656,5.296875 a 0.41783994,0.41783994 0 0 1 0.1308594,0.089844 l 7.429687,7.4414062 a 0.41783994,0.41783994 0 0 1 0,0.589844 l -1.40039,1.40039 a 0.41783994,0.41783994 0 0 1 -0.589844,0 L 5.265625,7.3867188 A 0.41783994,0.41783994 0 0 1 5.1757812,7.2558594 L 4.1152344,4.7871094 A 0.41783994,0.41783994 0 0 1 4.5097656,4.2050781 Z m 14.5703124,11.3476559 -3.65039,3.650391 1.625,1.625 c 1.010062,1.010062 2.640328,1.010062 3.65039,0 1.010062,-1.010062 1.010062,-2.640327 0,-3.650391 z"
id="path889"
style="fill:#ffffff;stroke-width:21.16535378;stroke-miterlimit:4;stroke-dasharray:none"
inkscape:original="M 3.5 2.1230469 L 2 3.6230469 L 3.859375 8.2226562 L 11.570312 15.931641 L 10.150391 17.341797 L 12.279297 19.472656 L 19.349609 12.402344 L 17.220703 10.273438 L 15.810547 11.693359 L 8.0996094 3.9824219 L 3.5 2.1230469 z M 4.5 4.6230469 L 6.9707031 5.6816406 L 14.400391 13.123047 L 13 14.523438 L 5.5605469 7.0917969 L 4.5 4.6230469 z M 19.080078 14.962891 L 14.839844 19.203125 L 16.759766 21.123047 C 17.929766 22.293047 19.83 22.293047 21 21.123047 C 22.17 19.953047 22.17 18.052813 21 16.882812 L 19.080078 14.962891 z "
inkscape:radius="-0.41779816"
sodipodi:type="inkscape:offset" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.5 KiB

File diff suppressed because one or more lines are too long

View File

@@ -1,40 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" style="margin: auto; background: rgb(255, 216, 68); display: block;"
width="600px" height="412px" viewBox="0 0 100 100" preserveAspectRatio="xMidYMid">
<circle cx="75" cy="50" fill="#89b99a" r="5">
<animate attributeName="r" values="2;2;4;2;2" times="0;0.1;0.2;0.3;1" dur="1s" repeatCount="indefinite" begin="-0.9166666666666666s"></animate>
<animate attributeName="fill" values="#89b99a;#89b99a;#43a7ce;#89b99a;#89b99a" repeatCount="indefinite" times="0;0.1;0.2;0.3;1" dur="1s" begin="-0.9166666666666666s"></animate>
</circle><circle cx="71.65063509461098" cy="62.5" fill="#89b99a" r="5">
<animate attributeName="r" values="2;2;4;2;2" times="0;0.1;0.2;0.3;1" dur="1s" repeatCount="indefinite" begin="-0.8333333333333334s"></animate>
<animate attributeName="fill" values="#89b99a;#89b99a;#43a7ce;#89b99a;#89b99a" repeatCount="indefinite" times="0;0.1;0.2;0.3;1" dur="1s" begin="-0.8333333333333334s"></animate>
</circle><circle cx="62.5" cy="71.65063509461096" fill="#89b99a" r="5">
<animate attributeName="r" values="2;2;4;2;2" times="0;0.1;0.2;0.3;1" dur="1s" repeatCount="indefinite" begin="-0.75s"></animate>
<animate attributeName="fill" values="#89b99a;#89b99a;#43a7ce;#89b99a;#89b99a" repeatCount="indefinite" times="0;0.1;0.2;0.3;1" dur="1s" begin="-0.75s"></animate>
</circle><circle cx="50" cy="75" fill="#89b99a" r="5">
<animate attributeName="r" values="2;2;4;2;2" times="0;0.1;0.2;0.3;1" dur="1s" repeatCount="indefinite" begin="-0.6666666666666666s"></animate>
<animate attributeName="fill" values="#89b99a;#89b99a;#43a7ce;#89b99a;#89b99a" repeatCount="indefinite" times="0;0.1;0.2;0.3;1" dur="1s" begin="-0.6666666666666666s"></animate>
</circle><circle cx="37.50000000000001" cy="71.65063509461098" fill="#89b99a" r="5">
<animate attributeName="r" values="2;2;4;2;2" times="0;0.1;0.2;0.3;1" dur="1s" repeatCount="indefinite" begin="-0.5833333333333334s"></animate>
<animate attributeName="fill" values="#89b99a;#89b99a;#43a7ce;#89b99a;#89b99a" repeatCount="indefinite" times="0;0.1;0.2;0.3;1" dur="1s" begin="-0.5833333333333334s"></animate>
</circle><circle cx="28.34936490538903" cy="62.5" fill="#89b99a" r="5">
<animate attributeName="r" values="2;2;4;2;2" times="0;0.1;0.2;0.3;1" dur="1s" repeatCount="indefinite" begin="-0.5s"></animate>
<animate attributeName="fill" values="#89b99a;#89b99a;#43a7ce;#89b99a;#89b99a" repeatCount="indefinite" times="0;0.1;0.2;0.3;1" dur="1s" begin="-0.5s"></animate>
</circle><circle cx="25" cy="50" fill="#89b99a" r="5">
<animate attributeName="r" values="2;2;4;2;2" times="0;0.1;0.2;0.3;1" dur="1s" repeatCount="indefinite" begin="-0.4166666666666667s"></animate>
<animate attributeName="fill" values="#89b99a;#89b99a;#43a7ce;#89b99a;#89b99a" repeatCount="indefinite" times="0;0.1;0.2;0.3;1" dur="1s" begin="-0.4166666666666667s"></animate>
</circle><circle cx="28.34936490538903" cy="37.50000000000001" fill="#89b99a" r="5">
<animate attributeName="r" values="2;2;4;2;2" times="0;0.1;0.2;0.3;1" dur="1s" repeatCount="indefinite" begin="-0.3333333333333333s"></animate>
<animate attributeName="fill" values="#89b99a;#89b99a;#43a7ce;#89b99a;#89b99a" repeatCount="indefinite" times="0;0.1;0.2;0.3;1" dur="1s" begin="-0.3333333333333333s"></animate>
</circle><circle cx="37.499999999999986" cy="28.349364905389038" fill="#89b99a" r="5">
<animate attributeName="r" values="2;2;4;2;2" times="0;0.1;0.2;0.3;1" dur="1s" repeatCount="indefinite" begin="-0.25s"></animate>
<animate attributeName="fill" values="#89b99a;#89b99a;#43a7ce;#89b99a;#89b99a" repeatCount="indefinite" times="0;0.1;0.2;0.3;1" dur="1s" begin="-0.25s"></animate>
</circle><circle cx="49.99999999999999" cy="25" fill="#89b99a" r="5">
<animate attributeName="r" values="2;2;4;2;2" times="0;0.1;0.2;0.3;1" dur="1s" repeatCount="indefinite" begin="-0.16666666666666666s"></animate>
<animate attributeName="fill" values="#89b99a;#89b99a;#43a7ce;#89b99a;#89b99a" repeatCount="indefinite" times="0;0.1;0.2;0.3;1" dur="1s" begin="-0.16666666666666666s"></animate>
</circle><circle cx="62.5" cy="28.349364905389034" fill="#89b99a" r="5">
<animate attributeName="r" values="2;2;4;2;2" times="0;0.1;0.2;0.3;1" dur="1s" repeatCount="indefinite" begin="-0.08333333333333333s"></animate>
<animate attributeName="fill" values="#89b99a;#89b99a;#43a7ce;#89b99a;#89b99a" repeatCount="indefinite" times="0;0.1;0.2;0.3;1" dur="1s" begin="-0.08333333333333333s"></animate>
</circle><circle cx="71.65063509461096" cy="37.499999999999986" fill="#89b99a" r="5">
<animate attributeName="r" values="2;2;4;2;2" times="0;0.1;0.2;0.3;1" dur="1s" repeatCount="indefinite" begin="0s"></animate>
<animate attributeName="fill" values="#89b99a;#89b99a;#43a7ce;#89b99a;#89b99a" repeatCount="indefinite" times="0;0.1;0.2;0.3;1" dur="1s" begin="0s"></animate>
</circle>
</svg>

Before

Width:  |  Height:  |  Size: 4.7 KiB

View File

@@ -1,57 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg width="100%" height="100%" viewBox="0 0 1920 680" version="1.1" xmlns="http://www.w3.org/2000/svg"
xml:space="preserve"
style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
<g transform="matrix(1,0,0,1,-1986,-1146)">
<g id="PhotonVision-Header-noBG" transform="matrix(1,0,0,0.62963,-0.666667,1146)">
<rect x="1986.67" y="0" width="1920" height="1080" style="fill:none;"/>
<g transform="matrix(1.87463,0,0,2.97735,-2372.42,-4617.27)">
<g transform="matrix(136.163,0,0,136.163,2412.48,1719.32)">
<path d="M0.061,0L0.061,-0.7L0.362,-0.7C0.409,-0.7 0.452,-0.689 0.491,-0.668C0.53,-0.647 0.561,-0.617 0.584,-0.58C0.607,-0.542 0.619,-0.498 0.619,-0.449C0.619,-0.4 0.607,-0.356 0.584,-0.318C0.561,-0.279 0.53,-0.249 0.491,-0.228C0.452,-0.207 0.409,-0.196 0.362,-0.196L0.229,-0.196L0.229,0L0.061,0ZM0.229,-0.337L0.343,-0.337C0.362,-0.337 0.38,-0.341 0.397,-0.35C0.413,-0.359 0.426,-0.371 0.436,-0.388C0.446,-0.405 0.451,-0.425 0.451,-0.448C0.451,-0.471 0.446,-0.491 0.436,-0.508C0.426,-0.525 0.413,-0.538 0.397,-0.547C0.38,-0.556 0.362,-0.56 0.343,-0.56L0.229,-0.56L0.229,-0.337Z" style="fill:rgb(255,216,67);fill-rule:nonzero;"/>
</g>
<g transform="matrix(136.163,0,0,136.163,2500.17,1719.32)">
<path d="M0.061,0L0.061,-0.7L0.229,-0.7L0.229,-0.425L0.502,-0.425L0.502,-0.7L0.67,-0.7L0.67,0L0.502,0L0.502,-0.285L0.229,-0.285L0.229,0L0.061,0Z" style="fill:rgb(255,216,67);fill-rule:nonzero;"/>
</g>
<g transform="matrix(136.163,0,0,136.163,2599.7,1719.32)">
<path d="M0.4,0.013C0.344,0.013 0.293,0.004 0.248,-0.015C0.203,-0.034 0.164,-0.06 0.131,-0.093C0.098,-0.126 0.073,-0.165 0.056,-0.209C0.038,-0.253 0.029,-0.3 0.029,-0.351C0.029,-0.402 0.038,-0.449 0.056,-0.493C0.073,-0.536 0.098,-0.575 0.131,-0.608C0.164,-0.641 0.203,-0.667 0.248,-0.686C0.293,-0.704 0.344,-0.713 0.4,-0.713C0.455,-0.713 0.506,-0.704 0.552,-0.686C0.597,-0.667 0.636,-0.641 0.669,-0.608C0.701,-0.575 0.726,-0.536 0.744,-0.493C0.762,-0.449 0.771,-0.402 0.771,-0.351C0.771,-0.3 0.762,-0.253 0.744,-0.209C0.726,-0.165 0.701,-0.126 0.669,-0.093C0.636,-0.06 0.597,-0.034 0.552,-0.015C0.506,0.004 0.455,0.013 0.4,0.013ZM0.4,-0.13C0.429,-0.13 0.457,-0.136 0.482,-0.147C0.506,-0.158 0.528,-0.173 0.546,-0.193C0.564,-0.213 0.578,-0.236 0.588,-0.264C0.598,-0.291 0.603,-0.32 0.603,-0.351C0.603,-0.382 0.598,-0.411 0.588,-0.438C0.578,-0.465 0.564,-0.488 0.546,-0.508C0.528,-0.528 0.506,-0.544 0.482,-0.555C0.457,-0.566 0.429,-0.571 0.4,-0.571C0.37,-0.571 0.343,-0.566 0.318,-0.555C0.293,-0.544 0.272,-0.528 0.254,-0.508C0.235,-0.488 0.221,-0.465 0.212,-0.438C0.202,-0.411 0.197,-0.382 0.197,-0.351C0.197,-0.32 0.202,-0.291 0.212,-0.264C0.221,-0.236 0.235,-0.213 0.254,-0.193C0.272,-0.173 0.293,-0.158 0.318,-0.147C0.343,-0.136 0.37,-0.13 0.4,-0.13Z" style="fill:rgb(255,216,67);fill-rule:nonzero;"/>
</g>
<g transform="matrix(136.163,0,0,136.163,2704.41,1719.32)">
<path d="M0.421,0C0.378,0 0.341,-0.01 0.309,-0.029C0.276,-0.048 0.251,-0.073 0.233,-0.105C0.214,-0.137 0.205,-0.173 0.205,-0.212L0.205,-0.56L0.015,-0.56L0.015,-0.7L0.562,-0.7L0.562,-0.56L0.373,-0.56L0.373,-0.197C0.373,-0.182 0.378,-0.168 0.389,-0.157C0.4,-0.146 0.413,-0.14 0.429,-0.14L0.484,-0.14L0.484,0L0.421,0Z" style="fill:rgb(255,216,67);fill-rule:nonzero;"/>
</g>
<g transform="matrix(136.163,0,0,136.163,2778.89,1719.32)">
<path d="M0.4,0.013C0.344,0.013 0.293,0.004 0.248,-0.015C0.203,-0.034 0.164,-0.06 0.131,-0.093C0.098,-0.126 0.073,-0.165 0.056,-0.209C0.038,-0.253 0.029,-0.3 0.029,-0.351C0.029,-0.402 0.038,-0.449 0.056,-0.493C0.073,-0.536 0.098,-0.575 0.131,-0.608C0.164,-0.641 0.203,-0.667 0.248,-0.686C0.293,-0.704 0.344,-0.713 0.4,-0.713C0.455,-0.713 0.506,-0.704 0.552,-0.686C0.597,-0.667 0.636,-0.641 0.669,-0.608C0.701,-0.575 0.726,-0.536 0.744,-0.493C0.762,-0.449 0.771,-0.402 0.771,-0.351C0.771,-0.3 0.762,-0.253 0.744,-0.209C0.726,-0.165 0.701,-0.126 0.669,-0.093C0.636,-0.06 0.597,-0.034 0.552,-0.015C0.506,0.004 0.455,0.013 0.4,0.013ZM0.4,-0.13C0.429,-0.13 0.457,-0.136 0.482,-0.147C0.506,-0.158 0.528,-0.173 0.546,-0.193C0.564,-0.213 0.578,-0.236 0.588,-0.264C0.598,-0.291 0.603,-0.32 0.603,-0.351C0.603,-0.382 0.598,-0.411 0.588,-0.438C0.578,-0.465 0.564,-0.488 0.546,-0.508C0.528,-0.528 0.506,-0.544 0.482,-0.555C0.457,-0.566 0.429,-0.571 0.4,-0.571C0.37,-0.571 0.343,-0.566 0.318,-0.555C0.293,-0.544 0.272,-0.528 0.254,-0.508C0.235,-0.488 0.221,-0.465 0.212,-0.438C0.202,-0.411 0.197,-0.382 0.197,-0.351C0.197,-0.32 0.202,-0.291 0.212,-0.264C0.221,-0.236 0.235,-0.213 0.254,-0.193C0.272,-0.173 0.293,-0.158 0.318,-0.147C0.343,-0.136 0.37,-0.13 0.4,-0.13Z" style="fill:rgb(255,216,67);fill-rule:nonzero;"/>
</g>
<g transform="matrix(136.163,0,0,136.163,2887.69,1719.32)">
<path d="M0.058,0L0.058,-0.422C0.058,-0.482 0.071,-0.534 0.096,-0.577C0.121,-0.62 0.157,-0.654 0.202,-0.677C0.247,-0.7 0.298,-0.712 0.356,-0.712C0.414,-0.712 0.465,-0.7 0.51,-0.677C0.555,-0.654 0.59,-0.62 0.616,-0.577C0.641,-0.534 0.654,-0.482 0.654,-0.422L0.654,0L0.488,0L0.488,-0.431C0.488,-0.457 0.483,-0.481 0.471,-0.502C0.461,-0.523 0.445,-0.54 0.426,-0.552C0.407,-0.564 0.383,-0.57 0.356,-0.57C0.329,-0.57 0.306,-0.564 0.286,-0.552C0.266,-0.54 0.251,-0.523 0.24,-0.502C0.229,-0.481 0.224,-0.457 0.224,-0.431L0.224,0L0.058,0Z" style="fill:rgb(255,216,67);fill-rule:nonzero;"/>
</g>
</g>
<g transform="matrix(2.05664,0,0,3.26643,-2793.17,-4723.02)">
<g transform="matrix(136.163,0,0,136.163,2412.48,1719.32)">
<path d="M0.341,0.012C0.314,0.012 0.29,0.005 0.269,-0.008C0.248,-0.021 0.231,-0.043 0.22,-0.072L0.001,-0.7L0.133,-0.7L0.319,-0.121C0.322,-0.114 0.325,-0.109 0.328,-0.106C0.331,-0.102 0.336,-0.1 0.341,-0.1C0.346,-0.1 0.351,-0.102 0.355,-0.106C0.358,-0.109 0.361,-0.114 0.363,-0.121L0.555,-0.7L0.681,-0.7L0.462,-0.072C0.452,-0.044 0.436,-0.023 0.414,-0.009C0.393,0.005 0.368,0.012 0.341,0.012Z" style="fill:white;fill-rule:nonzero;"/>
</g>
<g transform="matrix(136.163,0,0,136.163,2505.34,1719.32)">
<path d="M0.052,0L0.052,-0.105L0.182,-0.105L0.182,-0.595L0.052,-0.595L0.052,-0.7L0.432,-0.7L0.432,-0.595L0.302,-0.595L0.302,-0.105L0.432,-0.105L0.432,0L0.052,0Z" style="fill:white;fill-rule:nonzero;"/>
</g>
<g transform="matrix(136.163,0,0,136.163,2571.25,1719.32)">
<path d="M0.071,0L0.071,-0.105L0.372,-0.105C0.391,-0.105 0.408,-0.11 0.423,-0.119C0.437,-0.128 0.448,-0.141 0.457,-0.156C0.465,-0.171 0.469,-0.186 0.469,-0.203C0.469,-0.221 0.465,-0.237 0.457,-0.252C0.449,-0.267 0.438,-0.278 0.424,-0.287C0.409,-0.296 0.393,-0.3 0.374,-0.3L0.249,-0.3C0.21,-0.3 0.174,-0.308 0.143,-0.325C0.113,-0.341 0.088,-0.364 0.07,-0.393C0.052,-0.422 0.043,-0.457 0.043,-0.497C0.043,-0.537 0.052,-0.572 0.069,-0.603C0.086,-0.633 0.11,-0.657 0.14,-0.674C0.17,-0.691 0.204,-0.7 0.242,-0.7L0.543,-0.7L0.543,-0.595L0.253,-0.595C0.236,-0.595 0.22,-0.591 0.206,-0.582C0.192,-0.573 0.181,-0.562 0.173,-0.548C0.166,-0.534 0.162,-0.519 0.162,-0.503C0.162,-0.486 0.166,-0.471 0.173,-0.458C0.181,-0.444 0.192,-0.433 0.205,-0.425C0.219,-0.416 0.235,-0.412 0.252,-0.412L0.38,-0.412C0.423,-0.412 0.46,-0.404 0.491,-0.387C0.522,-0.37 0.546,-0.347 0.563,-0.318C0.58,-0.289 0.588,-0.255 0.588,-0.217C0.588,-0.172 0.579,-0.134 0.562,-0.102C0.544,-0.069 0.52,-0.044 0.49,-0.027C0.459,-0.009 0.425,0 0.387,0L0.071,0Z" style="fill:white;fill-rule:nonzero;"/>
</g>
<g transform="matrix(136.163,0,0,136.163,2656.62,1719.32)">
<path d="M0.052,0L0.052,-0.105L0.182,-0.105L0.182,-0.595L0.052,-0.595L0.052,-0.7L0.432,-0.7L0.432,-0.595L0.302,-0.595L0.302,-0.105L0.432,-0.105L0.432,0L0.052,0Z" style="fill:white;fill-rule:nonzero;"/>
</g>
<g transform="matrix(136.163,0,0,136.163,2722.52,1719.32)">
<path d="M0.401,0.013C0.346,0.013 0.296,0.003 0.251,-0.016C0.205,-0.035 0.167,-0.061 0.136,-0.094C0.104,-0.127 0.079,-0.166 0.062,-0.21C0.045,-0.254 0.036,-0.301 0.036,-0.351C0.036,-0.401 0.045,-0.448 0.062,-0.492C0.079,-0.535 0.104,-0.574 0.136,-0.607C0.167,-0.64 0.205,-0.666 0.251,-0.685C0.296,-0.704 0.346,-0.713 0.402,-0.713C0.457,-0.713 0.507,-0.704 0.552,-0.685C0.597,-0.666 0.635,-0.64 0.667,-0.607C0.699,-0.574 0.724,-0.535 0.741,-0.492C0.758,-0.448 0.767,-0.401 0.767,-0.351C0.767,-0.301 0.758,-0.254 0.741,-0.21C0.724,-0.166 0.699,-0.127 0.667,-0.094C0.635,-0.061 0.597,-0.035 0.552,-0.016C0.507,0.003 0.457,0.013 0.401,0.013ZM0.401,-0.093C0.436,-0.093 0.469,-0.099 0.499,-0.112C0.529,-0.125 0.555,-0.143 0.578,-0.167C0.6,-0.19 0.617,-0.218 0.629,-0.249C0.641,-0.28 0.647,-0.314 0.647,-0.351C0.647,-0.388 0.641,-0.422 0.629,-0.453C0.617,-0.484 0.6,-0.511 0.578,-0.535C0.555,-0.558 0.529,-0.576 0.499,-0.589C0.469,-0.602 0.436,-0.608 0.401,-0.608C0.366,-0.608 0.334,-0.602 0.304,-0.589C0.274,-0.576 0.248,-0.558 0.226,-0.535C0.203,-0.511 0.186,-0.484 0.173,-0.453C0.161,-0.422 0.155,-0.388 0.155,-0.351C0.155,-0.314 0.161,-0.28 0.173,-0.249C0.186,-0.218 0.203,-0.19 0.226,-0.167C0.248,-0.143 0.274,-0.125 0.304,-0.112C0.334,-0.099 0.366,-0.093 0.401,-0.093Z" style="fill:white;fill-rule:nonzero;"/>
</g>
<g transform="matrix(136.163,0,0,136.163,2831.86,1719.32)">
<path d="M0.069,0L0.069,-0.426C0.069,-0.485 0.081,-0.536 0.106,-0.579C0.13,-0.622 0.163,-0.655 0.206,-0.678C0.249,-0.701 0.298,-0.712 0.353,-0.712C0.408,-0.712 0.457,-0.701 0.5,-0.678C0.543,-0.655 0.576,-0.622 0.601,-0.579C0.626,-0.536 0.638,-0.485 0.638,-0.426L0.638,0L0.518,0L0.518,-0.436C0.518,-0.467 0.511,-0.496 0.498,-0.522C0.485,-0.548 0.466,-0.569 0.442,-0.584C0.417,-0.599 0.388,-0.607 0.353,-0.607C0.32,-0.607 0.291,-0.599 0.266,-0.584C0.241,-0.569 0.222,-0.548 0.209,-0.522C0.195,-0.496 0.188,-0.467 0.188,-0.436L0.188,0L0.069,0Z" style="fill:white;fill-rule:nonzero;"/>
</g>
</g>
<g transform="matrix(1.47838,0,0,2.34801,-955.236,-8562.95)">
<path d="M3055.09,3977.51C3050.3,3984.25 3045,3990.56 3039.21,3996.35C2987.91,4047.65 2917.1,4038.77 2881.16,3976.54C2845.23,3914.3 2857.71,3822.13 2909.01,3770.83C2960.31,3719.53 3031.13,3728.41 3067.06,3790.64C3069.85,3795.48 3072.35,3800.49 3074.56,3805.67L3039.78,3811.64C3012.82,3769.64 2962.9,3764.58 2926.45,3801.04C2888.89,3838.59 2879.76,3906.07 2906.07,3951.63C2932.37,3997.19 2984.22,4003.69 3021.77,3966.14L3021.89,3966.01L3055.09,3977.51ZM3085.02,3841.47C3090.86,3875.56 3086.6,3912.35 3073.22,3944.57L3043.91,3934.42C3056.74,3907.59 3060.53,3875.54 3054.13,3846.78L3085.02,3841.47Z" style="fill:white;"/>
</g>
<g transform="matrix(1.47838,0,0,2.34801,-955.236,-3103.7)">
<path d="M2906.78,1571.77L3111.02,1642.48L3116.61,1626.34L3147.2,1664.74L3099.42,1675.99L3105,1659.86L2910.03,1592.35C2908.25,1585.69 2907.18,1578.77 2906.78,1571.77ZM2917.45,1517.07L3114.77,1483.17L3111.88,1466.34L3157.2,1485.21L3120.78,1518.13L3117.88,1501.3L2910.22,1536.97C2911.99,1530.09 2914.41,1523.4 2917.45,1517.07Z" style="fill:rgb(255,216,67);"/>
</g>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 49 KiB

View File

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

Before

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

View File

@@ -1,24 +0,0 @@
@import "@fontsource/prompt";
$default-font: "Prompt", sans-serif !default;
$body-font-family: $default-font;
$heading-font-family: $default-font;
.v-application {
font-family: $default-font !important;
}
.v-row-group__header {
background: #005281 !important;
}
.theme--dark.v-data-table
> .v-data-table__wrapper
> table
> tbody
> tr:hover:not(.v-data-table__expanded__content):not(.v-data-table__empty-wrapper) {
background: #005281 !important;
}
.v-card__title {
word-break: break-word !important;
}

View File

@@ -1,206 +0,0 @@
<script setup lang="ts">
import type { PhotonTarget } from "@/types/PhotonTrackingTypes";
import { onBeforeUnmount, onMounted, watchEffect } from "vue";
import {
ArrowHelper,
BoxGeometry,
Color,
ConeGeometry,
Mesh,
MeshNormalMaterial,
type Object3D,
PerspectiveCamera,
Quaternion,
Scene,
Vector3,
WebGLRenderer
} from "three";
import { TrackballControls } from "three/examples/jsm/controls/TrackballControls";
const props = defineProps<{
targets: PhotonTarget[];
}>();
let scene: Scene | undefined;
let camera: PerspectiveCamera | undefined;
let renderer: WebGLRenderer | undefined;
let controls: TrackballControls | undefined;
let previousTargets: Object3D[] = [];
const drawTargets = (targets: PhotonTarget[]) => {
// Check here, since if we check in watchEffect this never gets called
if (scene === undefined || camera === undefined || renderer === undefined || controls === undefined) {
return;
}
scene.remove(...previousTargets);
previousTargets = [];
targets.forEach((target) => {
if (target.pose === undefined) return;
const geometry = new BoxGeometry(0.3 / 5, 0.2, 0.2);
const material = new MeshNormalMaterial();
const quaternion = new Quaternion(target.pose.qx, target.pose.qy, target.pose.qz, target.pose.qw);
const cube = new Mesh(geometry, material);
cube.position.set(target.pose.x, target.pose.y, target.pose.z);
cube.rotation.setFromQuaternion(quaternion);
previousTargets.push(cube);
let arrow = new ArrowHelper(new Vector3(1, 0, 0).normalize(), new Vector3(0, 0, 0), 1, 0xff0000, 0.1, 0.1);
arrow.rotation.setFromQuaternion(quaternion);
arrow.rotateZ(-Math.PI / 2);
arrow.position.set(target.pose.x, target.pose.y, target.pose.z);
previousTargets.push(arrow);
arrow = new ArrowHelper(new Vector3(1, 0, 0).normalize(), new Vector3(0, 0, 0), 1, 0x00ff00, 0.1, 0.1);
arrow.rotation.setFromQuaternion(quaternion);
arrow.position.set(target.pose.x, target.pose.y, target.pose.z);
previousTargets.push(arrow);
arrow = new ArrowHelper(new Vector3(1, 0, 0).normalize(), new Vector3(0, 0, 0), 1, 0x0000ff, 0.1, 0.1);
arrow.setRotationFromQuaternion(quaternion);
arrow.rotateX(Math.PI / 2);
arrow.position.set(target.pose.x, target.pose.y, target.pose.z);
previousTargets.push(arrow);
});
if (previousTargets.length > 0) {
scene.add(...previousTargets);
}
};
const onWindowResize = () => {
const container = document.getElementById("container");
const canvas = document.getElementById("view");
if (container === null || canvas === null || camera === undefined || renderer === undefined) {
return;
}
canvas.style.width = container.clientWidth * 0.75 + "px";
canvas.style.height = container.clientWidth * 0.35 + "px";
camera.aspect = canvas.clientWidth / canvas.clientHeight;
camera.updateProjectionMatrix();
renderer.setSize(canvas.clientWidth, canvas.clientHeight);
};
const resetCamFirstPerson = () => {
if (scene === undefined || camera === undefined || controls === undefined) {
return;
}
controls.reset();
camera.position.set(0.2, 0, 0);
camera.up.set(0, 0, 1);
controls.target.set(4.0, 0.0, 0.0);
controls.update();
if (previousTargets.length > 0) {
scene.add(...previousTargets);
}
};
const resetCamThirdPerson = () => {
if (scene === undefined || camera === undefined || controls === undefined) {
return;
}
controls.reset();
camera.position.set(-1.39, -1.09, 1.17);
camera.up.set(0, 0, 1);
controls.target.set(4.0, 0.0, 0.0);
controls.update();
if (previousTargets.length > 0) {
scene.add(...previousTargets);
}
};
onMounted(() => {
scene = new Scene();
camera = new PerspectiveCamera(75, 800 / 800, 0.1, 1000);
const canvas = document.getElementById("view");
if (canvas === null) return;
renderer = new WebGLRenderer({ canvas: canvas });
scene.background = new Color(0xa9a9a9);
onWindowResize();
window.addEventListener("resize", onWindowResize);
const referenceFrameCues: Object3D[] = [];
referenceFrameCues.push(
new ArrowHelper(new Vector3(1, 0, 0).normalize(), new Vector3(0, 0, 0), 1, 0xff0000, 0.1, 0.1)
);
referenceFrameCues.push(
new ArrowHelper(new Vector3(0, 1, 0).normalize(), new Vector3(0, 0, 0), 1, 0x00ff00, 0.1, 0.1)
);
referenceFrameCues.push(
new ArrowHelper(new Vector3(0, 0, 1).normalize(), new Vector3(0, 0, 0), 1, 0x0000ff, 0.1, 0.1)
);
// Draw the Camera Body
const camSize = 0.2;
const camBodyGeometry = new BoxGeometry(camSize, camSize, camSize);
const camLensGeometry = new ConeGeometry(camSize * 0.4, camSize * 0.8, 30);
const camMaterial = new MeshNormalMaterial();
const camBody = new Mesh(camBodyGeometry, camMaterial);
const camLens = new Mesh(camLensGeometry, camMaterial);
camBody.position.set(0, 0, 0);
camLens.rotateZ(Math.PI / 2);
camLens.position.set(camSize * 0.8, 0, 0);
referenceFrameCues.push(camBody);
referenceFrameCues.push(camLens);
controls = new TrackballControls(camera, renderer.domElement);
controls.rotateSpeed = 1.0;
controls.zoomSpeed = 1.2;
controls.panSpeed = 0.8;
controls.noZoom = false;
controls.noPan = false;
controls.staticMoving = true;
controls.dynamicDampingFactor = 0.3;
scene.add(...referenceFrameCues);
resetCamThirdPerson();
controls.update();
const animate = () => {
if (scene === undefined || camera === undefined || renderer === undefined || controls === undefined) {
return;
}
requestAnimationFrame(animate);
controls.update();
renderer.render(scene, camera);
};
drawTargets(props.targets);
animate();
});
onBeforeUnmount(() => {
window.removeEventListener("resize", onWindowResize);
});
watchEffect(() => {
drawTargets(props.targets);
});
</script>
<template>
<div id="container" style="width: 100%">
<v-row>
<v-col align-self="stretch" style="display: flex; justify-content: center">
<canvas id="view" />
</v-col>
</v-row>
<v-row style="margin-bottom: 24px">
<v-col style="display: flex; justify-content: center">
<v-btn color="secondary" @click="resetCamFirstPerson"> First Person </v-btn>
</v-col>
<v-col style="display: flex; justify-content: center">
<v-btn color="secondary" @click="resetCamThirdPerson"> Third Person </v-btn>
</v-col>
</v-row>
</div>
</template>

View File

@@ -1,101 +0,0 @@
<script setup lang="ts">
import { computed, inject } from "vue";
import { useCameraSettingsStore } from "@/stores/settings/CameraSettingsStore";
import { useStateStore } from "@/stores/StateStore";
import loadingImage from "@/assets/images/loading.svg";
import type { StyleValue } from "vue/types/jsx";
import PvIcon from "@/components/common/pv-icon.vue";
const props = defineProps<{
streamType: "Raw" | "Processed";
id: string;
}>();
const streamSrc = computed<string>(() => {
const port =
useCameraSettingsStore().currentCameraSettings.stream[props.streamType === "Raw" ? "inputPort" : "outputPort"];
if (!useStateStore().backendConnected || port === 0) {
return loadingImage;
}
return `http://${inject("backendHostname")}:${port}/stream.mjpg`;
});
const streamDesc = computed<string>(() => `${props.streamType} Stream View`);
const streamStyle = computed<StyleValue>(() => {
if (useStateStore().colorPickingMode) {
return { width: "100%", cursor: "crosshair" };
}
return { width: "100%" };
});
const overlayStyle = computed<StyleValue>(() => {
if (useStateStore().colorPickingMode || streamSrc.value == loadingImage) {
return { display: "none" };
} else {
return {};
}
});
const handleCaptureClick = () => {
if (props.streamType === "Raw") {
useCameraSettingsStore().saveInputSnapshot();
} else {
useCameraSettingsStore().saveOutputSnapshot();
}
};
const handlePopoutClick = () => {
window.open(streamSrc.value);
};
const handleFullscreenRequest = () => {
const stream = document.getElementById(props.id);
if (!stream) return;
stream.requestFullscreen();
};
</script>
<template>
<div class="stream-container">
<img :id="id" crossorigin="anonymous" :src="streamSrc" :alt="streamDesc" :style="streamStyle" />
<div class="stream-overlay" :style="overlayStyle">
<pv-icon
icon-name="mdi-camera-image"
tooltip="Capture and save a frame of this stream"
class="ma-1 mr-2"
@click="handleCaptureClick"
/>
<pv-icon
icon-name="mdi-fullscreen"
tooltip="Open this stream in fullscreen"
class="ma-1 mr-2"
@click="handleFullscreenRequest"
/>
<pv-icon
icon-name="mdi-open-in-new"
tooltip="Open this stream in a new window"
class="ma-1 mr-2"
@click="handlePopoutClick"
/>
</div>
</div>
</template>
<style scoped>
.stream-container {
position: relative;
}
.stream-overlay {
display: flex;
opacity: 0;
transition: 0.1s ease;
position: absolute;
top: 0;
right: 0;
}
.stream-container:hover .stream-overlay {
opacity: 1;
}
</style>

View File

@@ -1,16 +0,0 @@
<script setup lang="ts">
import { useStateStore } from "@/stores/StateStore";
</script>
<template>
<v-snackbar
v-model="useStateStore().snackbarData.show"
top
:color="useStateStore().snackbarData.color"
:timeout="useStateStore().snackbarData.timeout"
>
<p style="padding: 0; margin: 0; text-align: center">
{{ useStateStore().snackbarData.message }}
</p>
</v-snackbar>
</template>

View File

@@ -1,127 +0,0 @@
<script setup lang="ts">
import { computed, inject, ref } from "vue";
import { LogLevel, type LogMessage } from "@/types/SettingTypes";
import { useStateStore } from "@/stores/StateStore";
const selectedLogLevels = ref<LogLevel[]>([LogLevel.ERROR, LogLevel.WARN, LogLevel.INFO]);
const logs = computed<LogMessage[]>(() =>
useStateStore().logMessages.filter((message) => selectedLogLevels.value.includes(message.level))
);
const backendHost = inject<string>("backendHost");
const getLogColor = (level: LogLevel): string => {
switch (level) {
case LogLevel.ERROR:
return "red";
case LogLevel.WARN:
return "yellow";
case LogLevel.INFO:
return "green";
case LogLevel.DEBUG:
return "white";
}
return "";
};
const getLogLevelFromIndex = (index: number): string => {
return LogLevel[index];
};
const exportLogFile = ref();
const handleLogExport = () => {
exportLogFile.value.click();
};
document.addEventListener("keydown", (e) => {
switch (e.key) {
case "`":
useStateStore().$patch((state) => (state.showLogModal = !state.showLogModal));
break;
}
});
</script>
<template>
<v-dialog v-model="useStateStore().showLogModal" width="1500" dark>
<v-card dark class="pt-3" color="primary" flat>
<v-row class="heading-container pl-6 pr-6">
<v-col>
<v-card-title>View Program Logs</v-card-title>
</v-col>
<v-col class="align-self-center">
<v-btn
color="secondary"
style="margin-left: auto; max-width: 500px; width: 100%"
depressed
@click="handleLogExport"
>
<v-icon left class="open-icon"> mdi-download </v-icon>
<span class="open-label">Download Current Log</span>
<!-- Special hidden link that gets 'clicked' when the user exports journalctl logs -->
<a
ref="exportLogFile"
style="color: black; text-decoration: none; display: none"
:href="`http://${backendHost}/api/utils/photonvision-journalctl.txt`"
download="photonvision-journalctl.txt"
target="_blank"
/>
</v-btn>
</v-col>
</v-row>
<div class="pr-6 pl-6">
<v-btn-toggle v-model="selectedLogLevels" dark multiple class="fill mb-4 overflow-x-auto">
<v-btn v-for="level in [0, 1, 2, 3]" :key="level" color="secondary" class="fill">
{{ getLogLevelFromIndex(level) }}
</v-btn>
</v-btn-toggle>
<v-card-text v-if="logs.length === 0" style="font-size: 18px; font-weight: 600">
There are no logs to show
</v-card-text>
<v-virtual-scroll v-else :items="logs" item-height="50" height="600">
<template #default="{ item }">
<div :class="[getLogColor(item.level) + '--text', 'log-item']">
{{ item.message }}
</div>
</template>
</v-virtual-scroll>
</div>
<v-divider />
<v-card-actions>
<v-spacer />
<v-btn color="white" text @click="() => (useStateStore().showLogModal = false)"> Close </v-btn>
</v-card-actions>
</v-card>
</v-dialog>
</template>
<style scoped>
.v-btn-toggle.fill {
width: 100%;
height: 100%;
}
.v-btn-toggle.fill > .v-btn {
width: 25%;
height: 100%;
}
@media only screen and (max-width: 512px) {
.heading-container {
flex-direction: column;
padding-bottom: 14px;
}
}
@media only screen and (max-width: 312px) {
.open-icon {
margin: 0 !important;
}
.open-label {
display: none;
}
}
</style>

View File

@@ -1,122 +0,0 @@
<script setup lang="ts">
import { computed, getCurrentInstance } from "vue";
import { useSettingsStore } from "@/stores/settings/GeneralSettingsStore";
import { useStateStore } from "@/stores/StateStore";
const compact = computed<boolean>({
get: () => {
return useStateStore().sidebarFolded;
},
set: (val) => {
useStateStore().setSidebarFolded(val);
}
});
// Vuetify2 doesn't yet support the useDisplay API so this is required to access the prop when using the Composition API
const mdAndUp = computed<boolean>(() => getCurrentInstance()?.proxy.$vuetify.breakpoint.mdAndUp || false);
</script>
<template>
<v-navigation-drawer dark app permanent :mini-variant="compact || !mdAndUp" color="primary">
<v-list>
<!-- List item for the heading; note that there are some tricks in setting padding and image width make things look right -->
<v-list-item :class="compact || !mdAndUp ? 'pr-0 pl-0' : ''" style="display: flex; justify-content: center">
<v-list-item-icon class="mr-0">
<img v-if="!(compact || !mdAndUp)" class="logo" src="@/assets/images/logoLarge.svg" alt="large logo" />
<img v-else class="logo" src="@/assets/images/logoSmall.svg" alt="small logo" />
</v-list-item-icon>
</v-list-item>
<v-list-item link to="/dashboard">
<v-list-item-icon>
<v-icon>mdi-view-dashboard</v-icon>
</v-list-item-icon>
<v-list-item-content>
<v-list-item-title>Dashboard</v-list-item-title>
</v-list-item-content>
</v-list-item>
<v-list-item ref="camerasTabOpener" link to="/cameras">
<v-list-item-icon>
<v-icon>mdi-camera</v-icon>
</v-list-item-icon>
<v-list-item-content>
<v-list-item-title>Cameras</v-list-item-title>
</v-list-item-content>
</v-list-item>
<v-list-item link to="/settings">
<v-list-item-icon>
<v-icon>mdi-cog</v-icon>
</v-list-item-icon>
<v-list-item-content>
<v-list-item-title>Settings</v-list-item-title>
</v-list-item-content>
</v-list-item>
<v-list-item link to="/docs">
<v-list-item-icon>
<v-icon>mdi-bookshelf</v-icon>
</v-list-item-icon>
<v-list-item-content>
<v-list-item-title>Documentation</v-list-item-title>
</v-list-item-content>
</v-list-item>
<v-list-item v-if="mdAndUp" link @click="() => (compact = !compact)">
<v-list-item-icon>
<v-icon v-if="compact || !mdAndUp"> mdi-chevron-right </v-icon>
<v-icon v-else> mdi-chevron-left </v-icon>
</v-list-item-icon>
<v-list-item-content>
<v-list-item-title>Compact Mode</v-list-item-title>
</v-list-item-content>
</v-list-item>
<div style="position: absolute; bottom: 0; left: 0">
<v-list-item>
<v-list-item-icon>
<v-icon v-if="useSettingsStore().network.runNTServer"> mdi-server </v-icon>
<v-icon v-else-if="useStateStore().ntConnectionStatus.connected"> mdi-robot </v-icon>
<v-icon v-else style="border-radius: 100%"> mdi-robot-off </v-icon>
</v-list-item-icon>
<v-list-item-content>
<v-list-item-title v-if="useSettingsStore().network.runNTServer" class="text-wrap">
NetworkTables server running for
<span class="accent--text">{{ useStateStore().ntConnectionStatus.clients || 0 }}</span> clients
</v-list-item-title>
<v-list-item-title
v-else-if="useStateStore().ntConnectionStatus.connected && useStateStore().backendConnected"
class="text-wrap"
style="flex-direction: column; display: flex"
>
NetworkTables Server Connected!
<span class="accent--text">
{{ useStateStore().ntConnectionStatus.address }}
</span>
</v-list-item-title>
<v-list-item-title v-else class="text-wrap" style="flex-direction: column; display: flex">
Not connected to NetworkTables Server!
</v-list-item-title>
</v-list-item-content>
</v-list-item>
<v-list-item>
<v-list-item-icon>
<v-icon v-if="useStateStore().backendConnected"> mdi-server-network </v-icon>
<v-icon v-else style="border-radius: 100%"> mdi-server-network-off </v-icon>
</v-list-item-icon>
<v-list-item-content>
<v-list-item-title class="text-wrap">
{{ useStateStore().backendConnected ? "Backend connected" : "Trying to connect to backend" }}
</v-list-item-title>
</v-list-item-content>
</v-list-item>
</div>
</v-list>
</v-navigation-drawer>
</template>
<style scoped>
.logo {
width: 100%;
height: 70px;
object-fit: contain;
}
</style>

View File

@@ -1,571 +0,0 @@
<script setup lang="ts">
import { computed, ref } from "vue";
import { useCameraSettingsStore } from "@/stores/settings/CameraSettingsStore";
import { CalibrationBoardTypes, type VideoFormat } from "@/types/SettingTypes";
import JsPDF from "jspdf";
import { font as PromptRegular } from "@/assets/fonts/PromptRegular";
import MonoLogo from "@/assets/images/logoMono.png";
import PvSlider from "@/components/common/pv-slider.vue";
import { useStateStore } from "@/stores/StateStore";
import PvSwitch from "@/components/common/pv-switch.vue";
import PvSelect from "@/components/common/pv-select.vue";
import PvNumberInput from "@/components/common/pv-number-input.vue";
import { WebsocketPipelineType } from "@/types/WebsocketDataTypes";
import { getResolutionString, resolutionsAreEqual } from "@/lib/PhotonUtils";
import CameraCalibrationInfoCard from "@/components/cameras/CameraCalibrationInfoCard.vue";
import { useSettingsStore } from "@/stores/settings/GeneralSettingsStore";
const settingsValid = ref(true);
const getUniqueVideoFormatsByResolution = (): VideoFormat[] => {
const uniqueResolutions: VideoFormat[] = [];
useCameraSettingsStore().currentCameraSettings.validVideoFormats.forEach((format, index) => {
if (!uniqueResolutions.some((v) => resolutionsAreEqual(v.resolution, format.resolution))) {
format.index = index;
const calib = useCameraSettingsStore().getCalibrationCoeffs(format.resolution);
if (calib !== undefined) {
// Is this the right formula for RMS error? who knows! not me!
const perViewSumSquareReprojectionError = calib.observations.flatMap((it) =>
it.reprojectionErrors.flatMap((it2) => [it2.x, it2.y])
);
// For each error, square it, sum the squares, and divide by total points N
format.mean = Math.sqrt(
perViewSumSquareReprojectionError.map((it) => Math.pow(it, 2)).reduce((a, b) => a + b, 0) /
perViewSumSquareReprojectionError.length
);
format.horizontalFOV =
2 * Math.atan2(format.resolution.width / 2, calib.cameraIntrinsics.data[0]) * (180 / Math.PI);
format.verticalFOV =
2 * Math.atan2(format.resolution.height / 2, calib.cameraIntrinsics.data[4]) * (180 / Math.PI);
format.diagonalFOV =
2 *
Math.atan2(
Math.sqrt(
format.resolution.width ** 2 +
(format.resolution.height / (calib.cameraIntrinsics.data[4] / calib.cameraIntrinsics.data[0])) ** 2
) / 2,
calib.cameraIntrinsics.data[0]
) *
(180 / Math.PI);
}
uniqueResolutions.push(format);
}
});
uniqueResolutions.sort(
(a, b) => b.resolution.width + b.resolution.height - (a.resolution.width + a.resolution.height)
);
return uniqueResolutions;
};
const getUniqueVideoResolutionStrings = (): { name: string; value: number }[] =>
getUniqueVideoFormatsByResolution().map<{ name: string; value: number }>((f) => ({
name: `${getResolutionString(f.resolution)}`,
// Index won't ever be undefined
value: f.index || 0
}));
const calibrationDivisors = computed(() =>
[1, 2, 4].filter((v) => {
const currentRes = useCameraSettingsStore().currentVideoFormat.resolution;
return (currentRes.width / v >= 300 && currentRes.height / v >= 220) || v === 1;
})
);
const squareSizeIn = ref(1);
const patternWidth = ref(8);
const patternHeight = ref(8);
const boardType = ref<CalibrationBoardTypes>(CalibrationBoardTypes.Chessboard);
const useMrCalRef = ref(true);
const useMrCal = computed<boolean>({
get() {
return useMrCalRef.value && useSettingsStore().general.mrCalWorking;
},
set(value) {
useMrCalRef.value = value && useSettingsStore().general.mrCalWorking;
}
});
const downloadCalibBoard = () => {
const doc = new JsPDF({ unit: "in", format: "letter" });
doc.addFileToVFS("Prompt-Regular.tff", PromptRegular);
doc.addFont("Prompt-Regular.tff", "Prompt-Regular", "normal");
doc.setFont("Prompt-Regular");
doc.setFontSize(12);
const paperWidth = 8.5;
const paperHeight = 11.0;
switch (boardType.value) {
case CalibrationBoardTypes.Chessboard:
// eslint-disable-next-line no-case-declarations
const chessboardStartX = (paperWidth - patternWidth.value * squareSizeIn.value) / 2;
// eslint-disable-next-line no-case-declarations
const chessboardStartY = (paperHeight - patternWidth.value * squareSizeIn.value) / 2;
for (let squareY = 0; squareY < patternHeight.value; squareY++) {
for (let squareX = 0; squareX < patternWidth.value; squareX++) {
const xPos = chessboardStartX + squareX * squareSizeIn.value;
const yPos = chessboardStartY + squareY * squareSizeIn.value;
// Only draw the odd squares to create the chessboard pattern
if ((xPos + yPos + 0.25) % 2 === 0) {
doc.rect(xPos, yPos, squareSizeIn.value, squareSizeIn.value, "F");
}
}
}
break;
case CalibrationBoardTypes.DotBoard:
// eslint-disable-next-line no-case-declarations
const dotgridStartX =
(paperWidth - (2 * (patternWidth.value - 1) + ((patternHeight.value - 1) % 2)) * squareSizeIn.value) / 2.0;
// eslint-disable-next-line no-case-declarations
const dotgridStartY = (paperHeight - (patternHeight.value - squareSizeIn.value)) / 2;
for (let squareY = 0; squareY < patternHeight.value; squareY++) {
for (let squareX = 0; squareX < patternWidth.value; squareX++) {
const xPos = dotgridStartX + (2 * squareX + (squareY % 2)) * squareSizeIn.value;
const yPos = dotgridStartY + squareY * squareSizeIn.value;
doc.circle(xPos, yPos, squareSizeIn.value / 4, "F");
}
}
break;
}
// Draw ruler pattern
const lineStartX = 1.0;
const lineEndX = paperWidth - lineStartX;
const lineY = paperHeight - 1.0;
doc.setLineWidth(0.01);
doc.line(lineStartX, lineY, lineEndX, lineY);
for (let tickX = lineStartX; tickX <= lineEndX; tickX++) {
doc.line(tickX, lineY, tickX, lineY + 0.25);
doc.text(`${tickX - 1}${tickX - 1 === 0 ? " in" : ""}`, tickX + 0.1, lineY + 0.25);
}
// Add branding
const logoImage = new Image();
logoImage.src = MonoLogo;
doc.addImage(logoImage, "PNG", 1.0, 0.75, 1.4, 0.5);
doc.text(`${patternWidth.value} x ${patternHeight.value} | ${squareSizeIn.value}in`, paperWidth - 1, 1.0, {
maxWidth: (paperWidth - 2.0) / 2,
align: "right"
});
doc.save(`calibrationTarget-${CalibrationBoardTypes[boardType.value]}.pdf`);
};
const importCalibrationFromCalibDB = ref();
const openCalibUploadPrompt = () => {
importCalibrationFromCalibDB.value.click();
};
const readImportedCalibrationFromCalibDB = () => {
const files = importCalibrationFromCalibDB.value.files;
if (files.length === 0) return;
files[0].text().then((text) => {
useCameraSettingsStore()
.importCalibDB({ payload: text, filename: files[0].name })
.then((response) => {
useStateStore().showSnackbarMessage({
message: response.data.text || response.data,
color: response.status === 200 ? "success" : "error"
});
})
.catch((err) => {
if (err.request) {
useStateStore().showSnackbarMessage({
message: "Error while uploading calibration file! The backend didn't respond to the upload attempt.",
color: "error"
});
} else {
useStateStore().showSnackbarMessage({
message: "Error while uploading calibration file!",
color: "error"
});
}
});
});
};
const isCalibrating = ref(false);
const startCalibration = () => {
useCameraSettingsStore().startPnPCalibration({
squareSizeIn: squareSizeIn.value,
patternHeight: patternHeight.value,
patternWidth: patternWidth.value,
boardType: boardType.value,
useMrCal: useMrCal.value
});
// The Start PnP method already handles updating the backend so only a store update is required
useCameraSettingsStore().currentCameraSettings.currentPipelineIndex = WebsocketPipelineType.Calib3d;
isCalibrating.value = true;
calibCanceled.value = false;
};
const showCalibEndDialog = ref(false);
const calibCanceled = ref(false);
const calibSuccess = ref<boolean | undefined>(undefined);
const endCalibration = () => {
if (!useStateStore().calibrationData.hasEnoughImages) {
calibCanceled.value = true;
}
showCalibEndDialog.value = true;
// Check if calibration finished cleanly or was canceled
useCameraSettingsStore()
.endPnPCalibration()
.then(() => {
calibSuccess.value = true;
})
.catch(() => {
calibSuccess.value = false;
})
.finally(() => {
isCalibrating.value = false;
});
};
let showCalDialog = ref(false);
let selectedVideoFormat = ref<VideoFormat | undefined>(undefined);
const setSelectedVideoFormat = (format: VideoFormat) => {
selectedVideoFormat.value = format;
showCalDialog.value = true;
};
</script>
<template>
<div>
<v-card class="mb-3 pr-6 pb-3" color="primary" dark>
<v-card-title>Camera Calibration</v-card-title>
<div class="ml-5">
<v-row v-show="!isCalibrating" class="pb-12">
<v-card-subtitle class="pb-0 mb-0 pl-3">Complete Calibrations</v-card-subtitle>
<v-simple-table fixed-header height="100%" dense class="mt-2">
<thead>
<tr>
<th>Resolution</th>
<th>Mean Error</th>
<th>Horizontal FOV</th>
<th>Vertical FOV</th>
<th>Diagonal FOV</th>
</tr>
</thead>
<tbody>
<tr
v-for="(value, index) in getUniqueVideoFormatsByResolution()"
:key="index"
title="Click to get calibration specific information"
@click="setSelectedVideoFormat(value)"
>
<td>{{ getResolutionString(value.resolution) }}</td>
<td>
{{ value.mean !== undefined ? (isNaN(value.mean) ? "NaN" : value.mean.toFixed(2) + "px") : "-" }}
</td>
<td>{{ value.horizontalFOV !== undefined ? value.horizontalFOV.toFixed(2) + "°" : "-" }}</td>
<td>{{ value.verticalFOV !== undefined ? value.verticalFOV.toFixed(2) + "°" : "-" }}</td>
<td>{{ value.diagonalFOV !== undefined ? value.diagonalFOV.toFixed(2) + "°" : "-" }}</td>
</tr>
</tbody>
</v-simple-table>
</v-row>
<v-divider />
<v-row style="display: flex; flex-direction: column" class="mt-4">
<v-card-subtitle v-show="!isCalibrating" class="pl-3 pa-0 ma-0"> Configure New Calibration</v-card-subtitle>
<v-form ref="form" v-model="settingsValid" class="pl-4 mb-10 pr-5">
<pv-select
v-model="useStateStore().calibrationData.videoFormatIndex"
label="Resolution"
:select-cols="7"
:disabled="isCalibrating"
tooltip="Resolution to calibrate at (you will have to calibrate every resolution you use 3D mode on)"
:items="getUniqueVideoResolutionStrings()"
/>
<pv-select
v-show="isCalibrating"
v-model="useCameraSettingsStore().currentPipelineSettings.streamingFrameDivisor"
label="Decimation"
tooltip="Resolution to which camera frames are downscaled for detection. Calibration still uses full-res"
:items="calibrationDivisors"
:select-cols="7"
@input="(v) => useCameraSettingsStore().changeCurrentPipelineSetting({ streamingFrameDivisor: v }, false)"
/>
<pv-select
v-model="boardType"
label="Board Type"
tooltip="Calibration board pattern to use"
:select-cols="7"
:items="['Chessboard', 'Dotboard']"
:disabled="isCalibrating"
/>
<pv-number-input
v-model="squareSizeIn"
label="Pattern Spacing (in)"
tooltip="Spacing between pattern features in inches"
:disabled="isCalibrating"
:rules="[(v) => v > 0 || 'Size must be positive']"
:label-cols="5"
/>
<pv-number-input
v-model="patternWidth"
label="Board Width (squares)"
tooltip="Width of the board in dots or chessboard squares"
:disabled="isCalibrating"
:rules="[(v) => v >= 4 || 'Width must be at least 4']"
:label-cols="5"
/>
<pv-number-input
v-model="patternHeight"
label="Board Height (squares)"
tooltip="Height of the board in dots or chessboard squares"
:disabled="isCalibrating"
:rules="[(v) => v >= 4 || 'Height must be at least 4']"
:label-cols="5"
/>
<pv-switch
v-model="useMrCal"
label="Try using MrCal over OpenCV"
:disabled="!useSettingsStore().general.mrCalWorking || isCalibrating"
tooltip="If enabled, Photon will (try to) use MrCal instead of OpenCV for camera calibration."
:label-cols="5"
/>
<v-banner
v-show="!useSettingsStore().general.mrCalWorking"
rounded
color="red"
text-color="white"
class="mt-3"
icon="mdi-alert-circle-outline"
>
MrCal JNI could not be loaded! Consult journalctl logs for additional details.
</v-banner>
</v-form>
<v-row justify="center">
<v-chip
v-show="isCalibrating"
label
:color="useStateStore().calibrationData.hasEnoughImages ? 'secondary' : 'gray'"
class="mb-6"
>
Snapshots: {{ useStateStore().calibrationData.imageCount }} of at least
{{ useStateStore().calibrationData.minimumImageCount }}
</v-chip>
</v-row>
</v-row>
<v-row v-if="isCalibrating">
<v-col cols="12" class="pt-0">
<pv-slider
v-model="useCameraSettingsStore().currentPipelineSettings.cameraExposure"
:disabled="useCameraSettingsStore().currentCameraSettings.pipelineSettings.cameraAutoExposure"
label="Exposure"
tooltip="Directly controls how much light is allowed to fall onto the sensor, which affects apparent brightness"
:min="0"
:max="100"
:slider-cols="8"
:step="0.1"
@input="(args) => useCameraSettingsStore().changeCurrentPipelineSetting({ cameraExposure: args }, false)"
/>
<pv-slider
v-model="useCameraSettingsStore().currentPipelineSettings.cameraBrightness"
label="Brightness"
:min="0"
:max="100"
:slider-cols="8"
@input="
(args) => useCameraSettingsStore().changeCurrentPipelineSetting({ cameraBrightness: args }, false)
"
/>
<pv-switch
v-model="useCameraSettingsStore().currentPipelineSettings.cameraAutoExposure"
class="pt-2"
label="Auto Exposure"
:label-cols="4"
tooltip="Enables or Disables camera automatic adjustment for current lighting conditions"
@input="
(args) => useCameraSettingsStore().changeCurrentPipelineSetting({ cameraAutoExposure: args }, false)
"
/>
<pv-slider
v-if="useCameraSettingsStore().currentPipelineSettings.cameraGain >= 0"
v-model="useCameraSettingsStore().currentPipelineSettings.cameraGain"
label="Camera Gain"
tooltip="Controls camera gain, similar to brightness"
:min="0"
:max="100"
@input="(args) => useCameraSettingsStore().changeCurrentPipelineSetting({ cameraGain: args }, false)"
/>
<pv-slider
v-if="useCameraSettingsStore().currentPipelineSettings.cameraRedGain !== -1"
v-model="useCameraSettingsStore().currentPipelineSettings.cameraRedGain"
label="Red AWB Gain"
:min="0"
:max="100"
tooltip="Controls red automatic white balance gain, which affects how the camera captures colors in different conditions"
@input="(args) => useCameraSettingsStore().changeCurrentPipelineSetting({ cameraRedGain: args }, false)"
/>
<pv-slider
v-if="useCameraSettingsStore().currentPipelineSettings.cameraBlueGain !== -1"
v-model="useCameraSettingsStore().currentPipelineSettings.cameraBlueGain"
label="Blue AWB Gain"
:min="0"
:max="100"
tooltip="Controls blue automatic white balance gain, which affects how the camera captures colors in different conditions"
@input="(args) => useCameraSettingsStore().changeCurrentPipelineSetting({ cameraBlueGain: args }, false)"
/>
</v-col>
</v-row>
<v-row>
<v-col :cols="6">
<v-btn
small
color="secondary"
style="width: 100%"
:disabled="!settingsValid"
@click="isCalibrating ? useCameraSettingsStore().takeCalibrationSnapshot() : startCalibration()"
>
<v-icon left class="calib-btn-icon"> {{ isCalibrating ? "mdi-camera" : "mdi-flag-outline" }} </v-icon>
<span class="calib-btn-label">{{ isCalibrating ? "Take Snapshot" : "Start Calibration" }}</span>
</v-btn>
</v-col>
<v-col :cols="6">
<v-btn
small
:color="useStateStore().calibrationData.hasEnoughImages ? 'accent' : 'red'"
:class="useStateStore().calibrationData.hasEnoughImages ? 'black--text' : 'white---text'"
style="width: 100%"
:disabled="!isCalibrating || !settingsValid"
@click="endCalibration"
>
<v-icon left class="calib-btn-icon">
{{ useStateStore().calibrationData.hasEnoughImages ? "mdi-flag-checkered" : "mdi-flag-off-outline" }}
</v-icon>
<span class="calib-btn-label">{{
useStateStore().calibrationData.hasEnoughImages ? "Finish Calibration" : "Cancel Calibration"
}}</span>
</v-btn>
</v-col>
</v-row>
<v-row>
<v-col :cols="6">
<v-btn
color="accent"
small
outlined
style="width: 100%"
:disabled="!settingsValid"
@click="downloadCalibBoard"
>
<v-icon left class="calib-btn-icon"> mdi-download </v-icon>
<span class="calib-btn-label">Generate Board</span>
</v-btn>
</v-col>
<v-col :cols="6">
<v-btn color="secondary" :disabled="isCalibrating" small style="width: 100%" @click="openCalibUploadPrompt">
<v-icon left class="calib-btn-icon"> mdi-upload </v-icon>
<span class="calib-btn-label">Import From CalibDB</span>
</v-btn>
<input
ref="importCalibrationFromCalibDB"
type="file"
accept=".json"
style="display: none"
@change="readImportedCalibrationFromCalibDB"
/>
</v-col>
</v-row>
</div>
</v-card>
<v-dialog v-model="showCalibEndDialog" width="500px" :persistent="true">
<v-card color="primary" dark>
<v-card-title class="pb-8"> Camera Calibration </v-card-title>
<div class="ml-3">
<v-col style="text-align: center">
<template v-if="calibCanceled">
<v-icon color="blue" size="70"> mdi-cancel </v-icon>
<v-card-text
>Camera Calibration has been Canceled, the backend is attempting to cleanly cancel the calibration
process.</v-card-text
>
</template>
<template v-else-if="isCalibrating">
<v-progress-circular indeterminate :size="70" :width="8" color="accent" />
<v-card-text>Camera is being calibrated. This process may take several minutes...</v-card-text>
</template>
<template v-else-if="calibSuccess">
<v-icon color="green" size="70"> mdi-check-bold </v-icon>
<v-card-text>
Camera has been successfully calibrated for
{{
getUniqueVideoResolutionStrings().find(
(v) => v.value === useStateStore().calibrationData.videoFormatIndex
)?.name
}}!
</v-card-text>
</template>
<template v-else>
<v-icon color="red" size="70"> mdi-close </v-icon>
<v-card-text
>Camera calibration failed! Make sure that the photos are taken such that the rainbow grid circles align
with the corners of the chessboard, and try again. More information is available in the program
logs.</v-card-text
>
</template>
</v-col>
</div>
<v-card-actions>
<v-spacer />
<v-btn v-if="!isCalibrating" color="white" text @click="showCalibEndDialog = false"> OK </v-btn>
</v-card-actions>
</v-card>
</v-dialog>
<v-dialog v-model="showCalDialog" width="80em">
<CameraCalibrationInfoCard v-if="selectedVideoFormat" :video-format="selectedVideoFormat" />
</v-dialog>
</div>
</template>
<style scoped lang="scss">
.v-data-table {
text-align: center;
width: 100%;
th,
td {
background-color: #006492 !important;
font-size: 1rem !important;
}
tbody :hover td {
background-color: #005281 !important;
cursor: pointer;
}
::-webkit-scrollbar {
width: 0;
height: 0.55em;
border-radius: 5px;
}
::-webkit-scrollbar-track {
-webkit-box-shadow: inset 0 0 6px rgba(0, 0, 0, 0.3);
border-radius: 10px;
}
::-webkit-scrollbar-thumb {
background-color: #ffd843;
border-radius: 10px;
}
}
@media only screen and (max-width: 512px) {
.calib-btn-icon {
margin: 0 !important;
}
.calib-btn-label {
display: none;
}
}
</style>

View File

@@ -1,306 +0,0 @@
<script setup lang="ts">
import type { BoardObservation, CameraCalibrationResult, VideoFormat } from "@/types/SettingTypes";
import { useCameraSettingsStore } from "@/stores/settings/CameraSettingsStore";
import { useStateStore } from "@/stores/StateStore";
import { ref } from "vue";
import loadingImage from "@/assets/images/loading.svg";
import { getResolutionString, parseJsonFile } from "@/lib/PhotonUtils";
const props = defineProps<{
videoFormat: VideoFormat;
}>();
const getMeanFromView = (o: BoardObservation) => {
// Is this the right formula for RMS error? who knows! not me!
const perViewSumSquareReprojectionError = o.reprojectionErrors.flatMap((it2) => [it2.x, it2.y]);
// For each error, square it, sum the squares, and divide by total points N
return Math.sqrt(
perViewSumSquareReprojectionError.map((it) => Math.pow(it, 2)).reduce((a, b) => a + b, 0) /
perViewSumSquareReprojectionError.length
);
};
// Import and export functions
const downloadCalibration = () => {
const calibData = useCameraSettingsStore().getCalibrationCoeffs(props.videoFormat.resolution);
if (calibData === undefined) {
useStateStore().showSnackbarMessage({
color: "error",
message:
"Calibration data isn't available for the requested resolution, please calibrate the requested resolution first"
});
return;
}
const camUniqueName = useCameraSettingsStore().currentCameraSettings.uniqueName;
const filename = `photon_calibration_${camUniqueName}_${calibData.resolution.width}x${calibData.resolution.height}.json`;
const fileData = JSON.stringify(calibData);
const element = document.createElement("a");
element.style.display = "none";
element.setAttribute("href", "data:text/plain;charset=utf-8," + encodeURIComponent(fileData));
element.setAttribute("download", filename);
document.body.appendChild(element);
element.click();
document.body.removeChild(element);
};
const importCalibrationFromPhotonJson = ref();
const openUploadPhotonCalibJsonPrompt = () => {
importCalibrationFromPhotonJson.value.click();
};
const importCalibration = async () => {
const files = importCalibrationFromPhotonJson.value.files;
if (files.length === 0) return;
const uploadedJson = files[0];
const data = await parseJsonFile<CameraCalibrationResult>(uploadedJson);
if (
data.resolution.height != props.videoFormat.resolution.height ||
data.resolution.width != props.videoFormat.resolution.width
) {
useStateStore().showSnackbarMessage({
color: "error",
message: `The resolution of the calibration export doesn't match the current resolution ${props.videoFormat.resolution.height}x${props.videoFormat.resolution.width}`
});
return;
}
useCameraSettingsStore()
.importCalibrationFromData({ calibration: data })
.then((response) => {
useStateStore().showSnackbarMessage({
color: "success",
message: response.data.text || response.data
});
})
.catch((error) => {
if (error.response) {
useStateStore().showSnackbarMessage({
color: "error",
message: error.response.data.text || error.response.data
});
} else if (error.request) {
useStateStore().showSnackbarMessage({
color: "error",
message: "Error while trying to process the request! The backend didn't respond."
});
} else {
useStateStore().showSnackbarMessage({
color: "error",
message: "An error occurred while trying to process the request."
});
}
});
};
interface ObservationDetails {
snapshotSrc: any;
mean: number;
index: number;
}
const getObservationDetails = (): ObservationDetails[] | undefined => {
return useCameraSettingsStore()
.getCalibrationCoeffs(props.videoFormat.resolution)
?.observations.map((o, i) => ({
index: i,
mean: parseFloat(getMeanFromView(o).toFixed(2)),
snapshotSrc: o.includeObservationInCalibration ? "data:image/png;base64," + o.snapshotData.data : loadingImage
}));
};
</script>
<template>
<v-card color="primary" class="pa-6" dark>
<v-row>
<v-col cols="12" md="5">
<v-card-title class="pl-0 ml-0"
><span class="text-no-wrap" style="white-space: pre !important">Calibration Details: </span
><span class="text-no-wrap"
>{{ useCameraSettingsStore().currentCameraName }}@{{ getResolutionString(videoFormat.resolution) }}</span
></v-card-title
>
</v-col>
<v-col>
<v-btn color="secondary" class="mt-4" style="width: 100%" @click="openUploadPhotonCalibJsonPrompt">
<v-icon left> mdi-import</v-icon>
<span>Import</span>
</v-btn>
<input
ref="importCalibrationFromPhotonJson"
type="file"
accept=".json"
style="display: none"
@change="importCalibration"
/>
</v-col>
<v-col>
<v-btn
color="secondary"
class="mt-4"
:disabled="useCameraSettingsStore().getCalibrationCoeffs(props.videoFormat.resolution) === undefined"
style="width: 100%"
@click="downloadCalibration"
>
<v-icon left>mdi-export</v-icon>
<span>Export</span>
</v-btn>
</v-col>
</v-row>
<v-row
v-if="useCameraSettingsStore().getCalibrationCoeffs(props.videoFormat.resolution) !== undefined"
class="pt-2"
>
<v-card-subtitle>Calibration Details</v-card-subtitle>
<v-simple-table dense style="width: 100%" class="pl-2 pr-2">
<template #default>
<thead>
<tr>
<th class="text-left">Name</th>
<th class="text-left">Value</th>
</tr>
</thead>
<tbody>
<tr>
<td>Fx</td>
<td>
{{
useCameraSettingsStore()
.getCalibrationCoeffs(props.videoFormat.resolution)
?.cameraIntrinsics.data[0].toFixed(2) || 0.0
}}
mm
</td>
</tr>
<tr>
<td>Fy</td>
<td>
{{
useCameraSettingsStore()
.getCalibrationCoeffs(props.videoFormat.resolution)
?.cameraIntrinsics.data[4].toFixed(2) || 0.0
}}
mm
</td>
</tr>
<tr>
<td>Cx</td>
<td>
{{
useCameraSettingsStore()
.getCalibrationCoeffs(props.videoFormat.resolution)
?.cameraIntrinsics.data[2].toFixed(2) || 0.0
}}
px
</td>
</tr>
<tr>
<td>Cy</td>
<td>
{{
useCameraSettingsStore()
.getCalibrationCoeffs(props.videoFormat.resolution)
?.cameraIntrinsics.data[5].toFixed(2) || 0.0
}}
px
</td>
</tr>
<tr>
<td>Distortion</td>
<td>
{{
useCameraSettingsStore()
.getCalibrationCoeffs(props.videoFormat.resolution)
?.distCoeffs.data.map((it) => parseFloat(it.toFixed(3))) || []
}}
</td>
</tr>
<tr>
<td>Mean Err</td>
<td>
{{
videoFormat.mean !== undefined
? isNaN(videoFormat.mean)
? "NaN"
: videoFormat.mean.toFixed(2) + "px"
: "-"
}}
</td>
</tr>
<tr>
<td>Horizontal FOV</td>
<td>{{ videoFormat.horizontalFOV !== undefined ? videoFormat.horizontalFOV.toFixed(2) + "°" : "-" }}</td>
</tr>
<tr>
<td>Vertical FOV</td>
<td>{{ videoFormat.verticalFOV !== undefined ? videoFormat.verticalFOV.toFixed(2) + "°" : "-" }}</td>
</tr>
<tr>
<td>Diagonal FOV</td>
<td>{{ videoFormat.diagonalFOV !== undefined ? videoFormat.diagonalFOV.toFixed(2) + "°" : "-" }}</td>
</tr>
<!-- Board warp, only shown for mrcal-calibrated cameras -->
<tr
v-if="
useCameraSettingsStore().getCalibrationCoeffs(props.videoFormat.resolution)?.calobjectWarp?.length === 2
"
>
<td>Board warp, X/Y</td>
<td>
{{
useCameraSettingsStore()
.getCalibrationCoeffs(props.videoFormat.resolution)
?.calobjectWarp?.map((it) => (it * 1000).toFixed(2) + " mm")
.join(" / ")
}}
</td>
</tr>
</tbody>
</template>
</v-simple-table>
<hr style="width: 100%" class="ma-6" />
<v-card-subtitle>Per Observation Details</v-card-subtitle>
<v-data-table
dense
style="width: 100%"
class="pl-2 pr-2"
:headers="[
{ text: 'Observation Id', value: 'index' },
{ text: 'Mean Reprojection Error', value: 'mean' }
]"
:items="getObservationDetails()"
item-key="index"
show-expand
expand-icon="mdi-eye"
>
<template #expanded-item="{ headers, item }">
<td :colspan="headers.length">
<div style="display: flex; justify-content: center; width: 100%">
<img :src="item.snapshotSrc" alt="observation image" class="snapshot-preview pt-2 pb-2" />
</div>
</td>
</template>
</v-data-table>
</v-row>
<v-row v-else class="pt-2 mb-0 pb-0">
The selected video format doesn't have any additional information as it has yet to be calibrated.
</v-row>
</v-card>
</template>
<style scoped>
.v-data-table {
background-color: #006492 !important;
}
.snapshot-preview {
max-width: 55%;
}
@media only screen and (max-width: 512px) {
.snapshot-preview {
max-width: 100%;
}
}
</style>

View File

@@ -1,210 +0,0 @@
<script setup lang="ts">
import { ref } from "vue";
import axios from "axios";
import { useStateStore } from "@/stores/StateStore";
interface SnapshotMetadata {
snapshotName: string;
cameraNickname: string;
streamType: "input" | "output";
timeCreated: Date;
}
const getSnapshotMetadataFromName = (snapshotName: string): SnapshotMetadata => {
snapshotName = snapshotName.replace(/\.[^/.]+$/, "");
const data = snapshotName.split("_");
const cameraName = data.slice(0, data.length - 2).join("_");
const streamType = data[data.length - 2] as "input" | "output";
const dateStr = data[data.length - 1];
const year = parseInt(dateStr.substring(0, 4), 10);
const month = parseInt(dateStr.substring(5, 7), 10) - 1; // Months are zero-based
const day = parseInt(dateStr.substring(8, 10), 10);
const hours = parseInt(dateStr.substring(11, 13), 10);
const minutes = parseInt(dateStr.substring(13, 15), 10);
const seconds = parseInt(dateStr.substring(15, 17), 10);
const milliseconds = parseInt(dateStr.substring(17), 10);
return {
snapshotName: snapshotName,
cameraNickname: cameraName,
streamType: streamType,
timeCreated: new Date(year, month, day, hours, minutes, seconds, milliseconds)
};
};
interface Snapshot {
index: number;
snapshotName: string;
snapshotShortName: string;
cameraUniqueName: string;
cameraNickname: string;
streamType: "input" | "output";
timeCreated: Date;
snapshotSrc: string;
}
const imgData = ref<Snapshot[]>([]);
const fetchSnapshots = () => {
axios
.get("/utils/getImageSnapshots")
.then((response) => {
imgData.value = response.data.map(
(snapshotData: { snapshotName: string; cameraUniqueName: string; snapshotData: string }, index) => {
const metadata = getSnapshotMetadataFromName(snapshotData.snapshotName);
return {
index: index,
snapshotName: snapshotData.snapshotName,
snapshotShortName: metadata.snapshotName,
cameraUniqueName: snapshotData.cameraUniqueName,
cameraNickname: metadata.cameraNickname,
streamType: metadata.streamType,
timeCreated: metadata.timeCreated,
snapshotSrc: "data:image/jpg;base64," + snapshotData.snapshotData
};
}
);
showSnapshotViewerDialog.value = true;
})
.catch((error) => {
if (error.response) {
useStateStore().showSnackbarMessage({
color: "error",
message: error.response.data.text || error.response.data
});
} else if (error.request) {
useStateStore().showSnackbarMessage({
color: "error",
message: "Error while trying to process the request! The backend didn't respond."
});
} else {
useStateStore().showSnackbarMessage({
color: "error",
message: "An error occurred while trying to process the request."
});
}
});
};
const showSnapshotViewerDialog = ref(false);
const expanded = ref([]);
</script>
<template>
<v-card dark class="pr-6 pb-3" style="background-color: #006492">
<v-card-title>Camera Control</v-card-title>
<v-row class="pl-6">
<v-col>
<v-btn color="secondary" @click="fetchSnapshots">
<v-icon left class="open-icon"> mdi-folder </v-icon>
<span class="open-label">Show Saved Snapshots</span>
</v-btn>
</v-col>
</v-row>
<v-dialog v-model="showSnapshotViewerDialog">
<v-card dark class="pt-3 pl-5 pr-5" color="primary" flat>
<v-card-title> View Saved Frame Snapshots </v-card-title>
<v-divider />
<v-card-text v-if="imgData.length === 0" style="font-size: 18px; font-weight: 600" class="pt-4">
There are no snapshots saved
</v-card-text>
<div v-else class="pb-2">
<v-data-table
v-model:expanded="expanded"
:headers="[
{ text: 'Snapshot Name', value: 'snapshotShortName', sortable: false },
{ text: 'Camera Unique Name', value: 'cameraUniqueName' },
{ text: 'Camera Nickname', value: 'cameraNickname' },
{ text: 'Stream Type', value: 'streamType' },
{ text: 'Time Created', value: 'timeCreated' },
{ text: 'Actions', value: 'actions', sortable: false }
]"
:items="imgData"
group-by="cameraUniqueName"
class="elevation-0"
item-key="index"
show-expand
expand-icon="mdi-eye"
>
<template #expanded-item="{ headers, item }">
<td :colspan="headers.length">
<div style="display: flex; justify-content: center; width: 100%">
<img :src="item.snapshotSrc" alt="snapshot-image" class="snapshot-preview pt-2 pb-2" />
</div>
</td>
</template>
<!-- eslint-disable-next-line vue/valid-v-slot-->
<template #item.actions="{ item }">
<div style="display: flex; justify-content: center">
<a :download="item.snapshotName" :href="item.snapshotSrc">
<v-icon small> mdi-download </v-icon>
</a>
</div>
</template>
</v-data-table>
<span
>Snapshot Timestamps may be incorrect as they depend on when the coprocessor was last connected to the
internet</span
>
</div>
</v-card>
</v-dialog>
</v-card>
</template>
<style scoped lang="scss">
.v-divider {
border-color: white !important;
}
.v-btn {
width: 100%;
}
.v-data-table {
text-align: center;
background-color: #006492 !important;
th,
td {
background-color: #005281 !important;
font-size: 1rem !important;
}
tbody :hover tr {
background-color: #005281 !important;
}
::-webkit-scrollbar {
width: 0;
height: 0.55em;
border-radius: 5px;
}
::-webkit-scrollbar-track {
-webkit-box-shadow: inset 0 0 6px rgba(0, 0, 0, 0.3);
border-radius: 10px;
}
::-webkit-scrollbar-thumb {
background-color: #ffd843;
border-radius: 10px;
}
}
.snapshot-preview {
max-width: 55%;
}
@media only screen and (max-width: 512px) {
.snapshot-preview {
max-width: 100%;
}
}
@media only screen and (max-width: 351px) {
.open-icon {
margin: 0 !important;
}
.open-label {
display: none;
}
}
</style>

View File

@@ -1,150 +0,0 @@
<script setup lang="ts">
import PvSelect from "@/components/common/pv-select.vue";
import PvNumberInput from "@/components/common/pv-number-input.vue";
import { useCameraSettingsStore } from "@/stores/settings/CameraSettingsStore";
import { useStateStore } from "@/stores/StateStore";
import { computed, ref, watchEffect } from "vue";
import { type CameraSettingsChangeRequest, ValidQuirks } from "@/types/SettingTypes";
const tempSettingsStruct = ref<CameraSettingsChangeRequest>({
fov: useCameraSettingsStore().currentCameraSettings.fov.value,
quirksToChange: Object.assign({}, useCameraSettingsStore().currentCameraSettings.cameraQuirks.quirks)
});
const arducamSelectWrapper = computed<number>({
get: () => {
if (tempSettingsStruct.value.quirksToChange.ArduOV9281) return 1;
else if (tempSettingsStruct.value.quirksToChange.ArduOV2311) return 2;
else return 0;
},
set: (v) => {
switch (v) {
case 1:
tempSettingsStruct.value.quirksToChange.ArduOV9281 = true;
tempSettingsStruct.value.quirksToChange.ArduOV2311 = false;
break;
case 2:
tempSettingsStruct.value.quirksToChange.ArduOV9281 = false;
tempSettingsStruct.value.quirksToChange.ArduOV2311 = true;
break;
default:
tempSettingsStruct.value.quirksToChange.ArduOV9281 = false;
tempSettingsStruct.value.quirksToChange.ArduOV2311 = false;
break;
}
}
});
const currentCameraIsArducam = computed<boolean>(
() => useCameraSettingsStore().currentCameraSettings.cameraQuirks.quirks.ArduCamCamera
);
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;
}
return a.fov != b.fov.value;
};
const resetTempSettingsStruct = () => {
tempSettingsStruct.value.fov = useCameraSettingsStore().currentCameraSettings.fov.value;
tempSettingsStruct.value.quirksToChange = Object.assign(
{},
useCameraSettingsStore().currentCameraSettings.cameraQuirks.quirks
);
};
const saveCameraSettings = () => {
useCameraSettingsStore()
.updateCameraSettings(tempSettingsStruct.value)
.then((response) => {
useStateStore().showSnackbarMessage({
color: "success",
message: response.data.text || response.data
});
// Update the local settings cause the backend checked their validity. Assign is to deref value
useCameraSettingsStore().currentCameraSettings.fov.value = tempSettingsStruct.value.fov;
useCameraSettingsStore().currentCameraSettings.cameraQuirks.quirks = Object.assign(
{},
tempSettingsStruct.value.quirksToChange
);
})
.catch((error) => {
resetTempSettingsStruct();
if (error.response) {
useStateStore().showSnackbarMessage({
color: "error",
message: error.response.data.text || error.response.data
});
} else if (error.request) {
useStateStore().showSnackbarMessage({
color: "error",
message: "Error while trying to process the request! The backend didn't respond."
});
} else {
useStateStore().showSnackbarMessage({
color: "error",
message: "An error occurred while trying to process the request."
});
}
});
};
watchEffect(() => {
// Reset temp settings on remote camera settings change
resetTempSettingsStruct();
});
</script>
<template>
<v-card class="mb-3 pr-6 pb-3" color="primary" dark>
<v-card-title>Camera Settings</v-card-title>
<div class="ml-5">
<pv-select
v-model="useStateStore().currentCameraIndex"
label="Camera"
:items="useCameraSettingsStore().cameraNames"
:select-cols="8"
/>
<pv-number-input
v-model="tempSettingsStruct.fov"
:tooltip="
!useCameraSettingsStore().currentCameraSettings.fov.managedByVendor
? 'Field of view (in degrees) of the camera measured across the diagonal of the frame, in a video mode which covers the whole sensor area.'
: 'This setting is managed by a vendor'
"
label="Maximum Diagonal FOV"
:disabled="useCameraSettingsStore().currentCameraSettings.fov.managedByVendor"
:label-cols="4"
/>
<pv-select
v-show="currentCameraIsArducam"
v-model="arducamSelectWrapper"
label="Arducam Model"
:items="[
{ name: 'None', value: 0, disabled: true },
{ name: 'OV9281', value: 1 },
{ name: 'OV2311', value: 2 }
]"
:select-cols="8"
/>
<br />
<v-btn
class="mt-2 mb-3"
style="width: 100%"
small
color="secondary"
:disabled="!settingsHaveChanged()"
@click="saveCameraSettings"
>
<v-icon left> mdi-content-save </v-icon>
Save Changes
</v-btn>
</div>
</v-card>
</template>

View File

@@ -1,172 +0,0 @@
<script setup lang="ts">
import PhotonCameraStream from "@/components/app/photon-camera-stream.vue";
import { computed } from "vue";
import { useCameraSettingsStore } from "@/stores/settings/CameraSettingsStore";
import { PipelineType } from "@/types/PipelineTypes";
import { useStateStore } from "@/stores/StateStore";
import { useSettingsStore } from "@/stores/settings/GeneralSettingsStore";
const props = defineProps<{
// TODO fully update v-model usage in custom components on Vue3 update
value: number[];
}>();
const emit = defineEmits<{
(e: "input", value: number[]): void;
}>();
const localValue = computed({
get: () => props.value,
set: (v) => emit("input", v)
});
const driverMode = computed<boolean>({
get: () => useCameraSettingsStore().isDriverMode,
set: (v) =>
useCameraSettingsStore().changeCurrentPipelineIndex(
v ? -1 : useCameraSettingsStore().currentCameraSettings.lastPipelineIndex || 0,
true
)
});
const fpsTooLow = computed<boolean>(() => {
const currFPS = useStateStore().currentPipelineResults?.fps || 0;
const targetFPS = useCameraSettingsStore().currentVideoFormat.fps;
const driverMode = useCameraSettingsStore().isDriverMode;
const gpuAccel = useSettingsStore().general.gpuAcceleration !== undefined;
const isReflective = useCameraSettingsStore().currentPipelineSettings.pipelineType === PipelineType.Reflective;
return currFPS - targetFPS < -5 && currFPS !== 0 && !driverMode && gpuAccel && isReflective;
});
</script>
<template>
<v-card
id="camera-settings-camera-view-card"
class="camera-settings-camera-view-card mb-3 pb-3 pa-4"
color="primary"
dark
>
<v-card-title
class="pb-0 mb-2 pl-4 pt-1"
style="min-height: 50px; justify-content: space-between; align-content: center"
>
<div style="display: flex; flex-wrap: wrap">
<div>
<span class="mr-4" style="white-space: nowrap"> Cameras </span>
</div>
<div>
<v-chip
label
:color="fpsTooLow ? 'error' : 'transparent'"
:text-color="fpsTooLow ? '#C7EA46' : '#ff4d00'"
style="font-size: 1rem; padding: 0; margin: 0"
>
<span class="pr-1">
{{ Math.round(useStateStore().currentPipelineResults?.fps || 0) }}&nbsp;FPS &ndash;
{{ Math.min(Math.round(useStateStore().currentPipelineResults?.latency || 0), 9999) }} ms latency
</span>
</v-chip>
</div>
</div>
<div>
<v-switch
v-model="driverMode"
:disabled="useCameraSettingsStore().isCalibrationMode || useCameraSettingsStore().pipelineNames.length === 0"
label="Driver Mode"
style="margin-left: auto"
color="accent"
class="pt-2"
/>
</div>
</v-card-title>
<div class="stream-container pb-4">
<div class="stream">
<photon-camera-stream
v-show="value.includes(0)"
id="input-camera-stream"
stream-type="Raw"
style="max-width: 100%"
/>
</div>
<div class="stream">
<photon-camera-stream
v-show="value.includes(1)"
id="output-camera-stream"
stream-type="Processed"
style="max-width: 100%"
/>
</div>
</div>
<v-divider />
<div class="pt-4">
<p style="color: white">Stream Display</p>
<v-btn-toggle v-model="localValue" :multiple="true" mandatory dark class="fill" style="width: 100%">
<v-btn
color="secondary"
class="fill"
:disabled="useCameraSettingsStore().isDriverMode || useCameraSettingsStore().isCalibrationMode"
>
<v-icon left class="mode-btn-icon">mdi-import</v-icon>
<span class="mode-btn-label">Raw</span>
</v-btn>
<v-btn
color="secondary"
class="fill"
:disabled="useCameraSettingsStore().isDriverMode || useCameraSettingsStore().isCalibrationMode"
>
<v-icon left class="mode-btn-icon">mdi-export</v-icon>
<span class="mode-btn-label">Processed</span>
</v-btn>
</v-btn-toggle>
</div>
</v-card>
</template>
<style scoped>
.v-btn-toggle.fill {
width: 100%;
height: 100%;
}
.v-btn-toggle.fill > .v-btn {
width: 50%;
height: 100%;
}
th {
width: 80px;
text-align: center;
}
.stream-container {
display: flex;
justify-content: center;
flex-wrap: wrap;
align-items: center;
gap: 12px;
}
.stream {
display: flex;
justify-content: center;
}
@media only screen and (min-width: 512px) and (max-width: 960px) {
.stream-container {
flex-wrap: nowrap;
justify-content: center;
}
.stream {
max-width: 50%;
}
}
@media only screen and (max-width: 351px) {
.mode-btn-icon {
margin: 0 !important;
}
.mode-btn-label {
display: none;
}
}
</style>

View File

@@ -0,0 +1,54 @@
<template>
<div>
<v-tooltip
:right="right"
:bottom="!right"
nudge-right="10"
>
<template v-slot:activator="{ on }">
<v-icon
:class="hoverClass"
:color="color"
@click="handleClick"
v-on="on"
>
{{ text }}
</v-icon>
</template>
<span>{{ tooltip }}</span>
</v-tooltip>
</div>
</template>
<script>
export default {
name: 'Icon',
// eslint-disable-next-line vue/require-prop-types
props: ['color', 'tooltip', 'text', 'right', 'hover'],
data() {
return {}
},
computed: {
hoverClass: {
get() {
if (this.hover !== undefined) {
return "hover";
}
return "";
}
}
},
methods: {
handleClick() {
this.$emit('click');
}
},
}
</script>
<style scoped>
.hover:hover {
color: white !important;
}
</style>

View File

@@ -0,0 +1,62 @@
<template>
<img
:id="id"
crossOrigin="anonymous"
:style="styleObject"
:src="src"
alt=""
@click="e => $emit('click', e)"
>
</template>
<script>
export default {
name: "CvImage",
// eslint-disable-next-line vue/require-prop-types
props: ['address', 'scale', 'maxHeight', 'maxHeightMd', 'maxHeightXl', 'colorPicking', 'id', 'disconnected'],
data() {
return {
seed: 1.0,
}
},
computed: {
styleObject: {
get() {
let ret = {
"border-radius": "3px",
"display": "block",
"object-fit": "contain",
"object-position": "50% 50%",
"max-width": "100%",
"margin-left": "auto",
"margin-right": "auto",
"max-height": this.maxHeight,
height: `${this.scale}%`,
cursor: (this.colorPicking ? `url(${require("../../assets/eyedropper.svg")}),` : "") + "default",
};
if (this.$vuetify.breakpoint.xl) {
ret["max-height"] = this.maxHeightXl;
} else if (this.$vuetify.breakpoint.mdAndUp) {
ret["max-height"] = this.maxHeightMd;
}
return ret;
}
},
src: {
get() {
return this.disconnected ? require("../../assets/noStream.jpg") : this.address + "?" + this.seed // This prevents caching
},
},
},
mounted() {
this.reload(); // Force reload image on creation
},
methods: {
reload() {
this.seed = new Date().getTime();
}
},
}
</script>

View File

@@ -0,0 +1,64 @@
<template>
<div>
<v-row
dense
align="center"
>
<v-col :cols="12 - (inputCols || 8)">
<tooltipped-label
:tooltip="tooltip"
:text="name"
/>
</v-col>
<v-col :cols="inputCols || 8">
<v-text-field
v-model="localValue"
dark
dense
color="accent"
:disabled="disabled"
:error-messages="errorMessage"
:rules="rules"
class="mt-1 pt-2"
@keydown="handleKeyboard"
/>
</v-col>
</v-row>
</div>
</template>
s
<script>
import TooltippedLabel from "./cv-tooltipped-label";
export default {
name: 'Input',
components: {
TooltippedLabel
},
// eslint-disable-next-line vue/require-prop-types
props: ['name', 'value', 'disabled', 'errorMessage', 'inputCols', 'rules', 'tooltip'],
data() {
return {}
},
computed: {
localValue: {
get() {
return this.value;
},
set(value) {
this.$emit('input', value);
}
}
},
methods: {
handleKeyboard(event) {
if (event.key === "Enter") {
this.$emit("Enter");
}
}
}
}
</script>
<style lang="css" scoped>
</style>

View File

@@ -0,0 +1,57 @@
<template>
<div>
<v-row
dense
align="center"
>
<v-col :cols="labelCols || 2">
<tooltipped-label
:tooltip="tooltip"
:text="name"
/>
</v-col>
<v-col>
<v-text-field
v-model="localValue"
dark
class="mt-0 pt-0"
hide-details
single-line
color="accent"
type="number"
style="width: 70px"
:step="step"
:disabled="disabled"
:rules="rules"
/>
</v-col>
</v-row>
</div>
</template>
<script>
import TooltippedLabel from "./cv-tooltipped-label";
export default {
name: 'NumberInput',
components: {
TooltippedLabel,
},
// eslint-disable-next-line vue/require-prop-types
props: ['name', 'value', 'step', 'labelCols', 'rules', 'tooltip', 'disabled'],
computed: {
localValue: {
get() {
return this.value;
},
set(value) {
this.$emit('input', parseFloat(value));
}
}
}
}
</script>
<style lang="" scoped>
</style>

View File

@@ -0,0 +1,44 @@
<template>
<div>
<v-radio-group
v-model="localValue"
row
dark
:mandatory="true"
>
<v-radio
v-for="(name,index) in list"
:key="index"
color="#ffd843"
:label="name"
:value="index"
:disabled="disabled"
/>
</v-radio-group>
</div>
</template>
<script>
export default {
name: 'Radio',
// eslint-disable-next-line vue/require-prop-types
props: ['value', 'list', 'disabled'],
data() {
return {}
},
computed: {
localValue: {
get() {
return this.value;
},
set(value) {
this.$emit('input', value);
}
}
}
}
</script>
<style lang="" scoped>
</style>

View File

@@ -0,0 +1,132 @@
<template>
<div>
<v-row
dense
align="center"
>
<v-col cols="2">
<tooltipped-label
:tooltip="tooltip"
:text="name"
/>
</v-col>
<v-col cols="10">
<v-range-slider
:value="localValue"
:max="max"
:min="min"
:disabled="disabled"
hide-details
class="align-center"
dark
color="accent"
:step="step"
@input="handleInput"
@mousedown="$emit('rollback', localValue)"
>
<template v-slot:prepend>
<v-text-field
dark
color="accent"
:value="localValue[0]"
:max="max"
:min="min"
class="mt-0 pt-0"
hide-details
single-line
type="number"
style="width: 60px"
:step="step"
@input="handleChange"
@focus="prependFocused = true"
@blur="prependFocused = false"
/>
</template>
<template v-slot:append>
<v-text-field
dark
color="accent"
:value="localValue[1]"
:max="max"
:min="min"
class="mt-0 pt-0"
hide-details
single-line
type="number"
style="width: 60px"
:step="step"
@input="handleChange"
@focus="appendFocused = true"
@blur="appendFocused = false"
/>
</template>
</v-range-slider>
</v-col>
</v-row>
</div>
</template>
<script>
import TooltippedLabel from "./cv-tooltipped-label";
export default {
name: "RangeSlider",
components: {
TooltippedLabel,
},
// eslint-disable-next-line vue/require-prop-types
props: ["name", "min", "max", "value", "step", "tooltip", "disabled"],
data() {
return {
prependFocused: false,
appendFocused: false,
currentTempVal: null,
};
},
computed: {
localValue: {
get() {
return Object.values(this.value || [0, 0]);
},
set(value) {
this.$emit("input", value);
},
},
},
methods: {
delay(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
},
async handleChange(val) {
this.currentTempVal = val;
await this.delay(200).then(() => {
let i = 0;
if (this.prependFocused === false && this.appendFocused === true) {
i = 1;
}
// will get empty string if entry is not a number
if (this.currentTempVal !== val || val === "") return;
let parsed = parseFloat(val);
let tmp = this.localValue;
tmp[i] = Math.max(this.min, Math.min(parsed, this.max));
this.localValue = tmp;
this.$emit("rollback", this.localValue);
});
},
handleInput(val) {
if (!this.prependFocused || !this.appendFocused) {
this.localValue = val;
}
},
},
};
</script>
<style lang="" scoped>
</style>

View File

@@ -0,0 +1,65 @@
<template>
<div>
<v-row
dense
align="center"
>
<v-col :cols="12 - (selectCols || 9)">
<tooltipped-label
:tooltip="tooltip"
:text="name"
/>
</v-col>
<v-col :cols="selectCols || 9">
<v-select
v-model="localValue"
:items="indexList"
item-text="name"
item-value="index"
dark
color="accent"
item-color="secondary"
:disabled="disabled"
:rules="rules"
@change="$emit('rollback', localValue)"
/>
</v-col>
</v-row>
</div>
</template>
<script>
import TooltippedLabel from "./cv-tooltipped-label";
export default {
name: 'Select',
components: {
TooltippedLabel,
},
// eslint-disable-next-line vue/require-prop-types
props: ['list', 'name', 'value', 'disabled', 'selectCols', 'rules', 'tooltip'],
computed: {
localValue: {
get() {
return this.value;
},
set(value) {
this.$emit('input', value)
}
},
indexList() {
let list = [];
for (let i = 0; i < this.list.length; i++) {
list.push({
name: this.list[i],
index: i
});
}
return list;
}
}
}
</script>
<style>
</style>

View File

@@ -0,0 +1,108 @@
<template>
<div>
<v-row
dense
align="center"
>
<v-col :cols="12 - (sliderCols || 8)">
<tooltipped-label
:tooltip="tooltip"
:text="name"
/>
</v-col>
<v-col :cols="sliderCols || 8">
<v-slider
:value="localValue"
dark
class="align-center"
:max="max"
:min="min"
hide-details
color="accent"
:disabled="disabled"
:step="step"
@start="isClicked = true"
@end="isClicked = false"
@change="handleClick"
@input="handleInput"
@mousedown="$emit('rollback', localValue)"
>
<template v-slot:append>
<v-text-field
dark
color="accent"
:max="max"
:min="min"
:disabled="disabled"
:value="localValue"
class="mt-0 pt-0"
hide-details
single-line
type="number"
style="width: 50px"
:step="step"
@input="handleChange"
@focus="isFocused = true"
@blur="isFocused = false"
/>
</template>
</v-slider>
</v-col>
</v-row>
</div>
</template>
<script>
import TooltippedLabel from "./cv-tooltipped-label";
export default {
name: "Slider",
components: {
TooltippedLabel,
},
// eslint-disable-next-line vue/require-prop-types
props: ["min", "max", "name", "value", "step", "sliderCols", "disabled", "tooltip"],
data() {
return {
isFocused: false,
isClicked: false,
currentBoxVal: null
};
},
computed: {
localValue: {
get() {
return this.value;
},
set(value) {
this.$emit("input", value);
}
},
},
methods: {
handleChange(val) {
this.currentBoxVal = val;
setTimeout(() => {
if (this.currentBoxVal !== val) return;
// if (this.isFocused) {
this.localValue = parseFloat(val);
this.$emit("rollback", this.localValue);
// }
}, 200);
},
handleInput(val) {
if (!this.isFocused && this.isClicked) {
this.localValue = val;
}
},
handleClick(val) {
if (!this.isFocused) {
this.localValue = val;
}
}
}
};
</script>
<style lang="" scoped>
</style>

View File

@@ -0,0 +1,51 @@
<template>
<div>
<v-row
dense
align="center"
>
<v-col :cols="textCols || 2">
<tooltipped-label
:tooltip="tooltip"
:text="name"
/>
</v-col>
<v-col :cols="12 - (textCols || 2)">
<v-switch
v-model="localValue"
dark
:disabled="disabled"
color="#ffd843"
@change="$emit('rollback', localValue)"
/>
</v-col>
</v-row>
</div>
</template>
<script>
import TooltippedLabel from "./cv-tooltipped-label";
export default {
name: 'CVSwitch',
components: {
TooltippedLabel,
},
// eslint-disable-next-line vue/require-prop-types
props: ['name', 'value', 'disabled', 'textCols', 'tooltip'],
computed: {
localValue: {
get() {
return this.value;
},
set(value) {
this.$emit('input', value)
}
}
}
}
</script>
<style lang="" scoped>
</style>

View File

@@ -0,0 +1,27 @@
<template>
<div>
<v-tooltip
:disabled="tooltip === undefined"
right
open-delay="600"
>
<template v-slot:activator="{ on, attrs }">
<span
style="cursor: text !important;"
class="white--text"
v-bind="attrs"
v-on="on"
>{{ text }}</span>
</template>
<span>{{ tooltip }}</span>
</v-tooltip>
</div>
</template>
<script>
export default {
name: 'TooltippedLabel',
// eslint-disable-next-line vue/require-prop-types
props: ['text', 'tooltip'],
}
</script>

View File

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

View File

@@ -1,73 +0,0 @@
<script setup lang="ts">
import { computed } from "vue";
import TooltippedLabel from "@/components/common/pv-tooltipped-label.vue";
const props = withDefaults(
defineProps<{
label?: string;
tooltip?: string;
// TODO fully update v-model usage in custom components on Vue3 update
value: string;
disabled?: boolean;
errorMessage?: string;
placeholder?: string;
labelCols?: number;
inputCols?: number;
rules?: ((v: string) => boolean | string)[];
}>(),
{
disabled: false,
inputCols: 8
}
);
const emit = defineEmits<{
(e: "input", value: string): void;
(e: "onEnter", value: string): void;
(e: "onEscape"): void;
}>();
const localValue = computed({
get: () => props.value,
set: (v) => emit("input", v)
});
const handleKeydown = ({ key }) => {
switch (key) {
case "Enter":
// Explicitly check that all rule props return true
if (!props.rules?.every((rule) => rule(localValue.value) === true)) return;
emit("onEnter", localValue.value);
break;
case "Escape":
emit("onEscape");
break;
}
};
</script>
<template>
<div>
<v-row dense align="center">
<v-col :cols="labelCols || 12 - inputCols">
<tooltipped-label :tooltip="tooltip" :label="label" />
</v-col>
<v-col :cols="inputCols">
<v-text-field
v-model="localValue"
dark
dense
color="accent"
:placeholder="placeholder"
:disabled="disabled"
:error-messages="errorMessage"
:rules="rules"
class="mt-1 pt-2"
@keydown="handleKeydown"
/>
</v-col>
</v-row>
</div>
</template>

View File

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

View File

@@ -1,51 +0,0 @@
<script setup lang="ts">
import { computed } from "vue";
import TooltippedLabel from "@/components/common/pv-tooltipped-label.vue";
const props = withDefaults(
defineProps<{
label?: string;
tooltip?: string;
// TODO fully update v-model usage in custom components on Vue3 update
value: number;
disabled?: boolean;
inputCols?: number;
list: string[];
}>(),
{
disabled: false,
inputCols: 8
}
);
const emit = defineEmits<{
(e: "input", value: number): void;
}>();
const localValue = computed({
get: () => props.value,
set: (v) => emit("input", v)
});
</script>
<template>
<div>
<v-row dense align="center">
<v-col :cols="12 - inputCols">
<tooltipped-label :tooltip="tooltip" :label="label" />
</v-col>
<v-col :cols="inputCols">
<v-radio-group v-model="localValue" row dark :mandatory="true">
<v-radio
v-for="(radioName, index) in list"
:key="index"
color="#ffd843"
:label="radioName"
:value="index"
:disabled="disabled"
/>
</v-radio-group>
</v-col>
</v-row>
</div>
</template>

View File

@@ -1,118 +0,0 @@
<script setup lang="ts">
import { computed } from "vue";
import TooltippedLabel from "@/components/common/pv-tooltipped-label.vue";
const props = withDefaults(
defineProps<{
label?: string;
tooltip?: string;
// TODO fully update v-model usage in custom components on Vue3 update
// value: [number, number] | WebsocketNumberPair, // Vue doesnt like Union types for the value prop for some reason.
value: [number, number];
min: number;
max: number;
step?: number;
sliderCols?: number;
disabled?: boolean;
inverted?: boolean;
}>(),
{
step: 1,
disabled: false,
inverted: false,
sliderCols: 10
}
);
const emit = defineEmits<{
(e: "input", value: [number, number]): void;
}>();
const localValue = computed<[number, number]>({
get: (): [number, number] => {
return Object.values(props.value) as [number, number];
},
set: (v) => {
for (let i = 0; i < v.length; i++) {
v[i] = parseFloat(v[i] as unknown as string);
}
emit("input", v);
}
});
const changeFromSlot = (v: string, i: number) => {
// v comes in as a string, not a number, for some reason
// if v is undefined, take a guess and set it to 0
const val = Math.max(props.min, Math.min(parseFloat(v) || 0, props.max));
// localValue.value must be replaced for a reactive change to take place
const temp = localValue.value;
temp[i] = val;
localValue.value = temp;
};
const checkNumberRange = (v: string): boolean => {
const val: number = parseFloat(v);
return isFinite(val) && val >= props.min && val <= props.max;
};
</script>
<template>
<div>
<v-row dense align="center">
<v-col :cols="12 - sliderCols">
<tooltipped-label :tooltip="tooltip" :label="label" />
</v-col>
<v-col :cols="sliderCols">
<v-range-slider
v-model="localValue"
:max="max"
:min="min"
:disabled="disabled"
hide-details
class="align-center"
dark
:color="inverted ? 'rgba(255, 255, 255, 0.2)' : 'accent'"
:track-color="inverted ? 'accent' : undefined"
thumb-color="accent"
:step="step"
>
<template #prepend>
<v-text-field
:value="localValue[0]"
dark
color="accent"
class="mt-0 pt-0"
hide-details
single-line
:max="max"
:min="min"
:step="step"
:rules="[checkNumberRange]"
type="number"
style="width: 60px"
@input="(v) => changeFromSlot(v, 0)"
/>
</template>
<template #append>
<v-text-field
:value="localValue[1]"
dark
color="accent"
class="mt-0 pt-0"
hide-details
single-line
:max="max"
:min="min"
:step="step"
:rules="[checkNumberRange]"
type="number"
style="width: 60px"
@input="(v) => changeFromSlot(v, 1)"
/>
</template>
</v-range-slider>
</v-col>
</v-row>
</div>
</template>

View File

@@ -1,72 +0,0 @@
<script setup lang="ts">
import { computed } from "vue";
import TooltippedLabel from "@/components/common/pv-tooltipped-label.vue";
interface SelectItem {
name: string | number;
value: string | number;
disabled?: boolean;
}
const props = withDefaults(
defineProps<{
label?: string;
tooltip?: string;
selectCols?: number;
// TODO fully update v-model usage in custom components on Vue3 update
value: number;
disabled?: boolean;
items: string[] | number[] | SelectItem[];
}>(),
{
selectCols: 9,
disabled: false
}
);
const emit = defineEmits<{
(e: "input", value: number): void;
}>();
const localValue = computed({
get: () => props.value,
set: (v) => emit("input", v)
});
// Computed in case items changes
const items = computed<SelectItem[]>(() => {
// Trivial case for empty list; we have no data
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[];
}
return props.items.map((v, i) => ({ name: v, value: i }));
});
</script>
<template>
<div>
<v-row dense align="center">
<v-col :cols="12 - selectCols">
<tooltipped-label :tooltip="tooltip" :label="label" />
</v-col>
<v-col :cols="selectCols">
<v-select
v-model="localValue"
:items="items"
item-text="name"
item-value="value"
item-disabled="disabled"
dark
color="accent"
item-color="secondary"
:disabled="disabled"
/>
</v-col>
</v-row>
</div>
</template>

View File

@@ -1,72 +0,0 @@
<script setup lang="ts">
import { computed } from "vue";
import TooltippedLabel from "@/components/common/pv-tooltipped-label.vue";
const props = withDefaults(
defineProps<{
label?: string;
tooltip?: string;
// TODO fully update v-model usage in custom components on Vue3 update
value: number;
min: number;
max: number;
step?: number;
disabled?: boolean;
sliderCols?: number;
}>(),
{
step: 1,
disabled: false,
sliderCols: 8
}
);
const emit = defineEmits<{
(e: "input", value: number): void;
}>();
const localValue = computed({
get: () => props.value,
set: (v) => emit("input", parseFloat(v as unknown as string))
});
</script>
<template>
<div>
<v-row dense align="center">
<v-col :cols="12 - sliderCols">
<tooltipped-label :tooltip="tooltip" :label="label" />
</v-col>
<v-col :cols="sliderCols">
<v-slider
v-model="localValue"
dark
class="align-center"
:max="max"
:min="min"
hide-details
color="accent"
:disabled="disabled"
:step="step"
>
<template #append>
<v-text-field
v-model="localValue"
dark
color="accent"
:max="max"
:min="min"
:disabled="disabled"
class="mt-0 pt-0"
hide-details
single-line
type="number"
style="width: 50px"
:step="step"
/>
</template>
</v-slider>
</v-col>
</v-row>
</div>
</template>

View File

@@ -1,42 +0,0 @@
<script setup lang="ts">
import TooltippedLabel from "@/components/common/pv-tooltipped-label.vue";
import { computed } from "vue";
const props = withDefaults(
defineProps<{
label?: string;
tooltip?: string;
// TODO fully update v-model usage in custom components on Vue3 update
value: boolean;
disabled?: boolean;
labelCols?: number;
switchCols?: number;
}>(),
{
disabled: false,
labelCols: 2
}
);
const emit = defineEmits<{
(e: "input", value: boolean): void;
}>();
const localValue = computed({
get: () => props.value,
set: (v) => emit("input", v)
});
</script>
<template>
<div>
<v-row dense align="center">
<v-col :cols="12 - switchCols || labelCols">
<tooltipped-label :tooltip="tooltip" :label="label" />
</v-col>
<v-col :cols="switchCols || 12 - labelCols">
<v-switch v-model="localValue" dark :disabled="disabled" color="#ffd843" />
</v-col>
</v-row>
</div>
</template>

View File

@@ -1,17 +0,0 @@
<script setup lang="ts">
defineProps<{
label?: string;
tooltip?: string;
}>();
</script>
<template>
<div>
<v-tooltip :disabled="tooltip === undefined" right open-delay="300">
<template #activator="{ on, attrs }">
<span style="cursor: text !important" class="white--text" v-bind="attrs" v-on="on">{{ label }}</span>
</template>
<span>{{ tooltip }}</span>
</v-tooltip>
</div>
</template>

View File

@@ -1,423 +0,0 @@
<script setup lang="ts">
import PvSelect from "@/components/common/pv-select.vue";
import { useStateStore } from "@/stores/StateStore";
import { useCameraSettingsStore } from "@/stores/settings/CameraSettingsStore";
import { WebsocketPipelineType } from "@/types/WebsocketDataTypes";
import { computed, ref } from "vue";
import PvIcon from "@/components/common/pv-icon.vue";
import PvInput from "@/components/common/pv-input.vue";
import { PipelineType } from "@/types/PipelineTypes";
import { useSettingsStore } from "@/stores/settings/GeneralSettingsStore";
const changeCurrentCameraIndex = (index: number) => {
useCameraSettingsStore().setCurrentCameraIndex(index, true);
switch (useCameraSettingsStore().cameras[index].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_ \-)(.]*$/;
// Camera Name Edit
const isCameraNameEdit = ref(false);
const currentCameraName = ref(useCameraSettingsStore().currentCameraSettings.nickname);
const startCameraNameEdit = () => {
currentCameraName.value = useCameraSettingsStore().currentCameraSettings.nickname;
isCameraNameEdit.value = true;
};
const checkCameraName = (name: string): string | boolean => {
if (!nameChangeRegex.test(name))
return "A camera name can only contain letters, numbers, spaces, underscores, hyphens, parenthesis, and periods";
if (useCameraSettingsStore().cameraNames.some((cameraName) => cameraName === name))
return "This camera name has already been used";
return true;
};
const saveCameraNameEdit = (newName: string) => {
useCameraSettingsStore()
.changeCameraNickname(newName, false)
.then((response) => {
useStateStore().showSnackbarMessage({
color: "success",
message: response.data.text || response.data
});
useCameraSettingsStore().currentCameraSettings.nickname = newName;
})
.catch((error) => {
if (error.response) {
useStateStore().showSnackbarMessage({
color: "error",
message: error.response.data.text || error.response.data
});
} else if (error.request) {
useStateStore().showSnackbarMessage({
color: "error",
message: "Error while trying to process the request! The backend didn't respond."
});
} else {
useStateStore().showSnackbarMessage({
color: "error",
message: "An error occurred while trying to process the request."
});
}
currentCameraName.value = useCameraSettingsStore().currentCameraSettings.nickname;
})
.finally(() => (isCameraNameEdit.value = false));
};
const cancelCameraNameEdit = () => {
isCameraNameEdit.value = false;
currentCameraName.value = useCameraSettingsStore().currentCameraSettings.nickname;
};
// Pipeline Name Edit
const pipelineNamesWrapper = computed<{ name: string; value: number }[]>(() => {
const pipelineNames = useCameraSettingsStore().pipelineNames.map((name, index) => ({ name: name, value: index }));
if (useCameraSettingsStore().isDriverMode) {
pipelineNames.push({ name: "Driver Mode", value: WebsocketPipelineType.DriverMode });
}
if (useCameraSettingsStore().isCalibrationMode) {
pipelineNames.push({ name: "3D Calibration Mode", value: WebsocketPipelineType.Calib3d });
}
return pipelineNames;
});
const isPipelineNameEdit = ref(false);
const currentPipelineName = ref(useCameraSettingsStore().currentPipelineSettings.pipelineNickname);
const startPipelineNameEdit = () => {
currentPipelineName.value = useCameraSettingsStore().currentPipelineSettings.pipelineNickname;
isPipelineNameEdit.value = true;
};
const checkPipelineName = (name: string): string | boolean => {
if (!nameChangeRegex.test(name))
return "A pipeline name can only contain letters, numbers, spaces, underscores, hyphens, parenthesis, and periods";
if (useCameraSettingsStore().pipelineNames.some((pipelineName) => pipelineName === name))
return "This pipeline name has already been used";
return true;
};
const savePipelineNameEdit = (name: string) => {
useCameraSettingsStore().changeCurrentPipelineNickname(name);
isPipelineNameEdit.value = false;
};
const cancelPipelineNameEdit = () => {
isPipelineNameEdit.value = false;
currentPipelineName.value = useCameraSettingsStore().currentPipelineSettings.pipelineNickname;
};
// Pipeline Creation
const showPipelineCreationDialog = ref(false);
const newPipelineName = ref("");
const newPipelineType = ref<WebsocketPipelineType>(useCameraSettingsStore().currentWebsocketPipelineType);
const validNewPipelineTypes = computed(() => {
const pipelineTypes = [
{ name: "Reflective", value: WebsocketPipelineType.Reflective },
{ name: "Colored Shape", value: WebsocketPipelineType.ColoredShape },
{ name: "AprilTag", value: WebsocketPipelineType.AprilTag },
{ name: "Aruco", value: WebsocketPipelineType.Aruco }
];
if (useSettingsStore().general.rknnSupported) {
pipelineTypes.push({ name: "Object Detection", value: WebsocketPipelineType.ObjectDetection });
}
return pipelineTypes;
});
const showCreatePipelineDialog = () => {
newPipelineName.value = "";
newPipelineType.value = useCameraSettingsStore().currentWebsocketPipelineType;
showPipelineCreationDialog.value = true;
};
const createNewPipeline = () => {
const type = newPipelineType.value;
if (type === WebsocketPipelineType.DriverMode || type === WebsocketPipelineType.Calib3d) return;
useCameraSettingsStore().createNewPipeline(newPipelineName.value, type);
showPipelineCreationDialog.value = false;
};
const cancelPipelineCreation = () => {
showPipelineCreationDialog.value = false;
newPipelineName.value = "";
newPipelineType.value = useCameraSettingsStore().currentWebsocketPipelineType;
};
// Pipeline Deletion
const showPipelineDeletionConfirmationDialog = ref(false);
const confirmDeleteCurrentPipeline = () => {
useCameraSettingsStore().deleteCurrentPipeline();
showPipelineDeletionConfirmationDialog.value = false;
};
// Pipeline Type Change
const showPipelineTypeChangeDialog = ref(false);
const pipelineTypesWrapper = computed<{ name: string; value: number }[]>(() => {
const pipelineTypes = [
{ name: "Reflective", value: WebsocketPipelineType.Reflective },
{ name: "Colored Shape", value: WebsocketPipelineType.ColoredShape },
{ name: "AprilTag", value: WebsocketPipelineType.AprilTag },
{ name: "Aruco", value: WebsocketPipelineType.Aruco }
];
if (useSettingsStore().general.rknnSupported) {
pipelineTypes.push({ name: "Object Detection", value: WebsocketPipelineType.ObjectDetection });
}
if (useCameraSettingsStore().isDriverMode) {
pipelineTypes.push({ name: "Driver Mode", value: WebsocketPipelineType.DriverMode });
}
if (useCameraSettingsStore().isCalibrationMode) {
pipelineTypes.push({ name: "3D Calibration Mode", value: WebsocketPipelineType.Calib3d });
}
return pipelineTypes;
});
const pipelineType = ref<WebsocketPipelineType>(useCameraSettingsStore().currentWebsocketPipelineType);
const currentPipelineType = computed<WebsocketPipelineType>({
get: () => {
if (useCameraSettingsStore().isDriverMode) return WebsocketPipelineType.DriverMode;
if (useCameraSettingsStore().isCalibrationMode) return WebsocketPipelineType.Calib3d;
return pipelineType.value;
},
set: (v) => {
pipelineType.value = v;
}
});
const confirmChangePipelineType = () => {
const type = currentPipelineType.value;
if (type === WebsocketPipelineType.DriverMode || type === WebsocketPipelineType.Calib3d) return;
useCameraSettingsStore().changeCurrentPipelineType(type);
showPipelineTypeChangeDialog.value = false;
};
const cancelChangePipelineType = () => {
pipelineType.value = useCameraSettingsStore().currentWebsocketPipelineType;
showPipelineTypeChangeDialog.value = false;
};
// Pipeline duplication'
const duplicateCurrentPipeline = () => {
useCameraSettingsStore().duplicatePipeline(useCameraSettingsStore().currentCameraSettings.currentPipelineIndex);
};
// Change Props whenever the pipeline settings are changed
useCameraSettingsStore().$subscribe((mutation, state) => {
const currentCameraSettings = state.cameras[useStateStore().currentCameraIndex];
switch (currentCameraSettings.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;
}
});
</script>
<template>
<v-card color="primary">
<v-row style="padding: 12px 12px 0 24px">
<v-col cols="10" class="pa-0">
<pv-select
v-if="!isCameraNameEdit"
v-model="useStateStore().currentCameraIndex"
label="Camera"
:items="useCameraSettingsStore().cameraNames"
@input="changeCurrentCameraIndex"
/>
<pv-input
v-else
v-model="currentCameraName"
class="pt-2"
:input-cols="12 - 3"
:rules="[(v) => checkCameraName(v)]"
label="Camera"
@onEnter="saveCameraNameEdit"
@onEscape="cancelCameraNameEdit"
/>
</v-col>
<v-col cols="2" style="display: flex; align-items: center; justify-content: center">
<div v-if="isCameraNameEdit" style="display: flex; gap: 14px">
<pv-icon
icon-name="mdi-content-save"
color="#c5c5c5"
:disabled="checkCameraName(currentCameraName) !== true"
@click="() => saveCameraNameEdit(currentCameraName)"
/>
<pv-icon icon-name="mdi-cancel" color="red darken-2" @click="cancelCameraNameEdit" />
</div>
<pv-icon
v-else
color="#c5c5c5"
icon-name="mdi-pencil"
tooltip="Edit Camera Name"
@click="startCameraNameEdit"
/>
</v-col>
</v-row>
<v-row style="padding: 0 12px 0 24px">
<v-col cols="10" class="pa-0">
<pv-select
v-if="!isPipelineNameEdit"
:value="useCameraSettingsStore().currentCameraSettings.currentPipelineIndex"
label="Pipeline"
tooltip="Each pipeline runs on a camera output and stores a unique set of processing settings"
:disabled="useCameraSettingsStore().isDriverMode || useCameraSettingsStore().isCalibrationMode"
:items="pipelineNamesWrapper"
@input="(args) => useCameraSettingsStore().changeCurrentPipelineIndex(args, true)"
/>
<pv-input
v-else
v-model="currentPipelineName"
:input-cols="12 - 3"
:rules="[(v) => checkPipelineName(v)]"
label="Pipeline"
@onEnter="(v) => savePipelineNameEdit(v)"
@onEscape="cancelPipelineNameEdit"
/>
</v-col>
<v-col cols="2" class="pa-0" style="display: flex; align-items: center; justify-content: center">
<div v-if="isPipelineNameEdit" style="display: flex; gap: 14px">
<pv-icon
icon-name="mdi-content-save"
color="#c5c5c5"
:disabled="checkPipelineName(currentPipelineName) !== true"
@click="() => savePipelineNameEdit(currentPipelineName)"
/>
<pv-icon icon-name="mdi-cancel" color="red darken-2" @click="cancelPipelineNameEdit" />
</div>
<v-menu v-else-if="!useCameraSettingsStore().isDriverMode" offset-y nudge-bottom="7" auto>
<template #activator="{ on }">
<v-icon color="#c5c5c5" v-on="on" @click="cancelPipelineNameEdit"> mdi-menu </v-icon>
</template>
<v-list dark dense color="primary">
<v-list-item @click="startPipelineNameEdit">
<v-list-item-title>
<pv-icon color="#c5c5c5" :right="true" icon-name="mdi-pencil" tooltip="Edit pipeline name" />
</v-list-item-title>
</v-list-item>
<v-list-item @click="showCreatePipelineDialog">
<v-list-item-title>
<pv-icon color="#c5c5c5" :right="true" icon-name="mdi-plus" tooltip="Add new pipeline" />
</v-list-item-title>
</v-list-item>
<v-list-item @click="showPipelineDeletionConfirmationDialog = true">
<v-list-item-title>
<pv-icon color="red darken-2" :right="true" icon-name="mdi-delete" tooltip="Delete pipeline" />
</v-list-item-title>
</v-list-item>
<v-list-item @click="duplicateCurrentPipeline">
<v-list-item-title>
<pv-icon color="#c5c5c5" :right="true" icon-name="mdi-content-copy" tooltip="Duplicate pipeline" />
</v-list-item-title>
</v-list-item>
</v-list>
</v-menu>
<pv-icon
v-else-if="useCameraSettingsStore().isDriverMode && useCameraSettingsStore().pipelineNames.length === 0"
color="#c5c5c5"
:right="true"
icon-name="mdi-plus"
tooltip="Add new pipeline"
@click="showCreatePipelineDialog"
/>
</v-col>
</v-row>
<v-row style="padding: 0 12px 12px 24px">
<v-col cols="10" class="pa-0">
<pv-select
v-model="currentPipelineType"
label="Type"
tooltip="Changes the pipeline type, which changes the type of processing that will happen on input frames"
:disabled="useCameraSettingsStore().isDriverMode || useCameraSettingsStore().isCalibrationMode"
:items="pipelineTypesWrapper"
@input="showPipelineTypeChangeDialog = true"
/>
</v-col>
</v-row>
<v-dialog v-model="showPipelineCreationDialog" dark persistent width="500">
<v-card dark color="primary">
<v-card-title> Create New Pipeline </v-card-title>
<v-card-text>
<pv-input
v-model="newPipelineName"
placeholder="Pipeline Name"
:label-cols="3"
:input-cols="12 - 3"
label="Pipeline Name"
:rules="[(v) => checkPipelineName(v)]"
/>
<pv-select
v-model="newPipelineType"
:select-cols="12 - 3"
label="Tracking Type"
tooltip="Pipeline type, which changes the type of processing that will happen on input frames"
:items="validNewPipelineTypes"
/>
</v-card-text>
<v-divider />
<v-card-actions>
<v-spacer />
<v-btn color="#ffd843" :disabled="checkPipelineName(newPipelineName) !== true" @click="createNewPipeline">
Save
</v-btn>
<v-btn color="error" @click="cancelPipelineCreation"> Cancel </v-btn>
</v-card-actions>
</v-card>
</v-dialog>
<v-dialog v-model="showPipelineDeletionConfirmationDialog" dark width="500">
<v-card dark color="primary">
<v-card-title> Pipeline Deletion Confirmation </v-card-title>
<v-card-text>
Are you sure you want to delete the pipeline
<b style="color: white; font-weight: bold">{{
useCameraSettingsStore().currentPipelineSettings.pipelineNickname
}}</b
>? This cannot be undone.
</v-card-text>
<v-divider />
<v-card-actions>
<v-spacer />
<v-btn color="error" @click="confirmDeleteCurrentPipeline"> Yes, I'm sure </v-btn>
<v-btn color="#ffd843" @click="showPipelineDeletionConfirmationDialog = false"> No, take me back </v-btn>
</v-card-actions>
</v-card>
</v-dialog>
<v-dialog v-model="showPipelineTypeChangeDialog" persistent width="600">
<v-card color="primary" dark>
<v-card-title>Change Pipeline Type</v-card-title>
<v-card-text>
Are you sure you want to change the current pipeline type? This will cause all the pipeline settings to be
overwritten and they will be lost. If this isn't what you want, duplicate this pipeline first or export
settings.
</v-card-text>
<v-divider />
<v-card-actions>
<v-spacer />
<v-btn color="error" @click="confirmChangePipelineType"> Yes, I'm sure </v-btn>
<v-btn color="#ffd843" @click="cancelChangePipelineType"> No, take me back </v-btn>
</v-card-actions>
</v-card>
</v-dialog>
</v-card>
</template>

View File

@@ -1,111 +0,0 @@
<script setup lang="ts">
import { computed } from "vue";
import { useCameraSettingsStore } from "@/stores/settings/CameraSettingsStore";
import { useStateStore } from "@/stores/StateStore";
import { useSettingsStore } from "@/stores/settings/GeneralSettingsStore";
import { PipelineType } from "@/types/PipelineTypes";
import PhotonCameraStream from "@/components/app/photon-camera-stream.vue";
defineProps<{
// TODO fully update v-model usage in custom components on Vue3 update
value: number[];
}>();
const driverMode = computed<boolean>({
get: () => useCameraSettingsStore().isDriverMode,
set: (v) =>
useCameraSettingsStore().changeCurrentPipelineIndex(
v ? -1 : useCameraSettingsStore().currentCameraSettings.lastPipelineIndex || 0,
true
)
});
const fpsTooLow = computed<boolean>(() => {
const currFPS = useStateStore().currentPipelineResults?.fps || 0;
const targetFPS = useCameraSettingsStore().currentVideoFormat.fps;
const driverMode = useCameraSettingsStore().isDriverMode;
const gpuAccel = useSettingsStore().general.gpuAcceleration !== undefined;
const isReflective = useCameraSettingsStore().currentPipelineSettings.pipelineType === PipelineType.Reflective;
return currFPS - targetFPS < -5 && currFPS !== 0 && !driverMode && gpuAccel && isReflective;
});
const performanceRecommendation = computed<string>(() => {
if (
fpsTooLow.value &&
!useCameraSettingsStore().currentPipelineSettings.inputShouldShow &&
useCameraSettingsStore().currentPipelineSettings.pipelineType === PipelineType.Reflective
) {
return "HSV thresholds are too broad; narrow them for better performance";
} else if (fpsTooLow.value && useCameraSettingsStore().currentPipelineSettings.inputShouldShow) {
return "Stop viewing the raw stream for better performance";
} else {
return `${Math.min(Math.round(useStateStore().currentPipelineResults?.latency || 0), 9999)} ms latency`;
}
});
</script>
<template>
<v-card color="primary" height="100%" style="display: flex; flex-direction: column" dark>
<v-row>
<v-col class="align-self-center text-no-wrap">
<v-card-title>Cameras</v-card-title>
</v-col>
<v-col class="align-self-center" style="text-align: right; margin-right: 12px; padding-left: 24px">
<v-chip
label
:color="fpsTooLow ? 'error' : 'transparent'"
:text-color="fpsTooLow ? '#C7EA46' : '#ff4d00'"
style="font-size: 1rem; padding: 0; margin: 0"
>
<span class="pr-1"
>Processing @ {{ Math.round(useStateStore().currentPipelineResults?.fps || 0) }}&nbsp;FPS &ndash;</span
><span>{{ performanceRecommendation }}</span>
</v-chip>
</v-col>
<v-col
class="align-self-center"
style="
width: min-content;
flex-grow: 0;
display: flex;
justify-content: flex-end;
margin-right: 24px;
padding: 0;
"
>
<v-switch
v-model="driverMode"
:disabled="useCameraSettingsStore().isCalibrationMode || useCameraSettingsStore().pipelineNames.length === 0"
label="Driver Mode"
style="margin: 0; padding: 0; padding-left: 18px; margin-top: 14px"
color="accent"
/>
</v-col>
</v-row>
<v-divider style="border-color: white" />
<v-row class="stream-viewer-container pa-3">
<v-col v-show="value.includes(0)" class="stream-view">
<photon-camera-stream id="input-camera-stream" stream-type="Raw" style="width: 100%; height: auto" />
</v-col>
<v-col v-show="value.includes(1)" class="stream-view">
<photon-camera-stream id="output-camera-stream" stream-type="Processed" style="width: 100%; height: auto" />
</v-col>
</v-row>
</v-card>
</template>
<style scoped>
.stream-viewer-container {
display: flex;
justify-content: center;
}
.stream-view {
max-width: 500px;
}
@media only screen and (max-width: 512px) {
.stream-view {
min-width: 80%;
}
}
</style>

View File

@@ -1,181 +0,0 @@
<script setup lang="ts">
import type { Component } from "vue";
import { computed, getCurrentInstance, onBeforeUpdate, ref } from "vue";
import { useCameraSettingsStore } from "@/stores/settings/CameraSettingsStore";
import { useStateStore } from "@/stores/StateStore";
import InputTab from "@/components/dashboard/tabs/InputTab.vue";
import ThresholdTab from "@/components/dashboard/tabs/ThresholdTab.vue";
import ContoursTab from "@/components/dashboard/tabs/ContoursTab.vue";
import AprilTagTab from "@/components/dashboard/tabs/AprilTagTab.vue";
import ArucoTab from "@/components/dashboard/tabs/ArucoTab.vue";
import ObjectDetectionTab from "@/components/dashboard/tabs/ObjectDetectionTab.vue";
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 { WebsocketPipelineType } from "@/types/WebsocketDataTypes";
interface ConfigOption {
tabName: string;
component: Component;
}
const allTabs = Object.freeze({
inputTab: {
tabName: "Input",
component: InputTab
},
thresholdTab: {
tabName: "Threshold",
component: ThresholdTab
},
contoursTab: {
tabName: "Contours",
component: ContoursTab
},
apriltagTab: {
tabName: "AprilTag",
component: AprilTagTab
},
arucoTab: {
tabName: "Aruco",
component: ArucoTab
},
objectDetectionTab: {
tabName: "Object Detection",
component: ObjectDetectionTab
},
outputTab: {
tabName: "Output",
component: OutputTab
},
targetsTab: {
tabName: "Targets",
component: TargetsTab
},
pnpTab: {
tabName: "PnP",
component: PnPTab
},
map3dTab: {
tabName: "3D",
component: Map3DTab
}
});
const selectedTabs = ref([0, 0, 0, 0]);
const getTabGroups = (): ConfigOption[][] => {
const smAndDown = getCurrentInstance()?.proxy.$vuetify.breakpoint.smAndDown || false;
const mdAndDown = getCurrentInstance()?.proxy.$vuetify.breakpoint.mdAndDown || false;
const lgAndDown = getCurrentInstance()?.proxy.$vuetify.breakpoint.lgAndDown || false;
const xl = getCurrentInstance()?.proxy.$vuetify.breakpoint.xl || false;
if (smAndDown || useCameraSettingsStore().isDriverMode || (mdAndDown && !useStateStore().sidebarFolded)) {
return [Object.values(allTabs)];
} else if (mdAndDown || !useStateStore().sidebarFolded) {
return [
[
allTabs.inputTab,
allTabs.thresholdTab,
allTabs.contoursTab,
allTabs.apriltagTab,
allTabs.arucoTab,
allTabs.objectDetectionTab,
allTabs.outputTab
],
[allTabs.targetsTab, allTabs.pnpTab, allTabs.map3dTab]
];
} else if (lgAndDown) {
return [
[allTabs.inputTab],
[
allTabs.thresholdTab,
allTabs.contoursTab,
allTabs.apriltagTab,
allTabs.arucoTab,
allTabs.objectDetectionTab,
allTabs.outputTab
],
[allTabs.targetsTab, allTabs.pnpTab, allTabs.map3dTab]
];
} else if (xl) {
return [
[allTabs.inputTab],
[allTabs.thresholdTab],
[allTabs.contoursTab, allTabs.apriltagTab, allTabs.arucoTab, allTabs.objectDetectionTab, allTabs.outputTab],
[allTabs.targetsTab, allTabs.pnpTab, allTabs.map3dTab]
];
}
return [];
};
const tabGroups = computed<ConfigOption[][]>(() => {
// Just return the input tab because we know that is always the case in driver mode
if (useCameraSettingsStore().isDriverMode) return [[allTabs.inputTab]];
const allow3d = useCameraSettingsStore().currentPipelineSettings.solvePNPEnabled;
const isAprilTag = useCameraSettingsStore().currentWebsocketPipelineType === WebsocketPipelineType.AprilTag;
const isAruco = useCameraSettingsStore().currentWebsocketPipelineType === WebsocketPipelineType.Aruco;
const isObjectDetection =
useCameraSettingsStore().currentWebsocketPipelineType === WebsocketPipelineType.ObjectDetection;
return getTabGroups()
.map((tabGroup) =>
tabGroup.filter(
(tabConfig) =>
!(!allow3d && tabConfig.tabName === "3D") && //Filter out 3D tab any time 3D isn't calibrated
!((!allow3d || isAprilTag || isAruco || isObjectDetection) && tabConfig.tabName === "PnP") && //Filter out the PnP config tab if 3D isn't available, or we're doing AprilTags
!((isAprilTag || isAruco || isObjectDetection) && tabConfig.tabName === "Threshold") && //Filter out threshold tab if we're doing AprilTags
!((isAprilTag || isAruco || isObjectDetection) && tabConfig.tabName === "Contours") && //Filter out contours if we're doing AprilTags
!(!isAprilTag && tabConfig.tabName === "AprilTag") && //Filter out apriltag unless we actually are doing AprilTags
!(!isAruco && tabConfig.tabName === "Aruco") &&
!(!isObjectDetection && tabConfig.tabName === "Object Detection") //Filter out aruco unless we actually are doing Aruco
)
)
.filter((it) => it.length); // Remove empty tab groups
});
onBeforeUpdate(() => {
// Force the current tab to the input tab on driver mode change
if (useCameraSettingsStore().isDriverMode) {
selectedTabs.value[0] = 0;
}
});
</script>
<template>
<v-row no-gutters class="tabGroups">
<v-col
v-for="(tabGroupData, tabGroupIndex) in tabGroups"
:key="tabGroupIndex"
:class="tabGroupIndex !== tabGroups.length - 1 && 'pr-3'"
>
<v-card color="primary" height="100%" class="pr-4 pl-4">
<v-tabs
v-model="selectedTabs[tabGroupIndex]"
grow
background-color="primary"
dark
height="48"
slider-color="accent"
>
<v-tab v-for="(tabConfig, index) in tabGroupData" :key="index">
{{ tabConfig.tabName }}
</v-tab>
</v-tabs>
<div class="pl-4 pr-4 pt-4 pb-2">
<KeepAlive>
<Component :is="tabGroupData[selectedTabs[tabGroupIndex]].component" />
</KeepAlive>
</div>
</v-card>
</v-col>
</v-row>
</template>
<style>
.v-slide-group__next--disabled,
.v-slide-group__prev--disabled {
display: none !important;
}
</style>

View File

@@ -1,94 +0,0 @@
<script setup lang="ts">
import { computed } from "vue";
import { useCameraSettingsStore } from "@/stores/settings/CameraSettingsStore";
import { useStateStore } from "@/stores/StateStore";
const props = defineProps<{
// TODO fully update v-model usage in custom components on Vue3 update
value: number[];
}>();
const emit = defineEmits<{
(e: "input", value: number[]): void;
}>();
const localValue = computed({
get: () => props.value,
set: (v) => emit("input", v)
});
const processingMode = computed<number>({
get: () => (useCameraSettingsStore().currentPipelineSettings.solvePNPEnabled ? 1 : 0),
set: (v) => {
if (useCameraSettingsStore().isCurrentVideoFormatCalibrated) {
useCameraSettingsStore().changeCurrentPipelineSetting({ solvePNPEnabled: v === 1 }, true);
}
}
});
</script>
<template>
<v-card
:disabled="useCameraSettingsStore().isDriverMode || useStateStore().colorPickingMode"
class="mt-3"
color="primary"
style="height: 100%; display: flex; flex-direction: column"
>
<v-row align="center" class="pa-3 pb-0">
<v-col>
<p style="color: white">Processing Mode</p>
<v-btn-toggle v-model="processingMode" mandatory dark class="fill">
<v-btn color="secondary">
<v-icon left>mdi-square-outline</v-icon>
<span>2D</span>
</v-btn>
<v-btn color="secondary" :disabled="!useCameraSettingsStore().isCurrentVideoFormatCalibrated">
<v-icon left>mdi-cube-outline</v-icon>
<span>3D</span>
</v-btn>
</v-btn-toggle>
</v-col>
</v-row>
<v-row align="center" class="pa-3 pt-0">
<v-col>
<p style="color: white">Stream Display</p>
<v-btn-toggle v-model="localValue" :multiple="true" mandatory dark class="fill">
<v-btn color="secondary" class="fill">
<v-icon left class="mode-btn-icon">mdi-import</v-icon>
<span class="mode-btn-label">Raw</span>
</v-btn>
<v-btn color="secondary" class="fill">
<v-icon left class="mode-btn-icon">mdi-export</v-icon>
<span class="mode-btn-label">Processed</span>
</v-btn>
</v-btn-toggle>
</v-col>
</v-row>
</v-card>
</template>
<style scoped>
.v-btn-toggle.fill {
width: 100%;
height: 100%;
}
.v-btn-toggle.fill > .v-btn {
width: 50%;
height: 100%;
}
th {
width: 80px;
text-align: center;
}
@media only screen and (max-width: 351px) {
.mode-btn-icon {
margin: 0 !important;
}
.mode-btn-label {
display: none;
}
}
</style>

View File

@@ -1,93 +0,0 @@
<script setup lang="ts">
import { PipelineType } from "@/types/PipelineTypes";
import PvSelect from "@/components/common/pv-select.vue";
import PvSlider from "@/components/common/pv-slider.vue";
import PvSwitch from "@/components/common/pv-switch.vue";
import { computed, getCurrentInstance } from "vue";
import { useStateStore } from "@/stores/StateStore";
import type { ActivePipelineSettings } from "@/types/PipelineTypes";
import { useCameraSettingsStore } from "@/stores/settings/CameraSettingsStore";
// 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 interactiveCols = computed(() =>
(getCurrentInstance()?.proxy.$vuetify.breakpoint.mdAndDown || false) &&
(!useStateStore().sidebarFolded || useCameraSettingsStore().isDriverMode)
? 9
: 8
);
</script>
<template>
<div v-if="currentPipelineSettings.pipelineType === PipelineType.AprilTag">
<pv-select
v-model="currentPipelineSettings.tagFamily"
label="Target family"
:items="['AprilTag 36h11 (6.5in)', 'AprilTag 25h9 (6in)', 'AprilTag 16h5 (6in)']"
:select-cols="interactiveCols"
@input="(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ tagFamily: value }, false)"
/>
<pv-slider
v-model="currentPipelineSettings.decimate"
class="pt-2"
:slider-cols="interactiveCols"
label="Decimate"
tooltip="Increases FPS at the expense of range by reducing image resolution initially"
:min="1"
:max="8"
@input="(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ decimate: value }, false)"
/>
<pv-slider
v-model="currentPipelineSettings.blur"
class="pt-2"
:slider-cols="interactiveCols"
label="Blur"
tooltip="Gaussian blur added to the image, high FPS cost for slightly decreased noise"
:min="0"
:max="5"
:step="0.1"
@input="(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ blur: value }, false)"
/>
<pv-slider
v-model="currentPipelineSettings.threads"
class="pt-2"
:slider-cols="interactiveCols"
label="Threads"
tooltip="Number of threads spawned by the AprilTag detector"
:min="1"
:max="8"
@input="(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ threads: value }, false)"
/>
<pv-switch
v-model="currentPipelineSettings.refineEdges"
class="pt-2"
label="Refine Edges"
tooltip="Further refines the AprilTag corner position initial estimate, suggested left on"
@input="(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ refineEdges: value }, false)"
/>
<pv-slider
v-model="currentPipelineSettings.decisionMargin"
class="pt-2 pb-4"
:slider-cols="interactiveCols"
label="Decision Margin Cutoff"
tooltip="Tags with a 'margin' (decoding quality score) less than this wil be rejected. Increase this to reduce the number of false positive detections"
:min="0"
:max="250"
@input="(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ decisionMargin: value }, false)"
/>
<pv-slider
v-model="currentPipelineSettings.numIterations"
class="pt-2 pb-4"
:slider-cols="interactiveCols"
label="Pose Estimation Iterations"
tooltip="Number of iterations the pose estimation algorithm will run, 50-100 is a good starting point"
:min="0"
:max="500"
@input="(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ numIterations: value }, false)"
/>
</div>
</template>

View File

@@ -1,83 +0,0 @@
<script setup lang="ts">
import { useCameraSettingsStore } from "@/stores/settings/CameraSettingsStore";
import { PipelineType, type ActivePipelineSettings } 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";
import PvSelect from "@/components/common/pv-select.vue";
import { computed, getCurrentInstance } from "vue";
import { useStateStore } from "@/stores/StateStore";
// 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 interactiveCols = computed(() =>
(getCurrentInstance()?.proxy.$vuetify.breakpoint.mdAndDown || false) &&
(!useStateStore().sidebarFolded || useCameraSettingsStore().isDriverMode)
? 9
: 8
);
</script>
<template>
<div v-if="currentPipelineSettings.pipelineType === PipelineType.Aruco">
<pv-select
v-model="currentPipelineSettings.tagFamily"
label="Target family"
:items="['AprilTag Family 36h11', 'AprilTag Family 25h9', 'AprilTag Family 16h5']"
:select-cols="interactiveCols"
@input="(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ tagFamily: value }, false)"
/>
<pv-switch
v-model="currentPipelineSettings.useCornerRefinement"
class="pt-2"
label="Refine Corners"
tooltip="Further refine the initial corners with subpixel accuracy."
:switch-cols="interactiveCols"
@input="(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ useCornerRefinement: value }, false)"
/>
<pv-range-slider
v-model="currentPipelineSettings.threshWinSizes"
label="Thresh Min/Max Size"
tooltip="The minimum and maximum adaptive threshold window size. Larger windows tend more towards global thresholding, but small windows can be weak to noise."
:min="3"
:max="255"
:slider-cols="interactiveCols"
:step="2"
@input="(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ threshWinSizes: value }, false)"
/>
<pv-slider
v-model="currentPipelineSettings.threshStepSize"
class="pt-2"
:slider-cols="interactiveCols"
label="Thresh Step Size"
tooltip="Smaller values will cause more steps between the min/max sizes. More, varied steps can improve detection robustness to lighting, but may decrease performance."
:min="2"
:max="128"
:step="1"
@input="(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ threshStepSize: value }, false)"
/>
<pv-slider
v-model="currentPipelineSettings.threshConstant"
class="pt-2"
:slider-cols="interactiveCols"
label="Thresh Constant"
tooltip="Affects the threshold window mean value cutoff for all steps. Higher values can improve performance, but may harm detection rate."
:min="0"
:max="128"
:step="1"
@input="(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ threshConstant: value }, false)"
/>
<pv-switch
v-model="currentPipelineSettings.debugThreshold"
class="pt-2"
label="Debug Threshold"
tooltip="Display the first threshold step to the color stream."
:switch-cols="interactiveCols"
@input="(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ debugThreshold: value }, false)"
/>
</div>
</template>

View File

@@ -1,232 +0,0 @@
<script setup lang="ts">
import { useCameraSettingsStore } from "@/stores/settings/CameraSettingsStore";
import { type ActivePipelineSettings, PipelineType } from "@/types/PipelineTypes";
import PvRangeSlider from "@/components/common/pv-range-slider.vue";
import PvSelect from "@/components/common/pv-select.vue";
import PvSlider from "@/components/common/pv-slider.vue";
import { computed, getCurrentInstance } from "vue";
import { useStateStore } from "@/stores/StateStore";
// 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
);
// TODO fix pv-range-slider so that store access doesn't need to be deferred
const contourArea = computed<[number, number]>({
get: () => Object.values(useCameraSettingsStore().currentPipelineSettings.contourArea) as [number, number],
set: (v) => (useCameraSettingsStore().currentPipelineSettings.contourArea = v)
});
const contourRatio = computed<[number, number]>({
get: () => Object.values(useCameraSettingsStore().currentPipelineSettings.contourRatio) as [number, number],
set: (v) => (useCameraSettingsStore().currentPipelineSettings.contourRatio = v)
});
const contourFullness = computed<[number, number]>({
get: () => Object.values(useCameraSettingsStore().currentPipelineSettings.contourFullness) as [number, number],
set: (v) => (useCameraSettingsStore().currentPipelineSettings.contourFullness = v)
});
const contourPerimeter = computed<[number, number]>({
get: () =>
currentPipelineSettings.value.pipelineType === PipelineType.ColoredShape
? (Object.values(currentPipelineSettings.value.contourPerimeter) as [number, number])
: ([0, 0] as [number, number]),
set: (v) => {
if (currentPipelineSettings.value.pipelineType === PipelineType.ColoredShape) {
currentPipelineSettings.value.contourPerimeter = v;
}
}
});
const contourRadius = computed<[number, number]>({
get: () =>
currentPipelineSettings.value.pipelineType === PipelineType.ColoredShape
? (Object.values(currentPipelineSettings.value.contourRadius) as [number, number])
: ([0, 0] as [number, number]),
set: (v) => {
if (currentPipelineSettings.value.pipelineType === PipelineType.ColoredShape) {
currentPipelineSettings.value.contourRadius = v;
}
}
});
const interactiveCols = computed(() =>
(getCurrentInstance()?.proxy.$vuetify.breakpoint.mdAndDown || false) &&
(!useStateStore().sidebarFolded || useCameraSettingsStore().isDriverMode)
? 9
: 8
);
</script>
<template>
<div>
<pv-range-slider
v-model="contourArea"
label="Area"
:min="0"
:max="100"
:slider-cols="interactiveCols"
:step="0.01"
@input="(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ contourArea: value }, false)"
/>
<pv-range-slider
v-if="useCameraSettingsStore().currentPipelineType !== PipelineType.ColoredShape"
v-model="contourRatio"
label="Ratio (W/H)"
tooltip="Min and max ratio between the width and height of a contour's bounding rectangle"
:min="0"
:max="100"
:slider-cols="interactiveCols"
:step="0.1"
@input="(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ contourRatio: value }, false)"
/>
<pv-select
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']"
:select-cols="interactiveCols"
@input="
(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ contourTargetOrientation: value }, false)
"
/>
<pv-range-slider
v-if="useCameraSettingsStore().currentPipelineType === PipelineType.ColoredShape"
v-model="contourFullness"
label="Fullness"
tooltip="Min and max ratio between a contour's area and its bounding rectangle"
:min="0"
:max="100"
:slider-cols="interactiveCols"
@input="(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ contourFullness: value }, false)"
/>
<pv-range-slider
v-if="currentPipelineSettings.pipelineType === PipelineType.ColoredShape"
v-model="contourPerimeter"
label="Perimeter"
tooltip="Min and max perimeter of the shape, in pixels"
:min="0"
:max="4000"
:slider-cols="interactiveCols"
@input="(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ contourPerimeter: value }, false)"
/>
<pv-slider
v-model="useCameraSettingsStore().currentPipelineSettings.contourSpecklePercentage"
label="Speckle Rejection"
tooltip="Rejects contours whose average area is less than the given percentage of the average area of all the other contours"
:min="0"
:max="100"
:slider-cols="interactiveCols"
@input="
(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ contourSpecklePercentage: value }, false)
"
/>
<template v-if="currentPipelineSettings.pipelineType === PipelineType.Reflective">
<pv-slider
v-model="currentPipelineSettings.contourFilterRangeX"
label="X Filter Tightness"
tooltip="Rejects contours whose center X is further than X standard deviations left/right of the mean X location"
:min="0.1"
:max="4"
:step="0.1"
:slider-cols="interactiveCols"
@input="(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ contourFilterRangeX: value }, false)"
/>
<pv-slider
v-model="currentPipelineSettings.contourFilterRangeY"
label="Y Filter Tightness"
tooltip="Rejects contours whose center Y is further than X standard deviations above/below the mean Y location"
:min="0.1"
:max="4"
:step="0.1"
:slider-cols="interactiveCols"
@input="(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ contourFilterRangeY: value }, false)"
/>
<pv-select
v-model="useCameraSettingsStore().currentPipelineSettings.contourGroupingMode"
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']"
@input="(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ contourGroupingMode: value }, false)"
/>
<pv-select
v-model="useCameraSettingsStore().currentPipelineSettings.contourIntersection"
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']"
:disabled="useCameraSettingsStore().currentPipelineSettings.contourGroupingMode === 0"
@input="(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ contourIntersection: value }, false)"
/>
</template>
<template v-else-if="currentPipelineSettings.pipelineType === PipelineType.ColoredShape">
<v-divider class="mt-3" />
<pv-select
v-model="currentPipelineSettings.contourShape"
label="Target Shape"
tooltip="The shape of targets to look for"
:select-cols="interactiveCols"
:items="['Circle', 'Polygon', 'Triangle', 'Quadrilateral']"
@input="(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ contourShape: value }, false)"
/>
<pv-slider
v-model="currentPipelineSettings.accuracyPercentage"
:disabled="currentPipelineSettings.contourShape < 1"
label="Shape Simplification"
tooltip="How much we should simply the input contour before checking how many sides it has"
:min="0"
:max="100"
:slider-cols="interactiveCols"
@input="(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ accuracyPercentage: value }, false)"
/>
<pv-slider
v-model="currentPipelineSettings.circleDetectThreshold"
:disabled="currentPipelineSettings.contourShape !== 0"
label="Circle match distance"
tooltip="How close the centroid of a contour must be to the center of a circle in order for them to be matched"
:min="1"
:max="100"
:slider-cols="interactiveCols"
@input="
(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ circleDetectThreshold: value }, false)
"
/>
<pv-range-slider
v-model="contourRadius"
:disabled="currentPipelineSettings.contourShape !== 0"
label="Radius"
:min="0"
:max="100"
:slider-cols="interactiveCols"
@input="(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ contourRadius: value }, false)"
/>
<pv-slider
v-model="currentPipelineSettings.maxCannyThresh"
:disabled="currentPipelineSettings.contourShape !== 0"
label="Max Canny Threshold"
:min="1"
:max="100"
:slider-cols="interactiveCols"
@input="(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ maxCannyThresh: value }, false)"
/>
<pv-slider
v-model="currentPipelineSettings.circleAccuracy"
:disabled="currentPipelineSettings.contourShape !== 0"
label="Circle Accuracy"
:min="1"
:max="100"
:slider-cols="interactiveCols"
@input="(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ circleAccuracy: value }, false)"
/>
<v-divider class="mt-3" />
</template>
<pv-select
v-model="useCameraSettingsStore().currentPipelineSettings.contourSortMode"
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']"
@input="(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ contourSortMode: value }, false)"
/>
</div>
</template>

View File

@@ -1,158 +0,0 @@
<script setup lang="ts">
import PvSlider from "@/components/common/pv-slider.vue";
import { useCameraSettingsStore } from "@/stores/settings/CameraSettingsStore";
import PvSwitch from "@/components/common/pv-switch.vue";
import PvSelect from "@/components/common/pv-select.vue";
import { computed, getCurrentInstance } from "vue";
import { useSettingsStore } from "@/stores/settings/GeneralSettingsStore";
import { useStateStore } from "@/stores/StateStore";
import { getResolutionString } from "@/lib/PhotonUtils";
// Due to something with libcamera or something else IDK much about, the 90° rotations need to be disabled if the libcamera drivers are being used.
const cameraRotations = computed(() =>
["Normal", "90° CW", "180°", "90° CCW"].map((v, i) => ({
name: v,
value: i,
disabled: useCameraSettingsStore().isCSICamera ? [1, 3].includes(i) : false
}))
);
const streamDivisors = [1, 2, 4, 6];
const getFilteredStreamDivisors = (): number[] => {
const currentResolutionWidth = useCameraSettingsStore().currentVideoFormat.resolution.width;
return streamDivisors.filter(
(x) =>
useCameraSettingsStore().isDriverMode ||
!useSettingsStore().gpuAccelerationEnabled ||
currentResolutionWidth / x < 400
);
};
const getNumberOfSkippedDivisors = () => streamDivisors.length - getFilteredStreamDivisors().length;
const cameraResolutions = computed(() =>
useCameraSettingsStore().currentCameraSettings.validVideoFormats.map(
(f) => `${getResolutionString(f.resolution)} at ${f.fps} FPS, ${f.pixelFormat}`
)
);
const handleResolutionChange = (value: number) => {
useCameraSettingsStore().changeCurrentPipelineSetting({ cameraVideoModeIndex: value }, false);
useCameraSettingsStore().changeCurrentPipelineSetting({ streamingFrameDivisor: getNumberOfSkippedDivisors() }, false);
useCameraSettingsStore().currentPipelineSettings.streamingFrameDivisor = 0;
if (!useCameraSettingsStore().isCurrentVideoFormatCalibrated) {
useCameraSettingsStore().changeCurrentPipelineSetting({ solvePNPEnabled: false }, true);
}
};
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)
})}`
);
});
const handleStreamResolutionChange = (value: number) => {
useCameraSettingsStore().changeCurrentPipelineSetting(
{ streamingFrameDivisor: value + getNumberOfSkippedDivisors() },
false
);
};
const interactiveCols = computed(() =>
(getCurrentInstance()?.proxy.$vuetify.breakpoint.mdAndDown || false) &&
(!useStateStore().sidebarFolded || useCameraSettingsStore().isDriverMode)
? 9
: 8
);
</script>
<template>
<div>
<pv-slider
v-model="useCameraSettingsStore().currentPipelineSettings.cameraExposure"
:disabled="useCameraSettingsStore().currentCameraSettings.pipelineSettings.cameraAutoExposure"
label="Exposure"
tooltip="Directly controls how much light is allowed to fall onto the sensor, which affects apparent brightness"
:min="0"
:max="100"
:slider-cols="interactiveCols"
:step="0.1"
@input="(args) => useCameraSettingsStore().changeCurrentPipelineSetting({ cameraExposure: args }, false)"
/>
<pv-slider
v-model="useCameraSettingsStore().currentPipelineSettings.cameraBrightness"
label="Brightness"
:min="0"
:max="100"
:slider-cols="interactiveCols"
@input="(args) => useCameraSettingsStore().changeCurrentPipelineSetting({ cameraBrightness: args }, false)"
/>
<pv-switch
v-model="useCameraSettingsStore().currentPipelineSettings.cameraAutoExposure"
class="pt-2"
label="Auto Exposure"
:switch-cols="interactiveCols"
tooltip="Enables or Disables camera automatic adjustment for current lighting conditions"
@input="(args) => useCameraSettingsStore().changeCurrentPipelineSetting({ cameraAutoExposure: args }, false)"
/>
<pv-slider
v-if="useCameraSettingsStore().currentPipelineSettings.cameraGain >= 0"
v-model="useCameraSettingsStore().currentPipelineSettings.cameraGain"
label="Camera Gain"
tooltip="Controls camera gain, similar to brightness"
:min="0"
:max="100"
:slider-cols="interactiveCols"
@input="(args) => useCameraSettingsStore().changeCurrentPipelineSetting({ cameraGain: args }, false)"
/>
<pv-slider
v-if="useCameraSettingsStore().currentPipelineSettings.cameraRedGain !== -1"
v-model="useCameraSettingsStore().currentPipelineSettings.cameraRedGain"
label="Red AWB Gain"
:min="0"
:max="100"
:slider-cols="interactiveCols"
tooltip="Controls red automatic white balance gain, which affects how the camera captures colors in different conditions"
@input="(args) => useCameraSettingsStore().changeCurrentPipelineSetting({ cameraRedGain: args }, false)"
/>
<pv-slider
v-if="useCameraSettingsStore().currentPipelineSettings.cameraBlueGain !== -1"
v-model="useCameraSettingsStore().currentPipelineSettings.cameraBlueGain"
label="Blue AWB Gain"
:min="0"
:max="100"
:slider-cols="interactiveCols"
tooltip="Controls blue automatic white balance gain, which affects how the camera captures colors in different conditions"
@input="(args) => useCameraSettingsStore().changeCurrentPipelineSetting({ cameraBlueGain: args }, false)"
/>
<pv-select
v-model="useCameraSettingsStore().currentPipelineSettings.inputImageRotationMode"
label="Orientation"
tooltip="Rotates the camera stream"
:items="cameraRotations"
:select-cols="interactiveCols"
@input="(args) => useCameraSettingsStore().changeCurrentPipelineSetting({ inputImageRotationMode: args }, false)"
/>
<pv-select
v-model="useCameraSettingsStore().currentPipelineSettings.cameraVideoModeIndex"
label="Resolution"
tooltip="Resolution and FPS the camera should directly capture at"
:items="cameraResolutions"
:select-cols="interactiveCols"
@input="(args) => handleResolutionChange(args)"
/>
<pv-select
v-model="useCameraSettingsStore().currentPipelineSettings.streamingFrameDivisor"
label="Stream Resolution"
tooltip="Resolution to which camera frames are downscaled for streaming to the dashboard"
:items="streamResolutions"
:select-cols="interactiveCols"
@input="(args) => handleStreamResolutionChange(args)"
/>
</div>
</template>

View File

@@ -1,23 +0,0 @@
<script setup lang="ts">
import { computed } from "vue";
import type { PhotonTarget } from "@/types/PhotonTrackingTypes";
import { useStateStore } from "@/stores/StateStore";
import Photon3dVisualizer from "@/components/app/photon-3d-visualizer.vue";
const trackedTargets = computed<PhotonTarget[]>(() => useStateStore().currentPipelineResults?.targets || []);
</script>
<template>
<div>
<v-row style="width: 100%">
<v-col>
<span class="white--text">Target Visualization</span>
</v-col>
</v-row>
<v-row style="width: 100%">
<v-col style="display: flex; align-items: center; justify-content: center">
<photon3d-visualizer :targets="trackedTargets" />
</v-col>
</v-row>
</div>
</template>

View File

@@ -1,83 +0,0 @@
<script setup lang="ts">
import { useCameraSettingsStore } from "@/stores/settings/CameraSettingsStore";
import { type ActivePipelineSettings, PipelineType } from "@/types/PipelineTypes";
import PvSlider from "@/components/common/pv-slider.vue";
import { computed, getCurrentInstance } from "vue";
import { useStateStore } from "@/stores/StateStore";
// 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
);
// TODO fix pv-range-slider so that store access doesn't need to be deferred
const contourArea = computed<[number, number]>({
get: () => Object.values(useCameraSettingsStore().currentPipelineSettings.contourArea) as [number, number],
set: (v) => (useCameraSettingsStore().currentPipelineSettings.contourArea = v)
});
const contourRatio = computed<[number, number]>({
get: () => Object.values(useCameraSettingsStore().currentPipelineSettings.contourRatio) as [number, number],
set: (v) => (useCameraSettingsStore().currentPipelineSettings.contourRatio = v)
});
const interactiveCols = computed(() =>
(getCurrentInstance()?.proxy.$vuetify.breakpoint.mdAndDown || false) &&
(!useStateStore().sidebarFolded || useCameraSettingsStore().isDriverMode)
? 9
: 8
);
</script>
<template>
<div v-if="currentPipelineSettings.pipelineType === PipelineType.ObjectDetection">
<pv-slider
v-model="currentPipelineSettings.confidence"
class="pt-2"
:slider-cols="interactiveCols"
label="Confidence"
tooltip="The minimum confidence for a detection to be considered valid. Bigger numbers mean fewer but more probable detections are allowed through."
:min="0"
:max="1"
:step="0.01"
@input="(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ confidence: value }, false)"
/>
<pv-range-slider
v-model="contourArea"
label="Area"
:min="0"
:max="100"
:slider-cols="interactiveCols"
:step="0.01"
@input="(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ contourArea: value }, false)"
/>
<pv-range-slider
v-model="contourRatio"
label="Ratio (W/H)"
tooltip="Min and max ratio between the width and height of a contour's bounding rectangle"
:min="0"
:max="100"
:slider-cols="interactiveCols"
:step="0.01"
@input="(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ contourRatio: value }, false)"
/>
<pv-select
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']"
:select-cols="interactiveCols"
@input="
(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ contourTargetOrientation: value }, false)
"
/>
<pv-select
v-model="currentPipelineSettings.contourSortMode"
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']"
@input="(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ contourSortMode: value }, false)"
/>
</div>
</template>

View File

@@ -1,224 +0,0 @@
<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 PvSwitch from "@/components/common/pv-switch.vue";
import { computed, getCurrentInstance } from "vue";
import { RobotOffsetType } from "@/types/SettingTypes";
import { useStateStore } from "@/stores/StateStore";
const isTagPipeline = computed(
() =>
useCameraSettingsStore().currentPipelineType === PipelineType.AprilTag ||
useCameraSettingsStore().currentPipelineType === PipelineType.Aruco
);
interface MetricItem {
header: string;
value?: string;
}
const offsetPoints = computed<MetricItem[]>(() => {
switch (useCameraSettingsStore().currentPipelineSettings.offsetRobotOffsetMode) {
case RobotOffsetPointMode.Single:
const value = Object.values(useCameraSettingsStore().currentPipelineSettings.offsetSinglePoint);
return [{ header: "Offset Point", value: `(${value[0].toFixed(2)}°, ${value[1].toFixed(2)}°)` }];
case RobotOffsetPointMode.Dual:
const firstPoint = Object.values(useCameraSettingsStore().currentPipelineSettings.offsetDualPointA);
const firstPointArea = useCameraSettingsStore().currentPipelineSettings.offsetDualPointAArea;
const secondPoint = Object.values(useCameraSettingsStore().currentPipelineSettings.offsetDualPointB);
const secondPointArea = useCameraSettingsStore().currentPipelineSettings.offsetDualPointBArea;
return [
{ header: "First Offset Point", value: `(${firstPoint[0].toFixed(2)}°, ${firstPoint[1].toFixed(2)}°)` },
{ header: "First Offset Point Area", value: `${firstPointArea.toFixed(2)}%` },
{ header: "Second Offset Point", value: `(${secondPoint[0].toFixed(2)}°, ${secondPoint[1].toFixed(2)}°)` },
{ header: "Second Offset Point Area", value: `${secondPointArea.toFixed(2)}%` }
];
default:
case RobotOffsetPointMode.None:
return [];
}
});
// 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 interactiveCols = computed(() =>
(getCurrentInstance()?.proxy.$vuetify.breakpoint.mdAndDown || false) &&
(!useStateStore().sidebarFolded || useCameraSettingsStore().isDriverMode)
? 9
: 8
);
</script>
<template>
<div>
<pv-select
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']"
:select-cols="interactiveCols"
@input="
(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ contourTargetOffsetPointEdge: value }, false)
"
/>
<pv-select
v-if="!isTagPipeline"
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']"
:select-cols="interactiveCols"
@input="
(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ contourTargetOrientation: value }, false)
"
/>
<pv-switch
v-model="useCameraSettingsStore().currentPipelineSettings.outputShowMultipleTargets"
label="Show Multiple Targets"
tooltip="If enabled, up to five targets will be displayed and sent via PhotonLib, instead of just one"
:disabled="isTagPipeline"
:switch-cols="interactiveCols"
@input="
(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ outputShowMultipleTargets: value }, false)
"
/>
<pv-switch
v-if="
(currentPipelineSettings.pipelineType === PipelineType.AprilTag ||
currentPipelineSettings.pipelineType === PipelineType.Aruco) &&
useCameraSettingsStore().isCurrentVideoFormatCalibrated &&
useCameraSettingsStore().currentPipelineSettings.solvePNPEnabled
"
v-model="currentPipelineSettings.doMultiTarget"
label="Do Multi-Target Estimation"
tooltip="If enabled, all visible fiducial targets will be used to provide a single pose estimate from their combined model."
:switch-cols="interactiveCols"
:disabled="!isTagPipeline"
@input="(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ doMultiTarget: value }, false)"
/>
<pv-switch
v-if="
(currentPipelineSettings.pipelineType === PipelineType.AprilTag ||
currentPipelineSettings.pipelineType === PipelineType.Aruco) &&
useCameraSettingsStore().isCurrentVideoFormatCalibrated &&
useCameraSettingsStore().currentPipelineSettings.solvePNPEnabled
"
v-model="currentPipelineSettings.doSingleTargetAlways"
label="Always Do Single-Target Estimation"
tooltip="If disabled, visible fiducial targets used for multi-target estimation will not also be used for single-target estimation."
:switch-cols="interactiveCols"
:disabled="!isTagPipeline || !currentPipelineSettings.doMultiTarget"
@input="(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ doSingleTargetAlways: value }, false)"
/>
<v-divider />
<table
v-if="useCameraSettingsStore().currentPipelineSettings.offsetRobotOffsetMode !== RobotOffsetPointMode.None"
class="metrics-table mt-3 mb-3"
>
<tr>
<th v-for="(item, itemIndex) in offsetPoints" :key="itemIndex" class="metric-item metric-item-title">
{{ item.header }}
</th>
</tr>
<tr>
<td v-for="(item, itemIndex) in offsetPoints" :key="itemIndex" class="metric-item">
{{ item.value }}
</td>
</tr>
</table>
<pv-select
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']"
:select-cols="interactiveCols"
@input="(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ offsetRobotOffsetMode: value }, false)"
/>
<v-row
v-if="useCameraSettingsStore().currentPipelineSettings.offsetRobotOffsetMode !== RobotOffsetPointMode.None"
align="center"
justify="start"
>
<v-row
v-if="useCameraSettingsStore().currentPipelineSettings.offsetRobotOffsetMode === RobotOffsetPointMode.Single"
>
<v-col>
<v-btn
small
color="accent"
style="width: 100%"
class="black--text"
@click="useCameraSettingsStore().takeRobotOffsetPoint(RobotOffsetType.Single)"
>
Take Point
</v-btn>
</v-col>
</v-row>
<v-row
v-else-if="useCameraSettingsStore().currentPipelineSettings.offsetRobotOffsetMode === RobotOffsetPointMode.Dual"
>
<v-col>
<v-btn
small
color="accent"
style="width: 100%"
class="black--text"
@click="useCameraSettingsStore().takeRobotOffsetPoint(RobotOffsetType.DualFirst)"
>
Take First Point
</v-btn>
</v-col>
<v-col>
<v-btn
small
color="accent"
style="width: 100%"
class="black--text"
@click="useCameraSettingsStore().takeRobotOffsetPoint(RobotOffsetType.DualSecond)"
>
Take Second Point
</v-btn>
</v-col>
</v-row>
<v-col>
<v-btn
small
color="yellow darken-3"
style="width: 100%"
@click="useCameraSettingsStore().takeRobotOffsetPoint(RobotOffsetType.Clear)"
>
Clear All Points
</v-btn>
</v-col>
</v-row>
</div>
</template>
<style scoped>
.metrics-table {
border-collapse: separate;
border-spacing: 0;
border-radius: 5px;
border: 1px solid white;
width: 100%;
text-align: center;
}
.metric-item {
padding: 1px 15px 1px 10px;
border-right: 1px solid;
font-weight: normal;
color: white;
}
.metric-item-title {
font-size: 18px;
text-decoration: underline;
text-decoration-color: #ffd843;
}
</style>

View File

@@ -1,47 +0,0 @@
<script setup lang="ts">
import PvSelect from "@/components/common/pv-select.vue";
import { useCameraSettingsStore } from "@/stores/settings/CameraSettingsStore";
import { TargetModel } from "@/types/PipelineTypes";
import PvSlider from "@/components/common/pv-slider.vue";
import { computed, getCurrentInstance } from "vue";
import { useStateStore } from "@/stores/StateStore";
const interactiveCols = computed(() =>
(getCurrentInstance()?.proxy.$vuetify.breakpoint.mdAndDown || false) &&
(!useStateStore().sidebarFolded || useCameraSettingsStore().isDriverMode)
? 9
: 8
);
</script>
<template>
<div>
<pv-select
v-model="useCameraSettingsStore().currentPipelineSettings.targetModel"
label="Target Model"
:items="[
{ name: '2016 High Goal', value: TargetModel.StrongholdHighGoal },
{ name: '2019 Dual Target', value: TargetModel.DeepSpaceDualTarget },
{ name: '2020 High Goal Outer', value: TargetModel.InfiniteRechargeHighGoalOuter },
{ name: '2020 Power Cell (7in)', value: TargetModel.CircularPowerCell7in },
{ name: '2022 Cargo Ball (9.5in)', value: TargetModel.RapidReactCircularCargoBall },
{ name: '2023 AprilTag 6in (16h5)', value: TargetModel.AprilTag6in_16h5 },
{ name: '2024 AprilTag 6.5in (36h11)', value: TargetModel.AprilTag6p5in_36h11 }
]"
:select-cols="interactiveCols"
@input="(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ targetModel: value }, false)"
/>
<pv-slider
v-model="useCameraSettingsStore().currentPipelineSettings.cornerDetectionAccuracyPercentage"
class="pt-2"
:slider-cols="interactiveCols"
label="Contour simplification Percentage"
:min="0"
:max="100"
@input="
(value) =>
useCameraSettingsStore().changeCurrentPipelineSetting({ cornerDetectionAccuracyPercentage: value }, false)
"
/>
</div>
</template>

View File

@@ -1,314 +0,0 @@
<script setup lang="ts">
import { useCameraSettingsStore } from "@/stores/settings/CameraSettingsStore";
import { type ActivePipelineSettings, PipelineType } from "@/types/PipelineTypes";
import { useStateStore } from "@/stores/StateStore";
import { angleModulus, toDeg } from "@/lib/MathUtils";
import { computed } from "vue";
// 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 calculateStdDev = (values: number[]): number => {
if (values.length < 2) return 0;
// Use mean of cosine/sine components to handle angle wrapping
const cosines = values.map((it) => Math.cos(it));
const sines = values.map((it) => Math.sin(it));
const cosmean = cosines.reduce((sum, number) => sum + number, 0) / values.length;
const sinmean = sines.reduce((sum, number) => sum + number, 0) / values.length;
// Borrowed from WPILib's Rotation2d
const hypot = Math.hypot(cosmean, sinmean);
const mean = hypot > 1e-6 ? Math.atan2(sinmean / hypot, cosmean / hypot) : 0;
return Math.sqrt(values.map((x) => Math.pow(angleModulus(x - mean), 2)).reduce((a, b) => a + b) / values.length);
};
const resetCurrentBuffer = () => {
// Need to clear the array in place
while (useStateStore().currentMultitagBuffer?.length != 0) useStateStore().currentMultitagBuffer?.pop();
};
</script>
<template>
<div>
<v-row align="start" class="pb-4">
<v-simple-table dense class="pt-2 pb-12">
<template #default>
<thead>
<tr>
<th
v-if="
currentPipelineSettings.pipelineType === PipelineType.AprilTag ||
currentPipelineSettings.pipelineType === PipelineType.Aruco
"
class="text-center white--text"
>
Fiducial ID
</th>
<template v-if="currentPipelineSettings.pipelineType === PipelineType.ObjectDetection">
<th class="text-center white--text">Class</th>
<th class="text-center white--text">Confidence</th>
</template>
<template v-if="!useCameraSettingsStore().currentPipelineSettings.solvePNPEnabled">
<th class="text-center white--text">Pitch &theta;&deg;</th>
<th class="text-center white--text">Yaw &theta;&deg;</th>
<th class="text-center white--text">Skew &theta;&deg;</th>
<th class="text-center white--text">Area %</th>
</template>
<template v-else>
<th class="text-center white--text">X meters</th>
<th class="text-center white--text">Y meters</th>
<th class="text-center white--text">Z Angle &theta;&deg;</th>
</template>
<template
v-if="
(currentPipelineSettings.pipelineType === PipelineType.AprilTag ||
currentPipelineSettings.pipelineType === PipelineType.Aruco) &&
useCameraSettingsStore().currentPipelineSettings.solvePNPEnabled
"
>
<th class="text-center white--text">Ambiguity Ratio</th>
</template>
</tr>
</thead>
<tbody>
<tr
v-for="(target, index) in useStateStore().currentPipelineResults?.targets"
:key="index"
class="white--text"
>
<td
v-if="
currentPipelineSettings.pipelineType === PipelineType.AprilTag ||
currentPipelineSettings.pipelineType === PipelineType.Aruco
"
class="text-center"
>
{{ target.fiducialId }}
</td>
<td
v-if="currentPipelineSettings.pipelineType === PipelineType.ObjectDetection"
class="text-center white--text"
>
{{ useStateStore().currentPipelineResults?.classNames[target.classId] }}
</td>
<td
v-if="currentPipelineSettings.pipelineType === PipelineType.ObjectDetection"
class="text-center white--text"
>
{{ target.confidence.toFixed(2) }}
</td>
<template v-if="!useCameraSettingsStore().currentPipelineSettings.solvePNPEnabled">
<td class="text-center">{{ target.pitch.toFixed(2) }}&deg;</td>
<td class="text-center">{{ target.yaw.toFixed(2) }}&deg;</td>
<td class="text-center">{{ target.skew.toFixed(2) }}&deg;</td>
<td class="text-center">{{ target.area.toFixed(2) }}&deg;</td>
</template>
<template v-else>
<td class="text-center">{{ target.pose?.x.toFixed(2) }}&nbsp;m</td>
<td class="text-center">{{ target.pose?.y.toFixed(2) }}&nbsp;m</td>
<td class="text-center">{{ toDeg(target.pose?.angle_z || 0).toFixed(2) }}&deg;</td>
</template>
<template
v-if="
(currentPipelineSettings.pipelineType === PipelineType.AprilTag ||
currentPipelineSettings.pipelineType === PipelineType.Aruco) &&
useCameraSettingsStore().currentPipelineSettings.solvePNPEnabled
"
>
<td class="text-center">
{{ target.ambiguity >= 0 ? target.ambiguity.toFixed(2) : "(In Multi-Target)" }}
</td>
</template>
</tr>
</tbody>
</template>
</v-simple-table>
</v-row>
<v-container
v-if="
(currentPipelineSettings.pipelineType === PipelineType.AprilTag ||
currentPipelineSettings.pipelineType === PipelineType.Aruco) &&
currentPipelineSettings.doMultiTarget &&
useCameraSettingsStore().isCurrentVideoFormatCalibrated &&
useCameraSettingsStore().currentPipelineSettings.solvePNPEnabled
"
>
<v-row class="pb-4 white--text">
<v-card-subtitle class="ma-0 pa-0 pb-4" style="font-size: 16px"
>Multi-tag pose, field-to-camera</v-card-subtitle
>
<v-simple-table dense>
<template #default>
<thead>
<tr class="white--text">
<th class="text-center white--text">X meters</th>
<th class="text-center white--text">Y meters</th>
<th class="text-center white--text">Z meters</th>
<th class="text-center white--text">X Angle &theta;&deg;</th>
<th class="text-center white--text">Y Angle &theta;&deg;</th>
<th class="text-center white--text">Z Angle &theta;&deg;</th>
<th class="text-center white--text">Tags</th>
</tr>
</thead>
<tbody v-show="useStateStore().currentPipelineResults?.multitagResult">
<tr>
<td class="text-center white--text">
{{ useStateStore().currentPipelineResults?.multitagResult?.bestTransform.x.toFixed(2) }}&nbsp;m
</td>
<td class="text-center white--text">
{{ useStateStore().currentPipelineResults?.multitagResult?.bestTransform.y.toFixed(2) }}&nbsp;m
</td>
<td class="text-center white--text">
{{ useStateStore().currentPipelineResults?.multitagResult?.bestTransform.z.toFixed(2) }}&nbsp;m
</td>
<td class="text-center white--text">
{{
toDeg(useStateStore().currentPipelineResults?.multitagResult?.bestTransform.angle_x || 0).toFixed(
2
)
}}&deg;
</td>
<td class="text-center white--text">
{{
toDeg(useStateStore().currentPipelineResults?.multitagResult?.bestTransform.angle_y || 0).toFixed(
2
)
}}&deg;
</td>
<td class="text-center white--text">
{{
toDeg(useStateStore().currentPipelineResults?.multitagResult?.bestTransform.angle_z || 0).toFixed(
2
)
}}&deg;
</td>
<td class="text-center white--text">
{{ useStateStore().currentPipelineResults?.multitagResult?.fiducialIDsUsed }}
</td>
</tr>
</tbody>
</template>
</v-simple-table>
</v-row>
<v-row class="pb-4 white--text" style="display: flex; flex-direction: column">
<v-card-subtitle class="ma-0 pa-0 pb-4 pr-4" style="font-size: 16px"
>Multi-tag pose standard deviation over the last
{{ useStateStore().currentMultitagBuffer?.length || "NaN" }}/100 samples
</v-card-subtitle>
<v-btn color="secondary" class="mb-4 mt-1" style="width: min-content" depressed @click="resetCurrentBuffer"
>Reset Samples</v-btn
>
<v-simple-table dense>
<template #default>
<thead>
<tr>
<th class="text-center white--text">X meters</th>
<th class="text-center white--text">Y meters</th>
<th class="text-center white--text">Z meters</th>
<th class="text-center white--text">X Angle &theta;&deg;</th>
<th class="text-center white--text">Y Angle &theta;&deg;</th>
<th class="text-center white--text">Z Angle &theta;&deg;</th>
</tr>
</thead>
<tbody v-show="useStateStore().currentPipelineResults?.multitagResult">
<tr>
<td class="text-center white--text">
{{
calculateStdDev(useStateStore().currentMultitagBuffer?.map((v) => v.bestTransform.x) || []).toFixed(
5
)
}}&nbsp;m
</td>
<td class="text-center white--text">
{{
calculateStdDev(useStateStore().currentMultitagBuffer?.map((v) => v.bestTransform.y) || []).toFixed(
5
)
}}&nbsp;m
</td>
<td class="text-center white--text">
{{
calculateStdDev(useStateStore().currentMultitagBuffer?.map((v) => v.bestTransform.z) || []).toFixed(
5
)
}}&nbsp;m
</td>
<td class="text-center white--text">
{{
calculateStdDev(
useStateStore().currentMultitagBuffer?.map((v) => toDeg(v.bestTransform.angle_x)) || []
).toFixed(5)
}}&deg;
</td>
<td class="text-center white--text">
{{
calculateStdDev(
useStateStore().currentMultitagBuffer?.map((v) => toDeg(v.bestTransform.angle_y)) || []
).toFixed(5)
}}&deg;
</td>
<td class="text-center white--text">
{{
calculateStdDev(
useStateStore().currentMultitagBuffer?.map((v) => toDeg(v.bestTransform.angle_z)) || []
).toFixed(5)
}}&deg;
</td>
</tr>
</tbody>
</template>
</v-simple-table>
</v-row>
</v-container>
</div>
</template>
<style scoped lang="scss">
.v-data-table {
background-color: #006492 !important;
width: 100%;
font-size: 1rem !important;
thead {
tr {
th {
font-size: 1rem !important;
color: white !important;
}
}
}
tbody {
:hover {
td {
background-color: #005281 !important;
}
}
tr {
td {
font-size: 1rem !important;
color: white !important;
}
}
}
::-webkit-scrollbar {
width: 0;
height: 0.55em;
border-radius: 5px;
}
::-webkit-scrollbar-track {
-webkit-box-shadow: inset 0 0 6px rgba(0, 0, 0, 0.3);
border-radius: 10px;
}
::-webkit-scrollbar-thumb {
background-color: #ffd843;
border-radius: 10px;
}
}
</style>

View File

@@ -1,248 +0,0 @@
<script setup lang="ts">
import { useCameraSettingsStore } from "@/stores/settings/CameraSettingsStore";
import { computed, getCurrentInstance, onBeforeUnmount, onMounted } from "vue";
import PvRangeSlider from "@/components/common/pv-range-slider.vue";
import PvSwitch from "@/components/common/pv-switch.vue";
import { useStateStore } from "@/stores/StateStore";
import { ColorPicker, type HSV } from "@/lib/ColorPicker";
const averageHue = computed<number>(() => {
const isHueInverted = useCameraSettingsStore().currentPipelineSettings.hueInverted;
let val = Object.values(useCameraSettingsStore().currentPipelineSettings.hsvHue).reduce((a, b) => a + b, 0);
if (isHueInverted) val += 180;
if (val > 360) val -= 360;
return val;
});
// TODO fix pv-range-slider so that store access doesn't need to be deferred
const hsvHue = computed<[number, number]>({
get: () => Object.values(useCameraSettingsStore().currentPipelineSettings.hsvHue) as [number, number],
set: (v) => (useCameraSettingsStore().currentPipelineSettings.hsvHue = v)
});
const hsvSaturation = computed<[number, number]>({
get: () => Object.values(useCameraSettingsStore().currentPipelineSettings.hsvSaturation) as [number, number],
set: (v) => (useCameraSettingsStore().currentPipelineSettings.hsvSaturation = v)
});
const hsvValue = computed<[number, number]>({
get: () => Object.values(useCameraSettingsStore().currentPipelineSettings.hsvValue) as [number, number],
set: (v) => (useCameraSettingsStore().currentPipelineSettings.hsvValue = v)
});
let selectedEventMode: 0 | 1 | 2 | 3 = 0;
const handleStreamClick = (event: MouseEvent) => {
if (!useStateStore().colorPickingMode || selectedEventMode === 0) return;
const cameraStream = document.getElementById("input-camera-stream");
if (cameraStream === null) return;
const canvas = document.createElement("canvas");
canvas.width = cameraStream.clientWidth;
canvas.height = cameraStream.clientHeight;
// Get the (x, y) position of the click with (0, 0) in the top left corner
const rect = cameraStream.getBoundingClientRect();
const x = Math.round(((event.clientX - rect.left) / rect.width) * cameraStream.clientWidth);
const y = Math.round(((event.clientY - rect.top) / rect.height) * cameraStream.clientHeight);
const context = canvas.getContext("2d");
if (context === null) return;
context.drawImage(cameraStream as CanvasImageSource, 0, 0, cameraStream.clientWidth, cameraStream.clientHeight);
const colorPicker = new ColorPicker(context.getImageData(x, y, 1, 1).data);
// Calculate HSV values based on the mode
let selectedHSVData: [HSV, HSV] = [
[0, 0, 0],
[0, 0, 0]
];
if (selectedEventMode === 1) {
selectedHSVData = colorPicker.selectedColorRange();
} else {
const currentHue = Object.values(useCameraSettingsStore().currentPipelineSettings.hsvHue);
const currentSaturation = Object.values(useCameraSettingsStore().currentPipelineSettings.hsvSaturation);
const currentValue = Object.values(useCameraSettingsStore().currentPipelineSettings.hsvValue);
const currentData: [HSV, HSV] = [
[currentHue[0], currentSaturation[0], currentValue[0]],
[currentHue[1], currentSaturation[1], currentValue[1]]
];
if (selectedEventMode === 2) {
selectedHSVData = colorPicker.expandColorRange(currentData);
} else if (selectedEventMode === 3) {
selectedHSVData = colorPicker.shrinkColorRange(currentData);
}
}
// Update the store and backend with the new HSV values
useCameraSettingsStore().changeCurrentPipelineSetting(
{
hsvHue: [selectedHSVData[0][0], selectedHSVData[1][0]],
hsvSaturation: [selectedHSVData[0][1], selectedHSVData[1][1]],
hsvValue: [selectedHSVData[0][2], selectedHSVData[1][2]]
},
true
);
disableColorPicking();
};
// Put some default values in case color picking was enabled before the enableColorPicking method is called
let inputShowing = true;
let outputShowing = false;
const enableColorPicking = (mode: 1 | 2 | 3) => {
useStateStore().colorPickingMode = true;
inputShowing = useCameraSettingsStore().currentPipelineSettings.inputShouldShow;
outputShowing = useCameraSettingsStore().currentPipelineSettings.outputShouldShow;
useCameraSettingsStore().changeCurrentPipelineSetting(
{ outputShouldDraw: false, inputShouldShow: true, outputShouldShow: false },
true
);
selectedEventMode = mode;
};
const disableColorPicking = () => {
useStateStore().colorPickingMode = false;
useCameraSettingsStore().changeCurrentPipelineSetting(
{ outputShouldDraw: true, inputShouldShow: inputShowing, outputShouldShow: outputShowing },
true
);
selectedEventMode = 0;
};
onMounted(() => {
const cameraStream = document.getElementById("input-camera-stream");
if (cameraStream === null) return;
cameraStream.addEventListener("click", handleStreamClick);
});
onBeforeUnmount(() => {
const cameraStream = document.getElementById("input-camera-stream");
if (cameraStream === null) return;
cameraStream.removeEventListener("click", handleStreamClick);
});
const interactiveCols = computed(() =>
(getCurrentInstance()?.proxy.$vuetify.breakpoint.mdAndDown || false) &&
(!useStateStore().sidebarFolded || useCameraSettingsStore().isDriverMode)
? 9
: 8
);
</script>
<template>
<div class="threshold-modifiers" :style="{ '--averageHue': averageHue }">
<pv-range-slider
id="hue-slider"
v-model="hsvHue"
:class="useCameraSettingsStore().currentPipelineSettings.hueInverted ? 'inverted-slider' : 'normal-slider'"
label="Hue"
tooltip="Describes color"
:min="0"
:max="180"
:slider-cols="interactiveCols"
:inverted="useCameraSettingsStore().currentPipelineSettings.hueInverted"
@input="(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ hsvHue: value }, false)"
/>
<pv-range-slider
id="sat-slider"
v-model="hsvSaturation"
class="normal-slider"
label="Saturation"
tooltip="Describes colorfulness; the smaller this value the 'whiter' the color becomes"
:min="0"
:max="255"
:slider-cols="interactiveCols"
@input="(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ hsvSaturation: value }, false)"
/>
<pv-range-slider
id="value-slider"
v-model="hsvValue"
class="normal-slider"
label="Value"
tooltip="Describes lightness; the smaller this value the 'blacker' the color becomes"
:min="0"
:max="255"
:slider-cols="interactiveCols"
@input="(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ hsvValue: value }, false)"
/>
<pv-switch
v-model="useCameraSettingsStore().currentPipelineSettings.hueInverted"
label="Invert Hue"
:switch-cols="interactiveCols"
tooltip="Selects the hue range outside of the hue slider bounds instead of inside"
@input="(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ hueInverted: value }, false)"
/>
<v-divider class="mt-3" />
<div>
<div class="pt-3 white--text">Color Picker</div>
<v-row justify="center" class="mt-3 mb-3">
<template v-if="!useStateStore().colorPickingMode">
<v-btn
color="accent"
class="ma-2 black--text"
small
@click="enableColorPicking(useCameraSettingsStore().currentPipelineSettings.hueInverted ? 2 : 3)"
>
<v-icon left> mdi-minus </v-icon>
Shrink Range
</v-btn>
<v-btn color="accent" class="ma-2 black--text" small @click="enableColorPicking(1)">
<v-icon left> mdi-plus-minus </v-icon>
{{ useCameraSettingsStore().currentPipelineSettings.hueInverted ? "Exclude" : "Set to" }} Average
</v-btn>
<v-btn
color="accent"
class="ma-2 black--text"
small
@click="enableColorPicking(useCameraSettingsStore().currentPipelineSettings.hueInverted ? 3 : 2)"
>
<v-icon left> mdi-plus </v-icon>
Expand Range
</v-btn>
</template>
<template v-else>
<v-btn color="accent" class="ma-2 black--text" style="width: 30%" small @click="disableColorPicking">
Cancel
</v-btn>
</template>
</v-row>
</div>
</div>
</template>
<style scoped lang="css">
.threshold-modifiers {
--averageHue: 0;
}
#hue-slider >>> .v-slider {
background: linear-gradient(to right, #f00 0%, #ff0 17%, #0f0 33%, #0ff 50%, #00f 67%, #f0f 83%, #f00 100%);
border-radius: 10px;
/* prettier-ignore */
box-shadow: 0 0 5px #333, inset 0 0 3px #333;
}
#sat-slider >>> .v-slider {
background: linear-gradient(to right, #fff 0%, hsl(var(--averageHue), 100%, 50%) 100%);
border-radius: 10px;
/* prettier-ignore */
box-shadow: 0 0 5px #333, inset 0 0 3px #333;
}
#value-slider >>> .v-slider {
background: linear-gradient(to right, #000 0%, hsl(var(--averageHue), 100%, 50%) 100%);
border-radius: 10px;
/* prettier-ignore */
box-shadow: 0 0 5px #333, inset 0 0 3px #333;
}
>>> .v-slider__thumb {
outline: black solid thin;
}
.normal-slider >>> .v-slider__track-fill {
outline: black solid thin;
}
.inverted-slider >>> .v-slider__track-background {
outline: black solid thin;
}
</style>

View File

@@ -0,0 +1,154 @@
<template>
<div>
<v-row>
<v-col
align="center"
cols="12"
>
<span class="white--text">Target Location</span>
<canvas
id="canvasId"
class="mt-2"
width="800"
height="800"
/>
</v-col>
</v-row>
</div>
</template>
<script>
import theme from "../../../theme";
export default {
name: "MiniMap",
props: {
// eslint-disable-next-line vue/require-default-prop
targets: Array,
// eslint-disable-next-line vue/require-default-prop
horizontalFOV: Number
},
data() {
return {
ctx: undefined,
canvas: undefined,
x: 0,
y: 0,
targetWidth: 40,
targetHeight: 6
}
},
computed: {
hLen: {
get() {
return Math.tan(this.horizontalFOV / 2 * Math.PI / 180) * 150;
}
}
},
watch: {
targets: {
deep: true,
handler() {
this.draw();
}
},
horizontalFOV() {
this.draw();
}
},
mounted: function () {
const canvas = document.getElementById("canvasId"); // getting the canvas element
const ctx = canvas.getContext("2d"); // getting the canvas context
this.canvas = canvas; // setting the canvas as a vue variable
this.ctx = ctx; // setting the canvas context as a vue variable
this.grad = this.ctx.createLinearGradient(400, 800, 400, 600);
this.grad.addColorStop(0, "rgb(119,119,119)");
this.grad.addColorStop(0.05, "rgba(14,92,22,0.96)");
this.grad.addColorStop(0.8, 'rgba(43,43,43,0.48)');
// setting canvas context values for drawing
this.ctx.font = "26px Arial";
this.ctx.strokeStyle = "whitesmoke";
this.ctx.lineWidth = 2;
this.$nextTick(function () {
this.drawPlayer();
});
},
methods: {
draw() {
this.clearBoard();
this.drawPlayer();
for (let index in this.targets) {
this.drawTarget(index, this.targets[index].pose);
}
},
drawTarget(index, target) {
// first save the untranslated/unrotated context
let x = 800 - (160 * target.x); // getting meters as pixels
let y = 400 - (160 * target.y);
this.ctx.save();
this.ctx.beginPath();
// move the rotation point to the center of the rect
this.ctx.translate(y + this.targetWidth / 2, x + this.targetHeight / 2); // wpi lib makes x forward and back and y left to right
// rotate the rect
this.ctx.rotate(target.rot * -1 * Math.PI / 180.0);
// draw the rect on the transformed context
// Note: after transforming [0,0] is visually [x,y]
// so the rect needs to be offset accordingly when drawn
this.ctx.rect(-this.targetWidth / 2, -this.targetHeight / 2, this.targetWidth, this.targetHeight);
this.ctx.fillStyle = theme.accent;
this.ctx.fill();
// restore the context to its untranslated/unrotated state
this.ctx.restore();
this.ctx.fillStyle = "whitesmoke";
this.ctx.beginPath();
this.ctx.arc(y + this.targetWidth / 2, x + this.targetHeight / 2, 3, 0, 2 * Math.PI, true);
this.ctx.fill();
this.ctx.fillText(index, y - 30, x - 5);
},
drawPlayer() {
this.ctx.beginPath();
this.ctx.moveTo(400, 820);
this.ctx.lineTo(400 + this.hLen, 650);
this.ctx.lineTo(400 - this.hLen, 650);
this.ctx.closePath();
this.ctx.fillStyle = this.grad;
this.ctx.fill();
this.ctx.beginPath();
this.ctx.moveTo(400, 820);
this.ctx.lineTo(400 + this.hLen, 650);
this.ctx.stroke();
this.ctx.moveTo(400, 820);
this.ctx.lineTo(400 - this.hLen, 650);
this.ctx.stroke();
},
clearBoard() {
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height); // clearing the canvas
}
}
}
</script>
<style scoped>
#canvasId {
width: 400px;
height: 400px;
background-color: #232C37;
border-radius: 5px;
border: 2px solid grey;
box-shadow: 0 0 5px 1px;
}
th {
width: 80px;
text-align: center;
}
</style>

View File

@@ -0,0 +1,408 @@
<template>
<div>
<v-row align="center">
<v-col
cols="10"
md="5"
lg="10"
class="pt-0 pb-0 pl-6"
>
<CVselect
v-if="isCameraNameEdit === false"
v-model="currentCameraIndex"
name="Camera"
:list="$store.getters.cameraList"
@input="handleInput('currentCamera',currentCameraIndex)"
/>
<CVinput
v-else
v-model="newCameraName"
name="Camera"
input-cols="9"
:error-message="checkCameraName"
@Enter="saveCameraNameChange"
/>
</v-col>
<v-col
cols="2"
md="1"
lg="2"
>
<CVicon
v-if="isCameraNameEdit === false"
color="#c5c5c5"
:hover="true"
text="edit"
tooltip="Edit camera name"
@click="changeCameraName"
/>
<div v-else>
<CVicon
color="#c5c5c5"
style="display: inline-block;"
:hover="true"
text="save"
tooltip="Save Camera Name"
@click="saveCameraNameChange"
/>
<CVicon
color="error"
style="display: inline-block;"
:hover="true"
text="close"
tooltip="Discard Changes"
@click="discardCameraNameChange"
/>
</div>
</v-col>
<v-col
cols="10"
md="5"
lg="10"
class="pt-0 pb-0 pl-6"
>
<CVselect
v-model="currentPipelineIndex"
name="Pipeline"
tooltip="Each pipeline runs on a camera output and stores a unique set of processing settings"
:disabled="$store.getters.isDriverMode"
:list="($store.getters.isDriverMode ? ['Driver Mode'] : []).concat($store.getters.pipelineList)"
@input="handleInputWithIndex('currentPipeline', currentPipelineIndex)"
/>
</v-col>
<v-col
cols="2"
md="1"
lg="2"
>
<v-menu
v-if="!$store.getters.isDriverMode"
offset-y
auto
>
<template v-slot:activator="{ on }">
<v-icon
color="#c5c5c5"
v-on="on"
>
menu
</v-icon>
</template>
<v-list
dark
dense
color="primary"
>
<v-list-item @click="toPipelineNameChange">
<v-list-item-title>
<CVicon
color="#c5c5c5"
:right="true"
text="edit"
tooltip="Edit pipeline name"
/>
</v-list-item-title>
</v-list-item>
<v-list-item @click="toCreatePipeline">
<v-list-item-title>
<CVicon
color="#c5c5c5"
:right="true"
text="add"
tooltip="Add new pipeline"
/>
</v-list-item-title>
</v-list-item>
<v-list-item @click="deleteCurrentPipeline">
<v-list-item-title>
<CVicon
color="red darken-2"
:right="true"
text="delete"
tooltip="Delete pipeline"
/>
</v-list-item-title>
</v-list-item>
<v-list-item @click="duplicatePipeline">
<v-list-item-title>
<CVicon
color="#c5c5c5"
:right="true"
text="mdi-content-copy"
tooltip="Duplicate pipeline"
/>
</v-list-item-title>
</v-list-item>
</v-list>
</v-menu>
</v-col>
<v-col
v-if="currentPipelineType >= 0"
cols="10"
md="5"
lg="10"
class="pt-0 pb-0 pl-6 ml-16"
>
<CVselect
v-model="currentPipelineType"
name="Type"
:list="['Reflective', 'Shape']"
@input="e => showTypeDialog(e)"
/>
</v-col>
</v-row>
<!--pipeline naming dialog-->
<v-dialog
v-model="namingDialog"
dark
persistent
width="500"
height="357"
>
<v-card
dark
color="primary"
>
<v-card-title
class="headline"
primary-title
>
{{ isPipelineNameEdit ? "Edit Pipeline Name" : "Create Pipeline" }}
</v-card-title>
<v-card-text>
<CVinput
v-model="newPipelineName"
name="Pipeline"
:error-message="checkPipelineName"
/>
<CVselect
v-model="newPipelineType"
name="Pipeline Type"
:list="['Reflective', 'Shape']"
:disabled="isPipelineNameEdit"
/>
</v-card-text>
<v-divider />
<v-card-actions>
<v-spacer />
<v-btn
color="#ffd843"
:disabled="checkPipelineName !==''"
@click="savePipelineNameChange"
>
Save
</v-btn>
<v-btn
color="error"
@click="discardPipelineNameChange"
>
Cancel
</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
<v-dialog
v-model="showPipeTypeDialog"
width="600"
>
<v-card
color="primary"
dark
>
<v-card-title>Change Pipeline Type</v-card-title>
<v-card-text>
Changing the type of this pipeline will erase the current pipeline's settings and replace it with a new {{ ['Reflective', 'Shape'][proposedPipelineType] }} pipeline. <b class="red--text format_bold">You will lose all settings for the pipeline
"{{ ($store.getters.isDriverMode ? ['Driver Mode'] : []).concat($store.getters.pipelineList)[currentPipelineIndex] }}."</b> Are you sure you want to do this?
<v-row
class="mt-6"
style="display: flex; align-items: center; justify-content: center"
align="center"
>
<v-btn
class="mr-3"
color="red"
width="250"
@click="e => changePipeType(true)"
>
Yes, replace this pipeline
</v-btn>
<v-btn
class="ml-10"
color="secondary"
width="250"
@click="e => changePipeType(false)"
>
No, take me back
</v-btn>
</v-row>
</v-card-text>
</v-card>
</v-dialog>
</div>
</template>
<script>
import CVicon from '../common/cv-icon'
import CVselect from '../common/cv-select'
import CVinput from '../common/cv-input'
export default {
name: "CameraAndPipelineSelect",
components: {
CVicon,
CVselect,
CVinput
},
data: () => {
return {
re: RegExp("^[A-Za-z0-9 \\-)(]*[A-Za-z0-9][A-Za-z0-9 \\-)(.]*$"),
isCameraNameEdit: false,
newCameraName: "",
cameraNameError: "",
isPipelineNameEdit: false,
namingDialog: false,
newPipelineName: "",
newPipelineType: 0,
duplicateDialog: false,
showPipeTypeDialog: false,
proposedPipelineType : 0,
pipeIndexToDuplicate: undefined
}
},
computed: {
checkCameraName() {
if (this.newCameraName !== this.$store.getters.cameraList[this.currentCameraIndex]) {
if (this.re.test(this.newCameraName)) {
for (let cam in this.cameraList) {
if (this.cameraList.hasOwnProperty(cam)) {
if (this.newCameraName === this.cameraList[cam]) {
return "A camera by that name already Exists"
}
}
}
} else {
return "A camera name can only contain letters, numbers and spaces"
}
}
return "";
},
checkPipelineName() {
if (this.newPipelineName !== this.$store.getters.pipelineList[this.currentPipelineIndex - 1] || this.isPipelineNameEdit === false) {
if (this.re.test(this.newPipelineName)) {
for (let pipe in this.$store.getters.pipelineList) {
if (this.$store.getters.pipelineList.hasOwnProperty(pipe)) {
if (this.newPipelineName === this.$store.getters.pipelineList[pipe]) {
return "A pipeline with this name already exists"
}
}
}
} else {
return "A pipeline name can only contain letters, numbers, and spaces"
}
}
return ""
},
currentCameraIndex: {
get() {
return this.$store.getters.currentCameraIndex;
},
set(value) {
this.$store.commit('currentCameraIndex', value);
}
},
currentPipelineIndex: {
get() {
return this.$store.getters.currentPipelineIndex + (this.$store.getters.isDriverMode ? 1 : 0);
},
set(value) {
this.$store.commit('currentPipelineIndex', value - (this.$store.getters.isDriverMode ? 1 : 0));
}
},
currentPipelineType: {
get() {
return this.$store.getters.currentPipelineSettings.pipelineType - 2;
},
set(value) {
value; // nop, since we have the dialog for this
}
}
},
methods: {
showTypeDialog(idx) {
// Only show the dialog if it's a new type
this.showPipeTypeDialog = idx !== this.currentPipelineType;
this.proposedPipelineType = idx;
},
changePipeType(actuallyChange) {
const newIdx = actuallyChange ? this.proposedPipelineType : this.currentPipelineType
this.handleInputWithIndex('pipelineType', newIdx);
this.showPipeTypeDialog = false;
},
changeCameraName() {
this.newCameraName = this.$store.getters.cameraList[this.currentCameraIndex];
this.isCameraNameEdit = true;
},
saveCameraNameChange() {
if (this.checkCameraName === "") {
// this.handleInputWithIndex("changeCameraName", this.newCameraName);
this.axios.post('http://' + this.$address + '/api/setCameraNickname',
{name: this.newCameraName, cameraIndex: this.$store.getters.currentCameraIndex})
// eslint-disable-next-line
.then(r => {
this.$emit('camera-name-changed')
})
.catch(e => {
console.log("HTTP error while changing camera name " + e);
this.$emit('camera-name-changed')
})
this.discardCameraNameChange();
}
},
discardCameraNameChange() {
this.isCameraNameEdit = false;
this.newCameraName = "";
},
toPipelineNameChange() {
this.newPipelineName = this.$store.getters.pipelineList[this.currentPipelineIndex - 1];
this.isPipelineNameEdit = true;
this.namingDialog = true;
},
toCreatePipeline() {
this.newPipelineName = "New Pipeline";
this.isPipelineNameEdit = false;
this.namingDialog = true;
},
deleteCurrentPipeline() {
if (this.$store.getters.pipelineList.length > 1) {
this.handleInputWithIndex('deleteCurrentPipeline', {});
} else {
this.snackbar = true;
}
},
savePipelineNameChange() {
if (this.checkPipelineName === "") {
if (this.isPipelineNameEdit) {
this.handleInputWithIndex("changePipelineName", this.newPipelineName);
} else {
this.handleInputWithIndex("addNewPipeline", [this.newPipelineName, this.newPipelineType]); // 0 for reflective, 1 for colored shpae
}
this.discardPipelineNameChange();
}
},
duplicatePipeline() {
this.handleInputWithIndex("duplicatePipeline", this.currentPipelineIndex);
},
discardPipelineNameChange() {
this.namingDialog = false;
this.isPipelineNameEdit = false;
this.newPipelineName = "";
},
}
}
</script>
<style scoped>
</style>

View File

@@ -0,0 +1,63 @@
<template>
<div>
<v-row
align="center"
justify="start"
>
<v-col cols="4">
<v-btn
small
color="accent"
style="width: 100%;"
class="black--text"
@click="takePointA"
>
Take Point A
</v-btn>
</v-col>
<v-col cols="4">
<v-btn
small
color="accent"
style="width: 100%;"
class="black--text"
@click="takePointB"
>
Take Point B
</v-btn>
</v-col>
<v-col cols="4">
<v-btn
small
color="yellow darken-3"
style="width: 100%;"
@click="clearPoints"
>
Clear All Points
</v-btn>
</v-col>
</v-row>
</div>
</template>
<script>
export default {
name: "DualCalibration",
methods: {
clearPoints() {
this.handleInputWithIndex("robotOffsetPoint", 0, this.$store.state.currentCameraIndex)
},
takePointA() {
this.handleInputWithIndex("robotOffsetPoint", 2, this.$store.state.currentCameraIndex)
},
takePointB() {
this.handleInputWithIndex("robotOffsetPoint", 3, this.$store.state.currentCameraIndex)
}
}
}
</script>
<style scoped>
</style>

View File

@@ -0,0 +1,48 @@
<template>
<div>
<v-row
align="center"
justify="start"
>
<v-col cols="6">
<v-btn
small
color="accent"
class="black--text"
style="width: 100%;"
@click="takePoint"
>
Take Point
</v-btn>
</v-col>
<v-col cols="6">
<v-btn
small
color="yellow darken-3"
style="width: 100%;"
@click="clearPoint"
>
Clear Point
</v-btn>
</v-col>
</v-row>
</div>
</template>
<script>
export default {
name: "SingleCalibration",
methods: {
clearPoint() {
this.handleInputWithIndex("robotOffsetPoint", 0, this.$store.state.currentCameraIndex)
},
takePoint() {
this.handleInputWithIndex("robotOffsetPoint", 1, this.$store.state.currentCameraIndex)
}
}
}
</script>
<style scoped>
</style>

View File

@@ -1,93 +0,0 @@
<script setup lang="ts">
import { useSettingsStore } from "@/stores/settings/GeneralSettingsStore";
import { Euler, Quaternion as ThreeQuat } from "three";
import type { Quaternion } from "@/types/PhotonTrackingTypes";
import { toDeg } from "@/lib/MathUtils";
const quaternionToEuler = (rot_quat: Quaternion): { x: number; y: number; z: number } => {
const quat = new ThreeQuat(rot_quat.X, rot_quat.Y, rot_quat.Z, rot_quat.W);
const euler = new Euler().setFromQuaternion(quat, "ZYX");
return {
x: toDeg(euler.x),
y: toDeg(euler.y),
z: toDeg(euler.z)
};
};
</script>
<template>
<v-card dark class="pr-6 pb-3" style="background-color: #006492">
<v-card-title>AprilTag Field Layout</v-card-title>
<div class="ml-5">
<p>Field width: {{ useSettingsStore().currentFieldLayout.field.width.toFixed(2) }} meters</p>
<p>Field length: {{ useSettingsStore().currentFieldLayout.field.length.toFixed(2) }} meters</p>
<!-- Simple table height must be set here and in the CSS for the fixed-header to work -->
<v-simple-table fixed-header height="100%" dense dark>
<template #default>
<thead style="font-size: 1.25rem">
<tr>
<th class="text-center">ID</th>
<th class="text-center">X meters</th>
<th class="text-center">Y meters</th>
<th class="text-center">Z meters</th>
<th class="text-center">θ<sub>x</sub>&deg;</th>
<th class="text-center">θ<sub>y</sub>&deg;</th>
<th class="text-center">θ<sub>z</sub>&deg;</th>
</tr>
</thead>
<tbody>
<tr v-for="(tag, index) in useSettingsStore().currentFieldLayout.tags" :key="index">
<td>{{ tag.ID }}</td>
<td v-for="(val, idx) in Object.values(tag.pose.translation)" :key="idx">{{ val.toFixed(2) }}&nbsp;m</td>
<td v-for="(val, idx) in Object.values(quaternionToEuler(tag.pose.rotation.quaternion))" :key="idx + 4">
{{ val.toFixed(2) }}&deg;
</td>
</tr>
</tbody>
</template>
</v-simple-table>
</div>
</v-card>
</template>
<style scoped lang="scss">
.v-data-table {
width: 100%;
height: 100%;
text-align: center;
background-color: #006492 !important;
th,
td {
background-color: #006492 !important;
font-size: 1rem !important;
color: white !important;
}
td {
font-family: monospace !important;
}
tbody :hover td {
background-color: #005281 !important;
}
::-webkit-scrollbar {
width: 0;
height: 0.55em;
border-radius: 5px;
}
::-webkit-scrollbar-track {
-webkit-box-shadow: inset 0 0 6px rgba(0, 0, 0, 0.3);
border-radius: 10px;
}
::-webkit-scrollbar-thumb {
background-color: #ffd843;
border-radius: 10px;
}
}
</style>

View File

@@ -1,344 +0,0 @@
<script setup lang="ts">
import { inject, ref } from "vue";
import { useStateStore } from "@/stores/StateStore";
import PvSelect from "@/components/common/pv-select.vue";
import axios from "axios";
const restartProgram = () => {
axios
.post("/utils/restartProgram")
.then(() => {
useStateStore().showSnackbarMessage({
message: "Successfully sent program restart request",
color: "success"
});
})
.catch((error) => {
// This endpoint always return 204 regardless of outcome
if (error.request) {
useStateStore().showSnackbarMessage({
message: "Error while trying to process the request! The backend didn't respond.",
color: "error"
});
} else {
useStateStore().showSnackbarMessage({
message: "An error occurred while trying to process the request.",
color: "error"
});
}
});
};
const restartDevice = () => {
axios
.post("/utils/restartDevice")
.then(() => {
useStateStore().showSnackbarMessage({
message: "Successfully dispatched the restart command. It isn't confirmed if a device restart will occur.",
color: "success"
});
})
.catch((error) => {
if (error.response) {
useStateStore().showSnackbarMessage({
message: "The backend is unable to fulfil the request to restart the device.",
color: "error"
});
} else if (error.request) {
useStateStore().showSnackbarMessage({
message: "Error while trying to process the request! The backend didn't respond.",
color: "error"
});
} else {
useStateStore().showSnackbarMessage({
message: "An error occurred while trying to process the request.",
color: "error"
});
}
});
};
const address = inject<string>("backendHost");
const offlineUpdate = ref();
const openOfflineUpdatePrompt = () => {
offlineUpdate.value.click();
};
const handleOfflineUpdate = () => {
const files = offlineUpdate.value.files;
if (files.length === 0) return;
const formData = new FormData();
formData.append("jarData", files[0]);
useStateStore().showSnackbarMessage({
message: "New Software Upload in Progress...",
color: "secondary",
timeout: -1
});
axios
.post("/utils/offlineUpdate", formData, {
headers: { "Content-Type": "multipart/form-data" },
onUploadProgress: ({ progress }) => {
const uploadPercentage = (progress || 0) * 100.0;
if (uploadPercentage < 99.5) {
useStateStore().showSnackbarMessage({
message: "New Software Upload in Process, " + uploadPercentage.toFixed(2) + "% complete",
color: "secondary",
timeout: -1
});
} else {
useStateStore().showSnackbarMessage({
message: "Installing uploaded software...",
color: "secondary",
timeout: -1
});
}
}
})
.then((response) => {
useStateStore().showSnackbarMessage({
message: response.data.text || response.data,
color: "success"
});
})
.catch((error) => {
if (error.response) {
useStateStore().showSnackbarMessage({
color: "error",
message: error.response.data.text || error.response.data
});
} else if (error.request) {
useStateStore().showSnackbarMessage({
color: "error",
message: "Error while trying to process the request! The backend didn't respond."
});
} else {
useStateStore().showSnackbarMessage({
color: "error",
message: "An error occurred while trying to process the request."
});
}
});
};
const exportLogFile = ref();
const openExportLogsPrompt = () => {
exportLogFile.value.click();
};
const exportSettings = ref();
const openExportSettingsPrompt = () => {
exportSettings.value.click();
};
enum ImportType {
AllSettings,
HardwareConfig,
HardwareSettings,
NetworkConfig,
ApriltagFieldLayout
}
const showImportDialog = ref(false);
const importType = ref<ImportType | number>(-1);
const importFile = ref<File | null>(null);
const handleSettingsImport = () => {
if (importType.value === -1 || importFile.value === null) return;
const formData = new FormData();
formData.append("data", importFile.value);
let settingsEndpoint: string;
switch (importType.value) {
case ImportType.HardwareConfig:
settingsEndpoint = "/hardwareConfig";
break;
case ImportType.HardwareSettings:
settingsEndpoint = "/hardwareSettings";
break;
case ImportType.NetworkConfig:
settingsEndpoint = "/networkConfig";
break;
case ImportType.ApriltagFieldLayout:
settingsEndpoint = "/aprilTagFieldLayout";
break;
default:
case ImportType.AllSettings:
settingsEndpoint = "";
break;
}
axios
.post(`/settings${settingsEndpoint}`, formData, {
headers: { "Content-Type": "multipart/form-data" }
})
.then((response) => {
useStateStore().showSnackbarMessage({
message: response.data.text || response.data,
color: "success"
});
})
.catch((error) => {
if (error.response) {
useStateStore().showSnackbarMessage({
color: "error",
message: error.response.data.text || error.response.data
});
} else if (error.request) {
useStateStore().showSnackbarMessage({
color: "error",
message: "Error while trying to process the request! The backend didn't respond."
});
} else {
useStateStore().showSnackbarMessage({
color: "error",
message: "An error occurred while trying to process the request."
});
}
});
showImportDialog.value = false;
importType.value = -1;
importFile.value = null;
};
</script>
<template>
<v-card dark class="mb-3 pr-6 pb-3" style="background-color: #006492">
<v-card-title>Device Control</v-card-title>
<div class="ml-5">
<v-row>
<v-col cols="12" lg="4" md="6">
<v-btn color="red" @click="restartProgram">
<v-icon left class="open-icon"> mdi-restart </v-icon>
<span class="open-label">Restart PhotonVision</span>
</v-btn>
</v-col>
<v-col cols="12" lg="4" md="6">
<v-btn color="red" @click="restartDevice">
<v-icon left class="open-icon"> mdi-restart-alert </v-icon>
<span class="open-label">Restart Device</span>
</v-btn>
</v-col>
<v-col cols="12" lg="4">
<v-btn color="secondary" @click="openOfflineUpdatePrompt">
<v-icon left class="open-icon"> mdi-upload </v-icon>
<span class="open-label">Offline Update</span>
</v-btn>
<input ref="offlineUpdate" type="file" accept=".jar" style="display: none" @change="handleOfflineUpdate" />
</v-col>
</v-row>
<v-divider style="margin: 12px 0" />
<v-row>
<v-col cols="12" sm="6">
<v-btn color="secondary" @click="() => (showImportDialog = true)">
<v-icon left class="open-icon"> mdi-import </v-icon>
<span class="open-label">Import Settings</span>
</v-btn>
<v-dialog
v-model="showImportDialog"
width="600"
@input="
() => {
importType = -1;
importFile = null;
}
"
>
<v-card color="primary" dark>
<v-card-title>Import Settings</v-card-title>
<v-card-text>
Upload and apply previously saved or exported PhotonVision settings to this device
<v-row class="mt-6 ml-4">
<pv-select
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'
]"
:select-cols="10"
style="width: 100%"
/>
</v-row>
<v-row class="mt-6 ml-4 mr-8">
<v-file-input
v-model="importFile"
:disabled="importType === -1"
:error-messages="importType === -1 ? 'Settings type not selected' : ''"
:accept="importType === ImportType.AllSettings ? '.zip' : '.json'"
/>
</v-row>
<v-row
class="mt-12 ml-8 mr-8 mb-1"
style="display: flex; align-items: center; justify-content: center"
align="center"
>
<v-btn color="secondary" :disabled="importFile === null" @click="handleSettingsImport">
<v-icon left class="open-icon"> mdi-import </v-icon>
<span class="open-label">Import Settings</span>
</v-btn>
</v-row>
</v-card-text>
</v-card>
</v-dialog>
</v-col>
<v-col cols="12" sm="6">
<v-btn color="secondary" @click="openExportSettingsPrompt">
<v-icon left class="open-icon"> mdi-export </v-icon>
<span class="open-label">Export Settings</span>
</v-btn>
<a
ref="exportSettings"
style="color: black; text-decoration: none; display: none"
:href="`http://${address}/api/settings/photonvision_config.zip`"
download="photonvision-settings.zip"
target="_blank"
/>
</v-col>
<v-col cols="12" sm="6">
<v-btn color="secondary" @click="openExportLogsPrompt">
<v-icon left class="open-icon"> mdi-download </v-icon>
<span class="open-label">Download Current Log</span>
<!-- Special hidden link that gets 'clicked' when the user exports journalctl logs -->
<a
ref="exportLogFile"
style="color: black; text-decoration: none; display: none"
:href="`http://${address}/api/utils/photonvision-journalctl.txt`"
download="photonvision-journalctl.txt"
target="_blank"
/>
</v-btn>
</v-col>
<v-col cols="12" sm="6">
<v-btn color="secondary" @click="useStateStore().showLogModal = true">
<v-icon left class="open-icon"> mdi-eye </v-icon>
<span class="open-label">Show log viewer</span>
</v-btn>
</v-col>
</v-row>
</div>
</v-card>
</template>
<style scoped>
.v-divider {
border-color: white !important;
}
.v-btn {
width: 100%;
}
@media only screen and (max-width: 351px) {
.open-icon {
margin: 0 !important;
}
.open-label {
display: none;
}
}
</style>

View File

@@ -1,21 +0,0 @@
<script setup lang="ts">
import PvSlider from "@/components/common/pv-slider.vue";
import { useSettingsStore } from "@/stores/settings/GeneralSettingsStore";
</script>
<template>
<v-card dark class="mb-3 pr-6 pb-3" style="background-color: #006492">
<v-card-title>LED Control</v-card-title>
<div class="ml-5">
<pv-slider
v-model="useSettingsStore().lighting.brightness"
label="Brightness"
class="pt-2"
:slider-cols="12"
:min="0"
:max="100"
@input="(args) => useSettingsStore().changeLEDBrightness(args)"
/>
</div>
</v-card>
</template>

View File

@@ -1,204 +0,0 @@
<script setup lang="ts">
import { useSettingsStore } from "@/stores/settings/GeneralSettingsStore";
import { computed, onBeforeMount, ref } from "vue";
import { useStateStore } from "@/stores/StateStore";
import PvIcon from "@/components/common/pv-icon.vue";
interface MetricItem {
header: string;
value?: string;
}
const generalMetrics = computed<MetricItem[]>(() => [
{
header: "Version",
value: useSettingsStore().general.version || "Unknown"
},
{
header: "Hardware Model",
value: useSettingsStore().general.hardwareModel || "Unknown"
},
{
header: "Platform",
value: useSettingsStore().general.hardwarePlatform || "Unknown"
},
{
header: "GPU Acceleration",
value: useSettingsStore().general.gpuAcceleration || "Unknown"
}
]);
const platformMetrics = computed<MetricItem[]>(() => [
{
header: "CPU Temp",
value: useSettingsStore().metrics.cpuTemp === undefined ? "Unknown" : `${useSettingsStore().metrics.cpuTemp}°C`
},
{
header: "CPU Usage",
value: useSettingsStore().metrics.cpuUtil === undefined ? "Unknown" : `${useSettingsStore().metrics.cpuUtil}%`
},
{
header: "CPU Memory Usage",
value:
useSettingsStore().metrics.ramUtil === undefined || useSettingsStore().metrics.cpuMem === undefined
? "Unknown"
: `${useSettingsStore().metrics.ramUtil || "Unknown"}MB of ${useSettingsStore().metrics.cpuMem}MB`
},
{
header: "GPU Memory Usage",
value:
useSettingsStore().metrics.gpuMemUtil === undefined || useSettingsStore().metrics.gpuMem === undefined
? "Unknown"
: `${useSettingsStore().metrics.gpuMemUtil}MB of ${useSettingsStore().metrics.gpuMem}MB`
},
{
header: "CPU Throttling",
value: useSettingsStore().metrics.cpuThr || "Unknown"
},
{
header: "CPU Uptime",
value: useSettingsStore().metrics.cpuUptime || "Unknown"
},
{
header: "Disk Usage",
value: useSettingsStore().metrics.diskUtilPct || "Unknown"
}
]);
const metricsLastFetched = ref("Never");
const fetchMetrics = () => {
useSettingsStore()
.requestMetricsUpdate()
.catch((error) => {
if (error.request) {
useStateStore().showSnackbarMessage({
color: "error",
message: "Unable to fetch metrics! The backend didn't respond."
});
} else {
useStateStore().showSnackbarMessage({
color: "error",
message: "An error occurred while trying to fetch metrics."
});
}
})
.finally(() => {
const pad = (num: number): string => {
return String(num).padStart(2, "0");
};
const date = new Date();
metricsLastFetched.value = `${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}`;
});
};
onBeforeMount(() => {
fetchMetrics();
});
</script>
<template>
<v-card dark class="mb-3 pr-6 pb-3" style="background-color: #006492">
<v-card-title style="display: flex; justify-content: space-between">
<span>Stats</span>
<pv-icon icon-name="mdi-reload" color="white" tooltip="Reload Metrics" hover @click="fetchMetrics" />
</v-card-title>
<v-row class="pt-2 pa-4 ma-0 ml-5 pb-1">
<v-card-subtitle class="ma-0 pa-0 pb-2" style="font-size: 16px"> General Metrics </v-card-subtitle>
<v-simple-table class="metrics-table">
<thead>
<tr>
<th v-for="(item, itemIndex) in generalMetrics" :key="itemIndex" class="metric-item metric-item-title">
{{ item.header }}
</th>
</tr>
</thead>
<tbody>
<tr>
<td v-for="(item, itemIndex) in generalMetrics" :key="itemIndex" class="metric-item">
{{ item.value }}
</td>
</tr>
</tbody>
</v-simple-table>
</v-row>
<v-row class="pa-4 ma-0 ml-5">
<v-card-subtitle class="ma-0 pa-0 pb-2" style="font-size: 16px"> Hardware Metrics </v-card-subtitle>
<v-simple-table class="metrics-table">
<thead>
<tr>
<th v-for="(item, itemIndex) in platformMetrics" :key="itemIndex" class="metric-item metric-item-title">
{{ item.header }}
</th>
</tr>
</thead>
<tbody>
<tr>
<td v-for="(item, itemIndex) in platformMetrics" :key="itemIndex" class="metric-item">
<span v-if="useSettingsStore().metrics.cpuUtil !== undefined">{{ item.value }}</span>
<span v-else>---</span>
</td>
</tr>
</tbody>
</v-simple-table>
</v-row>
<div style="text-align: right">
<span>Last Fetched: {{ metricsLastFetched }}</span>
</div>
</v-card>
</template>
<style scoped lang="scss">
.metrics-table {
border-collapse: separate;
border-spacing: 0;
border-radius: 5px;
margin-bottom: 10px;
border: 1px solid white;
width: 100%;
text-align: center;
}
.metric-item {
font-size: 16px !important;
padding: 1px 15px 1px 10px;
border-right: 1px solid;
font-weight: normal;
color: white !important;
text-align: center !important;
}
.metric-item-title {
font-size: 18px !important;
text-decoration: underline;
text-decoration-color: #ffd843;
}
.v-data-table {
thead,
tbody {
background-color: #006492;
}
:hover {
tbody > tr {
background-color: #005281 !important;
}
}
::-webkit-scrollbar {
width: 0;
height: 0.55em;
border-radius: 5px;
}
::-webkit-scrollbar-track {
-webkit-box-shadow: inset 0 0 6px rgba(0, 0, 0, 0.3);
border-radius: 10px;
}
::-webkit-scrollbar-thumb {
background-color: #ffd843;
border-radius: 10px;
}
}
</style>

View File

@@ -1,292 +0,0 @@
<script setup lang="ts">
import { useSettingsStore } from "@/stores/settings/GeneralSettingsStore";
import { computed, ref, watchEffect } from "vue";
import PvInput from "@/components/common/pv-input.vue";
import PvRadio from "@/components/common/pv-radio.vue";
import PvSwitch from "@/components/common/pv-switch.vue";
import PvSelect from "@/components/common/pv-select.vue";
import { type ConfigurableNetworkSettings, NetworkConnectionType } from "@/types/SettingTypes";
import { useStateStore } from "@/stores/StateStore";
// Copy object to remove reference to store
const tempSettingsStruct = ref<ConfigurableNetworkSettings>(Object.assign({}, useSettingsStore().network));
const resetTempSettingsStruct = () => {
tempSettingsStruct.value = Object.assign({}, useSettingsStore().network);
};
const settingsValid = ref(true);
const isValidNetworkTablesIP = (v: string | undefined): boolean => {
// Check if it is a valid team number between 1-9999
const teamNumberRegex = /^[1-9][0-9]{0,3}$/;
// Check if it is a team number longer than 5 digits
const badTeamNumberRegex = /^[0-9]{5,}$/;
if (v === undefined) return false;
if (teamNumberRegex.test(v)) return true;
if (isValidIPv4(v)) return true;
// need to check these before the hostname. "0" and "99999" are valid hostnames, but we don't want to allow then
if (v === "0") return false;
if (badTeamNumberRegex.test(v)) return false;
return isValidHostname(v);
};
const isValidIPv4 = (v: string | undefined) => {
// https://stackoverflow.com/a/17871737
const ipv4Regex = /^((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])$/;
if (v === undefined) return false;
return ipv4Regex.test(v);
};
const isValidHostname = (v: string | undefined) => {
// https://stackoverflow.com/a/18494710
const hostnameRegex = /^([a-zA-Z0-9]+(-[a-zA-Z0-9]+)*)+(\.([a-zA-Z0-9]+(-[a-zA-Z0-9]+)*))*$/;
if (v === undefined) return false;
return hostnameRegex.test(v);
};
const settingsHaveChanged = (): boolean => {
const a = useSettingsStore().network;
const b = tempSettingsStruct.value;
return (
a.ntServerAddress !== b.ntServerAddress ||
a.connectionType !== b.connectionType ||
a.staticIp !== b.staticIp ||
a.hostname !== b.hostname ||
a.runNTServer !== b.runNTServer ||
a.shouldManage !== b.shouldManage ||
a.shouldPublishProto !== b.shouldPublishProto ||
a.networkManagerIface !== b.networkManagerIface ||
a.setStaticCommand !== b.setStaticCommand ||
a.setDHCPcommand !== b.setDHCPcommand
);
};
const saveGeneralSettings = () => {
const changingStaticIp = useSettingsStore().network.connectionType === NetworkConnectionType.Static;
// replace undefined members with empty strings for backend
const payload = {
connectionType: tempSettingsStruct.value.connectionType,
hostname: tempSettingsStruct.value.hostname,
networkManagerIface: tempSettingsStruct.value.networkManagerIface || "",
ntServerAddress: tempSettingsStruct.value.ntServerAddress,
runNTServer: tempSettingsStruct.value.runNTServer,
setDHCPcommand: tempSettingsStruct.value.setDHCPcommand || "",
setStaticCommand: tempSettingsStruct.value.setStaticCommand || "",
shouldManage: tempSettingsStruct.value.shouldManage,
shouldPublishProto: tempSettingsStruct.value.shouldPublishProto,
staticIp: tempSettingsStruct.value.staticIp
};
useSettingsStore()
.updateGeneralSettings(payload)
.then((response) => {
useStateStore().showSnackbarMessage({
message: response.data.text || response.data,
color: "success"
});
// Update the local settings cause the backend checked their validity. Assign is to deref value
useSettingsStore().network = {
...useSettingsStore().network,
...Object.assign({}, tempSettingsStruct.value)
};
})
.catch((error) => {
resetTempSettingsStruct();
if (error.response) {
if (error.status === 504 || changingStaticIp) {
useStateStore().showSnackbarMessage({
color: "error",
message: `Connection lost! Try the new static IP at ${useSettingsStore().network.staticIp}:5800 or ${
useSettingsStore().network.hostname
}:5800?`
});
} else {
useStateStore().showSnackbarMessage({
color: "error",
message: error.response.data.text || error.response.data
});
}
} else if (error.request) {
useStateStore().showSnackbarMessage({
color: "error",
message: "Error while trying to process the request! The backend didn't respond."
});
} else {
useStateStore().showSnackbarMessage({
color: "error",
message: "An error occurred while trying to process the request."
});
}
});
};
const currentNetworkInterfaceIndex = computed<number>({
get: () => useSettingsStore().networkInterfaceNames.indexOf(useSettingsStore().network.networkManagerIface || ""),
set: (v) => (tempSettingsStruct.value.networkManagerIface = useSettingsStore().networkInterfaceNames[v])
});
watchEffect(() => {
// Reset temp settings on remote network settings change
resetTempSettingsStruct();
});
</script>
<template>
<v-card dark class="mb-3 pr-6 pb-3" style="background-color: #006492">
<v-card-title>Networking</v-card-title>
<div class="ml-5">
<v-form ref="form" v-model="settingsValid">
<pv-input
v-model="tempSettingsStruct.ntServerAddress"
label="Team Number/NetworkTables Server Address"
tooltip="Enter the Team Number or the IP address of the NetworkTables Server"
:label-cols="4"
:disabled="tempSettingsStruct.runNTServer"
:rules="[
(v) =>
isValidNetworkTablesIP(v) ||
'The NetworkTables Server Address must be a valid Team Number, IP address, or Hostname'
]"
/>
<v-banner
v-show="!isValidNetworkTablesIP(tempSettingsStruct.ntServerAddress) && !tempSettingsStruct.runNTServer"
rounded
color="red"
text-color="white"
style="margin: 10px 0"
icon="mdi-alert-circle-outline"
>
The NetworkTables Server Address is not set or is invalid. NetworkTables is unable to connect.
</v-banner>
<pv-radio
v-show="!useSettingsStore().network.networkingDisabled"
v-model="tempSettingsStruct.connectionType"
label="IP Assignment Mode"
tooltip="DHCP will make the radio (router) automatically assign an IP address; this may result in an IP address that changes across reboots. Static IP assignment means that you pick the IP address and it won't change."
:input-cols="12 - 4"
:list="['DHCP', 'Static']"
:disabled="
!tempSettingsStruct.shouldManage ||
!useSettingsStore().network.canManage ||
useSettingsStore().network.networkingDisabled
"
/>
<pv-input
v-show="!useSettingsStore().network.networkingDisabled"
v-if="tempSettingsStruct.connectionType === NetworkConnectionType.Static"
v-model="tempSettingsStruct.staticIp"
:input-cols="12 - 4"
label="Static IP"
:rules="[(v) => isValidIPv4(v) || 'Invalid IPv4 address']"
:disabled="
!tempSettingsStruct.shouldManage ||
!useSettingsStore().network.canManage ||
useSettingsStore().network.networkingDisabled
"
/>
<pv-input
v-show="!useSettingsStore().network.networkingDisabled"
v-model="tempSettingsStruct.hostname"
label="Hostname"
:input-cols="12 - 4"
:rules="[(v) => isValidHostname(v) || 'Invalid hostname']"
:disabled="
!tempSettingsStruct.shouldManage ||
!useSettingsStore().network.canManage ||
useSettingsStore().network.networkingDisabled
"
/>
<v-divider class="pb-3" />
<span style="font-weight: 700">Advanced Networking</span>
<pv-switch
v-show="!useSettingsStore().network.networkingDisabled"
v-model="tempSettingsStruct.shouldManage"
:disabled="!useSettingsStore().network.canManage || useSettingsStore().network.networkingDisabled"
label="Manage Device Networking"
tooltip="If enabled, Photon will manage device hostname and network settings."
:label-cols="4"
class="pt-2"
/>
<pv-select
v-show="!useSettingsStore().network.networkingDisabled"
v-model="currentNetworkInterfaceIndex"
label="NetworkManager interface"
:disabled="
!tempSettingsStruct.shouldManage ||
!useSettingsStore().network.canManage ||
useSettingsStore().network.networkingDisabled
"
:select-cols="12 - 4"
tooltip="Name of the interface PhotonVision should manage the IP address of"
:items="useSettingsStore().networkInterfaceNames"
/>
<v-banner
v-show="
!useSettingsStore().networkInterfaceNames.length &&
tempSettingsStruct.shouldManage &&
useSettingsStore().network.canManage &&
!useSettingsStore().network.networkingDisabled
"
rounded
color="red"
text-color="white"
icon="mdi-information-outline"
>
Photon cannot detect any wired connections! Please send program logs to the developers for help.
</v-banner>
<pv-switch
v-model="tempSettingsStruct.runNTServer"
label="Run NetworkTables Server (Debugging Only)"
tooltip="If enabled, this device will create a NT server. This is useful for home debugging, but should be disabled on-robot."
class="mt-3 mb-2"
:label-cols="4"
/>
<v-banner
v-show="tempSettingsStruct.runNTServer"
rounded
color="red"
text-color="white"
icon="mdi-information-outline"
>
This mode is intended for debugging; it should be off for proper usage. PhotonLib will NOT work!
</v-banner>
<pv-switch
v-model="tempSettingsStruct.shouldPublishProto"
label="Also Publish Protobuf"
tooltip="If enabled, Photon will publish all pipeline results in both the Packet and Protobuf formats. This is useful for visualizing pipeline results from NT viewers such as glass and logging software such as AdvantageScope. Note: photon-lib will ignore this value and is not recommended on the field for performance."
class="mt-3 mb-2"
:label-cols="4"
/>
<v-banner
v-show="tempSettingsStruct.shouldPublishProto"
rounded
color="red"
class="mb-3"
text-color="white"
icon="mdi-information-outline"
>
This mode is intended for debugging; it should be off for field use. You may notice a performance hit by using
this mode.
</v-banner>
</v-form>
<v-btn
color="accent"
style="color: black; width: 100%"
:disabled="!settingsValid || !settingsHaveChanged()"
@click="saveGeneralSettings"
>
Save
</v-btn>
</div>
</v-card>
</template>
<style>
.v-banner__wrapper {
padding: 6px !important;
}
</style>

View File

@@ -1,97 +0,0 @@
import { decode, encode } from "@msgpack/msgpack";
import type { IncomingWebsocketData } from "@/types/WebsocketDataTypes";
/**
* {@link WebSocket} wrapper class that automatically reconnects to the provided host address if the connection was closed by the remote host or a connection failure.
* Data sent and received by the Websocket is automatically encoded and decoded using msgpack.
*/
export class AutoReconnectingWebsocket {
private readonly serverAddress: string | URL;
private websocket: WebSocket | null | undefined;
private readonly onConnect: () => void;
private readonly onData: (data: IncomingWebsocketData) => void;
private readonly onDisconnect: () => void;
/**
* Create an AutoReconnectingWebsocket
*
* @param serverAddress address of the websocket
* @param onConnect action to run on websocket connection (when the websocket changes to the OPEN state)
* @param onData decoded websocket message data consumer. The data is automatically decoded by msgpack.
* @param onDisconnect action to run on websocket disconnection (when the websocket changes to the CLOSED state)
*/
constructor(
serverAddress: string | URL,
onConnect: () => void,
onData: (data: IncomingWebsocketData) => void,
onDisconnect: () => void
) {
this.serverAddress = serverAddress;
this.onConnect = onConnect;
this.onData = onData;
this.onDisconnect = onDisconnect;
this.initializeWebsocket();
}
/**
* 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) {
// Only send data if the websocket is open
if (this.isConnected()) {
if (encodeData) {
this.websocket?.send(encode(data));
} else {
this.websocket?.send(data);
}
}
}
/**
* Check if the WebSocket is OPEN and connected
*/
isConnected(): boolean {
return this.websocket === null || this.websocket === undefined
? false
: this.websocket.readyState === WebSocket.OPEN;
}
/**
* Handles the creation of the websocket and the binding of the action consumers.
*
* @private
*/
private initializeWebsocket() {
this.websocket = new WebSocket(this.serverAddress);
this.websocket.binaryType = "arraybuffer";
this.websocket.onopen = () => {
console.debug("[WebSocket] Websocket Open");
this.onConnect();
};
this.websocket.onmessage = (event: MessageEvent) => {
this.onData(decode(event.data) as IncomingWebsocketData);
};
this.websocket.onclose = (event: CloseEvent) => {
this.onDisconnect();
this.websocket = null;
console.info("[WebSocket] The WebSocket was closed. Will reattempt in 500 milliseconds.", event.reason);
setTimeout(this.initializeWebsocket.bind(this), 500);
};
this.websocket.onerror = () => {
this.websocket?.close();
};
console.debug(`[WebSocket] Attempting to initialize Websocket connection to ${this.serverAddress}`);
}
}

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