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
482 changed files with 17732 additions and 35879 deletions

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

@@ -22,22 +22,26 @@ jobs:
working-directory: photon-client
# The type of runner that the job will run on.
runs-on: ubuntu-22.04
runs-on: ubuntu-latest
# Grab the docker container.
container:
image: docker://node:10
steps:
# Checkout code.
- uses: actions/checkout@v3
- uses: actions/checkout@v1
# Setup Node.js
- name: Setup Node.js
uses: actions/setup-node@v3
uses: actions/setup-node@v1
with:
node-version: 16
node-version: 10
# Run npm
- run: npm update -g npm
- run: npm ci
- run: npm run build --if-present
- run: |
npm ci
npm run build --if-present
# Upload client artifact.
- uses: actions/upload-artifact@master
@@ -45,122 +49,76 @@ jobs:
name: built-client
path: photon-client/dist/
photon-build-examples:
runs-on: ubuntu-22.04
name: "Build Examples"
steps:
# Checkout code.
- name: Checkout code
uses: actions/checkout@v3
with:
fetch-depth: 0
# Fetch tags.
- name: Fetch tags
run: git fetch --tags --force
# Install Java 17.
- name: Install Java 17
uses: actions/setup-java@v3
with:
java-version: 17
distribution: temurin
# Need to publish to maven local first, so that C++ sim can pick it up
# Still haven't figure out how to make the vendordep file be copied before trying to build examples
- name: Publish photonlib to maven local
run: |
chmod +x gradlew
./gradlew publishtomavenlocal -x check
- name: Build Java examples
working-directory: photonlib-java-examples
run: |
chmod +x gradlew
./gradlew copyPhotonlib -x check
./gradlew buildAllExamples -x check --max-workers 2
- name: Build C++ examples
working-directory: photonlib-cpp-examples
run: |
chmod +x gradlew
./gradlew copyPhotonlib -x check
./gradlew buildAllExamples -x check --max-workers 2
photon-build-all:
# The type of runner that the job will run on.
runs-on: ubuntu-22.04
runs-on: ubuntu-latest
steps:
# Checkout code.
- name: Checkout code
uses: actions/checkout@v3
with:
fetch-depth: 0
uses: actions/checkout@v1
# Fetch tags.
- name: Fetch tags
run: git fetch --tags --force
# Install Java 17.
- name: Install Java 17
uses: actions/setup-java@v3
# Install Java 11.
- name: Install Java 11
uses: actions/setup-java@v1
with:
java-version: 17
distribution: temurin
java-version: 11
# Run only build tasks, no checks??
# Run Gradle build.
- name: Gradle Build
run: |
chmod +x gradlew
./gradlew photon-server:build photon-lib:build -x check --max-workers 2
./gradlew build -x check --max-workers 1
# Run Gradle Tests.
- name: Gradle Tests
run: ./gradlew testHeadless -i --max-workers 1 --stacktrace
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@v3
uses: codecov/codecov-action@v1
with:
file: ./photon-server/build/reports/jacoco/test/jacocoTestReport.xml
- name: Publish Core Coverage Report
uses: codecov/codecov-action@v3
uses: codecov/codecov-action@v1
with:
file: ./photon-core/build/reports/jacoco/test/jacocoTestReport.xml
photonserver-build-offline-docs:
runs-on: ubuntu-22.04
runs-on: ubuntu-latest
steps:
# Checkout docs.
- uses: actions/checkout@v3
- uses: actions/checkout@v2
with:
repository: 'PhotonVision/photonvision-docs.git'
ref: master
# Install Python.
- uses: actions/setup-python@v4
- uses: actions/setup-python@v2
with:
python-version: '3.9'
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
@@ -173,55 +131,64 @@ jobs:
photonserver-check-lint:
# The type of runner that the job will run on.
runs-on: ubuntu-22.04
runs-on: ubuntu-latest
steps:
# Checkout code.
- uses: actions/checkout@v3
with:
fetch-depth: 0
- uses: actions/checkout@v1
# Install Java 17.
- uses: actions/setup-java@v3
# Install Java 11.
- uses: actions/setup-java@v1
with:
java-version: 17
distribution: temurin
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:
env:
MACOSX_DEPLOYMENT_TARGET: 10.14
strategy:
fail-fast: false
matrix:
include:
- os: windows-2022
- os: windows-latest
artifact-name: Win64
- os: macos-11
- os: macos-latest
artifact-name: macOS
- os: ubuntu-22.04
- os: ubuntu-latest
artifact-name: Linux
runs-on: ${{ matrix.os }}
name: "Photonlib - Build Host - ${{ matrix.artifact-name }}"
name: "Photonlib - Build - ${{ matrix.artifact-name }}"
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v2.3.4
with:
fetch-depth: 0
- uses: actions/setup-java@v3
- uses: actions/setup-java@v1
with:
java-version: 17
distribution: temurin
java-version: 11
- run: git fetch --tags --force
- run: |
chmod +x gradlew
./gradlew photon-lib:build --max-workers 1
- run: ./gradlew photon-lib:publish photon-targeting:publish
- run: ./gradlew photon-lib:publish
name: Publish
env:
ARTIFACTORY_API_KEY: ${{ secrets.ARTIFACTORY_API_KEY }}
@@ -232,29 +199,29 @@ jobs:
fail-fast: false
matrix:
include:
- container: wpilib/roborio-cross-ubuntu:2023-22.04
- container: wpilib/roborio-cross-ubuntu:2021-18.04
artifact-name: Athena
- container: wpilib/raspbian-cross-ubuntu:bullseye-22.04
- container: wpilib/raspbian-cross-ubuntu:10-18.04
artifact-name: Raspbian
- container: wpilib/aarch64-cross-ubuntu:bullseye-22.04
- container: wpilib/aarch64-cross-ubuntu:bionic-18.04
artifact-name: Aarch64
runs-on: ubuntu-22.04
runs-on: ubuntu-latest
container: ${{ matrix.container }}
name: "Photonlib - Build Docker - ${{ matrix.artifact-name }}"
name: "Photonlib - Build - ${{ matrix.artifact-name }}"
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v2.3.4
with:
fetch-depth: 0
- name: Config Git
run: |
git config --global --add safe.directory /__w/photonvision/photonvision
- name: Build PhotonLib
run: |
- 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
- name: Publish
run: |
- run: |
chmod +x gradlew
./gradlew photon-lib:publish
env:
@@ -263,167 +230,84 @@ jobs:
photonlib-wpiformat:
name: "wpiformat"
runs-on: ubuntu-22.04
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- 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@v4
uses: actions/setup-python@v2
with:
python-version: 3.8
- name: Install clang-format
run: |
sudo sh -c "echo 'deb http://archive.ubuntu.com/ubuntu/ $(lsb_release -cs)-proposed restricted main multiverse universe' >> /etc/apt/sources.list.d/proposed-repositories.list"
sudo apt-get update -q
sudo apt-get install -y clang-format-12
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 12
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@v3
- 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]
strategy:
fail-fast: false
matrix:
include:
- os: windows-latest
artifact-name: Win64
architecture: x64
arch-override: none
- os: macos-latest
artifact-name: macOS
architecture: x64
arch-override: none
- os: ubuntu-latest
artifact-name: Linux
architecture: x64
arch-override: none
- os: macos-latest
artifact-name: macOSArm
architecture: x64
arch-override: macarm64
- os: ubuntu-latest
artifact-name: LinuxArm32
architecture: x64
arch-override: linuxarm32
- os: ubuntu-latest
artifact-name: LinuxArm64
architecture: x64
arch-override: linuxarm64
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: ${{ matrix.os }}
name: "Build fat JAR - ${{ matrix.artifact-name }}"
runs-on: ubuntu-latest
steps:
# Checkout code.
- uses: actions/checkout@v3
with:
fetch-depth: 0
- uses: actions/checkout@v1
# Install Java 17.
- uses: actions/setup-java@v3
# Install Java 11.
- uses: actions/setup-java@v1
with:
java-version: 17
distribution: temurin
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
if: ${{ (matrix.os) != 'windows-latest' }}
- run: |
del photon-server\src\main\resources\web\*.*
mkdir photon-server\src\main\resources\web\docs
if: ${{ (matrix.os) == 'windows-latest' }}
# Download client artifact to resources folder.
- uses: actions/download-artifact@v3
- 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@v3
- uses: actions/download-artifact@v2
with:
name: built-docs
path: photon-server/src/main/resources/web/docs
# Build fat jar for both pi and everything
# Build fat jar.
- run: |
chmod +x gradlew
./gradlew photon-server:shadowJar --max-workers 2 -PArchOverride=${{ matrix.arch-override }}
if: ${{ (matrix.arch-override != 'none') }}
- run: |
chmod +x gradlew
./gradlew photon-server:shadowJar --max-workers 2
if: ${{ (matrix.arch-override == 'none') }}
# The image will only pull the Pi32 JAR in
- name: Generate image
if: ${{ github.event_name != 'pull_request' && (matrix.artifact-name) == 'LinuxArm32' }}
run: |
chmod +x scripts/generatePiImage.sh
./scripts/generatePiImage.sh
./gradlew photon-server:shadowJar --max-workers 1
# Upload final fat jar as artifact.
- uses: actions/upload-artifact@v3
- uses: actions/upload-artifact@master
with:
name: jar-${{ matrix.artifact-name }}
name: jar
path: photon-server/build/libs
# Upload image as well
- uses: actions/upload-artifact@v3
if: ${{ github.event_name != 'pull_request' && (matrix.artifact-name) == 'LinuxArm32' }}
with:
name: image-${{ matrix.artifact-name }}
path: photonvision*.xz
photon-release:
needs: [photon-build-package]
runs-on: ubuntu-22.04
steps:
# Download literally every single artifact. This also downloads client and docs,
# but the filtering below won't pick these up (I hope)
- uses: actions/download-artifact@v2
- run: find
# Push to dev release
- uses: pyTooling/Actions/releaser@r0
- uses: eine/tip@master
with:
token: ${{ secrets.GITHUB_TOKEN }}
tag: 'Dev'
rm: true
files: |
**/*.xz
**/*.jar
photon-server/build/libs/*.jar
if: github.event_name == 'push'
# Upload all jars and xz archives
- uses: softprops/action-gh-release@v1
with:
files: |
**/*.xz
**/*.jar
if: startsWith(github.ref, 'refs/tags/v')
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

18
.gitignore vendored
View File

@@ -30,7 +30,6 @@ backend/settings/
*.nar
*.ear
*.zip
*.xz
*.tar.gz
*.rar
@@ -116,14 +115,6 @@ fabric.properties
**/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,12 +135,3 @@ build/*
build
photon-lib/src/main/java/org/photonvision/PhotonVersion.java
/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/*

View File

@@ -1,23 +0,0 @@
Copyright (c) 2022 Photon Vision. All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in the
documentation and/or other materials provided with the distribution.
* Neither the name of FIRST, WPILib, nor the names of other WPILib
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY FIRST AND OTHER WPILIB CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY NONINFRINGEMENT AND FITNESS FOR A PARTICULAR
PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL FIRST OR CONTRIBUTORS BE LIABLE FOR
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

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,59 +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 winx32, winx64,
macx64, macarm64, linuxx64, linuxarm64, linuxarm32, and linuxathena.
- `-PtgtIp`: deploys (builds and copies the JAR) to the coprocessor at the specified IP
- `-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
```
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/)
@@ -68,11 +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)

View File

@@ -1,21 +1,17 @@
plugins {
id "com.diffplug.spotless" version "6.1.2"
id "com.github.johnrengelman.shadow" version "7.1.2"
id "com.github.node-gradle.node" version "3.1.1" apply false
id "edu.wpi.first.GradleJni" version "1.0.0"
id "edu.wpi.first.GradleVsCode" version "1.1.0"
id "edu.wpi.first.NativeUtils" version "2023.10.0" 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 "org.hidetake.ssh" version "2.10.1"
id 'edu.wpi.first.WpilibTools' version '1.0.0'
}
import org.gradle.api.internal.artifacts.dependencies.DefaultExternalModuleDependency;
allprojects {
repositories {
mavenCentral()
mavenLocal()
jcenter()
maven { url = "https://maven.photonvision.org/repository/internal/" }
}
wpilibRepositories.addAllReleaseRepositories(it)
@@ -26,41 +22,24 @@ allprojects {
apply from: "versioningHelper.gradle"
ext {
wpilibVersion = "2023.1.1-beta-7-15-g1e7fcd5"
opencvVersion = "4.6.0-4"
wpilibVersion = "2021.3.1"
opencvVersion = "3.4.7-5"
joglVersion = "2.4.0-rc-20200307"
pubVersion = versionString
isDev = pubVersion.startsWith("dev")
if(project.hasProperty('pionly')) {
jniPlatforms = ['linuxarm32']
} else if(project.hasProperty('winonly')) {
jniPlatforms = ['windowsx86-64']
} else if(project.hasProperty('aarch64only')) {
jniPlatforms = ['linuxaarch64bionic']
} else {
jniPlatforms = ['linuxarm64', 'linuxarm32', 'linuxx86-64', 'osxuniversal', 'windowsx86-64']
}
println("Building for archs " + jniPlatforms)
}
wpilibTools.deps.wpilibVersion = wpilibVersion
spotless {
java {
toggleOffOn()
googleJavaFormat()
paddedCell()
indentWithTabs(2)
indentWithSpaces(4)
removeUnusedImports()
trimTrailingWhitespace()
endWithNewline()
}
java {
target "**/*.java"
licenseHeaderFile "$rootDir/LicenseHeader.txt"
targetExclude("photon-core/src/main/java/org/photonvision/PhotonVersion.java")
targetExclude("photon-lib/src/main/java/org/photonvision/PhotonVersion.java")
}
}

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,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,5 +1,6 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-7.5.1-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-6.7.1-bin.zip
distributionSha256Sum=3239b5ed86c3838a37d983ac100573f64c1f3fd8e1eb6c89fa5f9529b5ec091d
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

217
gradlew vendored
View File

@@ -1,7 +1,7 @@
#!/bin/sh
#!/usr/bin/env sh
#
# Copyright <20> 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.
@@ -16,70 +16,79 @@
# limitations under the License.
#
##############################################################################
##
## 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
APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
SAVED="`pwd`"
cd "`dirname \"$PRG\"`/" >/dev/null
APP_HOME="`pwd -P`"
cd "$SAVED" >/dev/null
APP_NAME="Gradle"
APP_BASE_NAME=${0##*/}
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
@@ -88,7 +97,7 @@ Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
JAVACMD=java
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
@@ -96,95 +105,79 @@ location of your Java installation."
fi
# Increase the maximum file descriptors if we can.
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
case $MAX_FD in #(
max*)
MAX_FD=$( ulimit -H -n ) ||
warn "Could not query maximum file descriptor limit"
esac
case $MAX_FD in #(
'' | soft) :;; #(
*)
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
# Collect all arguments for the java command;
# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of
# shell script including quotes and variable substitutions, so put them in
# double quotes to make sure that they get re-expanded; and
# * put everything else in single quotes, so that it's not re-expanded.
# Escape application args
save () {
for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
echo " "
}
APP_ARGS=`save "$@"`
set -- \
"-Dorg.gradle.appname=$APP_BASE_NAME" \
-classpath "$CLASSPATH" \
org.gradle.wrapper.GradleWrapperMain \
"$@"
# 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" "$@"

25
gradlew.bat vendored
View File

@@ -29,9 +29,6 @@ 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"
@@ -40,7 +37,7 @@ if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if "%ERRORLEVEL%" == "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.
@@ -54,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%
@@ -64,14 +61,28 @@ 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

File diff suppressed because it is too large Load Diff

View File

@@ -13,13 +13,11 @@
"axios": "^0.19.2",
"core-js": "^2.6.11",
"downloadjs": "^1.4.7",
"jspdf": "^2.4.0",
"material-design-icons-iconfont": "^5.0.1",
"msgpack5": "^4.2.1",
"three-full": "^28.0.2",
"vue": "^2.6.12",
"vue-axios": "^2.1.5",
"vue-native-websocket": "git+https://git@github.com/PhotonVision/vue-native-websocket.git#5189f29",
"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"

View File

@@ -17,4 +17,4 @@
<!-- built files will be auto injected -->
</body>
</html>
</html>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

View File

@@ -1,317 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<title>ThinClient</title>
<style>
* {
margin: 0;
padding: 0;
}
.imgbox {
display: grid;
height: 100%;
width: 100%;
}
.center-fit {
width: 90vw;
margin: auto;
}
</style>
</head>
<body>
<hr>
<div class="imgbox">
<img id="streamImg" class="center-fit" src=''>
</div>
<hr>
<form id="frm1">
Host <input type="text" id="host" value="photonvision.local"><br>
Port <input type="text" id="port" value="1181"><br>
</form>
<button>Start Stream</button>
<script type="module">
class WebsocketVideoStream{
constructor(drawDiv, streamPort, host) {
this.drawDiv = drawDiv;
this.image = document.getElementById(this.drawDiv);
this.streamPort = streamPort;
this.newStreamPortReq = null;
this.serverAddr = "ws://" + host + "/websocket_cameras";
this.dispNoStream();
this.ws_connect();
this.imgData = null;
this.imgDataTime = -1;
this.imgObjURL = null;
this.frameRxCount = 0;
//Display state machine
this.DSM_DISCONNECTED = "DISCONNECTED";
this.DSM_WAIT_FOR_VALID_PORT = "WAIT_FOR_VALID_PORT";
this.DSM_SUBSCRIBE = "SUBSCRIBE";
this.DSM_WAIT_FOR_FIRST_FRAME = "WAIT_FOR_FIRST_FRAME";
this.DSM_SHOWING = "SHOWING";
this.DSM_RESTART_UNSUBSCRIBE = "UNSUBSCRIBE";
this.DSM_RESTART_WAIT = "WAIT_BEFORE_SUBSCRIBE";
this.dsm_cur_state = this.DSM_DISCONNECTED;
this.dsm_prev_state = this.DSM_DISCONNECTED;
this.dsm_restart_start_time = window.performance.now();
requestAnimationFrame(()=>this.animationLoop());
}
dispImageData(){
//From https://stackoverflow.com/questions/67507616/set-image-src-from-image-blob/67507685#67507685
if(this.imgObjURL != null){
URL.revokeObjectURL(this.imgObjURL)
}
this.imgObjURL = URL.createObjectURL(this.imgData);
//Update the image with the new mimetype and image
this.image.src = this.imgObjURL;
}
dispNoStream() {
this.image.src = "loading.gif";
}
animationLoop(){
// Update time metrics
var now = window.performance.now();
var timeInState = now - this.dsm_restart_start_time;
// Save previous state
this.dsm_prev_state = this.dsm_cur_state;
// Evaluate state transitions
if(this.serverConnectionActive == false){
//Any state - if the server connection goes false, always transition to disconnected
this.dsm_cur_state = this.DSM_DISCONNECTED;
} else {
//Conditional transitions
switch(this.dsm_cur_state) {
case this.DSM_DISCONNECTED:
//Immediately transition to waiting for the first frame
this.dsm_cur_state = this.DSM_WAIT_FOR_VALID_PORT;
break;
case this.DSM_WAIT_FOR_VALID_PORT:
// Wait until the user has configured a valid port
if(this.streamPort > 0){
this.dsm_cur_state = this.DSM_SUBSCRIBE;
} else {
this.dsm_cur_state = this.DSM_WAIT_FOR_VALID_PORT;
}
break;
case this.DSM_SUBSCRIBE:
// Immediately transition after subscriptions is sent
this.dsm_cur_state = this.DSM_WAIT_FOR_FIRST_FRAME;
break;
case this.DSM_WAIT_FOR_FIRST_FRAME:
if(this.imgData != null){
//we got some image data, start showing it
this.dsm_cur_state = this.DSM_SHOWING;
} else if (this.newStreamPortReq != null){
//Stream port requested changed, unsubscribe and restart
this.dsm_cur_state = this.DSM_RESTART_UNSUBSCRIBE;
} else {
this.dsm_cur_state = this.DSM_WAIT_FOR_FIRST_FRAME;
}
break;
case this.DSM_SHOWING:
if((now - this.imgDataTime) > 2500){
//timeout, begin the restart sequence
this.dsm_cur_state = this.DSM_RESTART_UNSUBSCRIBE;
} else if (this.newStreamPortReq != null){
//Stream port requested changed, unsubscribe and restart
this.dsm_cur_state = this.DSM_RESTART_UNSUBSCRIBE;
} else {
//stay in this state.
this.dsm_cur_state = this.DSM_SHOWING;
}
break;
case this.DSM_RESTART_UNSUBSCRIBE:
//Only should spend one loop in Unsubscribe, immediately transition
this.dsm_cur_state = this.DSM_RESTART_WAIT;
break;
case this.DSM_RESTART_WAIT:
if (timeInState > 250) {
//we've waited long enough, go to try to re-subscribe
this.dsm_cur_state = this.DSM_WAIT_FOR_VALID_PORT;
} else {
//stay in this state.
this.dsm_cur_state = this.DSM_RESTART_WAIT;
}
break;
default:
// Shouldn't get here, default back to init
this.dsm_cur_state = this.DSM_DISCONNECTED;
}
}
//take current-state or state-transition actions
if(this.dsm_cur_state != this.dsm_prev_state){
//Any state transition
console.log("State Change: " + this.dsm_prev_state + " -> " + this.dsm_cur_state);
}
if(this.dsm_cur_state == this.DSM_SHOWING){
// Currently in SHOWING
this.dispImageData();
}
if(this.dsm_cur_state != this.DSM_SHOWING && this.dsm_prev_state == this.DSM_SHOWING ){
//Any transition out of showing - no stream
this.dispNoStream();
}
if(this.dsm_cur_state == this.DSM_RESTART_UNSUBSCRIBE){
// Currently in UNSUBSCRIBE, do the unsubscribe actions
this.stopStream();
this.dsm_restart_start_time = now;
}
if(this.dsm_cur_state == this.DSM_SUBSCRIBE){
// Currently in SUBSCRIBE, do the subscribe actions
this.startStream();
this.dsm_restart_start_time = now;
}
if(this.dsm_cur_state == this.DSM_WAIT_FOR_VALID_PORT){
// Currently waiting for a vaild port to be requested
if(this.newStreamPortReq != null){
this.streamPort = this.newStreamPortReq;
this.newStreamPortReq = null;
}
}
requestAnimationFrame(()=>this.animationLoop());
}
startStream() {
console.log("Subscribing to port " + this.streamPort);
this.imgData = null;
this.ws.send(JSON.stringify({"cmd": "subscribe", "port":this.streamPort}));
}
stopStream() {
console.log("Unsubscribing");
this.ws.send(JSON.stringify({"cmd": "unsubscribe"}));
this.imgData = null;
}
setPort(streamPort){
console.log("Port set to " + streamPort);
this.newStreamPortReq = streamPort;
}
ws_onOpen() {
// Set the flag allowing general server communication
this.serverConnectionActive = true;
console.log("Connected!");
}
ws_onClose(e) {
//Clear flags to stop server communication
this.ws = null;
this.serverConnectionActive = false;
console.log('Camera Socket is closed. Reconnect will be attempted in 0.5 second.', e.reason);
setTimeout(this.ws_connect.bind(this), 500);
if(!e.wasClean){
console.error('Socket encountered error!');
}
}
ws_onError(e){
e; //prevent unused failure
this.ws.close();
}
ws_onMessage(e){
if(typeof e.data === 'string'){
//string data from host
//TODO - anything to recieve info here? Maybe "avaialble streams?"
} else {
if(e.data.size > 0){
//binary data - a frame
this.imgData = e.data;
this.imgDataTime = window.performance.now();
this.frameRxCount++;
} else {
//TODO - server is sending empty frames?
}
}
}
ws_connect() {
this.serverConnectionActive = false;
this.ws = new WebSocket(this.serverAddr);
this.ws.binaryType = "blob";
this.ws.onopen = this.ws_onOpen.bind(this);
this.ws.onmessage = this.ws_onMessage.bind(this);
this.ws.onclose = this.ws_onClose.bind(this);
this.ws.onerror = this.ws_onError.bind(this);
console.log("Connecting to server " + this.serverAddr);
}
ws_close(){
this.ws.close();
}
}
var stream = null;
function streamStartRequest() {
var host = document.getElementById("host").value + ":5800";
var port = document.getElementById("port").value;
if(stream == null){
stream = new WebsocketVideoStream("streamImg",port,host);
} else {
stream.setPort(port);
}
}
// Attach listener
document.querySelector('button').addEventListener('click', streamStartRequest);
// Deal with URLParams, validating inputs
const queryString = window.location.search;
const urlParams = new URLSearchParams(queryString);
const port_in = urlParams.get('port')
const host_in = urlParams.get('host')
if(port_in != ""){
document.getElementById("port").value = port_in;
}
if(host_in != ""){
document.getElementById("host").value = host_in;
}
if(port_in != "" & host_in != ""){
streamStartRequest(); //we got valid inputs, auto-start the stream
}
</script>
</body>
</html>

View File

@@ -1,17 +1,35 @@
<template>
<v-app>
<!-- 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-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">
<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
link
to="dashboard"
@click="rollbackPipelineIndex()"
>
<v-list-item-icon>
<v-icon>mdi-view-dashboard</v-icon>
</v-list-item-icon>
@@ -19,7 +37,12 @@
<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
ref="camerasTabOpener"
link
to="cameras"
@click="switchToDriverMode()"
>
<v-list-item-icon>
<v-icon>mdi-camera</v-icon>
</v-list-item-icon>
@@ -27,7 +50,11 @@
<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
link
to="settings"
@click="switchToSettingsTab()"
>
<v-list-item-icon>
<v-icon>mdi-settings</v-icon>
</v-list-item-icon>
@@ -35,7 +62,10 @@
<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
link
to="docs"
>
<v-list-item-icon>
<v-icon>mdi-bookshelf</v-icon>
</v-list-item-icon>
@@ -43,7 +73,11 @@
<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
v-if="this.$vuetify.breakpoint.mdAndUp"
link
@click.stop="toggleCompactMode"
>
<v-list-item-icon>
<v-icon v-if="compact">
mdi-chevron-right
@@ -53,59 +87,36 @@
</v-icon>
</v-list-item-icon>
<v-list-item-content>
<v-list-item-title>Compact Mode</v-list-item-title>
<v-list-item-title>Advanced 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="$store.state.settings.networkSettings.runNTServer">
mdi-server
</v-icon>
<img v-else-if="$store.state.ntConnectionInfo.connected" src="@/assets/robot.svg" alt="">
<img v-else class="pulse" style="border-radius: 100%" src="@/assets/robot-off.svg" alt="">
</v-list-item-icon>
<v-list-item-content>
<v-list-item-title v-if="$store.state.settings.networkSettings.runNTServer" class="text-wrap">
NetworkTables server running for {{ $store.state.ntConnectionInfo.clients ?
$store.state.ntConnectionInfo.clients : 'zero'
}} clients!
</v-list-item-title>
<v-list-item-title v-else-if="$store.state.ntConnectionInfo.connected && $store.state.backendConnected"
class="text-wrap">
Robot connected! {{ $store.state.ntConnectionInfo.address }}
</v-list-item-title>
<v-list-item-title v-else class="text-wrap">
Not connected to robot!
</v-list-item-title>
<router-link v-if="!$store.state.settings.networkSettings.runNTServer" to="settings" class="accent--text"
@click="switchToSettingsTab">
Team number is {{ $store.state.settings.networkSettings.teamNumber }}
</router-link>
</v-list-item-content>
</v-list-item>
<v-list-item>
<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 class="text-wrap">
{{ $store.state.backendConnected ? "Backend Connected" : "Trying to connect..." }}
</v-list-item-title>
</v-list-item-content>
</v-list-item>
</div>
<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 fluid fill-height>
<v-container
fluid
fill-height
>
<v-layout>
<v-flex>
<router-view @switch-to-cameras="switchToDriverMode" />
@@ -114,175 +125,145 @@
</v-container>
</v-main>
<v-dialog v-model="$store.state.logsOverlay" width="1500" dark>
<v-dialog
v-model="$store.state.logsOverlay"
width="1500"
dark
>
<logs />
</v-dialog>
<v-dialog v-model="needsTeamNumberSet" width="500" dark persistent>
<v-card dark color="primary" flat>
<v-card-title>No team number set!</v-card-title>
<v-card-text>
PhotonVision cannot connect to your robot! Please
<router-link to="settings" class="accent--text" @click="switchToSettingsTab">
visit the settings tab
</router-link>
and set your team number.
</v-card-text>
</v-card>
</v-dialog>
</v-app>
</template>
<script>
import Logs from "./views/LogsView"
// import {mapState} from "vuex";
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,
teamNumberDialog: true
}),
computed: {
needsTeamNumberSet: {
get() {
return this.$store.state.settings.networkSettings.teamNumber < 1
&& this.teamNumberDialog && this.$store.state.backendConnected
&& !this.$route.name.toLowerCase().includes("settings");
}
},
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;
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;
}
});
}
});
this.recreateWebsocket();
},
methods: {
recreateWebsocket() {
const wsDataURL = 'ws://' + this.$address + '/websocket_data';
let socket = new WebSocket(wsDataURL);
socket.binaryType = "arraybuffer";
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())
};
socket.onmessage = (event) => {
try {
let message = this.$msgPack.decode(event.data);
for (let prop in message) {
if (message.hasOwnProperty(prop)) {
this.handleMessage(prop, message[prop]);
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', {})
}
}
} catch (error) {
console.log(event)
console.error('error: ' + JSON.stringify(event.data) + " , " + error);
}
};
socket.onerror = () => {
socket.close();
this.$store.commit("backendConnected", false)
};
socket.onopen = () => {
clearInterval(this.timerId);
socket.onclose = () => {
this.$store.commit("backendConnected", false)
this.timerId = setInterval(() => {
this.recreateWebsocket();
}, 1000);
};
this.$store.commit("backendConnected", true)
this.$store.state.connectedCallbacks.forEach(it => it())
};
this.$store.commit("websocket", socket);
},
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 {
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">
@@ -290,77 +271,76 @@ export default {
</style>
<style>
.pulse {
animation: pulse-animation 2s infinite;
}
.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);
}
@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);
}
}
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;
}
.logo {
width: 100%;
height: 70px;
object-fit: contain;
}
::-webkit-scrollbar {
width: 0.5em;
border-radius: 5px;
}
::-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-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;
}
::-webkit-scrollbar-thumb {
background-color: #ffd843;
border-radius: 10px;
}
.container {
background-color: #232c37;
padding: 0 !important;
}
.container {
background-color: #232c37;
padding: 0 !important;
}
#title {
color: #ffd843;
}
#title {
color: #ffd843;
}
</style>
<style>
/* Hacks */
/* Hacks */
.v-divider {
border-color: white !important;
}
.v-divider {
border-color: white !important;
}
.v-input {
font-size: 1rem !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;
}
/* 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';
@import '~vuetify/src/styles/settings/_variables';
@media #{map-get($display-breakpoints, 'md-and-down')} {
html {
font-size: 14px !important;
}
}
</style>
@media #{map-get($display-breakpoints, 'md-and-down')} {
html {
font-size: 14px !important;
}
}
</style>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 76 KiB

After

Width:  |  Height:  |  Size: 48 KiB

View File

@@ -1 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="24" height="24" viewBox="0 0 24 24"><path fill="white" d="M23 15V18C23 18.5 22.64 18.88 22.17 18.97L18.97 15.77C19 15.68 19 15.59 19 15.5C19 14.12 17.88 13 16.5 13C16.41 13 16.32 13 16.23 13.03L10.2 7H11V5.73C10.4 5.39 10 4.74 10 4C10 2.9 10.9 2 12 2S14 2.9 14 4C14 4.74 13.6 5.39 13 5.73V7H14C17.87 7 21 10.13 21 14H22C22.55 14 23 14.45 23 15M22.11 21.46L20.84 22.73L19.89 21.78C19.62 21.92 19.32 22 19 22H5C3.9 22 3 21.11 3 20V19H2C1.45 19 1 18.55 1 18V15C1 14.45 1.45 14 2 14H3C3 11.53 4.29 9.36 6.22 8.11L1.11 3L2.39 1.73L22.11 21.46M10 15.5C10 14.12 8.88 13 7.5 13S5 14.12 5 15.5 6.12 18 7.5 18 10 16.88 10 15.5M16.07 17.96L14.04 15.93C14.23 16.97 15.04 17.77 16.07 17.96Z" /></svg>

Before

Width:  |  Height:  |  Size: 928 B

View File

@@ -1 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="24" height="24" viewBox="0 0 24 24"><path fill="white" d="M12,2C13.1,2 14,2.9 14,4C14,4.74 13.6,5.39 13,5.73V7H14C17.87,7 21,10.13 21,14H22C22.55,14 23,14.45 23,15V18C23,18.55 22.55,19 22,19H21V20C21,21.1 20.1,22 19,22H5C3.9,22 3,21.1 3,20V19H2C1.45,19 1,18.55 1,18V15C1,14.45 1.45,14 2,14H3C3,10.13 6.13,7 10,7H11V5.73C10.4,5.39 10,4.74 10,4C10,2.9 10.9,2 12,2M7.5,13C6.12,13 5,14.12 5,15.5C5,16.88 6.12,18 7.5,18C8.88,18 10,16.88 10,15.5C10,14.12 8.88,13 7.5,13M16.5,13C15.12,13 14,14.12 14,15.5C14,16.88 15.12,18 16.5,18C17.88,18 19,16.88 19,15.5C19,14.12 17.88,13 16.5,13Z" /></svg>

Before

Width:  |  Height:  |  Size: 827 B

View File

@@ -51,4 +51,4 @@
.hover:hover {
color: white !important;
}
</style>
</style>

View File

@@ -5,16 +5,15 @@
:style="styleObject"
:src="src"
alt=""
@click="clickHandler"
@error="loadErrHandler"
/>
@click="e => $emit('click', e)"
>
</template>
<script>
export default {
name: "CvImage",
// eslint-disable-next-line vue/require-prop-types
props: ['address', 'scale', 'maxHeight', 'maxHeightMd', 'maxHeightLg', 'maxHeightXl', 'colorPicking', 'id', 'disconnected'],
props: ['address', 'scale', 'maxHeight', 'maxHeightMd', 'maxHeightXl', 'colorPicking', 'id', 'disconnected'],
data() {
return {
seed: 1.0,
@@ -27,21 +26,18 @@
"border-radius": "3px",
"display": "block",
"object-fit": "contain",
"background-size:": "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")}),` : "pointer") + "default",
cursor: (this.colorPicking ? `url(${require("../../assets/eyedropper.svg")}),` : "") + "default",
};
if (this.$vuetify.breakpoint.xl) {
ret["max-height"] = this.maxHeightXl;
} else if (this.$vuetify.breakpoint.lg) {
ret["max-height"] = this.maxHeightLg;
} else if (this.$vuetify.breakpoint.md) {
} else if (this.$vuetify.breakpoint.mdAndUp) {
ret["max-height"] = this.maxHeightMd;
}
@@ -50,14 +46,7 @@
},
src: {
get() {
var port = this.getCurPort();
if(port <= 0){
//Invalid port, keep it spinny
return require("../../assets/loading.gif");
} else {
//Valid port, connect
return this.getSrcURLFromPort(port);
}
return this.disconnected ? require("../../assets/noStream.jpg") : this.address + "?" + this.seed // This prevents caching
},
},
},
@@ -65,46 +54,9 @@
this.reload(); // Force reload image on creation
},
methods: {
getCurPort(){
var port = -1;
if(this.disconnected){
//Disconnected, port is unknown.
port = -1;
} else {
//Connected - get the port
if(this.id == 'raw-stream'){
port = this.$store.state.cameraSettings[this.$store.state.currentCameraIndex].inputStreamPort
} else {
port = this.$store.state.cameraSettings[this.$store.state.currentCameraIndex].outputStreamPort
}
}
return port;
},
getSrcURLFromPort(port){
return "http://" + location.hostname + ":" + port + "/stream.mjpg" + "?" + this.seed;
},
loadErrHandler(event) {
console.log(event);
console.log("Error loading image, attempting to do it again...");
this.reload();
},
clickHandler(event) {
if(this.colorPicking){
this.$emit('click', event);
} else {
var port = this.getCurPort();
if(port <= 0){
console.log("No valid port, ignoring click.");
} else {
//Valid port, connect
window.open(this.getSrcURLFromPort(port), '_blank');
}
}
},
reload() {
this.seed = new Date().getTime();
}
},
}
</script>
</script>

View File

@@ -26,7 +26,7 @@
</v-row>
</div>
</template>
s
<script>
import TooltippedLabel from "./cv-tooltipped-label";
@@ -61,4 +61,4 @@
</script>
<style lang="css" scoped>
</style>
</style>

View File

@@ -54,4 +54,4 @@
<style lang="" scoped>
</style>
</style>

View File

@@ -1,46 +1,28 @@
<template>
<div>
<v-row
dense
align="center"
<v-radio-group
v-model="localValue"
row
dark
:mandatory="true"
>
<v-col :cols="12 - (inputCols || 8)">
<tooltipped-label
:tooltip="tooltip"
:text="name"
/>
</v-col>
<v-col :cols="inputCols || 8">
<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>
<v-radio
v-for="(name,index) in list"
:key="index"
color="#ffd843"
:label="name"
:value="index"
:disabled="disabled"
/>
</v-radio-group>
</div>
</template>
<script>
import TooltippedLabel from "./cv-tooltipped-label";
export default {
name: 'Radio',
components: {
TooltippedLabel
},
// eslint-disable-next-line vue/require-prop-types
props: ['name', 'value', 'list', 'disabled', 'inputCols', 'tooltip'],
props: ['value', 'list', 'disabled'],
data() {
return {}
},
@@ -59,4 +41,4 @@
<style lang="" scoped>
</style>
</style>

View File

@@ -19,9 +19,7 @@
hide-details
class="align-center"
dark
:color="inverted ? 'rgba(255, 255, 255, 0.2)' : 'accent'"
:track-color="inverted ? 'accent' : undefined"
thumb-color="accent"
color="accent"
:step="step"
@input="handleInput"
@mousedown="$emit('rollback', localValue)"
@@ -78,7 +76,7 @@ export default {
TooltippedLabel,
},
// eslint-disable-next-line vue/require-prop-types
props: ["name", "min", "max", "value", "step", "tooltip", "disabled", "inverted"],
props: ["name", "min", "max", "value", "step", "tooltip", "disabled"],
data() {
return {
prependFocused: false,
@@ -131,4 +129,4 @@ export default {
</script>
<style lang="" scoped>
</style>
</style>

View File

@@ -62,4 +62,4 @@ import TooltippedLabel from "./cv-tooltipped-label";
</script>
<style>
</style>
</style>

View File

@@ -105,4 +105,4 @@ export default {
</script>
<style lang="" scoped>
</style>
</style>

View File

@@ -48,4 +48,4 @@ export default {
<style lang="" scoped>
</style>
</style>

View File

@@ -3,7 +3,7 @@
<v-tooltip
:disabled="tooltip === undefined"
right
open-delay="300"
open-delay="600"
>
<template v-slot:activator="{ on, attrs }">
<span
@@ -24,4 +24,4 @@
// eslint-disable-next-line vue/require-prop-types
props: ['text', 'tooltip'],
}
</script>
</script>

View File

@@ -1,268 +1,154 @@
<template>
<div
id="MapContainer"
style="flex-grow:1"
>
<div>
<v-row>
<v-col
align="center"
cols="12"
>
<span class="white--text">Target Location</span>
</v-col>
</v-row>
<v-row>
<v-col
align="center"
cols="12"
align-self="stretch"
>
<canvas
id="canvasId"
style="width:100%;height:100%"
class="mt-2"
width="800"
height="800"
/>
</v-col>
<v-row>
<v-col>
<v-btn
class="ml-10"
color="secondary"
@click="resetCamFirstPerson"
>
First Person
</v-btn>
</v-col>
<v-col>
<v-btn
class="ml-10"
color="secondary"
@click="resetCamThirdPerson"
>
Third Person
</v-btn>
</v-col>
</v-row>
</v-row>
</div>
</template>
<script>
import theme from "../../../theme";
import {
ArrowHelper,
BoxGeometry,
ConeGeometry,
Mesh,
MeshNormalMaterial,
PerspectiveCamera,
Quaternion,
Scene,
TrackballControls,
Vector3,
Color,
WebGLRenderer
} from "three-full";
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)');
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 {
scene: undefined,
cubes: [],
// 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
}
}
}
},
watch: {
targets: {
deep: true,
handler() {
this.drawTargets();
}
},
},
mounted() {
const scene = new Scene();
this.scene = scene;
const camera = new PerspectiveCamera(75, 800 / 800, 0.1, 1000);
this.camera = camera;
const canvas = document.getElementById("canvasId"); // getting the canvas element
this.canvas = canvas;
const renderer = new WebGLRenderer({"canvas": canvas});
this.renderer = renderer;
scene.background = new Color(0xa9a9a9)
//Set up resize handlers
this.onWindowResize();
window.addEventListener( 'resize', this.onWindowResize, false );
//Add the reference frame cues
this.refFrameCues = []
// coordinate system
this.refFrameCues.push(new ArrowHelper(new Vector3(1, 0, 0).normalize(), new Vector3(0, 0, 0),
1, // length
0xff0000,
0.1,
0.1,
))
this.refFrameCues.push(new ArrowHelper(new Vector3(0, 1, 0).normalize(), new Vector3(0, 0, 0),
1, // length
0x00ff00,
0.1,
0.1,
))
this.refFrameCues.push(new ArrowHelper(new Vector3(0, 0, 1).normalize(), new Vector3(0, 0, 0),
1, // length
0x0000ff,
0.1,
0.1,
))
//something that looks vaguely like a camera
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);
this.refFrameCues.push(camBody)
this.refFrameCues.push(camLens)
var 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;
controls.keys = [65, 83, 68];
this.controls = controls;
this.scene.add(...this.refFrameCues)
this.resetCamFirstPerson();
controls.update();
function animate() {
requestAnimationFrame(animate);
controls.update();
renderer.render(scene, camera);
//camera.updateMatrixWorld();
//console.log("================")
//console.log(camera.position);
//console.log(camera.rotation);
//console.log(camera.up);
}
this.drawTargets()
animate();
},
methods: {
drawTargets() {
this.scene.remove(...this.cubes)
this.cubes = []
for (const target of this.targets) {
const geometry = new BoxGeometry(0.3 / 5, 0.2, 0.2);
const material = new MeshNormalMaterial();
let quat = (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(quat);
this.cubes.push(cube)
let arrow = (new ArrowHelper(new Vector3(1, 0, 0).normalize(), new Vector3(0, 0, 0),
1, // length
0xff0000,
0.1,
0.1,
));
arrow.rotation.setFromQuaternion(quat)
arrow.rotateZ(-Math.PI / 2)
arrow.position.set(target.pose.x, target.pose.y, target.pose.z)
this.cubes.push(arrow);
arrow = (new ArrowHelper(new Vector3(1, 0, 0).normalize(), new Vector3(0, 0, 0),
1, // length
0x00ff00,
0.1,
0.1,
));
arrow.rotation.setFromQuaternion(quat)
// arrow.rotateX(Math.PI / 2)
arrow.position.set(target.pose.x, target.pose.y, target.pose.z)
this.cubes.push(arrow);
arrow = (new ArrowHelper(new Vector3(1, 0, 0).normalize(), new Vector3(0, 0, 0),
1, // length
0x0000ff,
0.1,
0.1,
));
arrow.setRotationFromQuaternion(quat)
arrow.rotateX(Math.PI / 2)
arrow.position.set(target.pose.x, target.pose.y, target.pose.z)
this.cubes.push(arrow);
}
if(this.cubes.length > 0)
this.scene.add(...this.cubes);
},
onWindowResize() {
var container = document.getElementById("MapContainer")
if(container){
this.canvas.width = container.clientWidth * 0.95;
this.canvas.height = container.clientWidth * 0.85;
this.camera.aspect = this.canvas.width / this.canvas.height;
this.camera.updateProjectionMatrix();
this.renderer.setSize( this.canvas.width, this.canvas.height );
}
},
resetCamThirdPerson(){
//Sets camera to third person position
this.controls.reset();
this.camera.position.set(-1.39,-1.09,1.17);
this.camera.up.set(0,0,1);
this.controls.target.set(4.0,0.0,0.0);
this.controls.update();
this.scene.add(...this.refFrameCues)
},
resetCamFirstPerson(){
//Sets camera to first person position
this.controls.reset();
this.camera.position.set(-0.1,0,0);
this.camera.up.set(0,0,1);
this.controls.target.set(0.0,0.0,0.0);
this.controls.update();
this.scene.remove(...this.refFrameCues)
},
}
}
</script>
<style scoped>
</style>
#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

@@ -1,15 +1,11 @@
<template>
<div>
<v-row
align="center"
class="pl-6"
>
<v-row align="center">
<v-col
cols="10"
md="5"
lg="10"
no-gutters
class="pa-0"
class="pt-0 pb-0 pl-6"
>
<CVselect
v-if="isCameraNameEdit === false"
@@ -63,8 +59,7 @@
cols="10"
md="5"
lg="10"
no-gutters
class="pa-0"
class="pt-0 pb-0 pl-6"
>
<CVselect
v-model="currentPipelineIndex"
@@ -144,16 +139,14 @@
<v-col
v-if="currentPipelineType >= 0"
cols="10"
md="11"
md="5"
lg="10"
no-gutters
class="pa-0"
class="pt-0 pb-0 pl-6 ml-16"
>
<CVselect
v-model="currentPipelineType"
name="Type"
tooltip="Changes the pipeline type, which changes the type of processing that will happen on input frames"
:list="['Reflective Tape', 'Colored Shape', 'AprilTag']"
:list="['Reflective', 'Shape']"
@input="e => showTypeDialog(e)"
/>
</v-col>
@@ -180,9 +173,15 @@
<v-card-text>
<CVinput
v-model="newPipelineName"
name="Name"
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>
@@ -264,6 +263,7 @@ export default {
isPipelineNameEdit: false,
namingDialog: false,
newPipelineName: "",
newPipelineType: 0,
duplicateDialog: false,
showPipeTypeDialog: false,
proposedPipelineType : 0,
@@ -277,12 +277,12 @@ export default {
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"
return "A camera by that name already Exists"
}
}
}
} else {
return "A camera name can only contain letters, numbers, and spaces"
return "A camera name can only contain letters, numbers and spaces"
}
}
return "";
@@ -385,7 +385,7 @@ export default {
if (this.isPipelineNameEdit) {
this.handleInputWithIndex("changePipelineName", this.newPipelineName);
} else {
this.handleInputWithIndex("addNewPipeline", [this.newPipelineName, this.currentPipelineType]); // 0 for reflective, 1 for colored shpae
this.handleInputWithIndex("addNewPipeline", [this.newPipelineName, this.newPipelineType]); // 0 for reflective, 1 for colored shpae
}
this.discardPipelineNameChange();
}
@@ -405,4 +405,4 @@ export default {
<style scoped>
</style>
</style>

View File

@@ -60,4 +60,4 @@
<style scoped>
</style>
</style>

View File

@@ -45,4 +45,4 @@
<style scoped>
</style>
</style>

File diff suppressed because one or more lines are too long

View File

@@ -1,7 +0,0 @@
# JSPDF Fonts
These are .js interpretations of the .tff files in the branding folder. They are used by jspdf to apply branding-approprate fonts to any .pdf file generation (ex: calibration targets)
https://peckconsulting.s3.amazonaws.com/fontconverter/fontconverter.html is the converter used to generate them.
https://www.devlinpeck.com/tutorials/jspdf-custom-font has more info creating/using them.

View File

@@ -15,15 +15,16 @@ if (process.env.NODE_ENV === "production") {
Vue.prototype.$address = location.hostname + ":5800";
}
// const wsDataURL = '//' + Vue.prototype.$address + '/websocket_data';
// import VueNativeSock from 'vue-native-websocket';
// Vue.use(VueNativeSock, wsDataURL, {
// reconnection: true,
// reconnectionDelay: 100,
// connectManually: true,
// format: "arraybuffer",
// });
const wsURL = '//' + Vue.prototype.$address + '/websocket';
import VueNativeSock from 'vue-native-websocket';
Vue.use(VueNativeSock, wsURL, {
reconnection: true,
reconnectionDelay: 100,
connectManually: true,
format: "arraybuffer",
});
Vue.use(VueAxios, axios);
Vue.prototype.$msgPack = msgPack(true);

View File

@@ -2,14 +2,14 @@ export const dataHandleMixin = {
methods: {
handleInput(key, value) {
let msg = this.$msgPack.encode({[key]: value});
this.$store.state.websocket.send(msg);
this.$socket.send(msg);
},
handleInputWithIndex(key, value, cameraIndex = this.$store.getters.currentCameraIndex) {
let msg = this.$msgPack.encode({
[key]: value,
["cameraIndex"]: cameraIndex,
});
this.$store.state.websocket.send(msg);
this.$socket.send(msg);
},
handleData(val) {
this.handleInput(val, this[val]);
@@ -22,7 +22,7 @@ export const dataHandleMixin = {
["cameraIndex"]: this.$store.getters.currentCameraIndex
}
});
this.$store.state.websocket.send(msg);
this.$socket.send(msg);
this.$emit('update')
},
handlePipelineUpdate(key, val) {
@@ -32,7 +32,7 @@ export const dataHandleMixin = {
["cameraIndex"]: this.$store.getters.currentCameraIndex
}
});
this.$store.state.websocket.send(msg);
this.$socket.send(msg);
this.$emit('update')
},
handleTruthyPipelineData(val) {
@@ -42,7 +42,7 @@ export const dataHandleMixin = {
["cameraIndex"]: this.$store.getters.currentCameraIndex
}
});
this.$store.state.websocket.send(msg);
this.$socket.send(msg);
this.$emit('update')
},
rollback(val, e) {

View File

@@ -5,11 +5,9 @@ function initColorPicker() {
if (!canvas)
canvas = document.createElement('canvas');
image = document.querySelector('#raw-stream');
if (image !== null) {
canvas.width = image.width;
canvas.height = image.height;
}
image = document.querySelector('#normal-stream');
canvas.width = image.width;
canvas.height = image.height;
}
//Called on click of the image,
@@ -124,4 +122,4 @@ function shrinkRange(range, color) {
}
export default {initColorPicker, colorPickerClick, eyeDrop, expand, shrink}
export default {initColorPicker, colorPickerClick, eyeDrop, expand, shrink}

View File

@@ -1,359 +0,0 @@
// Circular buffer storage. Externally-apparent 'length' increases indefinitely
// while any items with indexes below length-n will be forgotten (undefined
// will be returned if you try to get them, trying to set is an exception).
// n represents the initial length of the array, not a maximum
class StatsHistoryBuffer{
constructor (){
this.windowLen = 10;
this._array= new Array(this.windowLen);
this.headPtr = 0;
this.frameCount = 0;
this.bitAvgAccum = 0;
//calculated vals
this.bitRate_Mbps = 0;
this.framerate_fps = 0;
}
putAndPop(v){
this.headPtr++;
var idx = (this.headPtr)%this._array.length;
var poppedVal = this._array[idx];
this._array[idx] = v;
return poppedVal;
}
addSample(time, frameSize_bits, dispFrame_count) {
var oldVal = this.putAndPop([time, frameSize_bits, dispFrame_count]);
this.bitAvgAccum += frameSize_bits;
if(oldVal !=null){
var oldTime = oldVal[0];
var oldFrameSize = oldVal[1];
var oldFrameCount = oldVal[2];
var deltaTime_s = (time - oldTime);
this.bitAvgAccum -= oldFrameSize;
//bitrate - total bits transferred over the time period, divided by the period length
// converted to mbps
this.bitRate_Mbps = ( this.bitAvgAccum / deltaTime_s ) * (1.0/1048576.0);
//framerate - total frames displayed over the time period, divided by the period length
this.framerate_fps = (dispFrame_count - oldFrameCount) / deltaTime_s;
}
}
getText(){
return "Streaming @ " + this.framerate_fps.toFixed(1) + "FPS " + this.bitRate_Mbps.toFixed(1) + "Mbps";
}
}
export class WebsocketVideoStream{
constructor(drawDiv, streamPort, host) {
console.log("host " + host + " port " + streamPort)
this.drawDiv = drawDiv;
this.image = document.getElementById(this.drawDiv);
this.streamPort = streamPort;
this.newStreamPortReq = null;
this.serverAddr = "ws://" + host + "/websocket_cameras";
this.imgData = null;
this.imgDataTime = -1;
this.prevImgDataTime = -1;
this.imgObjURL = null;
this.frameRxCount = 0;
this.dispFrameCount = 0;
this.stats = null;
//Set up div for stream stats info provided for users
this.statsTextDiv = this.image.parentNode.appendChild(document.createElement("div"));
//Centered over the image
this.statsTextDiv.style.position = "absolute";
this.statsTextDiv.style.left = "50%";
this.statsTextDiv.style.top = "50%";
this.statsTextDiv.style.transform = "translate(-50%, -50%)";
// Big enough for a line or two of text, with centered text
this.statsTextDiv.style.padding = "0.5em"
this.statsTextDiv.style.overflow = "hidden";
this.statsTextDiv.style.textAlign = "center";
this.statsTextDiv.style.verticalAlign = "middle";
// Styled to be black with grey text
this.statsTextDiv.style.backgroundColor = "black";
this.statsTextDiv.style.color = "#9E9E9E";
this.statsTextDiv.style.borderRadius = "3px";
//Default no text
this.statsTextDiv.innerHTML = "";
// Only show on mouseover, with opacity fade-in/fade-out
this.statsTextDiv.style.opacity = "0.0";
this.statsTextDiv.style.transition = "opacity 0.25s ease 0.25s";
this.statsTextDiv.style.transitionDelay = "opacity 0.5s";
this.image.addEventListener('mouseover', () => {this.statsTextDiv.style.opacity = "0.6";});
this.statsTextDiv.addEventListener('mouseover', () => {this.statsTextDiv.style.opacity = "0.6";});
this.image.addEventListener('mouseout', () => {this.statsTextDiv.style.opacity = "0.0";});
//Display state machine descriptions
this.DSM_DISCONNECTED = "Disconnected";
this.DSM_WAIT_FOR_VALID_PORT = "Waiting for valid port ID";
this.DSM_SUBSCRIBE = "Subscribing";
this.DSM_WAIT_FOR_FIRST_FRAME = "Waiting for frame data";
this.DSM_SHOWING = "Showing Frames";
this.DSM_RESTART_UNSUBSCRIBE = "Unsubscribing";
this.DSM_RESTART_WAIT = "Waiting before resubscribe";
this.dsm_cur_state = this.DSM_DISCONNECTED;
this.dsm_prev_state = this.DSM_DISCONNECTED;
this.dsm_restart_start_time = window.performance.now();
this.dispNoStream();
this.ws_connect();
requestAnimationFrame(()=>this.animationLoop());
}
dispImageData(){
if(this.prevImgDataTime != this.imgDataTime){
//From https://stackoverflow.com/questions/67507616/set-image-src-from-image-blob/67507685#67507685
//Ensure uniqueness by making the new one before revoking the old one.
var oldURL = this.imgObjURL
this.imgObjURL = URL.createObjectURL(this.imgData);
if(oldURL != null){
URL.revokeObjectURL(oldURL)
}
//Update the image with the new mimetype and image
this.image.src = this.imgObjURL;
this.dispFrameCount++;
this.prevImgDataTime = this.imgDataTime;
} // else no new image, don't update anything
}
dispNoStream() {
this.image.src = require("../assets/loading.gif");
}
animationLoop(){
// Update time metrics
var curTime_s = window.performance.now() / 1000.0;
var timeInState = curTime_s - this.dsm_restart_start_time;
// Save previous state
this.dsm_prev_state = this.dsm_cur_state;
// Evaluate state transitions
if(this.serverConnectionActive == false){
//Any state - if the server connection goes false, always transition to disconnected
this.dsm_cur_state = this.DSM_DISCONNECTED;
} else {
//Conditional transitions
switch(this.dsm_cur_state) {
case this.DSM_DISCONNECTED:
//Immediately transition to waiting for the first frame
this.dsm_cur_state = this.DSM_WAIT_FOR_VALID_PORT;
break;
case this.DSM_WAIT_FOR_VALID_PORT:
// Wait until the user has configured a valid port
if(this.streamPort > 0){
this.dsm_cur_state = this.DSM_SUBSCRIBE;
} else {
this.dsm_cur_state = this.DSM_WAIT_FOR_VALID_PORT;
}
break;
case this.DSM_SUBSCRIBE:
// Immediately transition after subscriptions is sent
this.dsm_cur_state = this.DSM_WAIT_FOR_FIRST_FRAME;
break;
case this.DSM_WAIT_FOR_FIRST_FRAME:
if(this.imgData != null){
//we got some image data, start showing it
this.dsm_cur_state = this.DSM_SHOWING;
} else if (this.newStreamPortReq != null){
//Stream port requested changed, unsubscribe and restart
this.dsm_cur_state = this.DSM_RESTART_UNSUBSCRIBE;
} else {
this.dsm_cur_state = this.DSM_WAIT_FOR_FIRST_FRAME;
}
break;
case this.DSM_SHOWING:
if((curTime_s - this.imgDataTime) > 2.5){
//timeout, begin the restart sequence
this.dsm_cur_state = this.DSM_RESTART_UNSUBSCRIBE;
} else if (this.newStreamPortReq != null){
//Stream port requested changed, unsubscribe and restart
this.dsm_cur_state = this.DSM_RESTART_UNSUBSCRIBE;
} else {
//stay in this state.
this.dsm_cur_state = this.DSM_SHOWING;
}
break;
case this.DSM_RESTART_UNSUBSCRIBE:
//Only should spend one loop in Unsubscribe, immediately transition
this.dsm_cur_state = this.DSM_RESTART_WAIT;
break;
case this.DSM_RESTART_WAIT:
if (timeInState > 0.25) {
//we've waited long enough, go to try to re-subscribe
this.dsm_cur_state = this.DSM_WAIT_FOR_VALID_PORT;
} else {
//stay in this state.
this.dsm_cur_state = this.DSM_RESTART_WAIT;
}
break;
default:
// Shouldn't get here, default back to init
this.dsm_cur_state = this.DSM_DISCONNECTED;
}
}
//take current-state or state-transition actions
if(this.dsm_cur_state != this.dsm_prev_state){
//Any state transition
console.log("State Change: " + this.dsm_prev_state + " -> " + this.dsm_cur_state);
}
if(this.dsm_cur_state == this.DSM_SHOWING){
// Currently in SHOWING
// Show image and update status text
this.dispImageData();
this.statsTextDiv.innerHTML = this.stats.getText();
} else {
//Just show the state for debug
this.statsTextDiv.innerHTML = this.dsm_cur_state;
}
if(this.dsm_cur_state != this.DSM_SHOWING && this.dsm_prev_state == this.DSM_SHOWING ){
//Any transition out of showing - no stream
this.dispNoStream();
}
if(this.dsm_cur_state == this.DSM_RESTART_UNSUBSCRIBE){
// Currently in UNSUBSCRIBE, do the unsubscribe actions
this.stopStream();
this.dsm_restart_start_time = curTime_s;
}
if(this.dsm_cur_state == this.DSM_SUBSCRIBE){
// Currently in SUBSCRIBE, do the subscribe actions
this.startStream();
this.dsm_restart_start_time = curTime_s;
}
if(this.dsm_cur_state == this.DSM_WAIT_FOR_VALID_PORT){
// Currently waiting for a vaild port to be requested
if(this.newStreamPortReq != null){
this.streamPort = this.newStreamPortReq;
this.newStreamPortReq = null;
}
}
//Update status text
requestAnimationFrame(()=>this.animationLoop());
}
startStream() {
console.log("Subscribing to port " + this.streamPort);
this.imgData = null;
this.ws.send(JSON.stringify({"cmd": "subscribe", "port":this.streamPort}));
}
stopStream() {
console.log("Unsubscribing");
this.ws.send(JSON.stringify({"cmd": "unsubscribe"}));
this.imgData = null;
}
setPort(streamPort){
console.log("Port set to " + streamPort);
this.newStreamPortReq = streamPort;
}
ws_onOpen() {
// Set the flag allowing general server communication
this.serverConnectionActive = true;
console.log("Camera Websockets Connected!");
// New websocket connection, reset stats
this.frameRxCount = 0;
this.dispFrameCount = 0;
this.stats = new StatsHistoryBuffer();
}
ws_onClose(e) {
//Clear flags to stop server communication
this.ws = null;
this.serverConnectionActive = false;
console.log('Camera Socket is closed. Reconnect will be attempted in 0.5 second.', e.reason);
setTimeout(this.ws_connect.bind(this), 500);
if(!e.wasClean){
console.error('Socket encountered error!');
}
}
ws_onError(e){
e; //prevent unused failure
this.ws.close();
}
ws_onMessage(e){
//console.log("Got message from " + this.serverAddr)
var msgTime_s = window.performance.now() / 1000.0;
if(typeof e.data === 'string'){
//string data from host
//TODO - anything to receive info here? Maybe "available streams?"
} else {
if(e.data.size > 0){
//binary data - a frame!
//Save frame data for display in the next animation thread
this.imgData = e.data;
this.imgDataTime = msgTime_s;
//Count the incoming frame
this.frameRxCount++;
//keep the stats up to date
this.stats.addSample(msgTime_s,this.imgData.size * 8,this.dispFrameCount);
} else {
console.log("WS Stream Error: Server sent empty frame!");
}
}
}
ws_connect() {
this.serverConnectionActive = false;
this.ws = new WebSocket(this.serverAddr);
this.ws.binaryType = "blob";
this.ws.onopen = this.ws_onOpen.bind(this);
this.ws.onmessage = this.ws_onMessage.bind(this);
this.ws.onclose = this.ws_onClose.bind(this);
this.ws.onerror = this.ws_onError.bind(this);
console.log("Connecting to server " + this.serverAddr);
}
ws_close(){
this.ws.close();
}
}
export default {WebsocketVideoStream}

View File

@@ -5,4 +5,4 @@ $body-font-family: $default-font;
$heading-font-family: $default-font;
.v-application {
font-family: $default-font !important;
}
}

View File

@@ -15,16 +15,6 @@ export default new Vuex.Store({
},
state: {
backendConnected: false,
websocket: null,
ntConnectionInfo: {
connected: false,
address: "",
clients: 0,
},
networkInfo: {
possibleRios: ["Loading..."],
deviceips: ["Loading..."],
},
connectedCallbacks: [],
colorPicking: false,
logsOverlay: false,
@@ -36,8 +26,8 @@ export default new Vuex.Store({
tiltDegrees: 0.0,
currentPipelineIndex: 0,
pipelineNicknames: ["Unknown"],
outputStreamPort: 0,
inputStreamPort: 0,
outputStreamPort: 1181,
inputStreamPort: 1182,
nickname: "Unknown",
videoFormatList: [
{
@@ -52,15 +42,13 @@ export default new Vuex.Store({
isFovConfigurable: true,
calibrated: false,
currentPipelineSettings: {
pipelineType: 4, // One of "calib", "driver", "reflective", "shape", "AprilTag"
pipelineType: 2, // One of "calib", "driver", "reflective", "shape"
// 2 is reflective
// Settings that apply to all pipeline types
cameraExposure: 1,
cameraBrightness: 2,
cameraAutoExposure: false,
cameraRedGain: 3,
cameraBlueGain: 4,
cameraGain: 3,
inputImageRotationMode: 0,
cameraVideoModeIndex: 0,
streamingFrameDivisor: 0,
@@ -69,13 +57,10 @@ export default new Vuex.Store({
hsvHue: [0, 15],
hsvSaturation: [0, 15],
hsvValue: [0, 25],
hueInverted: false,
contourArea: [0, 12],
contourRatio: [0, 12],
contourFullness: [0, 12],
contourSpecklePercentage: 5,
contourFilterRangeX: 5,
contourFilterRangeY: 5,
contourGroupingMode: 0,
contourIntersection: 0,
contourSortMode: 0,
@@ -90,16 +75,7 @@ export default new Vuex.Store({
cornerDetectionAccuracyPercentage: 10,
// Settings that apply to AprilTag
tagFamily: 0,
decimate: 1.0,
blur: 0.0,
threads: 1,
debug: false,
refineEdges: true,
numIterations: 1,
decisionMargin: 0,
hammingDist: 0,
// Settings that apply to shape
}
}
],
@@ -113,18 +89,9 @@ export default new Vuex.Store({
skew: 0,
area: 0,
// 3D only
pose: {x: 1, y: 1, z: 0, qw: 1, qx: 0, qy: 0, qz: 0},
},
{
// Available in both 2D and 3D
pitch: 0,
yaw: 0,
skew: 0,
area: 0,
// 3D only
pose: {x: 2, y: 3, z: 0, qw: 1, qx: 0, qy: 0, qz: 0},
}]
},
pose: {x: 0, y: 0, rot: 0},
}]
},
settings: {
general: {
version: "Unknown",
@@ -170,16 +137,12 @@ export default new Vuex.Store({
},
mutations: {
compactMode: set('compactMode'),
websocket: set('websocket'),
cameraSettings: set('cameraSettings'),
currentCameraIndex: set('currentCameraIndex'),
selectedOutputs: set('selectedOutputs'),
settings: set('settings'),
calibrationData: set('calibrationData'),
metrics: set('metrics'),
ntConnectionInfo: set('ntConnectionInfo'),
networkInfo: set('networkInfo'),
backendConnected: set('backendConnected'),
logString: (state, newStr) => {
const str = state.logMessages;
str.push(newStr);
@@ -284,4 +247,4 @@ export default new Vuex.Store({
calibrationList: state => state.cameraSettings[state.currentCameraIndex].calibrations,
pipelineType: state => state.cameraSettings[state.currentCameraIndex].currentPipelineSettings.pipelineType
}
})
})

View File

@@ -69,4 +69,4 @@ export default {
}
}
};
};

View File

@@ -5,4 +5,4 @@ const theme = Object.freeze({
background: "#232C37",
});
export default theme;
export default theme;

View File

@@ -19,8 +19,8 @@
<CVselect
v-model="currentCameraIndex"
name="Camera"
select-cols="10"
:list="$store.getters.cameraList"
:select-cols="$vuetify.breakpoint.mdAndUp ? 10 : 7"
@input="handleInput('currentCamera',currentCameraIndex)"
/>
<CVnumberinput
@@ -28,7 +28,13 @@
:tooltip="cameraSettings.isFovConfigurable ? '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'"
name="Maximum diagonal FOV"
:disabled="!cameraSettings.isFovConfigurable"
:label-cols="$vuetify.breakpoint.mdAndUp ? undefined : 7"
/>
<br>
<CVnumberinput
v-model="cameraSettings.tiltDegrees"
name="Camera pitch"
tooltip="How many degrees above the horizontal the physical camera is tilted"
:step="0.01"
/>
<br>
<v-btn
@@ -60,51 +66,43 @@
cols="12"
md="6"
>
<v-form
ref="form"
v-model="settingsValid"
>
<CVselect
v-model="selectedFilteredResIndex"
name="Resolution"
select-cols="7"
:list="stringResolutionList"
:disabled="isCalibrating"
tooltip="Resolution to calibrate at (you will have to calibrate every resolution you use 3D mode on)"
/>
<CVselect
v-model="boardType"
name="Board Type"
select-cols="7"
:list="['Chessboard', 'Dot Grid']"
:disabled="isCalibrating"
tooltip="Calibration board pattern to use"
/>
<CVnumberinput
v-model="squareSizeIn"
name="Pattern Spacing (in)"
tooltip="Spacing between pattern features in inches"
:disabled="isCalibrating"
:rules="[v => (v > 0) || 'Size must be positive']"
:label-cols="$vuetify.breakpoint.mdAndUp ? 5 : 7"
/>
<CVnumberinput
v-model="boardWidth"
name="Board width"
tooltip="Width of the board in dots or chessboard squares"
:disabled="isCalibrating"
:rules="[v => (v >= 4) || 'Width must be at least 4']"
:label-cols="$vuetify.breakpoint.mdAndUp ? 5 : 7"
/>
<CVnumberinput
v-model="boardHeight"
name="Board height"
tooltip="Height of the board in dots or chessboard squares"
:disabled="isCalibrating"
:rules="[v => (v >= 4) || 'Height must be at least 4']"
:label-cols="$vuetify.breakpoint.mdAndUp ? 5 : 7"
/>
</v-form>
<CVselect
v-model="selectedFilteredResIndex"
name="Resolution"
select-cols="7"
:list="stringResolutionList"
:disabled="isCalibrating"
tooltip="Resolution to calibrate at (you will have to calibrate every resolution you use 3D mode on)"
/>
<CVselect
v-model="boardType"
name="Board Type"
select-cols="7"
:list="['Chessboard', 'Dot Grid']"
:disabled="isCalibrating"
tooltip="Calibration board pattern to use"
/>
<CVnumberinput
v-model="squareSizeIn"
name="Pattern Spacing (in)"
label-cols="5"
tooltip="Spacing between pattern features in inches"
:disabled="isCalibrating"
/>
<CVnumberinput
v-model="boardWidth"
name="Board width"
label-cols="5"
tooltip="Width of the board in dots or chessboard squares; with the standard chessboard, this is usually 8"
:disabled="isCalibrating"
/>
<CVnumberinput
v-model="boardHeight"
name="Board height"
label-cols="5"
tooltip="Height of the board in dots or chessboard squares; with the standard chessboard, this is usually 8"
:disabled="isCalibrating"
/>
</v-col>
<!-- Calibrated table -->
@@ -138,24 +136,6 @@
text="Standard Deviation"
/>
</th>
<th class="text-center">
<tooltipped-label
tooltip="Estimated Horizontal FOV, in degrees"
text="Horizontal FOV"
/>
</th>
<th class="text-center">
<tooltipped-label
tooltip="Estimated Vertical FOV, in degrees"
text="Vertical FOV"
/>
</th>
<th class="text-center">
<tooltipped-label
tooltip="Estimated Diagonal FOV, in degrees"
text="Diagonal FOV"
/>
</th>
</tr>
</thead>
<tbody>
@@ -168,9 +148,6 @@
{{ isCalibrated(value) ? value.mean.toFixed(2) + "px" : "—" }}
</td>
<td> {{ isCalibrated(value) ? value.standardDeviation.toFixed(2) + "px" : "—" }} </td>
<td> {{ isCalibrated(value) ? value.horizontalFOV.toFixed(2) + "°" : "—" }} </td>
<td> {{ isCalibrated(value) ? value.verticalFOV.toFixed(2) + "°" : "—" }} </td>
<td> {{ isCalibrated(value) ? value.diagonalFOV.toFixed(2) + "°" : "—" }} </td>
</tr>
</tbody>
</v-simple-table>
@@ -194,13 +171,10 @@
>
<CVslider
v-model="$store.getters.currentPipelineSettings.cameraExposure"
:disabled="$store.getters.currentPipelineSettings.cameraAutoExposure"
name="Exposure"
:min="0"
:max="100"
slider-cols="8"
step="0.1"
tooltip="Directly controls how much light is allowed to fall onto the sensor, which affects apparent brightness"
@input="e => handlePipelineUpdate('cameraExposure', e)"
/>
<CVslider
@@ -211,43 +185,14 @@
slider-cols="8"
@input="e => handlePipelineUpdate('cameraBrightness', e)"
/>
<CVswitch
v-model="$store.getters.currentPipelineSettings.cameraAutoExposure"
class="pt-2"
name="Auto Exposure"
tooltip="Enables or Disables camera automatic adjustment for current lighting conditions"
@input="e => handlePipelineUpdate('cameraAutoExposure', e)"
/>
<CVslider
v-if="cameraGain >= 0"
v-model="cameraGain"
name="Camera Gain"
min="0"
max="100"
tooltip="Controls camera gain, similar to brightness"
:slider-cols="largeBox"
@input="handlePipelineData('cameraGain')"
@rollback="e => rollback('cameraGain', e)"
/>
<CVslider
v-if="$store.getters.currentPipelineSettings.cameraRedGain !== -1"
v-model="$store.getters.currentPipelineSettings.cameraRedGain"
name="Red AWB Gain"
min="0"
max="100"
tooltip="Controls red automatic white balance gain, which affects how the camera captures colors in different conditions"
:slider-cols="8"
@input="e => handlePipelineData('cameraRedGain', e)"
/>
<CVslider
v-if="$store.getters.currentPipelineSettings.cameraBlueGain !== -1"
v-model="$store.getters.currentPipelineSettings.cameraBlueGain"
name="Blue AWB Gain"
min="0"
max="100"
tooltip="Controls blue automatic white balance gain, which affects how the camera captures colors in different conditions"
:slider-cols="8"
@input="e => handlePipelineData('cameraBlueGain', e)"
v-if="$store.getters.currentPipelineSettings.cameraGain !== -1"
v-model="$store.getters.currentPipelineSettings.cameraGain"
name="Gain"
:min="0"
:max="100"
slider-cols="8"
@input="e => handlePipelineUpdate('cameraGain', e)"
/>
</v-col>
</v-row>
@@ -282,14 +227,19 @@
small
outlined
style="width: 100%;"
:disabled="!settingsValid"
@click="downloadBoard"
>
<v-icon left>
mdi-download
</v-icon>
Download Target
Download Chessboard
</v-btn>
<a
ref="calibrationFile"
style="color: black; text-decoration: none; display: none"
:href="require('../assets/chessboard.png')"
download="chessboard.png"
/>
</v-col>
</v-row>
</div>
@@ -302,8 +252,7 @@
>
<template>
<CVimage
:id="cameras-cal"
:idx=1
:address="$store.getters.streamAddress[1]"
:disconnected="!$store.state.backendConnected"
scale="100"
style="border-radius: 5px;"
@@ -374,11 +323,8 @@
import CVselect from '../components/common/cv-select';
import CVnumberinput from '../components/common/cv-number-input';
import CVslider from '../components/common/cv-slider';
import CVswitch from '../components/common/cv-switch';
import CVimage from "../components/common/cv-image";
import TooltippedLabel from "../components/common/cv-tooltipped-label";
import jsPDF from "jspdf";
import "../jsPDFFonts/Prompt-Regular-normal.js";
export default {
name: 'Cameras',
@@ -387,7 +333,6 @@ export default {
CVselect,
CVnumberinput,
CVslider,
CVswitch,
CVimage
},
data() {
@@ -396,12 +341,11 @@ export default {
calibrationInProgress: false,
calibrationFailed: false,
filteredVideomodeIndex: 0,
settingsValid: true,
}
},
computed: {
disallowCalibration() {
return !(this.calibrationData.boardType === 0 || this.calibrationData.boardType === 1) || !this.settingsValid;
return !(this.calibrationData.boardType === 0 || this.calibrationData.boardType === 1);
},
checkCancellation() {
if (this.isCalibrating) {
@@ -421,15 +365,6 @@ export default {
}
},
cameraGain: {
get() {
return parseInt(this.$store.getters.currentPipelineSettings.cameraGain)
},
set(val) {
this.$store.commit("mutatePipeline", {"cameraGain": parseInt(val)});
}
},
// Makes sure there's only one entry per resolution
filteredResolutionList: {
get() {
@@ -442,9 +377,6 @@ export default {
if (calib != null) {
it['standardDeviation'] = calib.standardDeviation;
it['mean'] = calib.perViewErrors.reduce((a, b) => a + b) / calib.perViewErrors.length;
it['horizontalFOV'] = 2 * Math.atan2(it.width/2,calib.intrinsics[0]) * (180/Math.PI);
it['verticalFOV'] = 2 * Math.atan2(it.height/2,calib.intrinsics[4]) * (180/Math.PI);
it['diagonalFOV'] = 2 * Math.atan2(Math.sqrt(it.width**2 + (it.height/(calib.intrinsics[4]/calib.intrinsics[0]))**2)/2,calib.intrinsics[0]) * (180/Math.PI);
}
filtered.push(it);
}
@@ -453,11 +385,13 @@ export default {
return filtered
}
},
stringResolutionList: {
get() {
return this.filteredResolutionList.map(res => `${res['width']} X ${res['height']}`);
}
},
cameraSettings: {
get() {
return this.$store.getters.currentCameraSettings;
@@ -466,6 +400,7 @@ export default {
this.$store.commit('cameraSettings', value);
}
},
boardType: {
get() {
return this.calibrationData.boardType
@@ -551,103 +486,16 @@ export default {
return ret;
},
downloadBoard() {
// Generates a .pdf of a board for calibration and downloads it
//Murica paper.
var doc = new jsPDF({unit: 'in', format:'letter'});
var paper_x = 8.5;
var paper_y = 11.0;
//Load in custom fonts
console.log(doc.getFontList());
doc.setFont('Prompt-Regular');
doc.setFontSize(12);
// Common Parameters
var num_x = this.boardWidth;
var num_y = this.boardHeight;
var patternSize = this.squareSizeIn;
var isCheckerboard = (this.boardType==0);
var x_coord = 0.0;
var y_coord = 0.0;
var x_idx = 0;
var y_idx = 0;
var start_x = 0;
var start_y = 0;
var annotation = num_x + " x " + num_y + " | " + patternSize + "in "
if(isCheckerboard){
///////////////////////////////////////////
// Checkerboard Pattern
start_x = paper_x/2.0 - (num_x * patternSize)/2.0;
start_y = paper_y/2.0 - (num_y * patternSize)/2.0;
for(y_idx = 0; y_idx < num_y; y_idx++){
for(x_idx = 0; x_idx < num_x; x_idx++){
x_coord = start_x + x_idx * patternSize;
y_coord = start_y + y_idx * patternSize;
if((x_idx + y_idx) % 2 == 0){
doc.rect(x_coord, y_coord, patternSize, patternSize, "F");
}
}
}
} else {
///////////////////////////////////////////
// Assymetric Dot-Grid Pattern
// see https://github.com/opencv/opencv/blob/b450dd7a87bc69997a8417d94bdfb87427a9fe62/modules/calib3d/src/circlesgrid.cpp#L437
// as well as FindBoardCornersPipe.java's Dotboard implementation
start_x = paper_x/2.0 - ((2*(num_x-1) + (num_y-1) % 2) * patternSize)/2.0;
start_y = paper_y/2.0 - (num_y-1 * patternSize)/2.0;
// Dot Grid Pattern
for(y_idx = 0; y_idx < num_y; y_idx++){
for(x_idx = 0; x_idx < num_x; x_idx++){
x_coord = start_x + (2*x_idx + y_idx % 2) * patternSize;
y_coord = start_y + y_idx * patternSize;
doc.circle(x_coord, y_coord, patternSize/4.0, "F");
}
}
}
///////////////////////////////////////////
// Draw a fixed size inch ruler pattern to
// help users debug their printers
var lineStartX = 1.0;
var lineEndX = paper_x - lineStartX;
var lineY = paper_y - 1.0;
doc.setFont('Prompt-Regular');
doc.setLineWidth(0.01);
doc.line(lineStartX, lineY, lineEndX, lineY);
var segIdx = 0;
for(var tickX = lineStartX; tickX <= lineEndX; tickX += 1.0){
doc.line(tickX, lineY, tickX, lineY + 0.25);
doc.text(String(segIdx) + (segIdx == 0 ? " in" : ""), tickX + 0.1, lineY + 0.25);
segIdx++;
}
///////////////////////////////////////////
// Annotate what was drawn + branding
var img = new Image();
img.src = require('@/assets/logoMono.png');
doc.addImage(img, 'PNG', 1.0, 0.75, 1.4, 0.5 );
doc.setFont('Prompt-Regular');
doc.text(annotation, paper_x-1.0, 1.0, {maxWidth:(paper_x - 2.0)/2, align:"right"});
doc.save("calibrationTarget.pdf");
this.axios.get("http://" + this.$address + require('../assets/chessboard.png'), {responseType: 'blob'}).then((response) => {
require('downloadjs')(response.data, "Calibration Board", "image/png");
});
},
sendCameraSettings() {
this.axios.post("http://" + this.$address + "/api/settings/camera", {
"settings": this.cameraSettings,
"index": this.$store.state.currentCameraIndex
}).then(response => {
}).then(
function (response) {
if (response.status === 200) {
this.$store.state.saveBar = true;
}
@@ -668,15 +516,14 @@ export default {
if (this.isCalibrating === true) {
data['takeCalibrationSnapshot'] = true
} else {
// This store prevents an edge case of a user not selecting a different resolution, which causes the set logic to not be called
this.$store.commit('mutateCalibrationState', {['videoModeIndex']: this.filteredResolutionList[this.selectedFilteredResIndex].index});
const calData = this.calibrationData;
calData.isCalibrating = true;
data['startPnpCalibration'] = calData;
console.log("starting calibration with index " + calData.videoModeIndex);
}
this.$store.commit('currentPipelineIndex', -2);
this.$store.state.websocket.send(this.$msgPack.encode(data));
this.$socket.send(this.$msgPack.encode(data));
},
sendCalibrationFinish() {
console.log("finishing calibration for index " + this.$store.getters.currentCameraIndex);
@@ -716,4 +563,4 @@ export default {
.v-data-table th, td {
font-size: 1rem !important;
}
</style>
</style>

View File

@@ -30,13 +30,13 @@
:class="fpsTooLow ? 'ml-2 mt-1' : 'mt-2'"
x-small
label
:color="fpsTooLow ? 'error' : 'transparent'"
:color="fpsTooLow ? 'red' : 'transparent'"
:text-color="fpsTooLow ? 'white' : 'grey'"
>
<span class="pr-1">Processing @ {{ Math.round($store.state.pipelineResults.fps) }}&nbsp;FPS &ndash;</span>
<span v-if="fpsTooLow && !$store.getters.currentPipelineSettings.inputShouldShow && $store.getters.pipelineType == 2">HSV thresholds are too broad; narrow them for better performance</span>
<span v-else-if="$fpsTooLow && getters.currentCameraSettings.inputShouldShow">stop viewing the raw stream for better performance</span>
<span v-else>{{ Math.min(Math.round($store.state.pipelineResults.latency), 9999) }} ms latency</span>
<span class="pr-1">{{ Math.round($store.state.pipelineResults.fps) }}&nbsp;FPS &ndash;</span>
<span v-if="!fpsTooLow">{{ Math.min(Math.round($store.state.pipelineResults.latency), 100) }} ms latency</span>
<span v-else-if="!$store.getters.currentPipelineSettings.inputShouldShow">HSV thresholds are too broad; narrow them for better performance</span>
<span v-else>stop viewing the color stream for better performance</span>
</v-chip>
<v-switch
v-model="driverMode"
@@ -58,16 +58,15 @@
>
<div style="position: relative; width: 100%; height: 100%;">
<cv-image
:id="idx === 0 ? 'raw-stream' : 'processed-stream'"
:id="idx === 0 ? 'normal-stream' : ''"
ref="streams"
:idx=idx
:address="$store.getters.streamAddress[idx]"
:disconnected="!$store.state.backendConnected"
scale="100"
:max-height="$store.getters.isDriverMode ? '40vh' : '300px'"
:max-height-md="$store.getters.isDriverMode ? '50vh' : '380px'"
:max-height-lg="$store.getters.isDriverMode ? '55vh' : '390px'"
:max-height-md="$store.getters.isDriverMode ? '50vh' : '320px'"
:max-height-xl="$store.getters.isDriverMode ? '60vh' : '450px'"
:alt="'Stream ' + idx"
:alt="'Stream' + idx"
:color-picking="$store.state.colorPicking && idx === 0"
@click="onImageClick"
/>
@@ -85,7 +84,10 @@
<v-card
color="primary"
>
<camera-and-pipeline-select />
<!-- <v-btn @click="onCamNameChange">-->
<!-- Reload-->
<!-- </v-btn>-->
<camera-and-pipeline-select @camera-name-changed="reloadStreams" />
</v-card>
<v-card
:disabled="$store.getters.isDriverMode || $store.state.colorPicking"
@@ -96,6 +98,7 @@
align="center"
class="pl-3 pr-3"
>
<!-- -->
<v-col lg="12">
<p style="color: white;">
Processing mode:
@@ -136,15 +139,15 @@
color="secondary"
class="fill"
>
<v-icon>mdi-import</v-icon>
<span>Raw</span>
<v-icon>mdi-palette</v-icon>
<span>Normal</span>
</v-btn>
<v-btn
color="secondary"
class="fill"
>
<v-icon>mdi-export</v-icon>
<span>Processed</span>
<v-icon>mdi-compare</v-icon>
<span>Threshold</span>
</v-btn>
</v-btn-toggle>
</v-col>
@@ -175,7 +178,7 @@
slider-color="accent"
>
<v-tab
v-for="(tab, i) in tabs"
v-for="(tab, i) in tabs.filter(it => it.name !== '3D' || $store.getters.currentPipelineSettings.solvePNPEnabled)"
:key="i"
>
{{ tab.name }}
@@ -195,24 +198,21 @@
</v-col>
</v-row>
</v-container>
<!-- snack bar and modal -->
<v-snackbar
v-model="showNTWarning"
color="error"
timeout="-1"
v-model="snackbar"
:timeout="3000"
top
color="error"
>
{{ $store.state.settings.networkSettings.runNTServer ?
"NetworkTables server enabled! PhotonLib may not work." :
"NetworkTables not connected! Are you on a network with a robot?" }}
<template v-slot:action>
<v-btn
text
@click="hideNTWarning = true"
>
Hide
</v-btn>
</template>
<span style="color:#000">Can not remove the only pipeline!</span>
<v-btn
color="black"
text
@click="snackbar = false"
>
Close
</v-btn>
</v-snackbar>
<v-dialog
@@ -261,9 +261,7 @@ import ThresholdTab from './PipelineViews/ThresholdTab';
import ContoursTab from './PipelineViews/ContoursTab';
import OutputTab from './PipelineViews/OutputTab';
import TargetsTab from "./PipelineViews/TargetsTab";
import Map3DTab from './PipelineViews/Map3DTab';
import PnPTab from './PipelineViews/PnPTab';
import AprilTagTab from './PipelineViews/AprilTagTab';
export default {
name: 'Pipeline',
@@ -275,17 +273,15 @@ export default {
ContoursTab,
OutputTab,
TargetsTab,
Map3DTab,
PnPTab,
AprilTagTab,
},
data() {
return {
selectedTabsData: [0, 0, 0, 0],
snackbar: false,
counterData: 0,
dialog: false,
processingModeOverride: false,
hideNTWarning: false,
processingModeOverride: false
}
},
computed: {
@@ -312,33 +308,20 @@ export default {
name: "Contours",
component: "ContoursTab",
},
apriltag: {
name: "AprilTag",
component: "AprilTagTab",
},
output: {
name: "Output",
component: "OutputTab",
},
targets: {
name: "Targets",
name: "Target Info",
component: "TargetsTab",
},
pnp: {
name: "PnP",
component: "PnPTab",
},
map3d: {
name: "3D",
component: "Map3DTab",
component: "PnPTab",
}
};
// If not in 3d, name "3D" is illegal
const allow3d = this.$store.getters.currentPipelineSettings.solvePNPEnabled;
// If in apriltag, "Threshold" and "Contours" are illegal -- otherwise "AprilTag" is
const isAprilTag = (this.$store.getters.currentPipelineSettings.pipelineType - 2) === 2;
// 2D array of tab names and component names; each sub-array is a separate tab group
let ret = [];
if (this.$vuetify.breakpoint.smAndDown || this.$store.getters.isDriverMode || (this.$vuetify.breakpoint.mdAndDown && !this.$store.state.compactMode)) {
@@ -346,37 +329,22 @@ export default {
ret[0] = Object.values(tabs);
} else if (this.$vuetify.breakpoint.mdAndDown || !this.$store.state.compactMode) {
// Two tab groups, one with "input, threshold, contours, output" and the other with "target info, 3D"
ret[0] = [tabs.input, tabs.threshold, tabs.contours, tabs.apriltag, tabs.output];
ret[1] = [tabs.targets, tabs.pnp, tabs.map3d];
ret[0] = [tabs.input, tabs.threshold, tabs.contours, tabs.output];
ret[1] = [tabs.targets, tabs.pnp];
} else if (this.$vuetify.breakpoint.lgAndDown) {
// Three tab groups, one with "input", one with "threshold, contours, output", and the other with "target info, 3D"
ret[0] = [tabs.input];
ret[1] = [tabs.threshold, tabs.contours, tabs.apriltag, tabs.output];
ret[2] = [tabs.targets, tabs.pnp, tabs.map3d];
ret[1] = [tabs.threshold, tabs.contours, tabs.output];
ret[2] = [tabs.targets, tabs.pnp];
} else if (this.$vuetify.breakpoint.xl) {
// Three tab groups, one with "input", one with "threshold, contours", and the other with "output, target info, 3D"
ret[0] = [tabs.input];
ret[1] = [tabs.threshold];
ret[2] = [tabs.contours, tabs.apriltag, tabs.output];
ret[3] = [tabs.targets, tabs.pnp, tabs.map3d];
ret[2] = [tabs.contours, tabs.output];
ret[3] = [tabs.targets, tabs.pnp];
}
for(let i = 0; i < ret.length; i++) {
const group = ret[i];
// All the tabs we allow
const filteredGroup = group.filter(it =>
!(!allow3d && it.name === "3D") //Filter out 3D tab any time 3D isn't calibrated
&& !((!allow3d || isAprilTag) && it.name === "PnP") //Filter out the PnP config tab if 3D isn't available, or we're doing Apriltags
&& !(isAprilTag && (it.name === "Threshold")) //Filter out threshold tab if we're doing apriltags
&& !(isAprilTag && (it.name === "Contours")) //Filter out contours if we're doing Apriltag
&& !(!isAprilTag && it.name === "AprilTag") //Filter out apriltag unless we actually are doing Apriltags
);
ret[i] = filteredGroup;
}
// One last filter to remove empty lists
return ret.filter(it => it !== undefined && it.length > 0);
return ret;
}
},
processingMode: {
@@ -453,17 +421,6 @@ export default {
.some(e => e.width === resolution.width && e.height === resolution.height)
}
},
isRobotConnected: {
get() {
// return this.$store.state.ntConnectionInfo.connected && this.$store.state.backendConnected;
return true;
}
},
showNTWarning: {
get() {
return (!this.$store.state.ntConnectionInfo.connected || this.$store.state.settings.networkSettings.runNTServer) && this.$store.state.settings.networkSettings.teamNumber > 0 && this.$store.state.backendConnected && !this.hideNTWarning;
}
},
},
created() {
this.$store.state.connectedCallbacks.push(this.reloadStreams)
@@ -512,4 +469,4 @@ th {
width: 80px;
text-align: center;
}
</style>
</style>

View File

@@ -1,196 +0,0 @@
<template>
<div>
<v-select
v-model="selectedFamily"
dark
color="accent"
item-color="secondary"
label="Select target family"
:items="familyList"
@input="handlePipelineUpdate('tagFamily', familyList.indexOf(selectedFamily))"
/>
<v-select
v-model="selectedModel"
dark
color="accent"
item-color="secondary"
label="Select a target model"
:items="targetList"
item-text="name"
item-value="data"
@input="handlePipelineUpdate('targetModel', targetList.indexOf(selectedModel) + 6)"
/>
<CVslider
v-model="decimate"
class="pt-2"
slider-cols="8"
name="Decimate"
min="1"
max="8"
step="1.0"
tooltip="Increases FPS at the expense of range by reducing image resolution initially"
@input="handlePipelineData('decimate')"
/>
<CVslider
v-model="blur"
class="pt-2"
slider-cols="8"
name="Blur"
min="0"
max="5"
step=".01"
tooltip="Gaussian blur added to the image, high FPS cost for slightly decreased noise"
@input="handlePipelineData('blur')"
/>
<CVslider
v-model="threads"
class="pt-2"
slider-cols="8"
name="Threads"
min="1"
max="8"
step="1"
tooltip="Number of threads spawned by the AprilTag detector"
@input="handlePipelineData('threads')"
/>
<CVswitch
v-model="refineEdges"
class="pt-2"
slider-cols="8"
name="Refine Edges"
tooltip="Further refines the apriltag corner position initial estimate, suggested left on"
@input="handlePipelineData('refineEdges')"
/>
<CVslider
v-model="hammingDist"
class="pt-2 pb-4"
slider-cols="8"
name="Max error bits"
min="0"
max="10"
step="1"
tooltip="Maximum number of error bits to correct; potential tags with more will be thrown out. For smaller tags (like 16h5), set this as low as possible."
@input="handlePipelineData('hammingDist')"
/>
<CVslider
v-model="decisionMargin"
class="pt-2 pb-4"
slider-cols="8"
name="Decision Margin Cutoff"
min="0"
max="250"
step="1"
tooltip="Tags with a 'margin' (decoding quality score) less than this wil be rejected. Increase this to reduce the number of false positive detections"
@input="handlePipelineData('decisionMargin')"
/>
<CVslider
v-model="numIterations"
class="pt-2 pb-4"
slider-cols="8"
name="Pose Estimation Iterations"
min="0"
max="500"
step="1"
tooltip="Number of iterations the pose estimation algorithm will run, 50-100 is a good starting point"
@input="handlePipelineData('numIterations')"
/>
</div>
</template>
<script>
import CVslider from '../../components/common/cv-slider'
import CVswitch from '../../components/common/cv-switch'
export default {
name: "AprilTag",
components: {
CVslider,
CVswitch,
},
data() {
return {
familyList: ["tag36h11", "tag25h9", "tag16h5"],
// Selected model is offset (ew) by 6 from the photon ordinal, as we only wanna show the 36h11 and 16h5 options
targetList: ['6.5in (36h11) AprilTag', '6in (16h5) AprilTag'], //Keep in sync with TargetModel.java
}
},
computed: {
selectedModel: {
get() {
let ret = this.$store.getters.currentPipelineSettings.targetModel - 6
return this.targetList[ret];
},
set(val) {
this.$store.commit("mutatePipeline", {"targetModel": this.targetList.indexOf(val) + 6})
}
},
selectedFamily: {
get() {
let ret = this.$store.getters.currentPipelineSettings.tagFamily
return this.familyList[ret];
},
set(val) {
this.$store.commit("mutatePipeline", {"tagFamily": this.familyList.indexOf(val)})
}
},
decimate: {
get() {
return this.$store.getters.currentPipelineSettings.decimate
},
set(val) {
this.$store.commit("mutatePipeline", {"decimate": val});
}
},
hammingDist: {
get() {
return this.$store.getters.currentPipelineSettings.hammingDist
},
set(val) {
this.$store.commit("mutatePipeline", {"hammingDist": val});
}
},
decisionMargin: {
get() {
return this.$store.getters.currentPipelineSettings.decisionMargin
},
set(val) {
this.$store.commit("mutatePipeline", {"decisionMargin": val});
}
},
numIterations: {
get() {
return this.$store.getters.currentPipelineSettings.numIterations
},
set(val) {
this.$store.commit("mutatePipeline", {"numIterations": val});
}
},
blur: {
get() {
return this.$store.getters.currentPipelineSettings.blur
},
set(val) {
this.$store.commit("mutatePipeline", {"blur": val});
}
},
threads: {
get() {
return this.$store.getters.currentPipelineSettings.threads
},
set(val) {
this.$store.commit("mutatePipeline", {"threads": val});
}
},
refineEdges: {
get() {
return this.$store.getters.currentPipelineSettings.refineEdges
},
set(val) {
this.$store.commit("mutatePipeline", {"refineEdges": val});
}
},
},
methods: {
}
}
</script>

View File

@@ -1,169 +1,140 @@
<template>
<div>
<CVrangeSlider
v-model="contourArea"
name="Area"
min="0"
max="100"
step="0.01"
@input="handlePipelineData('contourArea')"
v-model="contourArea"
name="Area"
min="0"
max="100"
step="0.1"
@input="handlePipelineData('contourArea')"
/>
<CVrangeSlider
v-if="currentPipelineType() !== 3"
v-model="contourRatio"
name="Ratio (W/H)"
tooltip="Min and max ratio between the width and height of a contour's bounding rectangle"
min="0"
max="100"
step="0.1"
@input="handlePipelineData('contourRatio')"
/>
<CVselect
v-model="contourTargetOrientation"
name="Target Orientation"
tooltip="Used to determine how to calculate target landmarks, as well as aspect ratio"
:list="['Portrait', 'Landscape']"
@input="handlePipelineData('contourTargetOrientation')"
@rollback="e=> rollback('contourTargetOrientation', e)"
v-model="contourRatio"
v-if="currentPipelineType() !== 3"
name="Ratio (W/H)"
tooltip="Min and max ratio between the width and height of a contour's bounding rectangle"
min="0"
max="100"
step="0.1"
@input="handlePipelineData('contourRatio')"
/>
<CVrangeSlider
v-if="currentPipelineType() !== 3"
v-model="contourFullness"
name="Fullness"
tooltip="Min and max ratio between a contour's area and its bounding rectangle"
min="0"
max="100"
@input="handlePipelineData('contourFullness')"
v-model="contourFullness"
v-if="currentPipelineType() !== 3"
name="Fullness"
tooltip="Min and max ratio between a contour's area and its bounding rectangle"
min="0"
max="100"
@input="handlePipelineData('contourFullness')"
/>
<CVrangeSlider
v-if="currentPipelineType() === 3"
v-model="contourPerimeter"
name="Perimeter"
tooltip="Min and max perimeter of the shape, in pixels"
min="0"
max="4000"
@input="handlePipelineData('contourPerimeter')"
v-model="contourPerimeter"
v-if="currentPipelineType() === 3"
name="Perimeter"
tooltip="Min and max perimeter of the shape, in pixels"
min="0"
max="4000"
@input="handlePipelineData('contourPerimeter')"
/>
<CVslider
v-model="contourSpecklePercentage"
name="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="largeBox"
@input="handlePipelineData('contourSpecklePercentage')"
v-model="contourSpecklePercentage"
name="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="largeBox"
@input="handlePipelineData('contourSpecklePercentage')"
/>
<template v-if="currentPipelineType() !== 3">
<CVslider
v-model="contourFilterRangeX"
name="X filter tightness"
tooltip="Rejects contours whose center X is further than X standard deviations above/below the mean X location"
min="0.1"
max="4"
step="0.1"
:slider-cols="largeBox"
@input="handlePipelineData('contourFilterRangeX')"
/>
<CVslider
v-model="contourFilterRangeY"
name="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="largeBox"
@input="handlePipelineData('contourFilterRangeY')"
<CVselect
v-model="contourGroupingMode"
name="Target Grouping"
tooltip="Whether or not every two targets are paired with each other (good for e.g. 2019 targets)"
:select-cols="largeBox"
:list="['Single','Dual']"
@input="handlePipelineData('contourGroupingMode')"
/>
<CVselect
v-model="contourGroupingMode"
name="Target Grouping"
tooltip="Whether or not every two targets are paired with each other (good for e.g. 2019 targets)"
:select-cols="largeBox"
:list="['Single','Dual','2orMore']"
@input="handlePipelineData('contourGroupingMode')"
/>
<CVselect
v-model="contourIntersection"
name="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="largeBox"
:list="['None','Up','Down','Left','Right']"
:disabled="contourGroupingMode === 0"
@input="handlePipelineData('contourIntersection')"
v-model="contourIntersection"
name="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="largeBox"
:list="['None','Up','Down','Left','Right']"
:disabled="contourGroupingMode === 0"
@input="handlePipelineData('contourIntersection')"
/>
</template>
<!-- If we arent not a shape, we are a shape-->
<template v-else>
<v-divider class="mt-3" />
<v-divider class="mt-3"/>
<CVselect
v-model="contourShape"
name="Target Shape"
tooltip="The shape of targets to look for"
:select-cols="largeBox"
:list="['Circle', 'Polygon', 'Triangle', 'Quadrilateral']"
@input="handlePipelineData('contourShape')"
v-model="contourShape"
name="Target Shape"
tooltip="The shape of targets to look for"
:select-cols="largeBox"
:list="['Circle', 'Polygon', 'Triangle', 'Quadrilateral']"
@input="handlePipelineData('contourShape')"
/>
<!-- Accuracy % is only for polygons-->
<CVslider
v-model="accuracyPercentage"
:disabled="currentPipelineSettings().contourShape < 1"
name="Shape Simplification"
tooltip="How much we should simply the input contour before checking how many sides it has"
min="0"
max="100"
:slider-cols="largeBox"
@input="handlePipelineData('accuracyPercentage')"
v-model="accuracyPercentage"
:disabled="currentPipelineSettings().contourShape < 1"
name="Shape Simplification"
tooltip="How much we should simply the input contour before checking how many sides it has"
min="0"
max="100"
:slider-cols="largeBox"
@input="handlePipelineData('accuracyPercentage')"
/>
<!-- Similarly, the threshold is only for circles -->
<CVslider
v-model="circleDetectThreshold"
:disabled="currentPipelineSettings().contourShape !== 0"
name="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="largeBox"
@input="handlePipelineData('circleDetectThreshold')"
v-model="circleDetectThreshold"
:disabled="currentPipelineSettings().contourShape !== 0"
name="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="largeBox"
@input="handlePipelineData('circleDetectThreshold')"
/>
<CVrangeSlider
v-model="contourRadius"
:disabled="currentPipelineSettings().contourShape !== 0"
name="Radius"
min="0"
max="100"
step="1"
label-cols="3"
@input="handlePipelineData('contourRadius')"
v-model="contourRadius"
:disabled="currentPipelineSettings().contourShape !== 0"
name="Radius"
min="0"
max="100"
step="1"
@input="handlePipelineData('contourRadius')"
/>
<CVslider
v-model="maxCannyThresh"
:disabled="currentPipelineSettings().contourShape !== 0"
name="Max Canny Threshold"
min="1"
max="100"
:slider-cols="largeBox"
@input="handlePipelineData('maxCannyThresh')"
v-model="maxCannyThresh"
:disabled="currentPipelineSettings().contourShape !== 0"
name="Max Canny Threshold"
min="1"
max="100"
:slider-cols="largeBox"
@input="handlePipelineData('maxCannyThresh')"
/>
<CVslider
v-model="circleAccuracy"
:disabled="currentPipelineSettings().contourShape !== 0"
name="Circle Accuracy"
min="1"
max="100"
:slider-cols="largeBox"
@input="handlePipelineData('circleAccuracy')"
v-model="circleAccuracy"
:disabled="currentPipelineSettings().contourShape !== 0"
name="Circle Accuracy"
min="0"
max="100"
:slider-cols="largeBox"
@input="handlePipelineData('circleAccuracy')"
/>
<v-divider class="mt-3" />
<v-divider class="mt-3"/>
</template>
<CVselect
v-model="contourSortMode"
name="Target Sort"
tooltip="Chooses the sorting mode used to determine the 'best' targets to provide to user code"
:select-cols="largeBox"
:list="['Largest','Smallest','Highest','Lowest','Rightmost','Leftmost','Centermost']"
@input="handlePipelineData('contourSortMode')"
@rollback="e => rollback('contourSortMode', e)"
v-model="contourSortMode"
name="Target Sort"
tooltip="Chooses the sorting mode used to determine the 'best' targets to provide to user code"
:select-cols="largeBox"
:list="['Largest','Smallest','Highest','Lowest','Rightmost','Leftmost','Centermost']"
@input="handlePipelineData('contourSortMode')"
@rollback="e => rollback('contourSortMode', e)"
/>
</div>
</template>
@@ -211,14 +182,6 @@ export default {
this.$store.commit("mutatePipeline", {"contourRatio": val});
}
},
contourTargetOrientation: {
get() {
return this.$store.getters.currentPipelineSettings.contourTargetOrientation
},
set(val) {
this.$store.commit("mutatePipeline", {"contourTargetOrientation": val});
}
},
contourFullness: {
get() {
return this.$store.getters.currentPipelineSettings.contourFullness
@@ -243,25 +206,6 @@ export default {
this.$store.commit("mutatePipeline", {"contourSpecklePercentage": val});
}
},
contourFilterRangeX: {
get() {
console.log(this.$store.getters.currentPipelineSettings.contourFilterRangeX)
return this.$store.getters.currentPipelineSettings.contourFilterRangeX
},
set(val) {
console.log("set")
console.log(val)
this.$store.commit("mutatePipeline", {"contourFilterRangeX": val});
}
},
contourFilterRangeY: {
get() {
return this.$store.getters.currentPipelineSettings.contourFilterRangeY
},
set(val) {
this.$store.commit("mutatePipeline", {"contourFilterRangeY": val});
}
},
contourGroupingMode: {
get() {
return this.$store.getters.currentPipelineSettings.contourGroupingMode
@@ -341,4 +285,4 @@ export default {
<style lang="" scoped>
</style>
</style>

View File

@@ -2,12 +2,11 @@
<div>
<CVslider
v-model="cameraExposure"
:disabled="cameraAutoExposure"
name="Exposure"
min="0"
max="100"
step="0.1"
tooltip="Directly controls how much light is allowed to fall onto the sensor, which affects apparent brightness"
tooltip="Directly controls how much light is allowed to fall onto the sensor, which affects brightness"
:slider-cols="largeBox"
@input="handlePipelineData('cameraExposure')"
@rollback="e => rollback('cameraExposure', e)"
@@ -22,46 +21,17 @@
@input="handlePipelineData('cameraBrightness')"
@rollback="e => rollback('cameraBrightness', e)"
/>
<CVswitch
v-model="cameraAutoExposure"
class="pt-2"
name="Auto Exposure"
tooltip="Enables or Disables camera automatic adjustment for current lighting conditions"
@input="handlePipelineData('cameraAutoExposure')"
/>
<CVslider
v-if="cameraGain >= 0"
v-if="cameraGain !== -1"
v-model="cameraGain"
name="Camera Gain"
name="Gain"
min="0"
max="100"
tooltip="Controls camera gain, similar to brightness"
tooltip="Controls automatic white balance gain, which affects how the camera captures colors in different conditions"
:slider-cols="largeBox"
@input="handlePipelineData('cameraGain')"
@rollback="e => rollback('cameraGain', e)"
/>
<CVslider
v-if="cameraRedGain !== -1"
v-model="cameraRedGain"
name="Red Balance"
min="0"
max="100"
tooltip="Controls red automatic white balance gain, which affects how the camera captures colors in different conditions"
:slider-cols="largeBox"
@input="handlePipelineData('cameraRedGain')"
@rollback="e => rollback('cameraRedGain', e)"
/>
<CVslider
v-if="cameraBlueGain !== -1"
v-model="cameraBlueGain"
name="Blue Balance"
min="0"
max="100"
tooltip="Controls blue automatic white balance gain, which affects how the camera captures colors in different conditions"
:slider-cols="largeBox"
@input="handlePipelineData('cameraBlueGain')"
@rollback="e => rollback('cameraBlueGain', e)"
/>
<CVselect
v-model="inputImageRotationMode"
name="Orientation"
@@ -94,7 +64,6 @@
<script>
import CVslider from '../../components/common/cv-slider'
import CVselect from '../../components/common/cv-select'
import CVswitch from '../../components/common/cv-switch'
const unfilteredStreamDivisors = [1, 2, 4, 6];
@@ -103,10 +72,14 @@
components: {
CVslider,
CVselect,
CVswitch,
},
// eslint-disable-next-line vue/require-prop-types
props: ['value'],
data() {
return {
rawStreamDivisorIndex: 0,
}
},
computed: {
largeBox: {
get() {
@@ -124,14 +97,6 @@
this.$store.commit("mutatePipeline", {"cameraExposure": parseFloat(val)});
}
},
cameraAutoExposure: {
get() {
return this.$store.getters.currentPipelineSettings.cameraAutoExposure;
},
set(val) {
this.$store.commit("mutatePipeline", {"cameraAutoExposure": val});
}
},
cameraBrightness: {
get() {
return parseInt(this.$store.getters.currentPipelineSettings.cameraBrightness)
@@ -148,22 +113,6 @@
this.$store.commit("mutatePipeline", {"cameraGain": parseInt(val)});
}
},
cameraRedGain: {
get() {
return parseInt(this.$store.getters.currentPipelineSettings.cameraRedGain)
},
set(val) {
this.$store.commit("mutatePipeline", {"cameraRedGain": parseInt(val)});
}
},
cameraBlueGain: {
get() {
return parseInt(this.$store.getters.currentPipelineSettings.cameraBlueGain)
},
set(val) {
this.$store.commit("mutatePipeline", {"cameraBlueGain": parseInt(val)});
}
},
inputImageRotationMode: {
get() {
return this.$store.getters.currentPipelineSettings.inputImageRotationMode
@@ -180,22 +129,15 @@
this.$store.commit("mutatePipeline", {"cameraVideoModeIndex": val});
this.handlePipelineUpdate("streamingFrameDivisor", this.getNumSkippedStreamDivisors());
this.$store.commit("mutatePipeline", {"streamingFrameDivisor": 0});
// If we don't have 3d mode calibrated at the new resolution either, we should disable it here
// (TODO) This probably belongs in the backend (Matt)
if (!this.$store.getters.isCalibrated) {
this.handlePipelineUpdate("solvePNPEnabled", false);
this.$store.commit("mutatePipeline", {"solvePNPEnabled": false});
}
this.rawStreamDivisorIndex = 0;
}
},
streamingFrameDivisor: {
get() {
return this.$store.getters.currentPipelineSettings.streamingFrameDivisor;
return this.rawStreamDivisorIndex;
},
set(val) {
this.$store.commit("mutatePipeline", {"streamingFrameDivisor": val});
this.rawStreamDivisorIndex = val;
this.handlePipelineUpdate("streamingFrameDivisor", this.getNumSkippedStreamDivisors() + val);
}
},
@@ -228,11 +170,7 @@
// It would probably be cleaner if this checked that we're on the Raspi 3 instead of checking for GPU accel status
const width = this.$store.getters.videoFormatList[
this.$store.getters.currentCameraSettings.currentPipelineSettings.cameraVideoModeIndex]['width'];
// If GPU acceleration is enabled, the downsized width must be below 400px
// This check should be skipped if we're currently in driver mode
return unfilteredStreamDivisors.filter((x) => this.$store.getters.isDriverMode
|| !this.$store.state.settings.general.gpuAcceleration || width / x < 400);
return unfilteredStreamDivisors.filter((x) => !this.$store.state.settings.general.gpuAcceleration || width / x < 400);
},
getNumSkippedStreamDivisors() {
return unfilteredStreamDivisors.length - this.getRawStreamDivisors().length;

View File

@@ -1,53 +0,0 @@
<template>
<div>
<mini-map
class="miniMapClass"
:targets="targets"
:horizontal-f-o-v="horizontalFOV"
/>
</div>
</template>
<script>
import miniMap from '../../components/pipeline/3D/MiniMap';
export default {
name: "Map3D",
components: {
miniMap
},
data() {
return {
}
},
computed: {
targets: {
get() {
return this.$store.getters.currentPipelineResults.targets;
}
},
horizontalFOV: {
get() {
let index = this.$store.getters.currentPipelineSettings.cameraVideoModeIndex;
let FOV = this.$store.getters.currentCameraSettings.fov;
let resolution = this.$store.getters.videoFormatList[index];
let diagonalView = FOV * (Math.PI / 180);
let diagonalAspect = Math.hypot(resolution.width, resolution.height);
return Math.atan(Math.tan(diagonalView / 2) * (resolution.width / diagonalAspect)) * 2 * (180 / Math.PI)
}
},
},
methods: {
}
}
</script>
<style scoped>
.miniMapClass {
width: 400px !important;
height: 100% !important;
margin-left: auto;
margin-right: auto;
}
</style>

View File

@@ -1,5 +1,8 @@
<template>
<div>
<span class="white--text">Target Manipulation</span>
<v-divider class="mt-2" />
<CVselect
v-model="contourTargetOffsetPointEdge"
name="Target Offset Point"
@@ -28,7 +31,8 @@
@rollback="e=> rollback('outputShowMultipleTargets', e)"
/>
<v-divider />
<span class="white--text">Robot Offset</span>
<v-divider class="mt-2" />
<CVselect
v-model="offsetRobotOffsetMode"
name="Robot Offset Mode"
@@ -152,4 +156,4 @@
</script>
<style scoped>
</style>
</style>

View File

@@ -6,6 +6,7 @@
type="file"
accept=".csv"
style="display: none;"
@change="readFile"
>
@@ -31,7 +32,11 @@
@input="handlePipelineData('cornerDetectionAccuracyPercentage')"
@rollback="e => rollback('cornerDetectionAccuracyPercentage', e)"
/>
<mini-map
class="miniMapClass"
:targets="targets"
:horizontal-f-o-v="horizontalFOV"
/>
<v-snackbar
v-model="snack"
top
@@ -44,16 +49,18 @@
<script>
import Papa from 'papaparse';
import miniMap from '../../components/pipeline/3D/MiniMap';
import CVslider from '../../components/common/cv-slider'
export default {
name: "PnP",
components: {
CVslider
CVslider,
miniMap
},
data() {
return {
targetList: ['2020 High Goal Outer', '2020 High Goal Inner', '2019 Dual Target', '2020 Power Cell (7in)','2022 Cargo Ball (9.5in)', '2016 High Goal', '6.5in (36h11) AprilTag', '6in (16h5) AprilTag'], //Keep in sync with TargetModel.java
targetList: ['2020 High Goal Outer', '2020 High Goal Inner', '2019 Dual Target', 'Power Cell (7in)', '2016 High Goal'], //Keep in sync with TargetModel.java
snackbar: {
color: "Success",
text: ""
@@ -65,6 +72,7 @@
selectedModel: {
get() {
let ret = this.$store.getters.currentPipelineSettings.targetModel
console.log(ret)
return this.targetList[ret];
},
set(val) {
@@ -79,6 +87,21 @@
this.$store.commit("mutatePipeline", {"cornerDetectionAccuracyPercentage": val});
}
},
targets: {
get() {
return this.$store.getters.currentPipelineResults.targets;
}
},
horizontalFOV: {
get() {
let index = this.$store.getters.currentPipelineSettings.cameraVideoModeIndex;
let FOV = this.$store.getters.currentCameraSettings.fov;
let resolution = this.$store.getters.videoFormatList[index];
let diagonalView = FOV * (Math.PI / 180);
let diagonalAspect = Math.hypot(resolution.width, resolution.height);
return Math.atan(Math.tan(diagonalView / 2) * (resolution.width / diagonalAspect)) * 2 * (180 / Math.PI)
}
},
},
methods: {
readFile(event) {
@@ -132,4 +155,4 @@
margin-left: auto;
margin-right: auto;
}
</style>
</style>

View File

@@ -18,40 +18,29 @@
<th class="text-center">
Target
</th>
<th
v-if="$store.getters.pipelineType === 4"
class="text-center"
>
Fiducial ID
</th>
<template v-if="!$store.getters.currentPipelineSettings.solvePNPEnabled">
<th class="text-center">
Pitch,&nbsp;&deg;
Pitch
</th>
<th class="text-center">
Yaw,&nbsp;&deg;
Yaw
</th>
<th class="text-center">
Skew,&nbsp;&deg;
</th>
<th class="text-center">
Area, %
Skew
</th>
</template>
<template v-else>
<th class="text-center">
Area
</th>
<template v-if="$store.getters.currentPipelineSettings.solvePNPEnabled">
<th class="text-center">
X,&nbsp;m
X
</th>
<th class="text-center">
Y,&nbsp;m
Y
</th>
<th class="text-center">
Z Angle,&nbsp;&deg;
</th>
</template>
<template v-if="$store.getters.pipelineType === 4 && $store.getters.currentPipelineSettings.solvePNPEnabled">
<th class="text-center">
Ambiguity
Angle
</th>
</template>
</tr>
@@ -62,29 +51,17 @@
:key="index"
>
<td>{{ index }}</td>
<td v-if="$store.getters.pipelineType === 4">
{{ parseInt(value.fiducialId) }}
</td>
<template v-if="!$store.getters.currentPipelineSettings.solvePNPEnabled">
<td>{{ parseFloat(value.pitch).toFixed(2) }}</td>
<td>{{ parseFloat(value.yaw).toFixed(2) }}</td>
<td>{{ parseFloat(value.skew).toFixed(2) }}</td>
<td>{{ parseFloat(value.area).toFixed(2) }}</td>
</template>
<template v-else-if="$store.getters.currentPipelineSettings.solvePNPEnabled && $store.getters.pipelineType === 4">
<td>{{ parseFloat(value.area).toFixed(2) }}</td>
<template v-if="$store.getters.currentPipelineSettings.solvePNPEnabled">
<!-- TODO: Make sure that units are correct -->
<td>{{ parseFloat(value.pose.x).toFixed(2) }}&nbsp;m</td>
<td>{{ parseFloat(value.pose.y).toFixed(2) }}&nbsp;m</td>
<td>{{ (parseFloat(value.pose.angle_z) * 180 / Math.PI).toFixed(2) }}&deg;</td>
</template>
<template v-else-if="$store.getters.currentPipelineSettings.solvePNPEnabled">
<td>{{ parseFloat(value.pose.x).toFixed(2) }}&nbsp;m</td>
<td>{{ parseFloat(value.pose.y).toFixed(2) }}&nbsp;m</td>
<td>{{ (parseFloat(value.pose.angle_z) * 180 / Math.PI).toFixed(2) }}&deg;</td>
</template>
<template v-if="$store.getters.pipelineType === 4 && $store.getters.currentPipelineSettings.solvePNPEnabled">
<td>
{{ parseFloat(value.ambiguity).toFixed(2) }}
</td>
<td>{{ parseFloat(value.pose.rot).toFixed(2) }}&deg;</td>
</template>
</tr>
</tbody>
@@ -120,4 +97,4 @@
.v-data-table td {
font-family: monospace !important;
}
</style>
</style>

View File

@@ -1,79 +1,66 @@
<template>
<div :style="{'--averageHue': averageHue}">
<div>
<CVrangeSlider
id="hue-slider"
v-model="hsvHue"
:class="hueInverted ? 'inverted-slider' : 'normal-slider'"
name="Hue"
tooltip="Describes color"
:min="0"
:max="180"
:inverted="hueInverted"
@input="handlePipelineData('hsvHue')"
@rollback="e => rollback('hue',e)"
v-model="hsvHue"
name="Hue"
tooltip="Describes color"
:min="0"
:max="180"
@input="handlePipelineData('hsvHue')"
@rollback="e => rollback('hue',e)"
/>
<CVrangeSlider
id="sat-slider"
v-model="hsvSaturation"
class="normal-slider"
name="Saturation"
tooltip="Describes colorfulness; the smaller this value the 'whiter' the color becomes"
:min="0"
:max="255"
@input="handlePipelineData('hsvSaturation')"
@rollback="e => rollback('saturation',e)"
v-model="hsvSaturation"
name="Saturation"
tooltip="Describes colorfulness; the smaller this value the 'whiter' the color becomes"
:min="0"
:max="255"
@input="handlePipelineData('hsvSaturation')"
@rollback="e => rollback('saturation',e)"
/>
<CVrangeSlider
id="value-slider"
v-model="hsvValue"
class="normal-slider"
name="Value"
tooltip="Describes lightness; the smaller this value the 'blacker' the color becomes"
:min="0"
:max="255"
@input="handlePipelineData('hsvValue')"
@rollback="e => rollback('value',e)"
v-model="hsvValue"
name="Value"
tooltip="Describes lightness; the smaller this value the 'blacker' the color becomes"
:min="0"
:max="255"
@input="handlePipelineData('hsvValue')"
@rollback="e => rollback('value',e)"
/>
<CVSwitch
v-model="hueInverted"
name="Invert hue"
tooltip="Selects the hue range outside of the hue slider bounds instead of inside"
@input="handlePipelineData('hueInverted')"
@rollback="e => rollback('hueInverted',e)"
/>
<template v-if="currentPipelineType() === 3">
<template v-if="this.currentPipelineType() === 3">
<CVSwitch
v-model="erode"
name="Erode"
tooltip="Removes pixels around the edges of white areas in the thresholded image"
@input="handlePipelineData('erode')"
@rollback="e => rollback('erode',e)"
v-model="erode"
name="Erode"
tooltip="Removes pixels around the edges of white areas in the thresholded image"
@input="handlePipelineData('erode')"
@rollback="e => rollback('erode',e)"
/>
<CVSwitch
v-model="dilate"
class="mb-0"
name="Dilate"
tooltip="Adds pixels around the edges of white areas in the thresholded image"
@input="handlePipelineData('dilate')"
@rollback="e => rollback('dilate',e)"
v-model="dilate"
class="mb-0"
name="Dilate"
tooltip="Adds pixels around the edges of white areas in the thresholded image"
@input="handlePipelineData('dilate')"
@rollback="e => rollback('dilate',e)"
/>
</template>
<v-divider class="mb-3 mt-3"/>
<div class="pt-3 white--text">
Color Picker
</div>
<v-divider
class="mt-3"
class="mt-3"
/>
<v-row
justify="center"
class="mt-3 mb-3"
justify="center"
class="mt-3 mb-3"
>
<template v-if="!$store.state.colorPicking">
<v-btn
color="accent"
class="ma-2 black--text"
small
@click="setFunction(hueInverted ? 2 : 3)"
color="accent"
class="ma-2 black--text"
small
@click="setFunction(3)"
>
<v-icon left>
mdi-minus
@@ -81,21 +68,21 @@
Shrink Range
</v-btn>
<v-btn
color="accent"
class="ma-2 black--text"
small
@click="setFunction(1)"
color="accent"
class="ma-2 black--text"
small
@click="setFunction(1)"
>
<v-icon left>
mdi-plus-minus
</v-icon>
{{ hueInverted ? "Exclude" : "Set to" }} Average
Set To Average
</v-btn>
<v-btn
color="accent"
class="ma-2 black--text"
small
@click="setFunction(hueInverted ? 3: 2)"
color="accent"
class="ma-2 black--text"
small
@click="setFunction(2)"
>
<v-icon left>
mdi-plus
@@ -105,17 +92,16 @@
</template>
<template v-else>
<v-btn
color="accent"
class="ma-2 black--text"
style="width: 30%;"
small
@click="setFunction(0)"
color="accent"
class="ma-2 black--text"
style="width: 30%;"
small
@click="setFunction(0)"
>
Cancel
</v-btn>
</template>
</v-row>
<v-divider class="mb-3" />
</div>
</template>
@@ -147,41 +133,9 @@ export default {
this.$store.commit("mutatePipeline", {"hsvHue": val})
}
},
averageHue: {
get() {
var isInverted = this.$store.getters.currentPipelineSettings.hueInverted;
const arr = this.$store.getters.currentPipelineSettings.hsvHue;
var retVal = 0;
if (Array.isArray(arr)) {
retVal = (arr[0] + arr[1]);
} else {
retVal = (arr.first + arr.second);
}
if(isInverted){
retVal += 180;
}
if(retVal > 360){
retVal -= 360;
}
return retVal;
},
},
hueInverted: {
get() {
return this.$store.getters.currentPipelineSettings.hueInverted;
},
set(val) {
this.$store.commit("mutatePipeline", {"hueInverted": val});
}
},
hsvSaturation: {
get() {
return this.$store.getters.currentPipelineSettings.hsvSaturation;
return this.$store.getters.currentPipelineSettings.hsvSaturation
},
set(val) {
this.$store.commit("mutatePipeline", {"hsvSaturation": val})
@@ -189,15 +143,15 @@ export default {
},
hsvValue: {
get() {
return this.$store.getters.currentPipelineSettings.hsvValue;
return this.$store.getters.currentPipelineSettings.hsvValue
},
set(val) {
this.$store.commit("mutatePipeline", {"hsvValue": val});
this.$store.commit("mutatePipeline", {"hsvValue": val})
}
},
erode: {
get() {
return this.$store.getters.currentPipelineSettings.erode;
return this.$store.getters.currentPipelineSettings.erode
},
set(val) {
this.$store.commit("mutatePipeline", {"erode": val});
@@ -205,7 +159,7 @@ export default {
},
dilate: {
get() {
return this.$store.getters.currentPipelineSettings.dilate;
return this.$store.getters.currentPipelineSettings.dilate
},
set(val) {
this.$store.commit("mutatePipeline", {"dilate": val});
@@ -247,7 +201,7 @@ export default {
'cameraIndex': this.$store.state.currentCameraIndex
}
});
this.$store.state.websocket.send(msg);
this.$socket.send(msg);
this.$emit('update');
}
},
@@ -278,32 +232,4 @@ export default {
}
}
</script>
<style lang="css" scoped>
#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;
box-shadow: 0px 0px 5px #333, inset 0px 0px 3px #333;
}
#sat-slider >>> .v-slider {
background: linear-gradient( to right, #fff 0%, hsl(var(--averageHue), 100%, 50%) 100% );
border-radius: 10px;
box-shadow: 0px 0px 5px #333, inset 0px 0px 3px #333;
}
#value-slider >>> .v-slider {
background: linear-gradient( to right, #000 0%, hsl(var(--averageHue), 100%, 50%) 100% );
border-radius: 10px;
box-shadow: 0px 0px 5px #333, inset 0px 0px 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>
</script>

View File

@@ -49,7 +49,6 @@
return {
selectedTab: 0,
snack: false,
calibrationInProgress: false,
snackbar: {
color: "accent",
text: ""
@@ -86,4 +85,4 @@
height: auto !important;
vertical-align: middle;
}
</style>
</style>

View File

@@ -49,46 +49,22 @@
<th class="infoElem">
Disk Usage
</th>
<th class="infoElem">
<v-tooltip top>
<template v-slot:activator="{ on, attrs }">
<span
v-bind="attrs"
v-on="on"
>
CPU Throttling
</span>
</template>
<span>
Current or Previous Reason for the cpu being held back from maximum performance.
</span>
</v-tooltip>
</th>
<th class="infoElem">
CPU Uptime
</th>
</tr>
<tr v-if="metrics.cpuUtil !== 'N/A'">
<td class="infoElem">
{{ metrics.cpuUtil }}%
{{ metrics.cpuUtil.replace(" ", "") }}%
</td>
<td class="infoElem">
{{ parseInt(metrics.cpuTemp) }}&deg;&nbsp;C
</td>
<td class="infoElem">
{{ metrics.ramUtil }}MB of {{ metrics.cpuMem }}MB
{{ metrics.ramUtil.replace(" ", "") }}MB of {{ metrics.cpuMem }}MB
</td>
<td class="infoElem">
{{ metrics.gpuMemUtil }}MB of {{ metrics.gpuMem }}MB
{{ metrics.gpuMemUtil.replace(" ", "") }}MB of {{ metrics.gpuMem }}MB
</td>
<td class="infoElem">
{{ metrics.diskUtilPct }}
</td>
<td class="infoElem">
{{ metrics.cpuThr }}
</td>
<td class="infoElem">
{{ metrics.cpuUptime }}
{{ metrics.diskUtilPct.replace(" ", "") }}
</td>
</tr>
<tr v-if="metrics.cpuUtil === 'N/A'">
@@ -107,12 +83,6 @@
<td class="infoElem">
---
</td>
<td class="infoElem">
---
</td>
<td class="infoElem">
---
</td>
</tr>
</table>
</v-row>
@@ -121,7 +91,7 @@
<v-col
cols="12"
sm="6"
md="4"
lg="3"
>
<v-btn
color="secondary"
@@ -136,7 +106,7 @@
<v-col
cols="12"
sm="6"
md="4"
lg="3"
>
<v-btn
color="secondary"
@@ -150,21 +120,7 @@
</v-col>
<v-col
cols="12"
md="4"
>
<v-btn
color="secondary"
@click="$refs.offlineUpdate.click()"
>
<v-icon left>
mdi-update
</v-icon>
Offline Update
</v-btn>
</v-col>
<v-col
cols="12"
lg="6"
lg="3"
>
<v-btn
color="red"
@@ -173,19 +129,19 @@
<v-icon left>
mdi-restart
</v-icon>
Restart PhotonVision
Restart Photon
</v-btn>
</v-col>
<v-col
cols="12"
lg="6"
lg="3"
>
<v-btn
color="red"
@click="restartDevice()"
>
<v-icon left>
mdi-restart-alert
mdi-restart
</v-icon>
Restart Device
</v-btn>
@@ -216,15 +172,6 @@
:href="'http://' + this.$address + '/api/settings/photonvision_config.zip'"
download="photonvision-settings.zip"
/>
<!-- Special hidden new jar upload input that gets 'clicked' when the user posts a new .jar -->
<input
ref="offlineUpdate"
type="file"
accept=".jar"
style="display: none;"
@change="doOfflineUpdate"
>
</div>
</template>
@@ -234,7 +181,6 @@ export default {
data() {
return {
snack: false,
uploadPercentage: 0.0,
snackbar: {
color: "success",
text: ""
@@ -280,7 +226,7 @@ export default {
{headers: {"Content-Type": "multipart/form-data"}}).then(() => {
this.snackbar = {
color: "success",
text: "Settings imported successfully! PhotonVision will restart in the background...",
text: "Settings imported successfully! Program will now exit...",
};
this.snack = true;
}).catch(err => {
@@ -288,7 +234,7 @@ export default {
this.snackbar = {
color: "error",
text: "Error while uploading settings file! Could not process provided file.",
};
};
} else if (err.request) {
this.snackbar = {
color: "error",
@@ -303,52 +249,6 @@ export default {
this.snack = true;
});
},
doOfflineUpdate(event) {
this.snackbar = {
color: "secondary",
text: "New Software Upload in Process..."
};
this.snack = true;
let formData = new FormData();
formData.append("jarData", event.target.files[0]);
this.axios.post("http://" + this.$address + "/api/settings/offlineUpdate", formData,
{headers: {"Content-Type": "multipart/form-data"},
onUploadProgress: function( progressEvent ) {
this.uploadPercentage = parseInt( Math.round( ( progressEvent.loaded / progressEvent.total ) * 100 ) );
if(this.uploadPercentage < 99.5){
this.snackbar.text = "New Software Upload in Process, " + this.uploadPercentage + "% complete";
} else {
this.snackbar.text = "Installing uploaded software...";
}
}.bind(this)
}).then(() => {
this.snackbar = {
color: "success",
text: "New .jar copied successfully! PhotonVision will restart in the background...",
};
this.snack = true;
}).catch(err => {
if (err.response) {
this.snackbar = {
color: "error",
text: "Error while uploading new .jar file! Could not process provided file.",
};
} else if (err.request) {
this.snackbar = {
color: "error",
text: "Error while uploading new .jar file! No respond to upload attempt.",
};
} else {
this.snackbar = {
color: "error",
text: "Error while uploading new .jar file!",
};
}
this.snack = true;
});
},
}
}
</script>
@@ -366,8 +266,6 @@ export default {
text-align: left;
margin-bottom: 10px;
width: 100%;
display: block;
overflow-x: auto;
}
.infoElem {
@@ -378,4 +276,4 @@ export default {
border-right: 1px solid;
}
</style>
</style>

View File

@@ -41,4 +41,4 @@
<style lang="" scoped>
</style>
</style>

View File

@@ -4,137 +4,46 @@
ref="form"
v-model="valid"
>
<CVSwitch
v-model="runNTServer"
name="Run NetworkTables Server"
tooltip="If enabled, this device will create a NT server. This is useful for home debugging, but should be disabled on-robot."
/>
<CVnumberinput
v-model="teamNumber"
:disabled="settings.runNTServer"
name="Team Number"
:rules="[v => (v > 0) || 'Team number must be greater than zero', v => (v < 10000) || 'Team number must have fewer than five digits']"
class="mb-4"
:label-cols="$vuetify.breakpoint.mdAndUp ? undefined : 5"
/>
<v-banner
v-show="(teamNumber < 1 || teamNumber > 10000) && !runNTServer"
rounded
color="red"
text-color="white"
>
Team number is unset or invalid. NetworkTables will not be able to connect.
</v-banner>
<CVradio
v-show="$store.state.settings.networkSettings.supported"
v-model="connectionType"
:input-cols="inputCols"
name="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."
:list="['DHCP','Static']"
:disabled="!$store.state.settings.networkSettings.supported"
/>
<CVinput
v-if="!isDHCP"
v-model="staticIp"
:input-cols="inputCols"
:rules="[v => isIPv4(v) || 'Invalid IPv4 address']"
name="IP"
/>
<template v-if="!isDHCP">
<CVinput
v-model="staticIp"
:input-cols="inputCols"
:rules="[v => isIPv4(v) || 'Invalid IPv4 address']"
name="IP"
/>
</template>
<CVinput
v-model="hostname"
:input-cols="inputCols"
:rules="[v => isHostname(v) || 'Invalid hostname']"
name="Hostname"
/>
<CVSwitch
v-model="runNTServer"
name="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-3"
:text-cols="$vuetify.breakpoint.mdAndUp ? undefined : 5"
/>
<v-banner
v-show="runNTServer"
rounded
color="red"
text-color="white"
>
This switch is intended for testing; it should be off on a robot. PhotonLib will NOT work!
</v-banner>
</v-form>
<v-btn
color="accent"
:class="runNTServer ? 'mt-3' : ''"
style="color: black; width: 100%;"
:disabled="!valid && !runNTServer"
@click="sendGeneralSettings()"
>
Save
</v-btn>
<v-snackbar
v-model="snack"
top
:color="snackbar.color"
timeout="5000"
>
<span>{{ snackbar.text }}</span>
</v-snackbar>
<v-divider class="mt-4 mb-4" />
<!-- TEMP - RIO finder is not currently enabled
<v-row>
<v-col
cols="12"
sm="6"
>
<v-simple-table
fixed-header
height="100%"
dense
>
<template v-slot:default>
<thead style="font-size: 1.25rem;">
<tr>
<th>
Device IPs
</th>
</tr>
</thead>
<tbody>
<tr
v-for="(value, index) in $store.state.networkInfo.deviceips"
:key="index"
>
<td>{{ value }}</td>
</tr>
</tbody>
</template>
</v-simple-table>
</v-col>
<v-col
cols="12"
sm="6"
>
<v-simple-table
fixed-header
height="100%"
dense
>
<template v-slot:default>
<thead style="font-size: 1.25rem;">
<tr>
<th>
Possible RoboRIOs
</th>
</tr>
</thead>
<tbody>
<tr
v-for="(value, index) in $store.state.networkInfo.possibleRios"
:key="index"
>
<td>{{ value }}</td>
</tr>
</tbody>
</template>
</v-simple-table>
</v-col>
</v-row>
-->
</div>
</template>
@@ -171,7 +80,7 @@ export default {
},
computed: {
inputCols() {
return this.$vuetify.breakpoint.mdAndUp ? 10 : 7;
return this.$vuetify.breakpoint.smAndUp ? 10 : 7;
},
isDHCP() {
return this.settings.connectionType === 0;
@@ -247,7 +156,7 @@ export default {
},
sendGeneralSettings() {
this.axios.post("http://" + this.$address + "/api/settings/general", this.settings).then(
response => {
function (response) {
if (response.status === 200) {
this.snackbar = {
color: "success",
@@ -256,7 +165,7 @@ export default {
this.snack = true;
}
},
error => {
function (error) {
this.snackbar = {
color: "error",
text: (error.response || {data: "Couldn't save settings"}).data
@@ -270,24 +179,6 @@ export default {
}
</script>
<style scoped>
.v-data-table {
/*text-align: center;*/
background-color: transparent !important;
width: 100%;
height: 100%;
overflow-y: auto;
}
<style lang="" scoped>
.v-data-table th {
background-color: #006492 !important;
}
.v-data-table th, td {
font-size: 1rem !important;
}
.v-data-table td {
font-family: monospace !important;
}
</style>

View File

@@ -9,7 +9,5 @@ build
build/*
photonvision/*
photonvision_config/*
photon-server/lib/*
photon-server/package-lock.json
src/main/java/org/photonvision/PhotonVersion.java
src/main/java/org/photonvision/PhotonVersion.java

View File

@@ -1,18 +1,22 @@
plugins {
id 'edu.wpi.first.WpilibTools' version '1.0.0'
}
import java.nio.file.Path
import java.time.LocalDateTime
import java.time.format.DateTimeFormatter
apply from: "${rootDir}/shared/common.gradle"
dependencies {
implementation project(':photon-targeting')
implementation 'io.javalin:javalin:4.2.0'
implementation 'io.javalin:javalin:3.7.0'
implementation 'org.msgpack:msgpack-core:0.9.0'
implementation 'org.msgpack:jackson-dataformat-msgpack:0.9.0'
implementation 'org.msgpack:msgpack-core:0.8.20'
implementation 'org.msgpack:jackson-dataformat-msgpack:0.8.20'
// wpiutil
compile "edu.wpi.first.wpiutil:wpiutil-jni:$wpilibVersion:linuxaarch64bionic"
compile "edu.wpi.first.wpiutil:wpiutil-jni:$wpilibVersion:linuxraspbian"
compile "edu.wpi.first.wpiutil:wpiutil-jni:$wpilibVersion:linuxx86-64"
compile "edu.wpi.first.wpiutil:wpiutil-jni:$wpilibVersion:osxx86-64"
compile "edu.wpi.first.wpiutil:wpiutil-jni:$wpilibVersion:windowsx86-64"
// JOGL stuff (currently we only distribute for aarch64, which is Pi 4)
implementation "org.jogamp.gluegen:gluegen-rt:$joglVersion"
@@ -22,36 +26,27 @@ dependencies {
implementation "org.jogamp.jogl:jogl-all:$joglVersion:natives-linux-aarch64"
// Zip
implementation 'org.zeroturnaround:zt-zip:1.14'
implementation wpilibTools.deps.wpilibJava("apriltag")
compile 'org.zeroturnaround:zt-zip:1.14'
}
task writeCurrentVersionJava {
def versionFileIn = file("${rootDir}/shared/PhotonVersion.java.in")
writePhotonVersionFile(versionFileIn, Path.of("$projectDir", "src", "main", "java", "org", "photonvision", "PhotonVersion.java"),
versionString)
String date = DateTimeFormatter.ofPattern("yyyy-M-d hh:mm:ss").format(LocalDateTime.now())
File versionFile = new File(java.nio.file.Path.of("$projectDir", "src", "main", "java", "org", "photonvision", "PhotonVersion.java")
.toAbsolutePath().toString())
versionFile.delete()
versionFile << "package org.photonvision;\n" +
"\n" +
"/*\n" +
" * Autogenerated file! Do not manually edit this file. This version is regenerated\n" +
" * any time the publish task is run, or when this file is deleted.\n" +
" */\n" +
"\n" +
"@SuppressWarnings(\"ALL\")\n" +
"public final class PhotonVersion {\n" +
" public static final String versionString = \"${versionString}\";\n" +
" public static final String buildDate = \"${date}\";\n" +
" public static final boolean isRelease = !versionString.startsWith(\"dev\");\n" +
"}"
}
build.dependsOn writeCurrentVersionJava
def testNativeConfigName = 'wpilibTestNative'
def testNativeConfig = configurations.create(testNativeConfigName)
def folder = project.layout.buildDirectory.dir('NativeTest')
def testNativeTasks = wpilibTools.createExtractionTasks {
taskPostfix = "Test"
configurationName = testNativeConfigName
rootTaskFolder.set(folder)
}
testNativeTasks.addToSourceSetResources(sourceSets.test)
testNativeConfig.dependencies.add wpilibTools.deps.cscore()
testNativeConfig.dependencies.add wpilibTools.deps.wpilib("ntcore")
testNativeConfig.dependencies.add wpilibTools.deps.wpilib("wpinet")
testNativeConfig.dependencies.add wpilibTools.deps.wpilib("hal")
testNativeConfig.dependencies.add wpilibTools.deps.wpilib("wpiutil")
testNativeConfig.dependencies.add wpilibTools.deps.wpilib("apriltag")
testNativeConfig.dependencies.add wpilibTools.deps.wpilib("wpimath")

View File

@@ -1 +1,2 @@
rootProject.name = 'photon-core'

View File

@@ -20,8 +20,8 @@ package org.photonvision.common.configuration;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonProperty;
import edu.wpi.first.wpilibj.geometry.Rotation2d;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import org.photonvision.common.logging.LogGroup;
import org.photonvision.common.logging.Logger;
@@ -46,12 +46,11 @@ public class CameraConfiguration {
/** Can be either path (ex /dev/videoX) or index (ex 1). */
public String path = "";
@JsonIgnore public String[] otherPaths = {};
public CameraType cameraType = CameraType.UsbCamera;
public double FOV = 70;
public final List<CameraCalibrationCoefficients> calibrations;
public int currentPipelineIndex = 0;
public Rotation2d camPitch = new Rotation2d();
public int streamIndex = 0; // 0 index means ports [1181, 1182], 1 means [1183, 1184], etc...
@@ -62,22 +61,19 @@ public class CameraConfiguration {
public DriverModePipelineSettings driveModeSettings = new DriverModePipelineSettings();
public CameraConfiguration(String baseName, String path) {
this(baseName, baseName, baseName, path, new String[0]);
this(baseName, baseName, baseName, path);
}
public CameraConfiguration(
String baseName, String uniqueName, String nickname, String path, String[] alternates) {
public CameraConfiguration(String baseName, String uniqueName, String nickname, String path) {
this.baseName = baseName;
this.uniqueName = uniqueName;
this.nickname = nickname;
this.path = path;
this.calibrations = new ArrayList<>();
this.otherPaths = alternates;
logger.debug(
"Creating USB camera configuration for "
+ cameraType
+ " "
+ baseName
+ " (AKA "
+ nickname
@@ -94,7 +90,8 @@ public class CameraConfiguration {
@JsonProperty("path") String path,
@JsonProperty("cameraType") CameraType cameraType,
@JsonProperty("calibration") List<CameraCalibrationCoefficients> calibrations,
@JsonProperty("currentPipelineIndex") int currentPipelineIndex) {
@JsonProperty("currentPipelineIndex") int currentPipelineIndex,
@JsonProperty("camPitch") Rotation2d camPitch) {
this.baseName = baseName;
this.uniqueName = uniqueName;
this.nickname = nickname;
@@ -103,11 +100,11 @@ public class CameraConfiguration {
this.cameraType = cameraType;
this.calibrations = calibrations != null ? calibrations : new ArrayList<>();
this.currentPipelineIndex = currentPipelineIndex;
this.camPitch = camPitch;
logger.debug(
"Creating camera configuration for "
+ cameraType
+ " "
+ baseName
+ " (AKA "
+ nickname
@@ -150,33 +147,4 @@ public class CameraConfiguration {
.ifPresent(calibrations::remove);
calibrations.add(calibration);
}
@Override
public String toString() {
return "CameraConfiguration [baseName="
+ baseName
+ ", uniqueName="
+ uniqueName
+ ", nickname="
+ nickname
+ ", path="
+ path
+ ", otherPaths="
+ Arrays.toString(otherPaths)
+ ", cameraType="
+ cameraType
+ ", FOV="
+ FOV
+ ", calibrations="
+ calibrations
+ ", currentPipelineIndex="
+ currentPipelineIndex
+ ", streamIndex="
+ streamIndex
+ ", pipelineSettings="
+ pipelineSettings
+ ", driveModeSettings="
+ driveModeSettings
+ "]";
}
}

View File

@@ -32,6 +32,7 @@ import java.util.*;
import java.util.stream.Collectors;
import org.photonvision.common.logging.LogGroup;
import org.photonvision.common.logging.Logger;
import org.photonvision.common.util.TimedTaskManager;
import org.photonvision.common.util.file.FileUtils;
import org.photonvision.common.util.file.JacksonUtils;
import org.photonvision.vision.pipeline.CVPipelineSettings;
@@ -56,7 +57,6 @@ public class ConfigManager {
final File configDirectoryFile;
private long saveRequestTimestamp = -1;
private Thread settingsSaveThread;
public static ConfigManager getInstance() {
if (INSTANCE == null) {
@@ -97,8 +97,7 @@ public class ConfigManager {
new File(Path.of(configDirectoryFile.toString(), NET_SET_FNAME).toUri());
this.camerasFolder = new File(Path.of(configDirectoryFile.toString(), "cameras").toUri());
settingsSaveThread = new Thread(this::saveAndWriteTask);
settingsSaveThread.start();
TimedTaskManager.getInstance().addTask("ConfigManager", this::checkSaveAndWrite, 1000);
}
public void load() {
@@ -426,24 +425,12 @@ public class ConfigManager {
saveRequestTimestamp = System.currentTimeMillis();
}
private void saveAndWriteTask() {
private void checkSaveAndWrite() {
// Only save if 1 second has past since the request was made
while (!Thread.currentThread().isInterrupted()) {
if (saveRequestTimestamp > 0 && (System.currentTimeMillis() - saveRequestTimestamp) > 1000L) {
saveRequestTimestamp = -1;
logger.debug("Saving to disk...");
saveToDisk();
}
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
logger.error("Exception waiting for settings semaphore", e);
}
if (saveRequestTimestamp > 0 && (System.currentTimeMillis() - saveRequestTimestamp) > 1000L) {
saveRequestTimestamp = -1;
logger.debug("Saving to disk...");
saveToDisk();
}
}
public void unloadCameraConfigs() {
this.config.getCameraConfigurations().clear();
}
}

View File

@@ -24,6 +24,7 @@ import java.util.List;
@JsonIgnoreProperties(ignoreUnknown = true)
public class HardwareConfig {
public final String deviceName;
public final String deviceLogoPath;
public final String supportURL;
@@ -41,8 +42,6 @@ public class HardwareConfig {
public final String cpuTempCommand;
public final String cpuMemoryCommand;
public final String cpuUtilCommand;
public final String cpuThrottleReasonCmd;
public final String cpuUptimeCommand;
public final String gpuMemoryCommand;
public final String ramUtilCommand;
public final String gpuMemUsageCommand;
@@ -67,8 +66,6 @@ public class HardwareConfig {
cpuTempCommand = "";
cpuMemoryCommand = "";
cpuUtilCommand = "";
cpuThrottleReasonCmd = "";
cpuUptimeCommand = "";
gpuMemoryCommand = "";
ramUtilCommand = "";
ledBlinkCommand = "";
@@ -95,8 +92,6 @@ public class HardwareConfig {
String cpuTempCommand,
String cpuMemoryCommand,
String cpuUtilCommand,
String cpuThrottleReasonCmd,
String cpuUptimeCommand,
String gpuMemoryCommand,
String ramUtilCommand,
String gpuMemUsageCommand,
@@ -117,8 +112,6 @@ public class HardwareConfig {
this.cpuTempCommand = cpuTempCommand;
this.cpuMemoryCommand = cpuMemoryCommand;
this.cpuUtilCommand = cpuUtilCommand;
this.cpuThrottleReasonCmd = cpuThrottleReasonCmd;
this.cpuUptimeCommand = cpuUptimeCommand;
this.gpuMemoryCommand = gpuMemoryCommand;
this.ramUtilCommand = ramUtilCommand;
this.gpuMemUsageCommand = gpuMemUsageCommand;
@@ -128,22 +121,7 @@ public class HardwareConfig {
this.blacklistedResIndices = blacklistedResIndices;
}
/** @return True if the FOV has been preset to a sane value, false otherwise */
public final boolean hasPresetFOV() {
return vendorFOV > 0;
}
/** @return True if any command has been configured to a non-default empty, false otherwise */
public final boolean hasCommandsConfigured() {
return cpuTempCommand != ""
|| cpuMemoryCommand != ""
|| cpuUtilCommand != ""
|| cpuThrottleReasonCmd != ""
|| cpuUptimeCommand != ""
|| gpuMemoryCommand != ""
|| ramUtilCommand != ""
|| ledBlinkCommand != ""
|| gpuMemUsageCommand != ""
|| diskUsageCommand != "";
}
}

View File

@@ -81,11 +81,11 @@ public class NetworkConfig {
@JsonGetter("shouldManage")
public boolean shouldManage() {
return this.shouldManage || Platform.isLinux();
return this.shouldManage || Platform.isRaspberryPi();
}
@JsonSetter("shouldManage")
public void setShouldManage(boolean shouldManage) {
this.shouldManage = shouldManage || Platform.isLinux();
this.shouldManage = shouldManage || Platform.isRaspberryPi();
}
}

View File

@@ -25,13 +25,14 @@ import java.util.stream.Collectors;
import org.photonvision.PhotonVersion;
import org.photonvision.common.hardware.Platform;
import org.photonvision.common.util.SerializationUtils;
import org.photonvision.raspi.LibCameraJNI;
import org.photonvision.raspi.PicamJNI;
import org.photonvision.vision.processes.VisionModule;
import org.photonvision.vision.processes.VisionModuleManager;
import org.photonvision.vision.processes.VisionSource;
// TODO rename this class
public class PhotonConfiguration {
private HardwareConfig hardwareConfig;
private HardwareSettings hardwareSettings;
private NetworkConfig networkConfig;
@@ -110,11 +111,11 @@ public class PhotonConfiguration {
generalSubmap.put("version", PhotonVersion.versionString);
generalSubmap.put(
"gpuAcceleration",
LibCameraJNI.isSupported()
? "Zerocopy Libcamera on " + LibCameraJNI.getSensorModel().getFriendlyName()
PicamJNI.isSupported()
? "Zerocopy MMAL on " + PicamJNI.getSensorModel().getFriendlyName()
: ""); // TODO add support for other types of GPU accel
generalSubmap.put("hardwareModel", hardwareConfig.deviceName);
generalSubmap.put("hardwarePlatform", Platform.getPlatformName());
generalSubmap.put("hardwarePlatform", Platform.getCurrentPlatform().toString());
settingsSubmap.put("general", generalSubmap);
map.put("settings", settingsSubmap);
@@ -128,8 +129,7 @@ public class PhotonConfiguration {
public static class UICameraConfiguration {
@SuppressWarnings("unused")
public double fov;
public double fov, tiltDegrees;
public String nickname;
public HashMap<String, Object> currentPipelineSettings;
public int currentPipelineIndex;

View File

@@ -27,6 +27,7 @@ import org.photonvision.common.logging.Logger;
@SuppressWarnings("rawtypes")
public class DataChangeService {
private static final Logger logger = new Logger(DataChangeService.class, LogGroup.WebServer);
private static class ThreadSafeSingleton {

View File

@@ -23,6 +23,7 @@ import org.photonvision.common.dataflow.DataChangeDestination;
import org.photonvision.common.dataflow.DataChangeSource;
public class IncomingWebSocketEvent<T> extends DataChangeEvent<T> {
public final Integer cameraIndex;
public final WsContext originContext;

View File

@@ -17,29 +17,23 @@
package org.photonvision.common.dataflow.networktables;
import edu.wpi.first.networktables.NetworkTableEvent;
import edu.wpi.first.networktables.NetworkTableInstance;
import edu.wpi.first.networktables.Subscriber;
import java.util.EnumSet;
import edu.wpi.first.networktables.EntryListenerFlags;
import edu.wpi.first.networktables.EntryNotification;
import edu.wpi.first.networktables.NetworkTableEntry;
import java.util.function.Consumer;
public class NTDataChangeListener {
private final NetworkTableInstance instance;
private final Subscriber watchedEntry;
private final NetworkTableEntry watchedEntry;
private final int listenerID;
public NTDataChangeListener(
NetworkTableInstance instance,
Subscriber watchedSubscriber,
Consumer<NetworkTableEvent> dataChangeConsumer) {
this.watchedEntry = watchedSubscriber;
this.instance = instance;
listenerID =
this.instance.addListener(
watchedEntry, EnumSet.of(NetworkTableEvent.Kind.kValueAll), dataChangeConsumer);
NetworkTableEntry watchedEntry, Consumer<EntryNotification> dataChangeConsumer) {
this.watchedEntry = watchedEntry;
listenerID = watchedEntry.addListener(dataChangeConsumer, EntryListenerFlags.kUpdate);
}
public void remove() {
this.instance.removeListener(listenerID);
watchedEntry.removeListener(listenerID);
}
}

View File

@@ -17,41 +17,48 @@
package org.photonvision.common.dataflow.networktables;
import edu.wpi.first.networktables.EntryNotification;
import edu.wpi.first.networktables.NetworkTable;
import edu.wpi.first.networktables.NetworkTableEvent;
import edu.wpi.first.networktables.NetworkTableEntry;
import java.util.ArrayList;
import java.util.List;
import java.util.function.BooleanSupplier;
import java.util.function.Consumer;
import java.util.function.Supplier;
import org.opencv.core.Point;
import org.photonvision.common.dataflow.CVPipelineResultConsumer;
import org.photonvision.common.dataflow.structures.Packet;
import org.photonvision.common.logging.LogGroup;
import org.photonvision.common.logging.Logger;
import org.photonvision.common.networktables.NTTopicSet;
import org.photonvision.targeting.PhotonPipelineResult;
import org.photonvision.targeting.PhotonTrackedTarget;
import org.photonvision.targeting.TargetCorner;
import org.photonvision.vision.pipeline.result.CVPipelineResult;
import org.photonvision.vision.target.TrackedTarget;
public class NTDataPublisher implements CVPipelineResultConsumer {
private final Logger logger = new Logger(NTDataPublisher.class, LogGroup.General);
private final NetworkTable rootTable = NetworkTablesManager.getInstance().kRootTable;
private NetworkTable subTable;
private NetworkTableEntry rawBytesEntry;
private NTTopicSet ts = new NTTopicSet();
NTDataChangeListener pipelineIndexListener;
private final Supplier<Integer> pipelineIndexSupplier;
private NetworkTableEntry pipelineIndexEntry;
private final Consumer<Integer> pipelineIndexConsumer;
NTDataChangeListener driverModeListener;
private final BooleanSupplier driverModeSupplier;
private NTDataChangeListener pipelineIndexListener;
private NetworkTableEntry driverModeEntry;
private final Consumer<Boolean> driverModeConsumer;
private NTDataChangeListener driverModeListener;
private long heartbeatCounter = 0;
private NetworkTableEntry latencyMillisEntry;
private NetworkTableEntry hasTargetEntry;
private NetworkTableEntry targetPitchEntry;
private NetworkTableEntry targetYawEntry;
private NetworkTableEntry targetAreaEntry;
private NetworkTableEntry targetPoseEntry;
private NetworkTableEntry targetSkewEntry;
// The raw position of the best target, in pixels.
private NetworkTableEntry bestTargetPosX;
private NetworkTableEntry bestTargetPosY;
private final Supplier<Integer> pipelineIndexSupplier;
private final BooleanSupplier driverModeSupplier;
public NTDataPublisher(
String cameraNickname,
@@ -68,67 +75,93 @@ public class NTDataPublisher implements CVPipelineResultConsumer {
updateEntries();
}
private void onPipelineIndexChange(NetworkTableEvent entryNotification) {
var newIndex = (int) entryNotification.valueData.value.getInteger();
private void onPipelineIndexChange(EntryNotification entryNotification) {
var newIndex = (int) entryNotification.value.getDouble();
var originalIndex = pipelineIndexSupplier.get();
// ignore indexes below 0
if (newIndex < 0) {
ts.pipelineIndexPublisher.set(originalIndex);
pipelineIndexEntry.forceSetNumber(originalIndex);
return;
}
if (newIndex == originalIndex) {
logger.debug("Pipeline index is already " + newIndex);
// TODO: Log
return;
}
pipelineIndexConsumer.accept(newIndex);
var setIndex = pipelineIndexSupplier.get();
if (newIndex != setIndex) { // set failed
ts.pipelineIndexPublisher.set(setIndex);
pipelineIndexEntry.forceSetNumber(setIndex);
// TODO: Log
}
logger.debug("Successfully set pipeline index to " + newIndex);
// TODO: Log
}
private void onDriverModeChange(NetworkTableEvent entryNotification) {
var newDriverMode = entryNotification.valueData.value.getBoolean();
private void onDriverModeChange(EntryNotification entryNotification) {
var newDriverMode = entryNotification.value.getBoolean();
var originalDriverMode = driverModeSupplier.getAsBoolean();
if (newDriverMode == originalDriverMode) {
logger.debug("Driver mode is already " + newDriverMode);
// TODO: Log
return;
}
driverModeConsumer.accept(newDriverMode);
logger.debug("Successfully set driver mode to " + newDriverMode);
// TODO: Log
}
@SuppressWarnings("DuplicatedCode")
private void removeEntries() {
if (rawBytesEntry != null) rawBytesEntry.delete();
if (pipelineIndexListener != null) pipelineIndexListener.remove();
if (pipelineIndexEntry != null) pipelineIndexEntry.delete();
if (driverModeListener != null) driverModeListener.remove();
ts.removeEntries();
if (driverModeEntry != null) driverModeEntry.delete();
if (latencyMillisEntry != null) latencyMillisEntry.delete();
if (hasTargetEntry != null) hasTargetEntry.delete();
if (targetPitchEntry != null) targetPitchEntry.delete();
if (targetAreaEntry != null) targetAreaEntry.delete();
if (targetYawEntry != null) targetYawEntry.delete();
if (targetPoseEntry != null) targetPoseEntry.delete();
if (targetSkewEntry != null) targetSkewEntry.delete();
if (bestTargetPosX != null) bestTargetPosX.delete();
if (bestTargetPosY != null) bestTargetPosY.delete();
}
private void updateEntries() {
if (pipelineIndexListener != null) pipelineIndexListener.remove();
if (driverModeListener != null) driverModeListener.remove();
ts.updateEntries();
rawBytesEntry = subTable.getEntry("rawBytes");
if (pipelineIndexListener != null) {
pipelineIndexListener.remove();
}
pipelineIndexEntry = subTable.getEntry("pipelineIndex");
pipelineIndexListener =
new NTDataChangeListener(
ts.subTable.getInstance(), ts.pipelineIndexSubscriber, this::onPipelineIndexChange);
new NTDataChangeListener(pipelineIndexEntry, this::onPipelineIndexChange);
driverModeListener =
new NTDataChangeListener(
ts.subTable.getInstance(), ts.driverModeSubscriber, this::onDriverModeChange);
if (driverModeListener != null) {
driverModeListener.remove();
}
driverModeEntry = subTable.getEntry("driverMode");
driverModeListener = new NTDataChangeListener(driverModeEntry, this::onDriverModeChange);
latencyMillisEntry = subTable.getEntry("latencyMillis");
hasTargetEntry = subTable.getEntry("hasTarget");
targetPitchEntry = subTable.getEntry("targetPitch");
targetAreaEntry = subTable.getEntry("targetArea");
targetYawEntry = subTable.getEntry("targetYaw");
targetPoseEntry = subTable.getEntry("targetPose");
targetSkewEntry = subTable.getEntry("targetSkew");
bestTargetPosX = subTable.getEntry("targetPixelsX");
bestTargetPosY = subTable.getEntry("targetPixelsY");
}
public void updateCameraNickname(String newCameraNickname) {
removeEntries();
ts.subTable = rootTable.getSubTable(newCameraNickname);
subTable = rootTable.getSubTable(newCameraNickname);
updateEntries();
}
@@ -140,72 +173,47 @@ public class NTDataPublisher implements CVPipelineResultConsumer {
Packet packet = new Packet(simplified.getPacketSize());
simplified.populatePacket(packet);
ts.rawBytesEntry.set(packet.getData());
rawBytesEntry.forceSetRaw(packet.getData());
ts.pipelineIndexPublisher.set(pipelineIndexSupplier.get());
ts.driverModePublisher.set(driverModeSupplier.getAsBoolean());
ts.latencyMillisEntry.set(result.getLatencyMillis());
ts.hasTargetEntry.set(result.hasTargets());
pipelineIndexEntry.forceSetNumber(pipelineIndexSupplier.get());
driverModeEntry.forceSetBoolean(driverModeSupplier.getAsBoolean());
latencyMillisEntry.forceSetDouble(result.getLatencyMillis());
hasTargetEntry.forceSetBoolean(result.hasTargets());
if (result.hasTargets()) {
var bestTarget = result.targets.get(0);
ts.targetPitchEntry.set(bestTarget.getPitch());
ts.targetYawEntry.set(bestTarget.getYaw());
ts.targetAreaEntry.set(bestTarget.getArea());
ts.targetSkewEntry.set(bestTarget.getSkew());
targetPitchEntry.forceSetDouble(bestTarget.getPitch());
targetYawEntry.forceSetDouble(bestTarget.getYaw());
targetAreaEntry.forceSetDouble(bestTarget.getArea());
targetSkewEntry.forceSetDouble(bestTarget.getSkew());
var pose = bestTarget.getBestCameraToTarget3d();
ts.targetPoseEntry.set(
new double[] {
pose.getTranslation().getX(),
pose.getTranslation().getY(),
pose.getTranslation().getZ(),
pose.getRotation().getQuaternion().getW(),
pose.getRotation().getQuaternion().getX(),
pose.getRotation().getQuaternion().getY(),
pose.getRotation().getQuaternion().getZ()
});
var poseX = bestTarget.getCameraToTarget().getTranslation().getX();
var poseY = bestTarget.getCameraToTarget().getTranslation().getY();
var poseRot = bestTarget.getCameraToTarget().getRotation().getDegrees();
targetPoseEntry.forceSetDoubleArray(new double[] {poseX, poseY, poseRot});
var targetOffsetPoint = bestTarget.getTargetOffsetPoint();
ts.bestTargetPosX.set(targetOffsetPoint.x);
ts.bestTargetPosY.set(targetOffsetPoint.y);
bestTargetPosX.forceSetDouble(targetOffsetPoint.x);
bestTargetPosY.forceSetDouble(targetOffsetPoint.y);
} else {
ts.targetPitchEntry.set(0);
ts.targetYawEntry.set(0);
ts.targetAreaEntry.set(0);
ts.targetSkewEntry.set(0);
ts.targetPoseEntry.set(new double[] {0, 0, 0});
ts.bestTargetPosX.set(0);
ts.bestTargetPosY.set(0);
targetPitchEntry.forceSetDouble(0);
targetYawEntry.forceSetDouble(0);
targetAreaEntry.forceSetDouble(0);
targetSkewEntry.forceSetDouble(0);
targetPoseEntry.forceSetDoubleArray(new double[] {0, 0, 0});
bestTargetPosX.forceSetDouble(0);
bestTargetPosY.forceSetDouble(0);
}
ts.heartbeatPublisher.set(heartbeatCounter++);
// TODO...nt4... is this needed?
rootTable.getInstance().flush();
}
public static List<PhotonTrackedTarget> simpleFromTrackedTargets(List<TrackedTarget> targets) {
var ret = new ArrayList<PhotonTrackedTarget>();
for (var t : targets) {
var points = new Point[4];
t.getMinAreaRect().points(points);
var cornerList = new ArrayList<TargetCorner>();
for (int i = 0; i < 4; i++) cornerList.add(new TargetCorner(points[i].x, points[i].y));
ret.add(
new PhotonTrackedTarget(
t.getYaw(),
t.getPitch(),
t.getArea(),
t.getSkew(),
t.getFiducialId(),
t.getBestCameraToTarget3d(),
t.getAltCameraToTarget3d(),
t.getPoseAmbiguity(),
cornerList));
t.getYaw(), t.getPitch(), t.getArea(), t.getSkew(), t.getCameraToTarget()));
}
return ret;
}

View File

@@ -17,32 +17,24 @@
package org.photonvision.common.dataflow.networktables;
import edu.wpi.first.networktables.LogMessage;
import edu.wpi.first.networktables.NetworkTable;
import edu.wpi.first.networktables.NetworkTableEvent;
import edu.wpi.first.networktables.NetworkTableInstance;
import java.util.HashMap;
import java.util.function.Consumer;
import org.photonvision.PhotonVersion;
import org.photonvision.common.configuration.ConfigManager;
import org.photonvision.common.configuration.NetworkConfig;
import org.photonvision.common.dataflow.DataChangeService;
import org.photonvision.common.dataflow.events.OutgoingUIEvent;
import org.photonvision.common.logging.LogGroup;
import org.photonvision.common.logging.Logger;
import org.photonvision.common.scripting.ScriptEventType;
import org.photonvision.common.scripting.ScriptManager;
import org.photonvision.common.util.TimedTaskManager;
public class NetworkTablesManager {
private final NetworkTableInstance ntInstance = NetworkTableInstance.getDefault();
private final String kRootTableName = "/photonvision";
public final NetworkTable kRootTable = ntInstance.getTable(kRootTableName);
private boolean isRetryingConnection = false;
private NetworkTablesManager() {
ntInstance.addLogger(0, 255, new NTLogger()); // to hide error messages
TimedTaskManager.getInstance().addTask("NTManager", this::ntTick, 5000);
ntInstance.addLogger(new NTLogger(), 0, 255); // to hide error messages
}
private static NetworkTablesManager INSTANCE;
@@ -54,95 +46,51 @@ public class NetworkTablesManager {
private static final Logger logger = new Logger(NetworkTablesManager.class, LogGroup.General);
private static class NTLogger implements Consumer<NetworkTableEvent> {
private static class NTLogger implements Consumer<LogMessage> {
private boolean hasReportedConnectionFailure = false;
private long lastConnectMessageMillis = 0;
@Override
public void accept(NetworkTableEvent event) {
if (!hasReportedConnectionFailure && event.logMessage.message.contains("timed out")) {
public void accept(LogMessage logMessage) {
if (!hasReportedConnectionFailure && logMessage.message.contains("timed out")) {
logger.error("NT Connection has failed! Will retry in background.");
hasReportedConnectionFailure = true;
getInstance().broadcastConnectedStatus();
} else if (event.logMessage.message.contains("connected")
} else if (logMessage.message.contains("connected")
&& System.currentTimeMillis() - lastConnectMessageMillis > 125) {
logger.info("NT Connected!");
hasReportedConnectionFailure = false;
lastConnectMessageMillis = System.currentTimeMillis();
ScriptManager.queueEvent(ScriptEventType.kNTConnected);
getInstance().broadcastVersion();
getInstance().broadcastConnectedStatus();
}
}
}
public void broadcastConnectedStatus() {
TimedTaskManager.getInstance().addOneShotTask(this::broadcastConnectedStatusImpl, 1000L);
}
private void broadcastConnectedStatusImpl() {
HashMap<String, Object> map = new HashMap<>();
var subMap = new HashMap<String, Object>();
subMap.put("connected", ntInstance.isConnected());
if (ntInstance.isConnected()) {
var connections = getInstance().ntInstance.getConnections();
if (connections.length > 0) {
subMap.put("address", connections[0].remote_ip + ":" + connections[0].remote_port);
}
subMap.put("clients", connections.length);
}
map.put("ntConnectionInfo", subMap);
DataChangeService.getInstance()
.publishEvent(new OutgoingUIEvent<>("networkTablesConnected", map));
}
private void broadcastVersion() {
kRootTable.getEntry("version").setString(PhotonVersion.versionString);
kRootTable.getEntry("buildDate").setString(PhotonVersion.buildDate);
}
public void setConfig(NetworkConfig config) {
if (config.runNTServer) {
setServerMode();
} else {
setClientMode(config.teamNumber);
}
broadcastVersion();
}
private void setClientMode(int teamNumber) {
if (!isRetryingConnection) logger.info("Starting NT Client");
logger.info("Starting NT Client");
ntInstance.stopServer();
ntInstance.startClient4("photonvision");
ntInstance.setServerTeam(teamNumber);
ntInstance.startClientTeam(teamNumber);
ntInstance.startDSClient();
broadcastVersion();
if (ntInstance.isConnected()) {
logger.info("[NetworkTablesManager] Connected to the robot!");
} else {
logger.error(
"[NetworkTablesManager] Could not connect to the robot! Will retry in the background...");
}
}
private void setServerMode() {
logger.info("Starting NT Server");
ntInstance.stopClient();
ntInstance.startServer();
broadcastVersion();
}
// So it seems like if Photon starts before the robot NT server does, and both aren't static IP,
// it'll never connect. This hack works around it by restarting the client/server while the nt
// instance
// isn't connected, same as clicking the save button in the settings menu (or restarting the
// service)
private void ntTick() {
if (!ntInstance.isConnected()
&& !ConfigManager.getInstance().getConfig().getNetworkConfig().runNTServer) {
setConfig(ConfigManager.getInstance().getConfig().getNetworkConfig());
}
if (!ntInstance.isConnected() && !isRetryingConnection) {
isRetryingConnection = true;
logger.error(
"[NetworkTablesManager] Could not connect to the robot! Will retry in the background...");
}
}
}

View File

@@ -21,6 +21,7 @@ import org.photonvision.common.configuration.HardwareConfig;
import org.photonvision.common.hardware.Platform;
public class CustomGPIO extends GPIOBase {
private boolean currentState;
private final int port;

View File

@@ -20,13 +20,14 @@ package org.photonvision.common.hardware.GPIO.pi;
import java.util.HashMap;
/**
* A class that defines the exceptions that can be thrown by Pigpio.
*
* <p>Credit to nkolban
* https://github.com/nkolban/jpigpio/blob/master/JPigpio/src/jpigpio/PigpioException.java
*/
* A class that defines the exceptions that can be thrown by Pigpio.
*
* <p>Credit to nkolban
* https://github.com/nkolban/jpigpio/blob/master/JPigpio/src/jpigpio/PigpioException.java
*/
@SuppressWarnings({"SpellCheckingInspection", "unused", "RedundantSuppression"})
public class PigpioException extends Exception {
private int rc = -99999999;
private static final long serialVersionUID = 443595760654129068L;
@@ -66,10 +67,10 @@ public class PigpioException extends Exception {
}
/**
* Retrieve the error code that was returned by the underlying Pigpio call.
*
* @return The error code that was returned by the underlying Pigpio call.
*/
* Retrieve the error code that was returned by the underlying Pigpio call.
*
* @return The error code that was returned by the underlying Pigpio call.
*/
public int getErrorCode() {
return rc;
} // End of getErrorCode

View File

@@ -24,6 +24,7 @@ import org.photonvision.common.logging.LogGroup;
import org.photonvision.common.logging.Logger;
public class PigpioPin extends GPIOBase {
public static final Logger logger = new Logger(PigpioPin.class, LogGroup.General);
private static final PigpioSocket piSocket = new PigpioSocket();

View File

@@ -23,14 +23,14 @@ public class PigpioPulse {
int delayMicros;
/**
* Initialises a pulse.
*
* @param gpioOn GPIO number to switch on at the start of the pulse. If zero, then no GPIO will be
* switched on.
* @param gpioOff GPIO number to switch off at the start of the pulse. If zero, then no GPIO will
* be switched off.
* @param delayMicros the delay in microseconds before the next pulse.
*/
* Initialises a pulse.
*
* @param gpioOn GPIO number to switch on at the start of the pulse. If zero, then no GPIO will be
* switched on.
* @param gpioOff GPIO number to switch off at the start of the pulse. If zero, then no GPIO will
* be switched off.
* @param delayMicros the delay in microseconds before the next pulse.
*/
public PigpioPulse(int gpioOn, int gpioOff, int delayMicros) {
this.gpioOn = gpioOn != 0 ? 1 << gpioOn : 0;
this.gpioOff = gpioOff != 0 ? 1 << gpioOff : 0;

View File

@@ -41,12 +41,12 @@ public class PigpioSocket {
}
/**
* Creates and starts a socket connection to a pigpio daemon on a remote host with the specified
* address and port
*
* @param addr Address of remote pigpio daemon
* @param port Port of remote pigpio daemon
*/
* Creates and starts a socket connection to a pigpio daemon on a remote host with the specified
* address and port
*
* @param addr Address of remote pigpio daemon
* @param port Port of remote pigpio daemon
*/
public PigpioSocket(String addr, int port) {
try {
commandSocket = new PigpioSocketLock(addr, port);
@@ -56,10 +56,10 @@ public class PigpioSocket {
}
/**
* Reconnects to the pigpio daemon
*
* @throws PigpioException on failure
*/
* Reconnects to the pigpio daemon
*
* @throws PigpioException on failure
*/
public void reconnect() throws PigpioException {
try {
commandSocket.reconnect();
@@ -70,10 +70,10 @@ public class PigpioSocket {
}
/**
* Terminates the connection to the pigpio daemon
*
* @throws PigpioException on failure
*/
* Terminates the connection to the pigpio daemon
*
* @throws PigpioException on failure
*/
public void gpioTerminate() throws PigpioException {
try {
commandSocket.terminate();
@@ -84,12 +84,12 @@ public class PigpioSocket {
}
/**
* Read the GPIO level
*
* @param pin Pin to read from
* @return Value of the pin
* @throws PigpioException on failure
*/
* Read the GPIO level
*
* @param pin Pin to read from
* @return Value of the pin
* @throws PigpioException on failure
*/
public boolean gpioRead(int pin) throws PigpioException {
try {
int retCode = commandSocket.sendCmd(PigpioCommand.PCMD_READ.value, pin);
@@ -102,12 +102,12 @@ public class PigpioSocket {
}
/**
* Write the GPIO level
*
* @param pin Pin to write to
* @param value Value to write
* @throws PigpioException on failure
*/
* Write the GPIO level
*
* @param pin Pin to write to
* @param value Value to write
* @throws PigpioException on failure
*/
public void gpioWrite(int pin, boolean value) throws PigpioException {
try {
int retCode = commandSocket.sendCmd(PigpioCommand.PCMD_WRITE.value, pin, value ? 1 : 0);
@@ -119,10 +119,10 @@ public class PigpioSocket {
}
/**
* Clears all waveforms and any data added by calls to {@link #waveAddGeneric(ArrayList)}
*
* @throws PigpioException on failure
*/
* Clears all waveforms and any data added by calls to {@link #waveAddGeneric(ArrayList)}
*
* @throws PigpioException on failure
*/
public void waveClear() throws PigpioException {
try {
int retCode = commandSocket.sendCmd(PigpioCommand.PCMD_WVCLR.value);
@@ -134,12 +134,12 @@ public class PigpioSocket {
}
/**
* Adds a number of pulses to the current waveform
*
* @param pulses ArrayList of pulses to add
* @return the new total number of pulses in the current waveform
* @throws PigpioException on failure
*/
* Adds a number of pulses to the current waveform
*
* @param pulses ArrayList of pulses to add
* @return the new total number of pulses in the current waveform
* @throws PigpioException on failure
*/
private int waveAddGeneric(ArrayList<PigpioPulse> pulses) throws PigpioException {
// pigpio wave message format
@@ -175,12 +175,12 @@ public class PigpioSocket {
}
/**
* Creates pulses and adds them to the current waveform
*
* @param pulseTimeMillis Pulse length in milliseconds
* @param blinks Number of times to pulse. -1 for repeat
* @param pinNo Pin to pulse
*/
* Creates pulses and adds them to the current waveform
*
* @param pulseTimeMillis Pulse length in milliseconds
* @param blinks Number of times to pulse. -1 for repeat
* @param pinNo Pin to pulse
*/
private void addBlinkPulsesToWaveform(int pulseTimeMillis, int blinks, int pinNo) {
boolean repeat = blinks == -1;
@@ -208,13 +208,13 @@ public class PigpioSocket {
}
/**
* Generates and sends a waveform to the given pins with the specified parameters.
*
* @param pulseTimeMillis Pulse length in milliseconds
* @param blinks Number of times to pulse. -1 for repeat
* @param pins Pins to pulse
* @throws PigpioException on failure
*/
* Generates and sends a waveform to the given pins with the specified parameters.
*
* @param pulseTimeMillis Pulse length in milliseconds
* @param blinks Number of times to pulse. -1 for repeat
* @param pins Pins to pulse
* @throws PigpioException on failure
*/
public void generateAndSendWaveform(int pulseTimeMillis, int blinks, int... pins)
throws PigpioException {
if (pins.length == 0) return;
@@ -263,11 +263,11 @@ public class PigpioSocket {
}
/**
* Stops the transmission of the current waveform
*
* @return success
* @throws PigpioException on failure
*/
* Stops the transmission of the current waveform
*
* @return success
* @throws PigpioException on failure
*/
public boolean waveTxStop() throws PigpioException {
try {
int retCode = commandSocket.sendCmd(PigpioCommand.PCMD_WVHLT.value);
@@ -280,12 +280,12 @@ public class PigpioSocket {
}
/**
* Creates a waveform from the data provided by the prior calls to {@link
* #waveAddGeneric(ArrayList)} Upon success a wave ID greater than or equal to 0 is returned
*
* @return ID of the created waveform
* @throws PigpioException on failure
*/
* Creates a waveform from the data provided by the prior calls to {@link
* #waveAddGeneric(ArrayList)} Upon success a wave ID greater than or equal to 0 is returned
*
* @return ID of the created waveform
* @throws PigpioException on failure
*/
public int waveCreate() throws PigpioException {
try {
int retCode = commandSocket.sendCmd(PigpioCommand.PCMD_WVCRE.value);
@@ -298,11 +298,11 @@ public class PigpioSocket {
}
/**
* Deletes the waveform with specified wave ID
*
* @param waveId ID of the waveform to delete
* @throws PigpioException on failure
*/
* Deletes the waveform with specified wave ID
*
* @param waveId ID of the waveform to delete
* @throws PigpioException on failure
*/
public void waveDelete(int waveId) throws PigpioException {
try {
int retCode = commandSocket.sendCmd(PigpioCommand.PCMD_WVDEL.value, waveId);
@@ -314,12 +314,12 @@ public class PigpioSocket {
}
/**
* Transmits the waveform with specified wave ID. The waveform is sent once
*
* @param waveId ID of the waveform to transmit
* @return The number of DMA control blocks in the waveform
* @throws PigpioException on failure
*/
* Transmits the waveform with specified wave ID. The waveform is sent once
*
* @param waveId ID of the waveform to transmit
* @return The number of DMA control blocks in the waveform
* @throws PigpioException on failure
*/
public int waveSendOnce(int waveId) throws PigpioException {
try {
int retCode = commandSocket.sendCmd(PigpioCommand.PCMD_WVTX.value, waveId);
@@ -331,13 +331,13 @@ public class PigpioSocket {
}
/**
* Transmits the waveform with specified wave ID. The waveform cycles until cancelled (either by
* the sending of a new waveform or {@link #waveTxStop()}
*
* @param waveId ID of the waveform to transmit
* @return The number of DMA control blocks in the waveform
* @throws PigpioException on failure
*/
* Transmits the waveform with specified wave ID. The waveform cycles until cancelled (either by
* the sending of a new waveform or {@link #waveTxStop()}
*
* @param waveId ID of the waveform to transmit
* @return The number of DMA control blocks in the waveform
* @throws PigpioException on failure
*/
public int waveSendRepeat(int waveId) throws PigpioException {
try {
int retCode = commandSocket.sendCmd(PigpioCommand.PCMD_WVTXR.value, waveId);
@@ -349,14 +349,14 @@ public class PigpioSocket {
}
/**
* Starts hardware PWM on a GPIO at the specified frequency and dutycycle
*
* @param pin GPIO pin to start PWM on
* @param pwmFrequency Frequency to run at (1Hz-125MHz). Frequencies above 30MHz are unlikely to
* work
* @param pwmDuty Duty cycle to run at (0-1,000,000)
* @throws PigpioException on failure
*/
* Starts hardware PWM on a GPIO at the specified frequency and dutycycle
*
* @param pin GPIO pin to start PWM on
* @param pwmFrequency Frequency to run at (1Hz-125MHz). Frequencies above 30MHz are unlikely to
* work
* @param pwmDuty Duty cycle to run at (0-1,000,000)
* @throws PigpioException on failure
*/
public void hardwarePWM(int pin, int pwmFrequency, int pwmDuty) throws PigpioException {
try {
ByteBuffer bb = ByteBuffer.allocate(4);

View File

@@ -24,9 +24,9 @@ import java.net.Socket;
import java.nio.ByteBuffer;
/**
* Credit to nkolban
* https://github.com/nkolban/jpigpio/blob/master/JPigpio/src/jpigpio/SocketLock.java
*/
* Credit to nkolban
* https://github.com/nkolban/jpigpio/blob/master/JPigpio/src/jpigpio/SocketLock.java
*/
final class PigpioSocketLock {
private static final int replyTimeoutMillis = 1000;
@@ -82,16 +82,16 @@ final class PigpioSocketLock {
}
/**
* Send extended command to pigpiod and return result code
*
* @param cmd Command to send
* @param p1 Command parameter 1
* @param p2 Command parameter 2
* @param p3 Command parameter 3 (usually length of extended data - see paramater ext)
* @param ext Array of bytes containing extended data
* @return Command result code
* @throws IOException in case of network connection error
*/
* Send extended command to pigpiod and return result code
*
* @param cmd Command to send
* @param p1 Command parameter 1
* @param p2 Command parameter 2
* @param p3 Command parameter 3 (usually length of extended data - see paramater ext)
* @param ext Array of bytes containing extended data
* @return Command result code
* @throws IOException in case of network connection error
*/
@SuppressWarnings("UnusedAssignment")
public synchronized int sendCmd(int cmd, int p1, int p2, int p3, byte[] ext) throws IOException {
ByteBuffer bb = ByteBuffer.allocate(16 + ext.length);
@@ -136,11 +136,11 @@ final class PigpioSocketLock {
}
/**
* Read all remaining bytes coming from pigpiod
*
* @param data Array to store read bytes.
* @throws IOException if unable to read from network
*/
* Read all remaining bytes coming from pigpiod
*
* @param data Array to store read bytes.
* @throws IOException if unable to read from network
*/
public void readBytes(byte[] data) throws IOException {
in.readFully(data);
}

View File

@@ -17,7 +17,7 @@
package org.photonvision.common.hardware;
import edu.wpi.first.networktables.IntegerEntry;
import edu.wpi.first.networktables.NetworkTableEntry;
import java.io.IOException;
import org.photonvision.common.ProgramStatus;
import org.photonvision.common.configuration.ConfigManager;
@@ -27,7 +27,7 @@ import org.photonvision.common.dataflow.networktables.NTDataChangeListener;
import org.photonvision.common.dataflow.networktables.NetworkTablesManager;
import org.photonvision.common.hardware.GPIO.CustomGPIO;
import org.photonvision.common.hardware.GPIO.pi.PigpioSocket;
import org.photonvision.common.hardware.metrics.MetricsManager;
import org.photonvision.common.hardware.metrics.MetricsBase;
import org.photonvision.common.logging.LogGroup;
import org.photonvision.common.logging.Logger;
import org.photonvision.common.util.ShellExec;
@@ -41,13 +41,11 @@ public class HardwareManager {
private final HardwareConfig hardwareConfig;
private final HardwareSettings hardwareSettings;
private final MetricsManager metricsManager;
@SuppressWarnings({"FieldCanBeLocal", "unused"})
private final StatusLED statusLED;
@SuppressWarnings("FieldCanBeLocal")
private final IntegerEntry ledModeEntry;
private final NetworkTableEntry ledModeEntry;
@SuppressWarnings({"FieldCanBeLocal", "unused"})
private final NTDataChangeListener ledModeListener;
@@ -67,11 +65,8 @@ public class HardwareManager {
private HardwareManager(HardwareConfig hardwareConfig, HardwareSettings hardwareSettings) {
this.hardwareConfig = hardwareConfig;
this.hardwareSettings = hardwareSettings;
this.metricsManager = new MetricsManager();
this.metricsManager.setConfig(hardwareConfig);
CustomGPIO.setConfig(hardwareConfig);
MetricsBase.setConfig(hardwareConfig);
if (Platform.isRaspberryPi()) {
pigpioSocket = new PigpioSocket();
@@ -94,16 +89,12 @@ public class HardwareManager {
hasBrightnessRange ? hardwareConfig.ledBrightnessRange.get(1) : 100,
pigpioSocket);
ledModeEntry =
NetworkTablesManager.getInstance().kRootTable.getIntegerTopic("ledMode").getEntry(0);
ledModeEntry.set(VisionLEDMode.kDefault.value);
ledModeEntry = NetworkTablesManager.getInstance().kRootTable.getEntry("ledMode");
ledModeEntry.setNumber(VisionLEDMode.kDefault.value);
ledModeListener =
visionLED == null
? null
: new NTDataChangeListener(
NetworkTablesManager.getInstance().kRootTable.getInstance(),
ledModeEntry,
visionLED::onLedModeChange);
: new NTDataChangeListener(ledModeEntry, visionLED::onLedModeChange);
Runtime.getRuntime().addShutdownHook(new Thread(this::onJvmExit));
@@ -131,7 +122,7 @@ public class HardwareManager {
}
public boolean restartDevice() {
if (Platform.isLinux()) {
if (Platform.isRaspberryPi()) {
try {
return shellExec.executeBashCommand("reboot now") == 0;
} catch (IOException e) {
@@ -167,8 +158,4 @@ public class HardwareManager {
public HardwareConfig getConfig() {
return hardwareConfig;
}
public void publishMetrics() {
metricsManager.publishMetrics();
}
}

View File

@@ -1,71 +0,0 @@
/*
* Copyright (C) Photon Vision.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.photonvision.common.hardware;
import java.io.IOException;
import org.photonvision.common.util.ShellExec;
public enum PiVersion {
PI_B("Pi Model B"),
COMPUTE_MODULE("Compute Module Rev"),
ZERO_W("Pi Zero W Rev 1.1"),
ZERO_2_W("Raspberry Pi Zero 2"),
PI_3("Pi 3"),
PI_4("Pi 4"),
COMPUTE_MODULE_3("Compute Module 3"),
UNKNOWN("UNKNOWN");
private final String identifier;
private static final ShellExec shell = new ShellExec(true, false);
private static final PiVersion currentPiVersion = calcPiVersion();
private PiVersion(String s) {
this.identifier = s.toLowerCase();
}
public static PiVersion getPiVersion() {
return currentPiVersion;
}
private static PiVersion calcPiVersion() {
if (!Platform.isRaspberryPi()) return PiVersion.UNKNOWN;
String piString = getPiVersionString();
for (PiVersion p : PiVersion.values()) {
if (piString.toLowerCase().contains(p.identifier)) return p;
}
return UNKNOWN;
}
// Query /proc/device-tree/model. This should return the model of the pi
// Versions here:
// https://github.com/raspberrypi/linux/blob/rpi-5.10.y/arch/arm/boot/dts/bcm2710-rpi-cm3.dts
private static String getPiVersionString() {
if (!Platform.isRaspberryPi()) return "";
try {
shell.executeBashCommand("cat /proc/device-tree/model");
} catch (IOException e) {
e.printStackTrace();
}
if (shell.getExitCode() == 0) {
// We expect it to be in the format "raspberry pi X model X"
return shell.getOutput();
}
return "";
}
}

View File

@@ -17,91 +17,54 @@
package org.photonvision.common.hardware;
import com.jogamp.common.os.Platform.OSType;
import edu.wpi.first.util.RuntimeDetector;
import java.io.BufferedReader;
import edu.wpi.first.wpiutil.RuntimeDetector;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;
import org.photonvision.common.util.ShellExec;
@SuppressWarnings("unused")
public enum Platform {
// WPILib Supported (JNI)
WINDOWS_64("Windows x64", false, OSType.WINDOWS, true),
LINUX_32("Linux x86", false, OSType.LINUX, true),
LINUX_64("Linux x64", false, OSType.LINUX, true),
LINUX_RASPBIAN32(
"Linux Raspbian 32-bit", true, OSType.LINUX, true), // Raspberry Pi 3/4 with a 32-bit image
LINUX_RASPBIAN64(
"Linux Raspbian 64-bit", true, OSType.LINUX, true), // Raspberry Pi 3/4 with a 64-bit image
LINUX_AARCH64("Linux AARCH64", false, OSType.LINUX, true), // Jetson Nano, Jetson TX2
// PhotonVision Supported (Manual build/install)
LINUX_ARM32("Linux ARM32", false, OSType.LINUX, true), // ODROID XU4, C1+
LINUX_ARM64("Linux ARM64", false, OSType.LINUX, true), // ODROID C2, N2
WINDOWS_32("Windows x32"),
WINDOWS_64("Windows x64"),
LINUX_64("Linux x64"),
LINUX_RASPBIAN("Linux Raspbian"), // Raspberry Pi 3/4
LINUX_AARCH64BIONIC("Linux AARCH64 Bionic"), // Jetson Nano, Jetson TX2
// PhotonVision Supported (Manual install)
LINUX_ARM32("Linux ARM32"), // ODROID XU4, C1+
LINUX_ARM64("Linux ARM64"), // ODROID C2, N2
// Completely unsupported
WINDOWS_32("Windows x86", false, OSType.WINDOWS, false),
MACOS("Mac OS", false, OSType.MACOS, false),
UNKNOWN("Unsupported Platform", false, OSType.UNKNOWN, false);
private enum OSType {
WINDOWS,
LINUX,
MACOS,
UNKNOWN
}
UNSUPPORTED("Unsupported Platform");
private static final ShellExec shell = new ShellExec(true, false);
public final String description;
public final boolean isPi;
public final OSType osType;
public final boolean isSupported;
public final String value;
public static final boolean isRoot = checkForRoot();
// Set once at init, shouldn't be needed after.
private static final Platform currentPlatform = getCurrentPlatform();
private static final boolean isRoot = checkForRoot();
Platform(String description, boolean isPi, OSType osType, boolean isSupported) {
this.description = description;
this.isPi = isPi;
this.osType = osType;
this.isSupported = isSupported;
Platform(String value) {
this.value = value;
}
//////////////////////////////////////////////////////
// Public API
private static final String OS_NAME = System.getProperty("os.name");
private static final String OS_ARCH = System.getProperty("os.arch");
public static final Platform CurrentPlatform = getCurrentPlatform();
private static String UnknownPlatformString =
String.format("Unknown Platform. OS: %s, Architecture: %s", OS_NAME, OS_ARCH);
public boolean isWindows() {
return this == WINDOWS_64 || this == WINDOWS_32;
}
// Checks specifically if unix shell and API are supported
public static boolean isLinux() {
return currentPlatform.osType == OSType.LINUX;
return getCurrentPlatform() == LINUX_64
|| getCurrentPlatform() == LINUX_RASPBIAN
|| getCurrentPlatform() == LINUX_ARM64;
}
public static boolean isRaspberryPi() {
return currentPlatform.isPi;
return CurrentPlatform.equals(LINUX_RASPBIAN);
}
public static String getPlatformName() {
if (currentPlatform.equals(UNKNOWN)) {
return UnknownPlatformString;
} else {
return currentPlatform.description;
}
}
public static boolean isRoot() {
return isRoot;
}
//////////////////////////////////////////////////////
// Debug info related to unknown platforms for debug help
private static final String OS_NAME = System.getProperty("os.name");
private static final String OS_ARCH = System.getProperty("os.arch");
private static final String UnknownPlatformString =
String.format("Unknown Platform. OS: %s, Architecture: %s", OS_NAME, OS_ARCH);
@SuppressWarnings("StatementWithEmptyBody")
private static boolean checkForRoot() {
if (isLinux()) {
@@ -125,92 +88,31 @@ public enum Platform {
return false;
}
private static Platform getCurrentPlatform() {
public static Platform getCurrentPlatform() {
if (RuntimeDetector.isWindows()) {
if (RuntimeDetector.is32BitIntel()) {
return WINDOWS_32;
} else if (RuntimeDetector.is64BitIntel()) {
return WINDOWS_64;
} else {
// please don't try this
return UNKNOWN;
}
if (RuntimeDetector.is32BitIntel()) return WINDOWS_32;
if (RuntimeDetector.is64BitIntel()) return WINDOWS_64;
}
if (RuntimeDetector.isMac()) {
// TODO - once we have real support, this might have to be more granular
return MACOS;
if (RuntimeDetector.is32BitIntel()) return UNSUPPORTED;
}
if (RuntimeDetector.isLinux()) {
if (isPiSBC()) {
if (RuntimeDetector.isArm32()) {
return LINUX_RASPBIAN32;
} else if (RuntimeDetector.isArm64()) {
return LINUX_RASPBIAN64;
} else {
// Unknown/exotic installation
return UNKNOWN;
}
} else if (isJetsonSBC()) {
if (RuntimeDetector.isArm64()) {
// TODO - do we need to check OS version?
return LINUX_AARCH64;
} else {
// Unknown/exotic installation
return UNKNOWN;
}
} else if (RuntimeDetector.is64BitIntel()) {
return LINUX_64;
} else if (RuntimeDetector.is32BitIntel()) {
return LINUX_32;
} else if (RuntimeDetector.isArm64()) {
// TODO - os detection needed?
return LINUX_AARCH64;
} else {
// Unknown or otherwise unsupported platform
return Platform.UNKNOWN;
}
if (RuntimeDetector.is32BitIntel()) return UNSUPPORTED;
if (RuntimeDetector.is64BitIntel()) return LINUX_64;
if (RuntimeDetector.isRaspbian()) return LINUX_RASPBIAN;
}
// If we fall through all the way to here,
return Platform.UNKNOWN;
System.out.println(UnknownPlatformString);
return Platform.UNSUPPORTED;
}
// Check for various known SBC types
private static boolean isPiSBC() {
return fileHasText("/proc/cpuinfo", "Raspberry Pi");
}
private static boolean isJetsonSBC() {
// https://forums.developer.nvidia.com/t/how-to-recognize-jetson-nano-device/146624
return fileHasText("/proc/device-tree/model", "NVIDIA Jetson");
}
// Checks for various names of linux OS
private static boolean isStretch() {
// TODO - this is a total guess
return fileHasText("/etc/os-release", "Stretch");
}
private static boolean isBuster() {
// TODO - this is a total guess
return fileHasText("/etc/os-release", "Buster");
}
private static boolean fileHasText(String filename, String text) {
try (BufferedReader reader = Files.newBufferedReader(Paths.get(filename))) {
while (true) {
String value = reader.readLine();
if (value == null) {
return false;
} else if (value.contains(text)) {
return true;
} // else, next line
}
} catch (IOException ex) {
return false;
public String toString() {
if (this.equals(UNSUPPORTED)) {
return UnknownPlatformString;
} else {
return this.value;
}
}
}

View File

@@ -17,7 +17,7 @@
package org.photonvision.common.hardware;
import edu.wpi.first.networktables.NetworkTableEvent;
import edu.wpi.first.networktables.EntryNotification;
import java.util.ArrayList;
import java.util.List;
import java.util.function.BooleanSupplier;
@@ -85,8 +85,6 @@ public class VisionLED {
pigpioSocket.generateAndSendWaveform(pulseLengthMillis, blinkCount, ledPins);
} catch (PigpioException e) {
logger.error("Failed to blink!", e);
} catch (NullPointerException e) {
logger.error("Failed to blink, pigpio internal issue!", e);
}
} else {
for (GPIOBase led : visionLEDs) {
@@ -102,19 +100,13 @@ public class VisionLED {
pigpioSocket.waveTxStop();
} catch (PigpioException e) {
logger.error("Failed to stop blink!", e);
} catch (NullPointerException e) {
logger.error("Failed to blink, pigpio internal issue!", e);
}
}
try {
// if the user has set an LED brightness other than 100%, use that instead
if (mappedBrightnessPercentage == 100 || !state) {
visionLEDs.forEach((led) -> led.setState(state));
} else {
visionLEDs.forEach((led) -> led.setBrightness(mappedBrightnessPercentage));
}
} catch (NullPointerException e) {
logger.error("Failed to blink, pigpio internal issue!", e);
// if the user has set an LED brightness other than 100%, use that instead
if (mappedBrightnessPercentage == 100 || !state) {
visionLEDs.forEach((led) -> led.setState(state));
} else {
visionLEDs.forEach((led) -> led.setBrightness(mappedBrightnessPercentage));
}
}
@@ -122,8 +114,8 @@ public class VisionLED {
setInternal(on ? VisionLEDMode.kOn : VisionLEDMode.kOff, false);
}
void onLedModeChange(NetworkTableEvent entryNotification) {
var newLedModeRaw = (int) entryNotification.valueData.value.getDouble();
void onLedModeChange(EntryNotification entryNotification) {
var newLedModeRaw = (int) entryNotification.value.getDouble();
if (newLedModeRaw != currentLedMode.value) {
VisionLEDMode newLedMode;
switch (newLedModeRaw) {
@@ -185,9 +177,6 @@ public class VisionLED {
case kOn:
setStateImpl(true);
break;
case kBlink:
blinkImpl(85, -1);
break;
}
}
logger.info("Changing LED internal state to " + newLedMode.toString());

View File

@@ -15,20 +15,30 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.photonvision.vision.pipe.impl;
package org.photonvision.common.hardware.metrics;
import java.awt.*;
import org.photonvision.vision.frame.FrameDivisor;
public class CPUMetrics extends MetricsBase {
public class Draw2dAprilTagsPipe extends Draw2dTargetsPipe {
public static class Draw2dAprilTagsParams extends Draw2dTargetsPipe.Draw2dTargetsParams {
public Draw2dAprilTagsParams(
boolean shouldDraw, boolean showMultipleTargets, FrameDivisor divisor) {
super(shouldDraw, showMultipleTargets, divisor);
// We want to show the polygon, not the rotated box
this.showRotatedBox = false;
this.showMaximumBox = false;
this.rotatedBoxColor = Color.RED;
private String cpuMemSplit = null;
public String getMemory() {
if (cpuMemoryCommand.isEmpty()) return "";
if (cpuMemSplit == null) {
cpuMemSplit = execute(cpuMemoryCommand);
}
return cpuMemSplit;
}
public String getTemp() {
if (cpuTemperatureCommand.isEmpty()) return "";
try {
return execute(cpuTemperatureCommand);
} catch (Exception e) {
return "N/A";
}
}
public String getUtilization() {
return execute(cpuUtilizationCommand);
}
}

View File

@@ -15,10 +15,11 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.photonvision.vision.frame;
package org.photonvision.common.hardware.metrics;
public enum FrameThresholdType {
NONE,
HSV,
GREYSCALE,
public class DiskMetrics extends MetricsBase {
public String getUsedDiskPct() {
if (diskUsageCommand.isEmpty()) return "";
return execute(diskUsageCommand);
}
}

View File

@@ -15,26 +15,19 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.photonvision.common.util;
package org.photonvision.common.hardware.metrics;
import java.nio.file.Path;
import java.nio.file.Paths;
public class GPUMetrics extends MetricsBase {
private String gpuMemSplit = null;
public class NativeLibHelper {
private static NativeLibHelper INSTANCE;
public static NativeLibHelper getInstance() {
if (INSTANCE == null) {
INSTANCE = new NativeLibHelper();
public String getGPUMemorySplit() {
if (gpuMemSplit == null) {
gpuMemSplit = execute(gpuMemoryCommand);
}
return INSTANCE;
return gpuMemSplit;
}
public final Path NativeLibPath;
private NativeLibHelper() {
String home = System.getProperty("user.home");
NativeLibPath = Paths.get(home, ".pvlibs", "nativecache");
public String getMallocedMemory() {
return execute(gpuMemUsageCommand);
}
}

View File

@@ -0,0 +1,92 @@
/*
* Copyright (C) Photon Vision.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.photonvision.common.hardware.metrics;
import java.io.PrintWriter;
import java.io.StringWriter;
import org.photonvision.common.configuration.HardwareConfig;
import org.photonvision.common.hardware.Platform;
import org.photonvision.common.logging.LogGroup;
import org.photonvision.common.logging.Logger;
import org.photonvision.common.util.ShellExec;
public abstract class MetricsBase {
private static final Logger logger = new Logger(MetricsBase.class, LogGroup.General);
// CPU
public static String cpuMemoryCommand = "vcgencmd get_mem arm | grep -Eo '[0-9]+'";
public static String cpuTemperatureCommand =
"sed 's/.\\{3\\}$/.&/' <<< cat /sys/class/thermal/thermal_zone0/temp";
public static String cpuUtilizationCommand =
"top -bn1 | grep \"Cpu(s)\" | sed \"s/.*, *\\([0-9.]*\\)%* id.*/\\1/\" | awk '{print 100 - $1}'";
// GPU
public static String gpuMemoryCommand = "vcgencmd get_mem gpu | grep -Eo '[0-9]+'";
public static String gpuMemUsageCommand = "vcgencmd get_mem malloc | grep -Eo '[0-9]+'";
// RAM
public static String ramUsageCommand = "free --mega | awk -v i=2 -v j=3 'FNR == i {print $j}'";
// Disk
public static String diskUsageCommand = "df ./ --output=pcent | tail -n +2";
private static ShellExec runCommand = new ShellExec(true, true);
public static void setConfig(HardwareConfig config) {
if (Platform.isRaspberryPi()) return;
cpuMemoryCommand = config.cpuMemoryCommand;
cpuTemperatureCommand = config.cpuTempCommand;
cpuUtilizationCommand = config.cpuUtilCommand;
gpuMemoryCommand = config.gpuMemoryCommand;
gpuMemUsageCommand = config.gpuMemUsageCommand;
diskUsageCommand = config.diskUsageCommand;
ramUsageCommand = config.ramUtilCommand;
}
public static synchronized String execute(String command) {
try {
runCommand.executeBashCommand(command);
return runCommand.getOutput();
} catch (Exception e) {
StringWriter sw = new StringWriter();
PrintWriter pw = new PrintWriter(sw);
e.printStackTrace(pw);
logger.error(
"Command: \""
+ command
+ "\" returned an error!"
+ "\nOutput Received: "
+ runCommand.getOutput()
+ "\nStandard Error: "
+ runCommand.getError()
+ "\nCommand completed: "
+ runCommand.isOutputCompleted()
+ "\nError completed: "
+ runCommand.isErrorCompleted()
+ "\nExit code: "
+ runCommand.getExitCode()
+ "\n Exception: "
+ e.toString()
+ sw.toString());
return "";
}
}
}

View File

@@ -1,161 +0,0 @@
/*
* Copyright (C) Photon Vision.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.photonvision.common.hardware.metrics;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.util.HashMap;
import org.photonvision.common.configuration.HardwareConfig;
import org.photonvision.common.dataflow.DataChangeService;
import org.photonvision.common.dataflow.events.OutgoingUIEvent;
import org.photonvision.common.hardware.Platform;
import org.photonvision.common.hardware.metrics.cmds.CmdBase;
import org.photonvision.common.hardware.metrics.cmds.FileCmds;
import org.photonvision.common.hardware.metrics.cmds.LinuxCmds;
import org.photonvision.common.hardware.metrics.cmds.PiCmds;
import org.photonvision.common.logging.LogGroup;
import org.photonvision.common.logging.Logger;
import org.photonvision.common.util.ShellExec;
public class MetricsManager {
final Logger logger = new Logger(MetricsManager.class, LogGroup.General);
CmdBase cmds;
private ShellExec runCommand = new ShellExec(true, true);
public void setConfig(HardwareConfig config) {
if (config.hasCommandsConfigured()) {
cmds = new FileCmds();
} else if (Platform.isRaspberryPi()) {
cmds = new PiCmds(); // Pi's can use a hardcoded command set
} else if (Platform.isLinux()) {
cmds = new LinuxCmds(); // Linux/Unix platforms assume a nominal command set
} else {
cmds = new CmdBase(); // default - base has no commands
}
cmds.initCmds(config);
}
public String safeExecute(String str) {
if (str.isEmpty()) return "";
try {
return execute(str);
} catch (Exception e) {
return "****";
}
}
private String cpuMemSave = null;
public String getMemory() {
if (cmds.cpuMemoryCommand.isEmpty()) return "";
if (cpuMemSave == null) {
// save the value and only run it once
cpuMemSave = execute(cmds.cpuMemoryCommand);
}
return cpuMemSave;
}
public String getTemp() {
return safeExecute(cmds.cpuTemperatureCommand);
}
public String getUtilization() {
return safeExecute(cmds.cpuUtilizationCommand);
}
public String getUptime() {
return safeExecute(cmds.cpuUptimeCommand);
}
public String getThrottleReason() {
return safeExecute(cmds.cpuThrottleReasonCmd);
}
private String gpuMemSave = null;
public String getGPUMemorySplit() {
if (gpuMemSave == null) {
// only needs to run once
gpuMemSave = safeExecute(cmds.gpuMemoryCommand);
}
return gpuMemSave;
}
public String getMallocedMemory() {
return safeExecute(cmds.gpuMemUsageCommand);
}
public String getUsedDiskPct() {
return safeExecute(cmds.diskUsageCommand);
}
// TODO: Output in MBs for consistency
public String getUsedRam() {
return safeExecute(cmds.ramUsageCommand);
}
public void publishMetrics() {
logger.debug("Publishing Metrics...");
final var metrics = new HashMap<String, String>();
metrics.put("cpuTemp", this.getTemp());
metrics.put("cpuUtil", this.getUtilization());
metrics.put("cpuMem", this.getMemory());
metrics.put("cpuThr", this.getThrottleReason());
metrics.put("cpuUptime", this.getUptime());
metrics.put("gpuMem", this.getGPUMemorySplit());
metrics.put("ramUtil", this.getUsedRam());
metrics.put("gpuMemUtil", this.getMallocedMemory());
metrics.put("diskUtilPct", this.getUsedDiskPct());
DataChangeService.getInstance().publishEvent(OutgoingUIEvent.wrappedOf("metrics", metrics));
}
public synchronized String execute(String command) {
try {
runCommand.executeBashCommand(command);
return runCommand.getOutput();
} catch (Exception e) {
StringWriter sw = new StringWriter();
PrintWriter pw = new PrintWriter(sw);
e.printStackTrace(pw);
logger.error(
"Command: \""
+ command
+ "\" returned an error!"
+ "\nOutput Received: "
+ runCommand.getOutput()
+ "\nStandard Error: "
+ runCommand.getError()
+ "\nCommand completed: "
+ runCommand.isOutputCompleted()
+ "\nError completed: "
+ runCommand.isErrorCompleted()
+ "\nExit code: "
+ runCommand.getExitCode()
+ "\n Exception: "
+ e.toString()
+ sw.toString());
return "";
}
}
}

View File

@@ -0,0 +1,74 @@
/*
* Copyright (C) Photon Vision.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.photonvision.common.hardware.metrics;
import java.util.HashMap;
import org.photonvision.common.dataflow.DataChangeService;
import org.photonvision.common.dataflow.events.OutgoingUIEvent;
import org.photonvision.common.hardware.Platform;
import org.photonvision.common.logging.LogGroup;
import org.photonvision.common.logging.Logger;
import org.photonvision.common.util.TimedTaskManager;
public class MetricsPublisher {
private static final Logger logger = new Logger(MetricsPublisher.class, LogGroup.General);
private static CPUMetrics cpuMetrics;
private static GPUMetrics gpuMetrics;
private static RAMMetrics ramMetrics;
private static DiskMetrics diskMetrics;
public static MetricsPublisher getInstance() {
return Singleton.INSTANCE;
}
private MetricsPublisher() {
cpuMetrics = new CPUMetrics();
gpuMetrics = new GPUMetrics();
ramMetrics = new RAMMetrics();
diskMetrics = new DiskMetrics();
}
public void stopTask() {
TimedTaskManager.getInstance().cancelTask("Metrics");
logger.info("This device does not support running bash commands. Stopped metrics thread.");
}
public void publish() {
if (!Platform.isRaspberryPi()) {
logger.debug("Ignoring metrics on non-Pi devices");
return;
}
logger.debug("Publishing Metrics...");
final var metrics = new HashMap<String, String>();
metrics.put("cpuTemp", cpuMetrics.getTemp());
metrics.put("cpuUtil", cpuMetrics.getUtilization());
metrics.put("cpuMem", cpuMetrics.getMemory());
metrics.put("gpuMem", gpuMetrics.getGPUMemorySplit());
metrics.put("ramUtil", ramMetrics.getUsedRam());
metrics.put("gpuMemUtil", gpuMetrics.getMallocedMemory());
metrics.put("diskUtilPct", diskMetrics.getUsedDiskPct());
DataChangeService.getInstance().publishEvent(OutgoingUIEvent.wrappedOf("metrics", metrics));
}
private static class Singleton {
public static final MetricsPublisher INSTANCE = new MetricsPublisher();
}
}

View File

@@ -15,20 +15,12 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.photonvision.vision.apriltag;
package org.photonvision.common.hardware.metrics;
public enum AprilTagFamily {
kTag36h11,
kTag25h9,
kTag16h5,
kTagCircle21h7,
kTagCircle49h12,
kTagStandard41h12,
kTagStandard52h13,
kTagCustom48h11;
public String getNativeName() {
// We wanna strip the leading kT and replace with "t"
return this.name().replaceFirst("kT", "t");
public class RAMMetrics extends MetricsBase {
// TODO: Output in MBs for consistency
public String getUsedRam() {
if (ramUsageCommand.isEmpty()) return "";
return execute(ramUsageCommand);
}
}

View File

@@ -1,40 +0,0 @@
/*
* Copyright (C) Photon Vision.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.photonvision.common.hardware.metrics.cmds;
import org.photonvision.common.configuration.HardwareConfig;
public class CmdBase {
// CPU
public String cpuMemoryCommand = "";
public String cpuTemperatureCommand = "";
public String cpuUtilizationCommand = "";
public String cpuThrottleReasonCmd = "";
public String cpuUptimeCommand = "";
// GPU
public String gpuMemoryCommand = "";
public String gpuMemUsageCommand = "";
// RAM
public String ramUsageCommand = "";
// Disk
public String diskUsageCommand = "";
public void initCmds(HardwareConfig config) {
return; // default - do nothing
}
}

View File

@@ -1,38 +0,0 @@
/*
* Copyright (C) Photon Vision.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.photonvision.common.hardware.metrics.cmds;
import org.photonvision.common.configuration.HardwareConfig;
public class FileCmds extends CmdBase {
@Override
public void initCmds(HardwareConfig config) {
cpuMemoryCommand = config.cpuMemoryCommand;
cpuTemperatureCommand = config.cpuTempCommand;
cpuUtilizationCommand = config.cpuUtilCommand;
cpuThrottleReasonCmd = config.cpuThrottleReasonCmd;
cpuUptimeCommand = config.cpuUptimeCommand;
gpuMemoryCommand = config.gpuMemoryCommand;
gpuMemUsageCommand = config.gpuMemUsageCommand;
diskUsageCommand = config.diskUsageCommand;
ramUsageCommand = config.ramUtilCommand;
}
}

View File

@@ -1,40 +0,0 @@
/*
* Copyright (C) Photon Vision.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.photonvision.common.hardware.metrics.cmds;
import org.photonvision.common.configuration.HardwareConfig;
public class LinuxCmds extends CmdBase {
public void initCmds(HardwareConfig config) {
// CPU
cpuMemoryCommand = "awk '/MemTotal:/ {print int($2 / 1000);}' /proc/meminfo";
// TODO: boards have lots of thermal devices. Hard to pick the CPU
cpuUtilizationCommand =
"top -bn1 | grep \"Cpu(s)\" | sed \"s/.*, *\\([0-9.]*\\)%* id.*/\\1/\" | awk '{print 100 - $1}'";
cpuUptimeCommand = "uptime -p | cut -c 4-";
// RAM
ramUsageCommand = "awk '/MemFree:/ {print int($2 / 1000);}' /proc/meminfo";
// Disk
diskUsageCommand = "df ./ --output=pcent | tail -n +2";
}
}

View File

@@ -1,41 +0,0 @@
/*
* Copyright (C) Photon Vision.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.photonvision.common.hardware.metrics.cmds;
import org.photonvision.common.configuration.HardwareConfig;
public class PiCmds extends LinuxCmds {
/** Applies pi-specific commands, ignoring any input configuration */
public void initCmds(HardwareConfig config) {
super.initCmds(config);
// CPU
cpuMemoryCommand = "vcgencmd get_mem arm | grep -Eo '[0-9]+'";
cpuTemperatureCommand = "sed 's/.\\{3\\}$/.&/' /sys/class/thermal/thermal_zone0/temp";
cpuThrottleReasonCmd =
"if (( $(( $(vcgencmd get_throttled | grep -Eo 0x[0-9a-fA-F]*) & 0x01 )) != 0x00 )); then echo \"LOW VOLTAGE\"; "
+ " elif (( $(( $(vcgencmd get_throttled | grep -Eo 0x[0-9a-fA-F]*) & 0x08 )) != 0x00 )); then echo \"HIGH TEMP\"; "
+ " elif (( $(( $(vcgencmd get_throttled | grep -Eo 0x[0-9a-fA-F]*) & 0x10000 )) != 0x00 )); then echo \"Prev. Low Voltage\"; "
+ " elif (( $(( $(vcgencmd get_throttled | grep -Eo 0x[0-9a-fA-F]*) & 0x80000 )) != 0x00 )); then echo \"Prev. High Temp\"; "
+ " else echo \"None\"; fi";
// GPU
gpuMemoryCommand = "vcgencmd get_mem gpu | grep -Eo '[0-9]+'";
gpuMemUsageCommand = "vcgencmd get_mem malloc | grep -Eo '[0-9]+'";
}
}

View File

@@ -36,6 +36,7 @@ import org.photonvision.common.util.TimedTaskManager;
@SuppressWarnings("unused")
public class Logger {
public static final String ANSI_RESET = "\u001B[0m";
public static final String ANSI_BLACK = "\u001B[30m";
public static final String ANSI_RED = "\u001B[31m";
@@ -126,9 +127,9 @@ public class Logger {
}
public static void cleanLogs(Path folderToClean) {
File[] logs = folderToClean.toFile().listFiles();
if (logs == null) return;
LinkedList<File> logFileList = new LinkedList<>(Arrays.asList(logs));
LinkedList<File> logFileList =
new LinkedList<>(Arrays.asList(folderToClean.toFile().listFiles()));
HashMap<File, Date> logFileStartDateMap = new HashMap<>();
// Remove any files from the list for which we can't parse a start date from their name.
@@ -233,12 +234,12 @@ public class Logger {
}
/**
* Logs an error message with the stack trace of a Throwable. The stacktrace will only be printed
* if the current LogLevel is TRACE
*
* @param message
* @param t
*/
* Logs an error message with the stack trace of a Throwable. The stacktrace will only be printed
* if the current LogLevel is TRACE
*
* @param message
* @param t
*/
public void error(String message, Throwable t) {
log(message, LogLevel.ERROR);
log(convertStackTraceToString(t), LogLevel.ERROR, LogLevel.DEBUG);

View File

@@ -24,6 +24,7 @@ import org.photonvision.common.logging.Logger;
import org.photonvision.common.util.ShellExec;
public class NetworkManager {
private static final Logger logger = new Logger(NetworkManager.class, LogGroup.General);
private NetworkManager() {}
@@ -47,7 +48,7 @@ public class NetworkManager {
var config = ConfigManager.getInstance().getConfig().getNetworkConfig();
logger.info("Setting " + config.connectionType + " with team team " + config.teamNumber);
if (Platform.isLinux()) {
if (!Platform.isRoot()) {
if (!Platform.isRoot) {
logger.error("Cannot manage network without root!");
return;
}

View File

@@ -1,118 +0,0 @@
/*
* Copyright (C) Photon Vision.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.photonvision.common.networking;
import edu.wpi.first.cscore.CameraServerJNI;
import java.io.IOException;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import org.photonvision.common.dataflow.DataChangeService;
import org.photonvision.common.dataflow.events.OutgoingUIEvent;
import org.photonvision.common.logging.LogGroup;
import org.photonvision.common.logging.Logger;
public class RoborioFinder {
private static RoborioFinder instance;
private static final Logger logger = new Logger(RoborioFinder.class, LogGroup.General);
public static RoborioFinder getInstance() {
if (instance == null) instance = new RoborioFinder();
return instance;
}
public void findRios() {
HashMap<String, Object> map = new HashMap<>();
var subMap = new HashMap<String, Object>();
// Seperate from the above so we don't hold stuff up
System.setProperty("java.net.preferIPv4Stack", "true");
subMap.put(
"deviceips",
Arrays.stream(CameraServerJNI.getNetworkInterfaces())
.filter(it -> !it.equals("0.0.0.0"))
.toArray());
logger.info("Searching for rios");
List<String> possibleRioList = new ArrayList<>();
for (var ip : CameraServerJNI.getNetworkInterfaces()) {
logger.info("Trying " + ip);
var possibleRioAddr = getPossibleRioAddress(ip);
if (possibleRioAddr != null) {
logger.info("Maybe found " + ip);
searchForHost(possibleRioList, possibleRioAddr);
} else {
logger.info("Didn't match RIO IP");
}
}
// String name =
// "roboRIO-"
// +
// ConfigManager.getInstance().getConfig().getNetworkConfig().teamNumber
// + "-FRC.local";
// searchForHost(possibleRioList, name);
// name =
// "roboRIO-"
// +
// ConfigManager.getInstance().getConfig().getNetworkConfig().teamNumber
// + "-FRC.lan";
// searchForHost(possibleRioList, name);
// name =
// "roboRIO-"
// +
// ConfigManager.getInstance().getConfig().getNetworkConfig().teamNumber
// + "-FRC.frc-field.local";
// searchForHost(possibleRioList, name);
// subMap.put("possibleRios", possibleRioList.toArray());
subMap.put("possibleRios", possibleRioList.toArray());
map.put("networkInfo", subMap);
DataChangeService.getInstance().publishEvent(new OutgoingUIEvent<>("deviceIpInfo", map));
}
String getPossibleRioAddress(String ip) {
try {
InetAddress addr = InetAddress.getByName(ip);
var address = addr.getAddress();
if (address[0] != (byte) (10 & 0xff)) return null;
address[3] = (byte) (2 & 0xff);
return InetAddress.getByAddress(address).getHostAddress();
} catch (UnknownHostException e) {
e.printStackTrace();
}
return null;
}
void searchForHost(List<String> list, String hostname) {
try {
logger.info("Looking up " + hostname);
InetAddress testAddr = InetAddress.getByName(hostname);
logger.info("Pinging " + hostname);
var canContact = testAddr.isReachable(500);
if (canContact) {
logger.info("Was able to connect to " + hostname);
if (!list.contains(hostname)) list.add(hostname);
} else {
logger.info("Unable to reach " + hostname);
}
} catch (IOException ignored) {
}
}
}

View File

@@ -31,6 +31,7 @@ import org.photonvision.common.util.TimedTaskManager;
import org.photonvision.common.util.file.JacksonUtils;
public class ScriptManager {
private static final Logger logger = new Logger(ScriptManager.class, LogGroup.General);
private ScriptManager() {}
@@ -66,7 +67,8 @@ public class ScriptManager {
private void handleEvent(ScriptEventType eventType) {
var toRun =
events.parallelStream()
events
.parallelStream()
.filter(e -> e.config.eventType == eventType)
.findFirst()
.orElse(null);
@@ -81,6 +83,7 @@ public class ScriptManager {
}
protected static class ScriptConfigManager {
// protected static final Path scriptConfigPath =
// Paths.get(ConfigManager.SettingsPath.toString(), "scripts.json");
static final Path scriptConfigPath = Paths.get(""); // TODO: Waiting on config
@@ -128,7 +131,7 @@ public class ScriptManager {
}
public static void queueEvent(ScriptEventType eventType) {
if (Platform.isLinux()) {
if (!Platform.CurrentPlatform.isWindows()) {
try {
queuedEvents.putLast(eventType);
logger.info("Queued event: " + eventType.name());

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