mirror of
https://github.com/PhotonVision/photonvision
synced 2026-07-03 03:01:40 +00:00
Compare commits
163 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e58c27caa2 | ||
|
|
f6e3c9b3ee | ||
|
|
88ed2ebf51 | ||
|
|
5f39123bde | ||
|
|
37a7d378fd | ||
|
|
811fef1212 | ||
|
|
d0162b0ed0 | ||
|
|
9d6997180d | ||
|
|
a985c6cf3a | ||
|
|
167a4661ca | ||
|
|
a16ac4af57 | ||
|
|
d9f99f9c9b | ||
|
|
357d8a518a | ||
|
|
073714f0bc | ||
|
|
39f6ab8805 | ||
|
|
5c66785095 | ||
|
|
53c67a07e4 | ||
|
|
7c985e3a84 | ||
|
|
80e16ece87 | ||
|
|
86b9d4b037 | ||
|
|
e12f360a29 | ||
|
|
d0641d0cb6 | ||
|
|
871aa8b44b | ||
|
|
beaee9f6c0 | ||
|
|
11f5069148 | ||
|
|
6716d41a62 | ||
|
|
63b3cfe7e1 | ||
|
|
967be84b4b | ||
|
|
16ca2671f0 | ||
|
|
5e977445ee | ||
|
|
8117b5814b | ||
|
|
087429dab9 | ||
|
|
dbe7464ea9 | ||
|
|
ebef19af3d | ||
|
|
bde023c025 | ||
|
|
0f427bb52b | ||
|
|
05198ef294 | ||
|
|
b263fe19cc | ||
|
|
e68e6f3181 | ||
|
|
326701b74f | ||
|
|
af6f5eb0c4 | ||
|
|
0b5256df12 | ||
|
|
971b471f92 | ||
|
|
aaa886bd73 | ||
|
|
7c49cfe625 | ||
|
|
ea293f57d2 | ||
|
|
dc663657ff | ||
|
|
eedbfe3d49 | ||
|
|
1ab5b66829 | ||
|
|
d0bf64af6c | ||
|
|
8028d1887c | ||
|
|
74b807343e | ||
|
|
15fbe29d34 | ||
|
|
550194152a | ||
|
|
3a10f49b54 | ||
|
|
7ff630dc44 | ||
|
|
4088a394f3 | ||
|
|
78ab5e7c1d | ||
|
|
14d263a567 | ||
|
|
cf1a45d35b | ||
|
|
2ebc27aa3b | ||
|
|
95c55f08cf | ||
|
|
4382b8ea3f | ||
|
|
b1905954bc | ||
|
|
548f52e117 | ||
|
|
1971744589 | ||
|
|
6c51d8ab51 | ||
|
|
8330bf9d92 | ||
|
|
e1b39a1723 | ||
|
|
915f784d9d | ||
|
|
96006fc501 | ||
|
|
4fd7533456 | ||
|
|
bb63af601d | ||
|
|
da1aabae3a | ||
|
|
643db9c435 | ||
|
|
ec7bef7a4b | ||
|
|
b72f4ca2a9 | ||
|
|
ffd741ec0a | ||
|
|
678961e4f2 | ||
|
|
4c004fc780 | ||
|
|
41a00bc90f | ||
|
|
dcad7f34a2 | ||
|
|
72d8f49145 | ||
|
|
df852410b0 | ||
|
|
3c7165bb0d | ||
|
|
f193a2331a | ||
|
|
c7aa84ca41 | ||
|
|
209cdbf45f | ||
|
|
e03ec862a8 | ||
|
|
8169da5ad4 | ||
|
|
916431b4ff | ||
|
|
7dd1719fbd | ||
|
|
b408a58e9e | ||
|
|
a64697e714 | ||
|
|
e971db2f52 | ||
|
|
7b6afd545b | ||
|
|
0f99044468 | ||
|
|
1412155c50 | ||
|
|
b1280e49d5 | ||
|
|
aaac6a4fbb | ||
|
|
b68b0ca5f6 | ||
|
|
45d99f1f6b | ||
|
|
a42fef67f2 | ||
|
|
bd4d74c192 | ||
|
|
c4500ce12b | ||
|
|
81d19672d2 | ||
|
|
04bde1b230 | ||
|
|
4f355f2749 | ||
|
|
5e604cf98d | ||
|
|
2d7a88e231 | ||
|
|
27198a3e32 | ||
|
|
fbf6fb304e | ||
|
|
d24a8d4188 | ||
|
|
def40484e3 | ||
|
|
aff163fc6a | ||
|
|
c392d5fa4d | ||
|
|
8dbd428359 | ||
|
|
ccd3a512d6 | ||
|
|
bfc5e45cd0 | ||
|
|
a1b09100e0 | ||
|
|
2bf7a77885 | ||
|
|
d1bfb86ab4 | ||
|
|
07904589df | ||
|
|
5540bbf115 | ||
|
|
c827afb25f | ||
|
|
87e7c3ca74 | ||
|
|
4d5904dd6d | ||
|
|
9bf589ebc6 | ||
|
|
1e4a92c71f | ||
|
|
4ad9d97508 | ||
|
|
2c6b0ddac3 | ||
|
|
dafee954e0 | ||
|
|
5ac541642e | ||
|
|
ad0474d42a | ||
|
|
4b4a0a1cd9 | ||
|
|
a764ace7f2 | ||
|
|
a3bcd3ac4f | ||
|
|
661f8b2c04 | ||
|
|
72717cecf0 | ||
|
|
971ff3ac40 | ||
|
|
b80e436f02 | ||
|
|
be1a053cbe | ||
|
|
f4555dc545 | ||
|
|
54fdd1db51 | ||
|
|
1805785cc6 | ||
|
|
e62f6419b5 | ||
|
|
fa7824c616 | ||
|
|
9090aa6bcc | ||
|
|
5655ca6890 | ||
|
|
50fdfd8bce | ||
|
|
3120a6439b | ||
|
|
ab3e8c8db7 | ||
|
|
5144819ce2 | ||
|
|
d779fe23f0 | ||
|
|
b2a3f34433 | ||
|
|
b09a6d6a2d | ||
|
|
9893cf1f7e | ||
|
|
fc91daf397 | ||
|
|
a3e205cb6f | ||
|
|
553bed32b5 | ||
|
|
6c91feaf3f | ||
|
|
4ddb9aa08f | ||
|
|
4aadebdbb2 |
332
.github/workflows/main.yml
vendored
332
.github/workflows/main.yml
vendored
@@ -22,26 +22,22 @@ jobs:
|
|||||||
working-directory: photon-client
|
working-directory: photon-client
|
||||||
|
|
||||||
# The type of runner that the job will run on.
|
# The type of runner that the job will run on.
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-22.04
|
||||||
|
|
||||||
# Grab the docker container.
|
|
||||||
container:
|
|
||||||
image: docker://node:10
|
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
# Checkout code.
|
# Checkout code.
|
||||||
- uses: actions/checkout@v1
|
- uses: actions/checkout@v3
|
||||||
|
|
||||||
# Setup Node.js
|
# Setup Node.js
|
||||||
- name: Setup Node.js
|
- name: Setup Node.js
|
||||||
uses: actions/setup-node@v1
|
uses: actions/setup-node@v3
|
||||||
with:
|
with:
|
||||||
node-version: 10
|
node-version: 16
|
||||||
|
|
||||||
# Run npm
|
# Run npm
|
||||||
- run: |
|
- run: npm update -g npm
|
||||||
npm ci
|
- run: npm ci
|
||||||
npm run build --if-present
|
- run: npm run build --if-present
|
||||||
|
|
||||||
# Upload client artifact.
|
# Upload client artifact.
|
||||||
- uses: actions/upload-artifact@master
|
- uses: actions/upload-artifact@master
|
||||||
@@ -49,34 +45,80 @@ jobs:
|
|||||||
name: built-client
|
name: built-client
|
||||||
path: photon-client/dist/
|
path: photon-client/dist/
|
||||||
|
|
||||||
photon-build-all:
|
photon-build-examples:
|
||||||
# The type of runner that the job will run on.
|
runs-on: ubuntu-22.04
|
||||||
runs-on: ubuntu-latest
|
name: "Build Examples"
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
# Checkout code.
|
# Checkout code.
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v1
|
uses: actions/checkout@v3
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
# Fetch tags.
|
# Fetch tags.
|
||||||
- name: Fetch tags
|
- name: Fetch tags
|
||||||
run: git fetch --tags --force
|
run: git fetch --tags --force
|
||||||
|
|
||||||
# Install Java 11.
|
# Install Java 17.
|
||||||
- name: Install Java 11
|
- name: Install Java 17
|
||||||
uses: actions/setup-java@v1
|
uses: actions/setup-java@v3
|
||||||
with:
|
with:
|
||||||
java-version: 11
|
java-version: 17
|
||||||
|
distribution: temurin
|
||||||
|
|
||||||
# Run Gradle build.
|
# 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
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
# Run only build tasks, no checks??
|
||||||
- name: Gradle Build
|
- name: Gradle Build
|
||||||
run: |
|
run: |
|
||||||
chmod +x gradlew
|
chmod +x gradlew
|
||||||
./gradlew build -x check --max-workers 1
|
./gradlew photon-server:build photon-lib:build -x check --max-workers 2
|
||||||
|
|
||||||
# Run Gradle Tests.
|
# Run Gradle Tests.
|
||||||
- name: Gradle Tests
|
- name: Gradle Tests
|
||||||
run: ./gradlew testHeadless -i --max-workers 1
|
run: ./gradlew testHeadless -i --max-workers 1 --stacktrace
|
||||||
|
|
||||||
# Generate Coverage Report.
|
# Generate Coverage Report.
|
||||||
- name: Gradle Coverage
|
- name: Gradle Coverage
|
||||||
@@ -84,29 +126,29 @@ jobs:
|
|||||||
|
|
||||||
# Publish Coverage Report.
|
# Publish Coverage Report.
|
||||||
- name: Publish Server Coverage Report
|
- name: Publish Server Coverage Report
|
||||||
uses: codecov/codecov-action@v1
|
uses: codecov/codecov-action@v3
|
||||||
with:
|
with:
|
||||||
file: ./photon-server/build/reports/jacoco/test/jacocoTestReport.xml
|
file: ./photon-server/build/reports/jacoco/test/jacocoTestReport.xml
|
||||||
|
|
||||||
- name: Publish Core Coverage Report
|
- name: Publish Core Coverage Report
|
||||||
uses: codecov/codecov-action@v1
|
uses: codecov/codecov-action@v3
|
||||||
with:
|
with:
|
||||||
file: ./photon-core/build/reports/jacoco/test/jacocoTestReport.xml
|
file: ./photon-core/build/reports/jacoco/test/jacocoTestReport.xml
|
||||||
|
|
||||||
photonserver-build-offline-docs:
|
photonserver-build-offline-docs:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-22.04
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
# Checkout docs.
|
# Checkout docs.
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v3
|
||||||
with:
|
with:
|
||||||
repository: 'PhotonVision/photonvision-docs.git'
|
repository: 'PhotonVision/photonvision-docs.git'
|
||||||
ref: master
|
ref: master
|
||||||
|
|
||||||
# Install Python.
|
# Install Python.
|
||||||
- uses: actions/setup-python@v2
|
- uses: actions/setup-python@v4
|
||||||
with:
|
with:
|
||||||
python-version: '3.6'
|
python-version: '3.9'
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: |
|
run: |
|
||||||
@@ -114,10 +156,11 @@ jobs:
|
|||||||
pip install sphinx sphinx_rtd_theme sphinx-tabs sphinxext-opengraph doc8
|
pip install sphinx sphinx_rtd_theme sphinx-tabs sphinxext-opengraph doc8
|
||||||
pip install -r requirements.txt
|
pip install -r requirements.txt
|
||||||
|
|
||||||
- name: Check the docs
|
# Don't check the docs. If a PR was merged to the docs repo, it ought to pass CI. No need to re-check here.
|
||||||
run: |
|
# - name: Check the docs
|
||||||
make linkcheck
|
# run: |
|
||||||
make lint
|
# make linkcheck
|
||||||
|
# make lint
|
||||||
|
|
||||||
- name: Build the docs
|
- name: Build the docs
|
||||||
run: |
|
run: |
|
||||||
@@ -131,46 +174,50 @@ jobs:
|
|||||||
|
|
||||||
photonserver-check-lint:
|
photonserver-check-lint:
|
||||||
# The type of runner that the job will run on.
|
# The type of runner that the job will run on.
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-22.04
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
# Checkout code.
|
# Checkout code.
|
||||||
- uses: actions/checkout@v1
|
- uses: actions/checkout@v3
|
||||||
|
|
||||||
# Install Java 11.
|
|
||||||
- uses: actions/setup-java@v1
|
|
||||||
with:
|
with:
|
||||||
java-version: 11
|
fetch-depth: 0
|
||||||
|
|
||||||
|
# Install Java 17.
|
||||||
|
- uses: actions/setup-java@v3
|
||||||
|
with:
|
||||||
|
java-version: 17
|
||||||
|
distribution: temurin
|
||||||
|
|
||||||
# Check server code with Spotless.
|
# Check server code with Spotless.
|
||||||
- run: |
|
- run: |
|
||||||
chmod +x gradlew
|
chmod +x gradlew
|
||||||
./gradlew spotlessCheck
|
./gradlew spotlessCheck
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# Building photonlib
|
# Building photonlib
|
||||||
photonlib-build-host:
|
photonlib-build-host:
|
||||||
|
env:
|
||||||
|
MACOSX_DEPLOYMENT_TARGET: 10.14
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
include:
|
include:
|
||||||
- os: windows-latest
|
- os: windows-2022
|
||||||
artifact-name: Win64
|
artifact-name: Win64
|
||||||
- os: macos-latest
|
- os: macos-11
|
||||||
artifact-name: macOS
|
artifact-name: macOS
|
||||||
- os: ubuntu-latest
|
- os: ubuntu-22.04
|
||||||
artifact-name: Linux
|
artifact-name: Linux
|
||||||
|
|
||||||
runs-on: ${{ matrix.os }}
|
runs-on: ${{ matrix.os }}
|
||||||
name: "Photonlib - Build - ${{ matrix.artifact-name }}"
|
name: "Photonlib - Build Host - ${{ matrix.artifact-name }}"
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2.3.4
|
- uses: actions/checkout@v3
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
- uses: actions/setup-java@v1
|
- uses: actions/setup-java@v3
|
||||||
with:
|
with:
|
||||||
java-version: 11
|
java-version: 17
|
||||||
|
distribution: temurin
|
||||||
- run: git fetch --tags --force
|
- run: git fetch --tags --force
|
||||||
- run: |
|
- run: |
|
||||||
chmod +x gradlew
|
chmod +x gradlew
|
||||||
@@ -186,27 +233,29 @@ jobs:
|
|||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
include:
|
include:
|
||||||
- container: wpilib/roborio-cross-ubuntu:2022-18.04
|
- container: wpilib/roborio-cross-ubuntu:2023-22.04
|
||||||
artifact-name: Athena
|
artifact-name: Athena
|
||||||
- container: wpilib/raspbian-cross-ubuntu:10-18.04
|
- container: wpilib/raspbian-cross-ubuntu:bullseye-22.04
|
||||||
artifact-name: Raspbian
|
artifact-name: Raspbian
|
||||||
- container: wpilib/aarch64-cross-ubuntu:bionic-18.04
|
- container: wpilib/aarch64-cross-ubuntu:bullseye-22.04
|
||||||
artifact-name: Aarch64
|
artifact-name: Aarch64
|
||||||
|
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-22.04
|
||||||
container: ${{ matrix.container }}
|
container: ${{ matrix.container }}
|
||||||
name: "Photonlib - Build - ${{ matrix.artifact-name }}"
|
name: "Photonlib - Build Docker - ${{ matrix.artifact-name }}"
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2.3.4
|
- uses: actions/checkout@v3
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
- uses: actions/setup-java@v1
|
- name: Config Git
|
||||||
with:
|
run: |
|
||||||
java-version: 11
|
git config --global --add safe.directory /__w/photonvision/photonvision
|
||||||
- run: |
|
- name: Build PhotonLib
|
||||||
|
run: |
|
||||||
chmod +x gradlew
|
chmod +x gradlew
|
||||||
./gradlew photon-lib:build --max-workers 1
|
./gradlew photon-lib:build --max-workers 1
|
||||||
- run: |
|
- name: Publish
|
||||||
|
run: |
|
||||||
chmod +x gradlew
|
chmod +x gradlew
|
||||||
./gradlew photon-lib:publish
|
./gradlew photon-lib:publish
|
||||||
env:
|
env:
|
||||||
@@ -215,16 +264,16 @@ jobs:
|
|||||||
|
|
||||||
photonlib-wpiformat:
|
photonlib-wpiformat:
|
||||||
name: "wpiformat"
|
name: "wpiformat"
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-22.04
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v3
|
||||||
- name: Fetch all history and metadata
|
- name: Fetch all history and metadata
|
||||||
run: |
|
run: |
|
||||||
git fetch --prune --unshallow
|
git fetch --prune --unshallow
|
||||||
git checkout -b pr
|
git checkout -b pr
|
||||||
git branch -f master origin/master
|
git branch -f master origin/master
|
||||||
- name: Set up Python 3.8
|
- name: Set up Python 3.8
|
||||||
uses: actions/setup-python@v2
|
uses: actions/setup-python@v4
|
||||||
with:
|
with:
|
||||||
python-version: 3.8
|
python-version: 3.8
|
||||||
- name: Install clang-format
|
- name: Install clang-format
|
||||||
@@ -241,40 +290,78 @@ jobs:
|
|||||||
- name: Generate diff
|
- name: Generate diff
|
||||||
run: git diff HEAD > wpiformat-fixes.patch
|
run: git diff HEAD > wpiformat-fixes.patch
|
||||||
if: ${{ failure() }}
|
if: ${{ failure() }}
|
||||||
- uses: actions/upload-artifact@v2
|
- uses: actions/upload-artifact@v3
|
||||||
with:
|
with:
|
||||||
name: wpiformat fixes
|
name: wpiformat fixes
|
||||||
path: wpiformat-fixes.patch
|
path: wpiformat-fixes.patch
|
||||||
if: ${{ failure() }}
|
if: ${{ failure() }}
|
||||||
|
|
||||||
photon-build-package:
|
photon-build-package:
|
||||||
needs: [photonclient-build, photon-build-all, photonserver-build-offline-docs, photonlib-build-host, photonlib-build-docker]
|
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
|
||||||
|
|
||||||
# The type of runner that the job will run on.
|
# The type of runner that the job will run on.
|
||||||
runs-on: ubuntu-latest
|
runs-on: ${{ matrix.os }}
|
||||||
|
name: "Build fat JAR - ${{ matrix.artifact-name }}"
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
# Checkout code.
|
# Checkout code.
|
||||||
- uses: actions/checkout@v1
|
- uses: actions/checkout@v3
|
||||||
|
|
||||||
# Install Java 11.
|
|
||||||
- uses: actions/setup-java@v1
|
|
||||||
with:
|
with:
|
||||||
java-version: 11
|
fetch-depth: 0
|
||||||
|
|
||||||
|
# Install Java 17.
|
||||||
|
- uses: actions/setup-java@v3
|
||||||
|
with:
|
||||||
|
java-version: 17
|
||||||
|
distribution: temurin
|
||||||
|
|
||||||
# Clear any existing web resources.
|
# Clear any existing web resources.
|
||||||
- run: |
|
- run: |
|
||||||
rm -rf photon-server/src/main/resources/web/*
|
rm -rf photon-server/src/main/resources/web/*
|
||||||
mkdir -p photon-server/src/main/resources/web/docs
|
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.
|
# Download client artifact to resources folder.
|
||||||
- uses: actions/download-artifact@v2
|
- uses: actions/download-artifact@v3
|
||||||
with:
|
with:
|
||||||
name: built-client
|
name: built-client
|
||||||
path: photon-server/src/main/resources/web/
|
path: photon-server/src/main/resources/web/
|
||||||
|
|
||||||
# Download docs artifact to resources folder.
|
# Download docs artifact to resources folder.
|
||||||
- uses: actions/download-artifact@v2
|
- uses: actions/download-artifact@v3
|
||||||
with:
|
with:
|
||||||
name: built-docs
|
name: built-docs
|
||||||
path: photon-server/src/main/resources/web/docs
|
path: photon-server/src/main/resources/web/docs
|
||||||
@@ -282,54 +369,89 @@ jobs:
|
|||||||
# Build fat jar for both pi and everything
|
# Build fat jar for both pi and everything
|
||||||
- run: |
|
- run: |
|
||||||
chmod +x gradlew
|
chmod +x gradlew
|
||||||
./gradlew photon-server:shadowJar --max-workers 1
|
./gradlew photon-server:shadowJar --max-workers 2 -PArchOverride=${{ matrix.arch-override }}
|
||||||
./gradlew photon-server:shadowJar --max-workers 1 -Ppionly
|
if: ${{ (matrix.arch-override != 'none') }}
|
||||||
|
- run: |
|
||||||
# The image will only pull the Pi JAR in
|
chmod +x gradlew
|
||||||
- name: Generate image
|
./gradlew photon-server:shadowJar --max-workers 2
|
||||||
if: github.event_name != 'pull_request'
|
if: ${{ (matrix.arch-override == 'none') }}
|
||||||
run: |
|
|
||||||
chmod +x scripts/generatePiImage.sh
|
|
||||||
./scripts/generatePiImage.sh
|
|
||||||
|
|
||||||
# Upload final fat jar as artifact.
|
# Upload final fat jar as artifact.
|
||||||
- uses: actions/upload-artifact@master
|
- uses: actions/upload-artifact@v3
|
||||||
with:
|
with:
|
||||||
name: jar
|
name: jar-${{ matrix.artifact-name }}
|
||||||
path: photon-server/build/libs
|
path: photon-server/build/libs
|
||||||
- uses: actions/upload-artifact@master
|
|
||||||
with:
|
|
||||||
name: image
|
|
||||||
path: photonvision*.zip
|
|
||||||
|
|
||||||
|
|
||||||
|
photon-image-generator:
|
||||||
|
needs: [photon-build-package]
|
||||||
|
if: ${{ github.event_name != 'pull_request' }}
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
include:
|
||||||
|
- os: ubuntu-latest
|
||||||
|
artifact-name: LinuxArm64
|
||||||
|
image_suffix: RaspberryPi
|
||||||
|
image_url: https://api.github.com/repos/photonvision/photon-pi-gen/releases/tags/v2023.1.1_arm64
|
||||||
|
- os: ubuntu-latest
|
||||||
|
artifact-name: LinuxArm64
|
||||||
|
image_suffix: limelight
|
||||||
|
image_url: https://api.github.com/repos/photonvision/photon-pi-gen/releases/tags/v2023.2.2_limelight-arm64
|
||||||
|
|
||||||
|
runs-on: ${{ matrix.os }}
|
||||||
|
name: "Build image - ${{ matrix.image_url }}"
|
||||||
|
|
||||||
|
steps:
|
||||||
|
# Checkout code.
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- uses: actions/download-artifact@v2
|
||||||
|
with:
|
||||||
|
name: jar-${{ matrix.artifact-name }}
|
||||||
|
|
||||||
|
- name: Generate image
|
||||||
|
run: |
|
||||||
|
chmod +x scripts/generatePiImage.sh
|
||||||
|
./scripts/generatePiImage.sh ${{ matrix.image_url }} ${{ matrix.image_suffix }}
|
||||||
|
|
||||||
|
- uses: actions/upload-artifact@v3
|
||||||
|
name: Upload image
|
||||||
|
with:
|
||||||
|
name: image-${{ matrix.image_suffix }}
|
||||||
|
path: photonvision*.xz
|
||||||
|
|
||||||
|
|
||||||
|
photon-release:
|
||||||
|
needs: [photon-build-package, photon-image-generator]
|
||||||
|
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: pyTooling/Actions/releaser@r0
|
||||||
with:
|
with:
|
||||||
token: ${{ secrets.GITHUB_TOKEN }}
|
token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
tag: 'Dev'
|
tag: 'Dev'
|
||||||
rm: true
|
rm: true
|
||||||
files: |
|
files: |
|
||||||
photon-server/build/libs/*.jar
|
**/*.xz
|
||||||
photonvision*.zip
|
**/*.jar
|
||||||
if: github.event_name == 'push'
|
if: github.event_name == 'push'
|
||||||
|
|
||||||
photon-release:
|
# Upload all jars and xz archives
|
||||||
if: startsWith(github.ref, 'refs/tags/v')
|
|
||||||
needs: [photon-build-package]
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
# This *should* pull in fat and pi-only jars
|
|
||||||
- uses: actions/download-artifact@v2
|
|
||||||
with:
|
|
||||||
name: jar
|
|
||||||
|
|
||||||
# And the image we made previously
|
|
||||||
- uses: actions/download-artifact@v2
|
|
||||||
with:
|
|
||||||
name: image
|
|
||||||
|
|
||||||
# All we've downloaded (ideally) is the fat jar, pi jar, and image. So just upload it all
|
|
||||||
- uses: softprops/action-gh-release@v1
|
- uses: softprops/action-gh-release@v1
|
||||||
with:
|
with:
|
||||||
files: '**/*'
|
files: |
|
||||||
|
**/*.xz
|
||||||
|
**/*.jar
|
||||||
|
if: startsWith(github.ref, 'refs/tags/v')
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|||||||
10
.gitignore
vendored
10
.gitignore
vendored
@@ -30,6 +30,7 @@ backend/settings/
|
|||||||
*.nar
|
*.nar
|
||||||
*.ear
|
*.ear
|
||||||
*.zip
|
*.zip
|
||||||
|
*.xz
|
||||||
*.tar.gz
|
*.tar.gz
|
||||||
*.rar
|
*.rar
|
||||||
|
|
||||||
@@ -143,3 +144,12 @@ build/*
|
|||||||
build
|
build
|
||||||
photon-lib/src/main/java/org/photonvision/PhotonVersion.java
|
photon-lib/src/main/java/org/photonvision/PhotonVersion.java
|
||||||
/photonlib-java-examples/bin/
|
/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/*
|
||||||
|
|||||||
@@ -11,8 +11,11 @@ cppSrcFileInclude {
|
|||||||
|
|
||||||
modifiableFileExclude {
|
modifiableFileExclude {
|
||||||
\.jpg$
|
\.jpg$
|
||||||
|
\.jpeg$
|
||||||
\.png$
|
\.png$
|
||||||
|
\.gif$
|
||||||
\.so$
|
\.so$
|
||||||
|
\.dll$
|
||||||
}
|
}
|
||||||
|
|
||||||
includeProject {
|
includeProject {
|
||||||
@@ -25,7 +28,3 @@ includeOtherLibs {
|
|||||||
^units/
|
^units/
|
||||||
^wpi/
|
^wpi/
|
||||||
}
|
}
|
||||||
|
|
||||||
licenseUpdateExclude {
|
|
||||||
\.java$
|
|
||||||
}
|
|
||||||
|
|||||||
23
LICENSE_MathUtils_orthogonalizeRotationMatrix.txt
Normal file
23
LICENSE_MathUtils_orthogonalizeRotationMatrix.txt
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
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.
|
||||||
@@ -1,16 +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/>.
|
|
||||||
*/
|
|
||||||
36
README.md
36
README.md
@@ -6,7 +6,7 @@ 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).
|
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/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.
|
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.
|
||||||
|
|
||||||
## Authors
|
## Authors
|
||||||
|
|
||||||
@@ -18,10 +18,42 @@ If you are interested in contributing code or documentation to the project, plea
|
|||||||
|
|
||||||
Note that these are case sensitive!
|
Note that these are case sensitive!
|
||||||
|
|
||||||
* `-Ppionly`: only builds for `linuxraspbian`, which reduces JAR size. The JAR name will have "-raspi" appended.
|
* `-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
|
- `-PtgtIp`: deploys (builds and copies the JAR) to the coprocessor at the specified IP
|
||||||
- `-Pprofile`: enables JVM profiling
|
- `-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
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
## Acknowledgments
|
## Acknowledgments
|
||||||
PhotonVision was forked from [Chameleon Vision](https://github.com/Chameleon-Vision/chameleon-vision/). Thank you to everyone who worked on the original project.
|
PhotonVision was forked from [Chameleon Vision](https://github.com/Chameleon-Vision/chameleon-vision/). Thank you to everyone who worked on the original project.
|
||||||
|
|
||||||
|
|||||||
28
build.gradle
28
build.gradle
@@ -4,14 +4,18 @@ plugins {
|
|||||||
id "com.github.node-gradle.node" version "3.1.1" apply false
|
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.GradleJni" version "1.0.0"
|
||||||
id "edu.wpi.first.GradleVsCode" version "1.1.0"
|
id "edu.wpi.first.GradleVsCode" version "1.1.0"
|
||||||
id "edu.wpi.first.NativeUtils" version "2022.8.1" apply false
|
id "edu.wpi.first.NativeUtils" version "2023.11.1" apply false
|
||||||
id "edu.wpi.first.wpilib.repositories.WPILibRepositoriesPlugin" version "2020.2"
|
id "edu.wpi.first.wpilib.repositories.WPILibRepositoriesPlugin" version "2020.2"
|
||||||
id "org.hidetake.ssh" version "2.10.1"
|
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 {
|
allprojects {
|
||||||
repositories {
|
repositories {
|
||||||
jcenter()
|
mavenCentral()
|
||||||
|
mavenLocal()
|
||||||
maven { url = "https://maven.photonvision.org/repository/internal/" }
|
maven { url = "https://maven.photonvision.org/repository/internal/" }
|
||||||
}
|
}
|
||||||
wpilibRepositories.addAllReleaseRepositories(it)
|
wpilibRepositories.addAllReleaseRepositories(it)
|
||||||
@@ -22,19 +26,24 @@ allprojects {
|
|||||||
apply from: "versioningHelper.gradle"
|
apply from: "versioningHelper.gradle"
|
||||||
|
|
||||||
ext {
|
ext {
|
||||||
wpilibVersion = "2022.1.1"
|
wpilibVersion = "2023.2.1"
|
||||||
opencvVersion = "4.5.2-1"
|
opencvVersion = "4.6.0-4"
|
||||||
joglVersion = "2.4.0-rc-20200307"
|
joglVersion = "2.4.0-rc-20200307"
|
||||||
pubVersion = versionString
|
pubVersion = versionString
|
||||||
isDev = pubVersion.startsWith("dev")
|
isDev = pubVersion.startsWith("dev")
|
||||||
|
|
||||||
|
// A list, for legacy reasons, with only the current platform contained
|
||||||
jniPlatforms = project.hasProperty('pionly') ? ['linuxraspbian']
|
String nativeName = wpilibTools.platformMapper.currentPlatform.platformName;
|
||||||
: ['linuxaarch64bionic', 'linuxraspbian', 'linuxx86-64', 'osxx86-64', 'windowsx86-64']
|
if (nativeName == "linuxx64") nativeName = "linuxx86-64";
|
||||||
|
if (nativeName == "winx64") nativeName = "windowsx86-64";
|
||||||
println("Building for archs " + jniPlatforms)
|
if (nativeName == "macx64") nativeName = "osxx86-64";
|
||||||
|
if (nativeName == "macarm64") nativeName = "osxarm64";
|
||||||
|
jniPlatform = nativeName
|
||||||
|
println("Building for platform " + jniPlatform)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
wpilibTools.deps.wpilibVersion = wpilibVersion
|
||||||
|
|
||||||
spotless {
|
spotless {
|
||||||
java {
|
java {
|
||||||
toggleOffOn()
|
toggleOffOn()
|
||||||
@@ -47,7 +56,6 @@ spotless {
|
|||||||
}
|
}
|
||||||
java {
|
java {
|
||||||
target "**/*.java"
|
target "**/*.java"
|
||||||
licenseHeaderFile "$rootDir/LicenseHeader.txt"
|
|
||||||
targetExclude("photon-core/src/main/java/org/photonvision/PhotonVersion.java")
|
targetExclude("photon-core/src/main/java/org/photonvision/PhotonVersion.java")
|
||||||
targetExclude("photon-lib/src/main/java/org/photonvision/PhotonVersion.java")
|
targetExclude("photon-lib/src/main/java/org/photonvision/PhotonVersion.java")
|
||||||
}
|
}
|
||||||
|
|||||||
2
gradle/wrapper/gradle-wrapper.properties
vendored
2
gradle/wrapper/gradle-wrapper.properties
vendored
@@ -1,5 +1,5 @@
|
|||||||
distributionBase=GRADLE_USER_HOME
|
distributionBase=GRADLE_USER_HOME
|
||||||
distributionPath=wrapper/dists
|
distributionPath=wrapper/dists
|
||||||
distributionUrl=https\://services.gradle.org/distributions/gradle-7.2-bin.zip
|
distributionUrl=https\://services.gradle.org/distributions/gradle-7.5.1-bin.zip
|
||||||
zipStoreBase=GRADLE_USER_HOME
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
zipStorePath=wrapper/dists
|
zipStorePath=wrapper/dists
|
||||||
|
|||||||
227
gradlew
vendored
227
gradlew
vendored
@@ -1,7 +1,7 @@
|
|||||||
#!/usr/bin/env sh
|
#!/bin/sh
|
||||||
|
|
||||||
#
|
#
|
||||||
# Copyright 2015 the original author or authors.
|
# Copyright <20> 2015-2021 the original authors.
|
||||||
#
|
#
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
# you may not use this file except in compliance with the License.
|
# you may not use this file except in compliance with the License.
|
||||||
@@ -16,68 +16,58 @@
|
|||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
#
|
#
|
||||||
|
|
||||||
##############################################################################
|
|
||||||
##
|
|
||||||
## Gradle start up script for UN*X
|
|
||||||
##
|
|
||||||
##############################################################################
|
##############################################################################
|
||||||
|
|
||||||
# Attempt to set APP_HOME
|
# Attempt to set APP_HOME
|
||||||
|
|
||||||
# Resolve links: $0 may be a link
|
# Resolve links: $0 may be a link
|
||||||
PRG="$0"
|
app_path=$0
|
||||||
# Need this for relative symlinks.
|
|
||||||
while [ -h "$PRG" ] ; do
|
# Need this for daisy-chained symlinks.
|
||||||
ls=`ls -ld "$PRG"`
|
while
|
||||||
link=`expr "$ls" : '.*-> \(.*\)$'`
|
APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
|
||||||
if expr "$link" : '/.*' > /dev/null; then
|
[ -h "$app_path" ]
|
||||||
PRG="$link"
|
do
|
||||||
else
|
ls=$( ls -ld "$app_path" )
|
||||||
PRG=`dirname "$PRG"`"/$link"
|
link=${ls#*' -> '}
|
||||||
fi
|
case $link in #(
|
||||||
|
/*) app_path=$link ;; #(
|
||||||
|
*) app_path=$APP_HOME$link ;;
|
||||||
|
esac
|
||||||
done
|
done
|
||||||
SAVED="`pwd`"
|
|
||||||
cd "`dirname \"$PRG\"`/" >/dev/null
|
APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
|
||||||
APP_HOME="`pwd -P`"
|
|
||||||
cd "$SAVED" >/dev/null
|
|
||||||
|
|
||||||
APP_NAME="Gradle"
|
APP_NAME="Gradle"
|
||||||
APP_BASE_NAME=`basename "$0"`
|
APP_BASE_NAME=${0##*/}
|
||||||
|
|
||||||
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
# 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"'
|
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
|
||||||
|
|
||||||
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
||||||
MAX_FD="maximum"
|
MAX_FD=maximum
|
||||||
|
|
||||||
warn () {
|
warn () {
|
||||||
echo "$*"
|
echo "$*"
|
||||||
}
|
} >&2
|
||||||
|
|
||||||
die () {
|
die () {
|
||||||
echo
|
echo
|
||||||
echo "$*"
|
echo "$*"
|
||||||
echo
|
echo
|
||||||
exit 1
|
exit 1
|
||||||
}
|
} >&2
|
||||||
|
|
||||||
# OS specific support (must be 'true' or 'false').
|
# OS specific support (must be 'true' or 'false').
|
||||||
cygwin=false
|
cygwin=false
|
||||||
msys=false
|
msys=false
|
||||||
darwin=false
|
darwin=false
|
||||||
nonstop=false
|
nonstop=false
|
||||||
case "`uname`" in
|
case "$( uname )" in #(
|
||||||
CYGWIN* )
|
CYGWIN* ) cygwin=true ;; #(
|
||||||
cygwin=true
|
Darwin* ) darwin=true ;; #(
|
||||||
;;
|
MSYS* | MINGW* ) msys=true ;; #(
|
||||||
Darwin* )
|
NONSTOP* ) nonstop=true ;;
|
||||||
darwin=true
|
|
||||||
;;
|
|
||||||
MINGW* )
|
|
||||||
msys=true
|
|
||||||
;;
|
|
||||||
NONSTOP* )
|
|
||||||
nonstop=true
|
|
||||||
;;
|
|
||||||
esac
|
esac
|
||||||
|
|
||||||
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
|
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
|
||||||
@@ -87,9 +77,9 @@ CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
|
|||||||
if [ -n "$JAVA_HOME" ] ; then
|
if [ -n "$JAVA_HOME" ] ; then
|
||||||
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
|
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
|
||||||
# IBM's JDK on AIX uses strange locations for the executables
|
# IBM's JDK on AIX uses strange locations for the executables
|
||||||
JAVACMD="$JAVA_HOME/jre/sh/java"
|
JAVACMD=$JAVA_HOME/jre/sh/java
|
||||||
else
|
else
|
||||||
JAVACMD="$JAVA_HOME/bin/java"
|
JAVACMD=$JAVA_HOME/bin/java
|
||||||
fi
|
fi
|
||||||
if [ ! -x "$JAVACMD" ] ; then
|
if [ ! -x "$JAVACMD" ] ; then
|
||||||
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
|
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
|
||||||
@@ -98,7 +88,7 @@ Please set the JAVA_HOME variable in your environment to match the
|
|||||||
location of your Java installation."
|
location of your Java installation."
|
||||||
fi
|
fi
|
||||||
else
|
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.
|
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
|
Please set the JAVA_HOME variable in your environment to match the
|
||||||
@@ -106,80 +96,95 @@ location of your Java installation."
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
# Increase the maximum file descriptors if we can.
|
# Increase the maximum file descriptors if we can.
|
||||||
if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
|
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
|
||||||
MAX_FD_LIMIT=`ulimit -H -n`
|
case $MAX_FD in #(
|
||||||
if [ $? -eq 0 ] ; then
|
max*)
|
||||||
if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
|
MAX_FD=$( ulimit -H -n ) ||
|
||||||
MAX_FD="$MAX_FD_LIMIT"
|
warn "Could not query maximum file descriptor limit"
|
||||||
fi
|
esac
|
||||||
ulimit -n $MAX_FD
|
case $MAX_FD in #(
|
||||||
if [ $? -ne 0 ] ; then
|
'' | soft) :;; #(
|
||||||
warn "Could not set maximum file descriptor limit: $MAX_FD"
|
*)
|
||||||
fi
|
ulimit -n "$MAX_FD" ||
|
||||||
else
|
warn "Could not set maximum file descriptor limit to $MAX_FD"
|
||||||
warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
# 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" = "true" -o "$msys" = "true" ] ; then
|
|
||||||
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
|
|
||||||
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
|
|
||||||
|
|
||||||
JAVACMD=`cygpath --unix "$JAVACMD"`
|
|
||||||
|
|
||||||
# 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
|
esac
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Escape application args
|
# Collect all arguments for the java command, stacking in reverse order:
|
||||||
save () {
|
# * args from the command line
|
||||||
for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
|
# * the main class name
|
||||||
echo " "
|
# * -classpath
|
||||||
}
|
# * -D...appname settings
|
||||||
APP_ARGS=`save "$@"`
|
# * --module-path (only if needed)
|
||||||
|
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
|
||||||
|
|
||||||
# Collect all arguments for the java command, following the shell quoting and substitution rules
|
# For Cygwin or MSYS, switch paths to Windows format before running java
|
||||||
eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
|
if "$cygwin" || "$msys" ; then
|
||||||
|
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
|
||||||
|
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
|
||||||
|
|
||||||
|
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
|
||||||
|
done
|
||||||
|
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.
|
||||||
|
|
||||||
|
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' ' '
|
||||||
|
)" '"$@"'
|
||||||
|
|
||||||
exec "$JAVACMD" "$@"
|
exec "$JAVACMD" "$@"
|
||||||
|
|||||||
178
gradlew.bat
vendored
178
gradlew.bat
vendored
@@ -1,89 +1,89 @@
|
|||||||
@rem
|
@rem
|
||||||
@rem Copyright 2015 the original author or authors.
|
@rem Copyright 2015 the original author or authors.
|
||||||
@rem
|
@rem
|
||||||
@rem Licensed under the Apache License, Version 2.0 (the "License");
|
@rem Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
@rem you may not use this file except in compliance with the License.
|
@rem you may not use this file except in compliance with the License.
|
||||||
@rem You may obtain a copy of the License at
|
@rem You may obtain a copy of the License at
|
||||||
@rem
|
@rem
|
||||||
@rem https://www.apache.org/licenses/LICENSE-2.0
|
@rem https://www.apache.org/licenses/LICENSE-2.0
|
||||||
@rem
|
@rem
|
||||||
@rem Unless required by applicable law or agreed to in writing, software
|
@rem Unless required by applicable law or agreed to in writing, software
|
||||||
@rem distributed under the License is distributed on an "AS IS" BASIS,
|
@rem distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
@rem See the License for the specific language governing permissions and
|
@rem See the License for the specific language governing permissions and
|
||||||
@rem limitations under the License.
|
@rem limitations under the License.
|
||||||
@rem
|
@rem
|
||||||
|
|
||||||
@if "%DEBUG%" == "" @echo off
|
@if "%DEBUG%" == "" @echo off
|
||||||
@rem ##########################################################################
|
@rem ##########################################################################
|
||||||
@rem
|
@rem
|
||||||
@rem Gradle startup script for Windows
|
@rem Gradle startup script for Windows
|
||||||
@rem
|
@rem
|
||||||
@rem ##########################################################################
|
@rem ##########################################################################
|
||||||
|
|
||||||
@rem Set local scope for the variables with windows NT shell
|
@rem Set local scope for the variables with windows NT shell
|
||||||
if "%OS%"=="Windows_NT" setlocal
|
if "%OS%"=="Windows_NT" setlocal
|
||||||
|
|
||||||
set DIRNAME=%~dp0
|
set DIRNAME=%~dp0
|
||||||
if "%DIRNAME%" == "" set DIRNAME=.
|
if "%DIRNAME%" == "" set DIRNAME=.
|
||||||
set APP_BASE_NAME=%~n0
|
set APP_BASE_NAME=%~n0
|
||||||
set APP_HOME=%DIRNAME%
|
set APP_HOME=%DIRNAME%
|
||||||
|
|
||||||
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
|
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
|
||||||
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
|
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.
|
@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"
|
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
|
||||||
|
|
||||||
@rem Find java.exe
|
@rem Find java.exe
|
||||||
if defined JAVA_HOME goto findJavaFromJavaHome
|
if defined JAVA_HOME goto findJavaFromJavaHome
|
||||||
|
|
||||||
set JAVA_EXE=java.exe
|
set JAVA_EXE=java.exe
|
||||||
%JAVA_EXE% -version >NUL 2>&1
|
%JAVA_EXE% -version >NUL 2>&1
|
||||||
if "%ERRORLEVEL%" == "0" goto execute
|
if "%ERRORLEVEL%" == "0" goto execute
|
||||||
|
|
||||||
echo.
|
echo.
|
||||||
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||||
echo.
|
echo.
|
||||||
echo Please set the JAVA_HOME variable in your environment to match the
|
echo Please set the JAVA_HOME variable in your environment to match the
|
||||||
echo location of your Java installation.
|
echo location of your Java installation.
|
||||||
|
|
||||||
goto fail
|
goto fail
|
||||||
|
|
||||||
:findJavaFromJavaHome
|
:findJavaFromJavaHome
|
||||||
set JAVA_HOME=%JAVA_HOME:"=%
|
set JAVA_HOME=%JAVA_HOME:"=%
|
||||||
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
|
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
|
||||||
|
|
||||||
if exist "%JAVA_EXE%" goto execute
|
if exist "%JAVA_EXE%" goto execute
|
||||||
|
|
||||||
echo.
|
echo.
|
||||||
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
|
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
|
||||||
echo.
|
echo.
|
||||||
echo Please set the JAVA_HOME variable in your environment to match the
|
echo Please set the JAVA_HOME variable in your environment to match the
|
||||||
echo location of your Java installation.
|
echo location of your Java installation.
|
||||||
|
|
||||||
goto fail
|
goto fail
|
||||||
|
|
||||||
:execute
|
:execute
|
||||||
@rem Setup the command line
|
@rem Setup the command line
|
||||||
|
|
||||||
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
|
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
|
||||||
|
|
||||||
|
|
||||||
@rem Execute Gradle
|
@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 %*
|
||||||
|
|
||||||
:end
|
:end
|
||||||
@rem End local scope for the variables with windows NT shell
|
@rem End local scope for the variables with windows NT shell
|
||||||
if "%ERRORLEVEL%"=="0" goto mainEnd
|
if "%ERRORLEVEL%"=="0" goto mainEnd
|
||||||
|
|
||||||
:fail
|
:fail
|
||||||
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
|
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
|
||||||
rem the _cmd.exe /c_ return code!
|
rem the _cmd.exe /c_ return code!
|
||||||
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
|
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
|
||||||
exit /b 1
|
exit /b 1
|
||||||
|
|
||||||
:mainEnd
|
:mainEnd
|
||||||
if "%OS%"=="Windows_NT" endlocal
|
if "%OS%"=="Windows_NT" endlocal
|
||||||
|
|
||||||
:omega
|
:omega
|
||||||
|
|||||||
25528
photon-client/package-lock.json
generated
25528
photon-client/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -16,9 +16,10 @@
|
|||||||
"jspdf": "^2.4.0",
|
"jspdf": "^2.4.0",
|
||||||
"material-design-icons-iconfont": "^5.0.1",
|
"material-design-icons-iconfont": "^5.0.1",
|
||||||
"msgpack5": "^4.2.1",
|
"msgpack5": "^4.2.1",
|
||||||
|
"three-full": "^28.0.2",
|
||||||
"vue": "^2.6.12",
|
"vue": "^2.6.12",
|
||||||
"vue-axios": "^2.1.5",
|
"vue-axios": "^2.1.5",
|
||||||
"vue-native-websocket": "git+https://github.com/PhotonVision/vue-native-websocket.git#7a32791",
|
"vue-native-websocket": "git+https://git@github.com/PhotonVision/vue-native-websocket.git#5189f29",
|
||||||
"vue-router": "^3.4.3",
|
"vue-router": "^3.4.3",
|
||||||
"vuetify": "^2.3.10",
|
"vuetify": "^2.3.10",
|
||||||
"vuex": "^3.5.1"
|
"vuex": "^3.5.1"
|
||||||
|
|||||||
BIN
photon-client/public/loading.gif
Normal file
BIN
photon-client/public/loading.gif
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 12 KiB |
317
photon-client/public/thinclient.html
Normal file
317
photon-client/public/thinclient.html
Normal file
@@ -0,0 +1,317 @@
|
|||||||
|
<!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>
|
||||||
@@ -1,35 +1,17 @@
|
|||||||
<template>
|
<template>
|
||||||
<v-app>
|
<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-->
|
<!-- 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
|
<v-navigation-drawer dark app permanent :mini-variant="compact" color="primary">
|
||||||
dark
|
|
||||||
app
|
|
||||||
permanent
|
|
||||||
:mini-variant="compact"
|
|
||||||
color="primary"
|
|
||||||
>
|
|
||||||
<v-list>
|
<v-list>
|
||||||
<!-- List item for the heading; note that there are some tricks in setting padding and image width make things look right -->
|
<!-- 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 :class="compact ? 'pr-0 pl-0' : ''">
|
||||||
<v-list-item-icon class="mr-0">
|
<v-list-item-icon class="mr-0">
|
||||||
<img
|
<img v-if="!compact" class="logo" src="./assets/logoLarge.png">
|
||||||
v-if="!compact"
|
<img v-else class="logo" src="./assets/logoSmall.png">
|
||||||
class="logo"
|
|
||||||
src="./assets/logoLarge.png"
|
|
||||||
>
|
|
||||||
<img
|
|
||||||
v-else
|
|
||||||
class="logo"
|
|
||||||
src="./assets/logoSmall.png"
|
|
||||||
>
|
|
||||||
</v-list-item-icon>
|
</v-list-item-icon>
|
||||||
</v-list-item>
|
</v-list-item>
|
||||||
|
|
||||||
<v-list-item
|
<v-list-item link to="dashboard" @click="rollbackPipelineIndex()">
|
||||||
link
|
|
||||||
to="dashboard"
|
|
||||||
@click="rollbackPipelineIndex()"
|
|
||||||
>
|
|
||||||
<v-list-item-icon>
|
<v-list-item-icon>
|
||||||
<v-icon>mdi-view-dashboard</v-icon>
|
<v-icon>mdi-view-dashboard</v-icon>
|
||||||
</v-list-item-icon>
|
</v-list-item-icon>
|
||||||
@@ -37,12 +19,7 @@
|
|||||||
<v-list-item-title>Dashboard</v-list-item-title>
|
<v-list-item-title>Dashboard</v-list-item-title>
|
||||||
</v-list-item-content>
|
</v-list-item-content>
|
||||||
</v-list-item>
|
</v-list-item>
|
||||||
<v-list-item
|
<v-list-item ref="camerasTabOpener" link to="cameras" @click="switchToDriverMode()">
|
||||||
ref="camerasTabOpener"
|
|
||||||
link
|
|
||||||
to="cameras"
|
|
||||||
@click="switchToDriverMode()"
|
|
||||||
>
|
|
||||||
<v-list-item-icon>
|
<v-list-item-icon>
|
||||||
<v-icon>mdi-camera</v-icon>
|
<v-icon>mdi-camera</v-icon>
|
||||||
</v-list-item-icon>
|
</v-list-item-icon>
|
||||||
@@ -50,11 +27,7 @@
|
|||||||
<v-list-item-title>Cameras</v-list-item-title>
|
<v-list-item-title>Cameras</v-list-item-title>
|
||||||
</v-list-item-content>
|
</v-list-item-content>
|
||||||
</v-list-item>
|
</v-list-item>
|
||||||
<v-list-item
|
<v-list-item link to="settings" @click="switchToSettingsTab()">
|
||||||
link
|
|
||||||
to="settings"
|
|
||||||
@click="switchToSettingsTab()"
|
|
||||||
>
|
|
||||||
<v-list-item-icon>
|
<v-list-item-icon>
|
||||||
<v-icon>mdi-settings</v-icon>
|
<v-icon>mdi-settings</v-icon>
|
||||||
</v-list-item-icon>
|
</v-list-item-icon>
|
||||||
@@ -62,10 +35,7 @@
|
|||||||
<v-list-item-title>Settings</v-list-item-title>
|
<v-list-item-title>Settings</v-list-item-title>
|
||||||
</v-list-item-content>
|
</v-list-item-content>
|
||||||
</v-list-item>
|
</v-list-item>
|
||||||
<v-list-item
|
<v-list-item link to="docs">
|
||||||
link
|
|
||||||
to="docs"
|
|
||||||
>
|
|
||||||
<v-list-item-icon>
|
<v-list-item-icon>
|
||||||
<v-icon>mdi-bookshelf</v-icon>
|
<v-icon>mdi-bookshelf</v-icon>
|
||||||
</v-list-item-icon>
|
</v-list-item-icon>
|
||||||
@@ -73,11 +43,7 @@
|
|||||||
<v-list-item-title>Documentation</v-list-item-title>
|
<v-list-item-title>Documentation</v-list-item-title>
|
||||||
</v-list-item-content>
|
</v-list-item-content>
|
||||||
</v-list-item>
|
</v-list-item>
|
||||||
<v-list-item
|
<v-list-item v-if="this.$vuetify.breakpoint.mdAndUp" link @click.stop="toggleCompactMode">
|
||||||
v-if="this.$vuetify.breakpoint.mdAndUp"
|
|
||||||
link
|
|
||||||
@click.stop="toggleCompactMode"
|
|
||||||
>
|
|
||||||
<v-list-item-icon>
|
<v-list-item-icon>
|
||||||
<v-icon v-if="compact">
|
<v-icon v-if="compact">
|
||||||
mdi-chevron-right
|
mdi-chevron-right
|
||||||
@@ -87,31 +53,36 @@
|
|||||||
</v-icon>
|
</v-icon>
|
||||||
</v-list-item-icon>
|
</v-list-item-icon>
|
||||||
<v-list-item-content>
|
<v-list-item-content>
|
||||||
<v-list-item-title>Advanced Mode</v-list-item-title>
|
<v-list-item-title>Compact Mode</v-list-item-title>
|
||||||
</v-list-item-content>
|
</v-list-item-content>
|
||||||
</v-list-item>
|
</v-list-item>
|
||||||
|
|
||||||
<div style="position: absolute; bottom: 0; left: 0;">
|
<div style="position: absolute; bottom: 0; left: 0;">
|
||||||
<v-list-item>
|
<v-list-item>
|
||||||
<v-list-item-icon>
|
<v-list-item-icon>
|
||||||
<v-icon v-if="$store.state.settings.networkSettings.runNTServer">mdi-server</v-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-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="">
|
<img v-else class="pulse" style="border-radius: 100%" src="@/assets/robot-off.svg" alt="">
|
||||||
</v-list-item-icon>
|
</v-list-item-icon>
|
||||||
<v-list-item-content>
|
<v-list-item-content>
|
||||||
<v-list-item-title class="text-wrap" v-if="$store.state.settings.networkSettings.runNTServer">
|
<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!
|
NetworkTables server running for {{ $store.state.ntConnectionInfo.clients ?
|
||||||
|
$store.state.ntConnectionInfo.clients : 'zero'
|
||||||
|
}} clients!
|
||||||
</v-list-item-title>
|
</v-list-item-title>
|
||||||
<v-list-item-title class="text-wrap" v-else-if="$store.state.ntConnectionInfo.connected && $store.state.backendConnected">
|
<v-list-item-title v-else-if="$store.state.ntConnectionInfo.connected && $store.state.backendConnected"
|
||||||
Robot connected! {{$store.state.ntConnectionInfo.address}}
|
class="text-wrap">
|
||||||
|
Robot connected! {{ $store.state.ntConnectionInfo.address }}
|
||||||
</v-list-item-title>
|
</v-list-item-title>
|
||||||
<v-list-item-title class="text-wrap" v-else>
|
<v-list-item-title v-else class="text-wrap">
|
||||||
Not connected to robot!
|
Not connected to robot!
|
||||||
</v-list-item-title>
|
</v-list-item-title>
|
||||||
<a
|
<router-link v-if="!$store.state.settings.networkSettings.runNTServer" to="settings" class="accent--text"
|
||||||
href="/#/settings"
|
@click="switchToSettingsTab">
|
||||||
style="color:#FFD843"
|
Team number is {{ $store.state.settings.networkSettings.teamNumber }}
|
||||||
>{{"Team: " + $store.state.settings.networkSettings.teamNumber}}</a>
|
</router-link>
|
||||||
</v-list-item-content>
|
</v-list-item-content>
|
||||||
</v-list-item>
|
</v-list-item>
|
||||||
|
|
||||||
@@ -120,11 +91,7 @@
|
|||||||
<v-icon v-if="$store.state.backendConnected">
|
<v-icon v-if="$store.state.backendConnected">
|
||||||
mdi-wifi
|
mdi-wifi
|
||||||
</v-icon>
|
</v-icon>
|
||||||
<v-icon
|
<v-icon v-else class="pulse" style="border-radius: 100%;">
|
||||||
v-else
|
|
||||||
class="pulse"
|
|
||||||
style="border-radius: 100%;"
|
|
||||||
>
|
|
||||||
mdi-wifi-off
|
mdi-wifi-off
|
||||||
</v-icon>
|
</v-icon>
|
||||||
</v-list-item-icon>
|
</v-list-item-icon>
|
||||||
@@ -135,14 +102,10 @@
|
|||||||
</v-list-item-content>
|
</v-list-item-content>
|
||||||
</v-list-item>
|
</v-list-item>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</v-list>
|
</v-list>
|
||||||
</v-navigation-drawer>
|
</v-navigation-drawer>
|
||||||
<v-main>
|
<v-main>
|
||||||
<v-container
|
<v-container fluid fill-height>
|
||||||
fluid
|
|
||||||
fill-height
|
|
||||||
>
|
|
||||||
<v-layout>
|
<v-layout>
|
||||||
<v-flex>
|
<v-flex>
|
||||||
<router-view @switch-to-cameras="switchToDriverMode" />
|
<router-view @switch-to-cameras="switchToDriverMode" />
|
||||||
@@ -151,31 +114,18 @@
|
|||||||
</v-container>
|
</v-container>
|
||||||
</v-main>
|
</v-main>
|
||||||
|
|
||||||
<v-dialog
|
<v-dialog v-model="$store.state.logsOverlay" width="1500" dark>
|
||||||
v-model="$store.state.logsOverlay"
|
|
||||||
width="1500"
|
|
||||||
dark
|
|
||||||
>
|
|
||||||
<logs />
|
<logs />
|
||||||
</v-dialog>
|
</v-dialog>
|
||||||
<v-dialog
|
<v-dialog v-model="needsTeamNumberSet" width="500" dark persistent>
|
||||||
v-model="needsTeamNumberSet"
|
<v-card dark color="primary" flat>
|
||||||
width="500"
|
|
||||||
dark
|
|
||||||
persistent
|
|
||||||
>
|
|
||||||
<v-card
|
|
||||||
dark
|
|
||||||
color="primary"
|
|
||||||
flat
|
|
||||||
>
|
|
||||||
<v-card-title>No team number set!</v-card-title>
|
<v-card-title>No team number set!</v-card-title>
|
||||||
<v-card-text>
|
<v-card-text>
|
||||||
PhotonVision cannot connect to your robot! Please
|
PhotonVision cannot connect to your robot! Please
|
||||||
<a
|
<router-link to="settings" class="accent--text" @click="switchToSettingsTab">
|
||||||
href="/#/settings"
|
visit the settings tab
|
||||||
style="color:#FFD843"
|
</router-link>
|
||||||
>head to the settings page</a> and set your team number.
|
and set your team number.
|
||||||
</v-card-text>
|
</v-card-text>
|
||||||
</v-card>
|
</v-card>
|
||||||
</v-dialog>
|
</v-dialog>
|
||||||
@@ -184,148 +134,143 @@
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
import Logs from "./views/LogsView"
|
import Logs from "./views/LogsView"
|
||||||
// import {mapState} from "vuex";
|
import { ReconnectingWebsocket } from "./plugins/ReconnectingWebsocket.js"
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'App',
|
name: 'App',
|
||||||
components: {
|
components: {
|
||||||
Logs
|
Logs
|
||||||
},
|
},
|
||||||
data: () => ({
|
data: () => ({
|
||||||
// Used so that we can switch back to the previously selected pipeline after camera calibration
|
// Used so that we can switch back to the previously selected pipeline after camera calibration
|
||||||
previouslySelectedIndices: [],
|
previouslySelectedIndices: [],
|
||||||
timer: undefined,
|
timer: undefined,
|
||||||
teamNumberDialog: true
|
teamNumberDialog: true,
|
||||||
}),
|
websocket: null,
|
||||||
computed: {
|
}),
|
||||||
needsTeamNumberSet: {
|
computed: {
|
||||||
get() {
|
needsTeamNumberSet: {
|
||||||
return this.$store.state.settings.networkSettings.teamNumber < 1
|
get() {
|
||||||
&& this.teamNumberDialog && this.$store.state.backendConnected
|
return this.$store.state.settings.networkSettings.teamNumber < 1
|
||||||
&& !this.$route.name.toLowerCase().includes("settings");
|
&& this.teamNumberDialog && this.$store.state.backendConnected
|
||||||
}
|
&& !this.$route.name.toLowerCase().includes("settings");
|
||||||
},
|
}
|
||||||
compact: {
|
},
|
||||||
get() {
|
compact: {
|
||||||
if (this.$store.state.compactMode === undefined) {
|
get() {
|
||||||
return this.$vuetify.breakpoint.smAndDown;
|
if (this.$store.state.compactMode === undefined) {
|
||||||
} else {
|
return this.$vuetify.breakpoint.smAndDown;
|
||||||
return this.$store.state.compactMode || 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);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
// ...mapState({
|
|
||||||
// ntServerMode: state => state.settings.networkSettings.runNTServer,
|
|
||||||
// ntClients: state => state.ntConnectionInfo.clients,
|
|
||||||
// ntConnected: state => state.ntConnectionInfo.connected,
|
|
||||||
// backendConnected: state => state.backendConnected
|
|
||||||
// })
|
|
||||||
},
|
|
||||||
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.$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.commit("backendConnected", true)
|
|
||||||
this.$store.state.connectedCallbacks.forEach(it => it())
|
|
||||||
};
|
|
||||||
|
|
||||||
let closed = () => {
|
|
||||||
this.$store.commit("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', {})
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
};
|
},
|
||||||
|
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;
|
||||||
|
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const wsDataURL = 'ws://' + this.$address + '/websocket_data';
|
||||||
|
this.websocket = new ReconnectingWebsocket(
|
||||||
|
wsDataURL,
|
||||||
|
|
||||||
|
// On data in
|
||||||
|
(event) => {
|
||||||
|
try {
|
||||||
|
let message = this.$msgPack.decode(event.data);
|
||||||
|
for (let prop in message) {
|
||||||
|
if (message.hasOwnProperty(prop)) {
|
||||||
|
this.handleMessage(prop, message[prop]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.log(event)
|
||||||
|
console.error('error: ' + JSON.stringify(event.data) + " , " + error);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// on connect
|
||||||
|
(event) => {
|
||||||
|
event; this.$store.commit("backendConnected", true);
|
||||||
|
this.$store.state.connectedCallbacks.forEach(it => it());
|
||||||
|
},
|
||||||
|
|
||||||
|
// on disconnect
|
||||||
|
(event) => { event; this.$store.commit("backendConnected", false) }
|
||||||
|
);
|
||||||
|
|
||||||
|
this.$store.commit("websocket", this.websocket);
|
||||||
|
},
|
||||||
|
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 {
|
||||||
|
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>
|
</script>
|
||||||
|
|
||||||
<style lang="sass">
|
<style lang="sass">
|
||||||
@@ -333,76 +278,77 @@ export default {
|
|||||||
</style>
|
</style>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.pulse {
|
.pulse {
|
||||||
animation: pulse-animation 2s infinite;
|
animation: pulse-animation 2s infinite;
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes pulse-animation {
|
@keyframes pulse-animation {
|
||||||
0% {
|
0% {
|
||||||
box-shadow: 0 0 0 0px rgba(0, 0, 0, 0.2);
|
box-shadow: 0 0 0 0px rgba(0, 0, 0, 0.2);
|
||||||
background-color: rgba(0, 0, 0, 0.2);
|
background-color: rgba(0, 0, 0, 0.2);
|
||||||
}
|
}
|
||||||
100% {
|
|
||||||
box-shadow: 0 0 0 20px rgba(0, 0, 0, 0);
|
|
||||||
background-color: rgba(0, 0, 0, 0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.logo {
|
100% {
|
||||||
width: 100%;
|
box-shadow: 0 0 0 20px rgba(0, 0, 0, 0);
|
||||||
height: 70px;
|
background-color: rgba(0, 0, 0, 0);
|
||||||
object-fit: contain;
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
::-webkit-scrollbar {
|
.logo {
|
||||||
width: 0.5em;
|
width: 100%;
|
||||||
border-radius: 5px;
|
height: 70px;
|
||||||
}
|
object-fit: contain;
|
||||||
|
}
|
||||||
|
|
||||||
::-webkit-scrollbar-track {
|
::-webkit-scrollbar {
|
||||||
-webkit-box-shadow: inset 0 0 6px rgba(0, 0, 0, 0.3);
|
width: 0.5em;
|
||||||
border-radius: 10px;
|
border-radius: 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-track {
|
||||||
|
-webkit-box-shadow: inset 0 0 6px rgba(0, 0, 0, 0.3);
|
||||||
|
border-radius: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
::-webkit-scrollbar-thumb {
|
::-webkit-scrollbar-thumb {
|
||||||
background-color: #ffd843;
|
background-color: #ffd843;
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.container {
|
.container {
|
||||||
background-color: #232c37;
|
background-color: #232c37;
|
||||||
padding: 0 !important;
|
padding: 0 !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
#title {
|
#title {
|
||||||
color: #ffd843;
|
color: #ffd843;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
/* Hacks */
|
/* Hacks */
|
||||||
|
|
||||||
.v-divider {
|
.v-divider {
|
||||||
border-color: white !important;
|
border-color: white !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.v-input {
|
.v-input {
|
||||||
font-size: 1rem !important;
|
font-size: 1rem !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* This is unfortunately the only way to override table background color */
|
/* 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) {
|
.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;
|
background: #005281 !important;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
@import '~vuetify/src/styles/settings/_variables';
|
@import '~vuetify/src/styles/settings/_variables';
|
||||||
|
|
||||||
@media #{map-get($display-breakpoints, 'md-and-down')} {
|
@media #{map-get($display-breakpoints, 'md-and-down')} {
|
||||||
html {
|
html {
|
||||||
font-size: 14px !important;
|
font-size: 14px !important;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
BIN
photon-client/src/assets/loading.gif
Normal file
BIN
photon-client/src/assets/loading.gif
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 12 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 48 KiB After Width: | Height: | Size: 76 KiB |
@@ -4,16 +4,17 @@
|
|||||||
crossOrigin="anonymous"
|
crossOrigin="anonymous"
|
||||||
:style="styleObject"
|
:style="styleObject"
|
||||||
:src="src"
|
:src="src"
|
||||||
alt=""
|
:alt="alt"
|
||||||
@click="e => $emit('click', e)"
|
@click="clickHandler"
|
||||||
>
|
@error="loadErrHandler"
|
||||||
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
export default {
|
export default {
|
||||||
name: "CvImage",
|
name: "CvImage",
|
||||||
// eslint-disable-next-line vue/require-prop-types
|
// eslint-disable-next-line vue/require-prop-types
|
||||||
props: ['address', 'scale', 'maxHeight', 'maxHeightMd', 'maxHeightXl', 'colorPicking', 'id', 'disconnected'],
|
props: ['address', 'scale', 'maxHeight', 'maxHeightMd', 'maxHeightLg', 'maxHeightXl', 'colorPicking', 'id', 'disconnected', 'alt'],
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
seed: 1.0,
|
seed: 1.0,
|
||||||
@@ -26,18 +27,21 @@
|
|||||||
"border-radius": "3px",
|
"border-radius": "3px",
|
||||||
"display": "block",
|
"display": "block",
|
||||||
"object-fit": "contain",
|
"object-fit": "contain",
|
||||||
|
"background-size:": "contain",
|
||||||
"object-position": "50% 50%",
|
"object-position": "50% 50%",
|
||||||
"max-width": "100%",
|
"max-width": "100%",
|
||||||
"margin-left": "auto",
|
"margin-left": "auto",
|
||||||
"margin-right": "auto",
|
"margin-right": "auto",
|
||||||
"max-height": this.maxHeight,
|
"max-height": this.maxHeight,
|
||||||
height: `${this.scale}%`,
|
height: `${this.scale}%`,
|
||||||
cursor: (this.colorPicking ? `url(${require("../../assets/eyedropper.svg")}),` : "") + "default",
|
cursor: (this.colorPicking ? `url(${require("../../assets/eyedropper.svg")}),` : "pointer") + "default",
|
||||||
};
|
};
|
||||||
|
|
||||||
if (this.$vuetify.breakpoint.xl) {
|
if (this.$vuetify.breakpoint.xl) {
|
||||||
ret["max-height"] = this.maxHeightXl;
|
ret["max-height"] = this.maxHeightXl;
|
||||||
} else if (this.$vuetify.breakpoint.mdAndUp) {
|
} else if (this.$vuetify.breakpoint.lg) {
|
||||||
|
ret["max-height"] = this.maxHeightLg;
|
||||||
|
} else if (this.$vuetify.breakpoint.md) {
|
||||||
ret["max-height"] = this.maxHeightMd;
|
ret["max-height"] = this.maxHeightMd;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -46,7 +50,14 @@
|
|||||||
},
|
},
|
||||||
src: {
|
src: {
|
||||||
get() {
|
get() {
|
||||||
return this.disconnected ? require("../../assets/noStream.jpg") : this.address + "?" + this.seed // This prevents caching
|
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);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -54,6 +65,43 @@
|
|||||||
this.reload(); // Force reload image on creation
|
this.reload(); // Force reload image on creation
|
||||||
},
|
},
|
||||||
methods: {
|
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() {
|
reload() {
|
||||||
this.seed = new Date().getTime();
|
this.seed = new Date().getTime();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,7 +26,7 @@
|
|||||||
</v-row>
|
</v-row>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
s
|
|
||||||
<script>
|
<script>
|
||||||
import TooltippedLabel from "./cv-tooltipped-label";
|
import TooltippedLabel from "./cv-tooltipped-label";
|
||||||
|
|
||||||
|
|||||||
@@ -1,28 +1,46 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<v-radio-group
|
<v-row
|
||||||
v-model="localValue"
|
dense
|
||||||
row
|
align="center"
|
||||||
dark
|
|
||||||
:mandatory="true"
|
|
||||||
>
|
>
|
||||||
<v-radio
|
<v-col :cols="12 - (inputCols || 8)">
|
||||||
v-for="(name,index) in list"
|
<tooltipped-label
|
||||||
:key="index"
|
:tooltip="tooltip"
|
||||||
color="#ffd843"
|
:text="name"
|
||||||
:label="name"
|
/>
|
||||||
:value="index"
|
</v-col>
|
||||||
:disabled="disabled"
|
<v-col :cols="inputCols || 8">
|
||||||
/>
|
<v-radio-group
|
||||||
</v-radio-group>
|
v-model="localValue"
|
||||||
|
row
|
||||||
|
dark
|
||||||
|
:mandatory="true"
|
||||||
|
>
|
||||||
|
<v-radio
|
||||||
|
v-for="(radioName,index) in list"
|
||||||
|
:key="index"
|
||||||
|
color="#ffd843"
|
||||||
|
:label="radioName"
|
||||||
|
:value="index"
|
||||||
|
:disabled="disabled"
|
||||||
|
/>
|
||||||
|
</v-radio-group>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
import TooltippedLabel from "./cv-tooltipped-label";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'Radio',
|
name: 'Radio',
|
||||||
|
components: {
|
||||||
|
TooltippedLabel
|
||||||
|
},
|
||||||
// eslint-disable-next-line vue/require-prop-types
|
// eslint-disable-next-line vue/require-prop-types
|
||||||
props: ['value', 'list', 'disabled'],
|
props: ['name', 'value', 'list', 'disabled', 'inputCols', 'tooltip'],
|
||||||
data() {
|
data() {
|
||||||
return {}
|
return {}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -19,7 +19,9 @@
|
|||||||
hide-details
|
hide-details
|
||||||
class="align-center"
|
class="align-center"
|
||||||
dark
|
dark
|
||||||
color="accent"
|
:color="inverted ? 'rgba(255, 255, 255, 0.2)' : 'accent'"
|
||||||
|
:track-color="inverted ? 'accent' : undefined"
|
||||||
|
thumb-color="accent"
|
||||||
:step="step"
|
:step="step"
|
||||||
@input="handleInput"
|
@input="handleInput"
|
||||||
@mousedown="$emit('rollback', localValue)"
|
@mousedown="$emit('rollback', localValue)"
|
||||||
@@ -76,7 +78,7 @@ export default {
|
|||||||
TooltippedLabel,
|
TooltippedLabel,
|
||||||
},
|
},
|
||||||
// eslint-disable-next-line vue/require-prop-types
|
// eslint-disable-next-line vue/require-prop-types
|
||||||
props: ["name", "min", "max", "value", "step", "tooltip", "disabled"],
|
props: ["name", "min", "max", "value", "step", "tooltip", "disabled", "inverted"],
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
prependFocused: false,
|
prependFocused: false,
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ import TooltippedLabel from "./cv-tooltipped-label";
|
|||||||
TooltippedLabel,
|
TooltippedLabel,
|
||||||
},
|
},
|
||||||
// eslint-disable-next-line vue/require-prop-types
|
// eslint-disable-next-line vue/require-prop-types
|
||||||
props: ['list', 'name', 'value', 'disabled', 'selectCols', 'rules', 'tooltip'],
|
props: ['list', 'name', 'value', 'disabled', 'filteredIndices', 'selectCols', 'rules', 'tooltip'],
|
||||||
computed: {
|
computed: {
|
||||||
localValue: {
|
localValue: {
|
||||||
get() {
|
get() {
|
||||||
@@ -50,6 +50,7 @@ import TooltippedLabel from "./cv-tooltipped-label";
|
|||||||
indexList() {
|
indexList() {
|
||||||
let list = [];
|
let list = [];
|
||||||
for (let i = 0; i < this.list.length; i++) {
|
for (let i = 0; i < this.list.length; i++) {
|
||||||
|
if (this.filteredIndices instanceof Set && this.filteredIndices.has(i)) continue;
|
||||||
list.push({
|
list.push({
|
||||||
name: this.list[i],
|
name: this.list[i],
|
||||||
index: i
|
index: i
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
<v-tooltip
|
<v-tooltip
|
||||||
:disabled="tooltip === undefined"
|
:disabled="tooltip === undefined"
|
||||||
right
|
right
|
||||||
open-delay="600"
|
open-delay="300"
|
||||||
>
|
>
|
||||||
<template v-slot:activator="{ on, attrs }">
|
<template v-slot:activator="{ on, attrs }">
|
||||||
<span
|
<span
|
||||||
|
|||||||
@@ -1,154 +1,268 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div
|
||||||
|
id="MapContainer"
|
||||||
|
style="flex-grow:1"
|
||||||
|
>
|
||||||
<v-row>
|
<v-row>
|
||||||
<v-col
|
<v-col
|
||||||
align="center"
|
align="center"
|
||||||
cols="12"
|
cols="12"
|
||||||
>
|
>
|
||||||
<span class="white--text">Target Location</span>
|
<span class="white--text">Target Location</span>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
<v-row>
|
||||||
|
<v-col
|
||||||
|
align="center"
|
||||||
|
cols="12"
|
||||||
|
align-self="stretch"
|
||||||
|
>
|
||||||
<canvas
|
<canvas
|
||||||
id="canvasId"
|
id="canvasId"
|
||||||
class="mt-2"
|
style="width:100%;height:100%"
|
||||||
width="800"
|
|
||||||
height="800"
|
|
||||||
/>
|
/>
|
||||||
</v-col>
|
</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>
|
</v-row>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import theme from "../../../theme";
|
|
||||||
|
|
||||||
export default {
|
import {
|
||||||
name: "MiniMap",
|
ArrowHelper,
|
||||||
props: {
|
BoxGeometry,
|
||||||
// eslint-disable-next-line vue/require-default-prop
|
ConeGeometry,
|
||||||
targets: Array,
|
Mesh,
|
||||||
// eslint-disable-next-line vue/require-default-prop
|
MeshNormalMaterial,
|
||||||
horizontalFOV: Number
|
PerspectiveCamera,
|
||||||
},
|
Quaternion,
|
||||||
data() {
|
Scene,
|
||||||
return {
|
TrackballControls,
|
||||||
ctx: undefined,
|
Vector3,
|
||||||
canvas: undefined,
|
Color,
|
||||||
x: 0,
|
WebGLRenderer
|
||||||
y: 0,
|
} from "three-full";
|
||||||
targetWidth: 40,
|
|
||||||
targetHeight: 6
|
|
||||||
}
|
|
||||||
},
|
|
||||||
computed: {
|
|
||||||
hLen: {
|
|
||||||
get() {
|
|
||||||
return Math.tan(this.horizontalFOV / 2 * Math.PI / 180) * 150;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
watch: {
|
|
||||||
targets: {
|
|
||||||
deep: true,
|
|
||||||
handler() {
|
|
||||||
this.draw();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
horizontalFOV() {
|
|
||||||
this.draw();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
mounted: function () {
|
|
||||||
const canvas = document.getElementById("canvasId"); // getting the canvas element
|
|
||||||
const ctx = canvas.getContext("2d"); // getting the canvas context
|
|
||||||
this.canvas = canvas; // setting the canvas as a vue variable
|
|
||||||
this.ctx = ctx; // setting the canvas context as a vue variable
|
|
||||||
this.grad = this.ctx.createLinearGradient(400, 800, 400, 600);
|
|
||||||
this.grad.addColorStop(0, "rgb(119,119,119)");
|
|
||||||
this.grad.addColorStop(0.05, "rgba(14,92,22,0.96)");
|
|
||||||
this.grad.addColorStop(0.8, 'rgba(43,43,43,0.48)');
|
|
||||||
|
|
||||||
// setting canvas context values for drawing
|
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: [],
|
||||||
|
|
||||||
|
|
||||||
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>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
#canvasId {
|
|
||||||
width: 400px;
|
|
||||||
height: 400px;
|
|
||||||
background-color: #232C37;
|
|
||||||
border-radius: 5px;
|
|
||||||
border: 2px solid grey;
|
|
||||||
box-shadow: 0 0 5px 1px;
|
|
||||||
}
|
|
||||||
|
|
||||||
th {
|
|
||||||
width: 80px;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,11 +1,15 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<v-row align="center">
|
<v-row
|
||||||
|
align="center"
|
||||||
|
class="pl-6"
|
||||||
|
>
|
||||||
<v-col
|
<v-col
|
||||||
cols="10"
|
cols="10"
|
||||||
md="5"
|
md="5"
|
||||||
lg="10"
|
lg="10"
|
||||||
class="pt-0 pb-0 pl-6"
|
no-gutters
|
||||||
|
class="pa-0"
|
||||||
>
|
>
|
||||||
<CVselect
|
<CVselect
|
||||||
v-if="isCameraNameEdit === false"
|
v-if="isCameraNameEdit === false"
|
||||||
@@ -59,7 +63,8 @@
|
|||||||
cols="10"
|
cols="10"
|
||||||
md="5"
|
md="5"
|
||||||
lg="10"
|
lg="10"
|
||||||
class="pt-0 pb-0 pl-6"
|
no-gutters
|
||||||
|
class="pa-0"
|
||||||
>
|
>
|
||||||
<CVselect
|
<CVselect
|
||||||
v-model="currentPipelineIndex"
|
v-model="currentPipelineIndex"
|
||||||
@@ -139,14 +144,16 @@
|
|||||||
<v-col
|
<v-col
|
||||||
v-if="currentPipelineType >= 0"
|
v-if="currentPipelineType >= 0"
|
||||||
cols="10"
|
cols="10"
|
||||||
md="5"
|
md="11"
|
||||||
lg="10"
|
lg="10"
|
||||||
class="pt-0 pb-0 pl-6"
|
no-gutters
|
||||||
|
class="pa-0"
|
||||||
>
|
>
|
||||||
<CVselect
|
<CVselect
|
||||||
v-model="currentPipelineType"
|
v-model="currentPipelineType"
|
||||||
name="Type"
|
name="Type"
|
||||||
:list="['Reflective', 'Shape']"
|
tooltip="Changes the pipeline type, which changes the type of processing that will happen on input frames"
|
||||||
|
:list="['Reflective Tape', 'Colored Shape', 'AprilTag']"
|
||||||
@input="e => showTypeDialog(e)"
|
@input="e => showTypeDialog(e)"
|
||||||
/>
|
/>
|
||||||
</v-col>
|
</v-col>
|
||||||
@@ -176,12 +183,6 @@
|
|||||||
name="Name"
|
name="Name"
|
||||||
:error-message="checkPipelineName"
|
:error-message="checkPipelineName"
|
||||||
/>
|
/>
|
||||||
<!-- <CVselect-->
|
|
||||||
<!-- v-model="currentPipelineType"-->
|
|
||||||
<!-- name="Pipeline Type"-->
|
|
||||||
<!-- :list="['Reflective', 'Shape']"-->
|
|
||||||
<!-- :disabled="true"-->
|
|
||||||
<!-- />-->
|
|
||||||
</v-card-text>
|
</v-card-text>
|
||||||
<v-divider />
|
<v-divider />
|
||||||
<v-card-actions>
|
<v-card-actions>
|
||||||
@@ -256,7 +257,7 @@ export default {
|
|||||||
},
|
},
|
||||||
data: () => {
|
data: () => {
|
||||||
return {
|
return {
|
||||||
re: RegExp("^[A-Za-z0-9 \\-)(]*[A-Za-z0-9][A-Za-z0-9 \\-)(.]*$"),
|
re: RegExp("^[A-Za-z0-9_ \\-)(]*[A-Za-z0-9][A-Za-z0-9_ \\-)(.]*$"),
|
||||||
isCameraNameEdit: false,
|
isCameraNameEdit: false,
|
||||||
newCameraName: "",
|
newCameraName: "",
|
||||||
cameraNameError: "",
|
cameraNameError: "",
|
||||||
@@ -276,12 +277,12 @@ export default {
|
|||||||
for (let cam in this.cameraList) {
|
for (let cam in this.cameraList) {
|
||||||
if (this.cameraList.hasOwnProperty(cam)) {
|
if (this.cameraList.hasOwnProperty(cam)) {
|
||||||
if (this.newCameraName === this.cameraList[cam]) {
|
if (this.newCameraName === this.cameraList[cam]) {
|
||||||
return "A camera by that name already Exists"
|
return "A camera by that name already exists"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
return "A camera name can only contain letters, numbers and spaces"
|
return "A camera name can only contain letters, numbers, and spaces"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return "";
|
return "";
|
||||||
@@ -384,7 +385,7 @@ export default {
|
|||||||
if (this.isPipelineNameEdit) {
|
if (this.isPipelineNameEdit) {
|
||||||
this.handleInputWithIndex("changePipelineName", this.newPipelineName);
|
this.handleInputWithIndex("changePipelineName", this.newPipelineName);
|
||||||
} else {
|
} else {
|
||||||
this.handleInputWithIndex("addNewPipeline", [this.newPipelineName, this.newPipelineType]); // 0 for reflective, 1 for colored shpae
|
this.handleInputWithIndex("addNewPipeline", [this.newPipelineName, this.currentPipelineType]); // 0 for reflective, 1 for colored shpae
|
||||||
}
|
}
|
||||||
this.discardPipelineNameChange();
|
this.discardPipelineNameChange();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,16 +15,15 @@ if (process.env.NODE_ENV === "production") {
|
|||||||
Vue.prototype.$address = location.hostname + ":5800";
|
Vue.prototype.$address = location.hostname + ":5800";
|
||||||
}
|
}
|
||||||
|
|
||||||
const wsURL = '//' + Vue.prototype.$address + '/websocket';
|
// 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",
|
||||||
|
// });
|
||||||
|
|
||||||
import VueNativeSock from 'vue-native-websocket';
|
|
||||||
|
|
||||||
Vue.use(VueNativeSock, wsURL, {
|
|
||||||
reconnection: true,
|
|
||||||
reconnectionDelay: 100,
|
|
||||||
connectManually: true,
|
|
||||||
format: "arraybuffer",
|
|
||||||
});
|
|
||||||
Vue.use(VueAxios, axios);
|
Vue.use(VueAxios, axios);
|
||||||
Vue.prototype.$msgPack = msgPack(true);
|
Vue.prototype.$msgPack = msgPack(true);
|
||||||
|
|
||||||
|
|||||||
@@ -2,14 +2,14 @@ export const dataHandleMixin = {
|
|||||||
methods: {
|
methods: {
|
||||||
handleInput(key, value) {
|
handleInput(key, value) {
|
||||||
let msg = this.$msgPack.encode({[key]: value});
|
let msg = this.$msgPack.encode({[key]: value});
|
||||||
this.$socket.send(msg);
|
this.$store.state.websocket.ws.send(msg);
|
||||||
},
|
},
|
||||||
handleInputWithIndex(key, value, cameraIndex = this.$store.getters.currentCameraIndex) {
|
handleInputWithIndex(key, value, cameraIndex = this.$store.getters.currentCameraIndex) {
|
||||||
let msg = this.$msgPack.encode({
|
let msg = this.$msgPack.encode({
|
||||||
[key]: value,
|
[key]: value,
|
||||||
["cameraIndex"]: cameraIndex,
|
["cameraIndex"]: cameraIndex,
|
||||||
});
|
});
|
||||||
this.$socket.send(msg);
|
this.$store.state.websocket.ws.send(msg);
|
||||||
},
|
},
|
||||||
handleData(val) {
|
handleData(val) {
|
||||||
this.handleInput(val, this[val]);
|
this.handleInput(val, this[val]);
|
||||||
@@ -22,7 +22,7 @@ export const dataHandleMixin = {
|
|||||||
["cameraIndex"]: this.$store.getters.currentCameraIndex
|
["cameraIndex"]: this.$store.getters.currentCameraIndex
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
this.$socket.send(msg);
|
this.$store.state.websocket.ws.send(msg);
|
||||||
this.$emit('update')
|
this.$emit('update')
|
||||||
},
|
},
|
||||||
handlePipelineUpdate(key, val) {
|
handlePipelineUpdate(key, val) {
|
||||||
@@ -32,7 +32,7 @@ export const dataHandleMixin = {
|
|||||||
["cameraIndex"]: this.$store.getters.currentCameraIndex
|
["cameraIndex"]: this.$store.getters.currentCameraIndex
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
this.$socket.send(msg);
|
this.$store.state.websocket.ws.send(msg);
|
||||||
this.$emit('update')
|
this.$emit('update')
|
||||||
},
|
},
|
||||||
handleTruthyPipelineData(val) {
|
handleTruthyPipelineData(val) {
|
||||||
@@ -42,7 +42,7 @@ export const dataHandleMixin = {
|
|||||||
["cameraIndex"]: this.$store.getters.currentCameraIndex
|
["cameraIndex"]: this.$store.getters.currentCameraIndex
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
this.$socket.send(msg);
|
this.$store.state.websocket.ws.send(msg);
|
||||||
this.$emit('update')
|
this.$emit('update')
|
||||||
},
|
},
|
||||||
rollback(val, e) {
|
rollback(val, e) {
|
||||||
|
|||||||
@@ -5,9 +5,11 @@ function initColorPicker() {
|
|||||||
if (!canvas)
|
if (!canvas)
|
||||||
canvas = document.createElement('canvas');
|
canvas = document.createElement('canvas');
|
||||||
|
|
||||||
image = document.querySelector('#normal-stream');
|
image = document.querySelector('#raw-stream');
|
||||||
canvas.width = image.width;
|
if (image !== null) {
|
||||||
canvas.height = image.height;
|
canvas.width = image.width;
|
||||||
|
canvas.height = image.height;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
//Called on click of the image,
|
//Called on click of the image,
|
||||||
|
|||||||
74
photon-client/src/plugins/ReconnectingWebsocket.js
Normal file
74
photon-client/src/plugins/ReconnectingWebsocket.js
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
/**
|
||||||
|
* Auto-reconnecting Websocket, a stripped down version of the NT4 client from
|
||||||
|
* https://raw.githubusercontent.com/wpilibsuite/NetworkTablesClients/2f8d378ac08d5ca703d590cfb019fc4af062db89/nt4/js/src/nt4.js
|
||||||
|
*/
|
||||||
|
export class ReconnectingWebsocket {
|
||||||
|
constructor(serverAddr,
|
||||||
|
onDataIn_in,
|
||||||
|
onConnect_in,
|
||||||
|
onDisconnect_in) {
|
||||||
|
|
||||||
|
this.onDataIn = onDataIn_in;
|
||||||
|
this.onConnect = onConnect_in;
|
||||||
|
this.onDisconnect = onDisconnect_in;
|
||||||
|
|
||||||
|
// WS Connection State (with defaults)
|
||||||
|
this.serverAddr = serverAddr;
|
||||||
|
this.serverConnectionActive = false;
|
||||||
|
|
||||||
|
//Trigger the websocket to connect automatically
|
||||||
|
this.ws_connect();
|
||||||
|
}
|
||||||
|
|
||||||
|
//////////////////////////////////////////////////////////////
|
||||||
|
// Websocket connection Maintenance
|
||||||
|
|
||||||
|
ws_onOpen() {
|
||||||
|
// Set the flag allowing general server communication
|
||||||
|
this.serverConnectionActive = true;
|
||||||
|
|
||||||
|
console.log("[WebSocket] Connected!");
|
||||||
|
|
||||||
|
// User connection-opened hook
|
||||||
|
this.onConnect();
|
||||||
|
}
|
||||||
|
|
||||||
|
ws_onClose(e) {
|
||||||
|
//Clear flags to stop server communication
|
||||||
|
this.ws = null;
|
||||||
|
this.serverConnectionActive = false;
|
||||||
|
|
||||||
|
// User connection-closed hook
|
||||||
|
this.onDisconnect();
|
||||||
|
|
||||||
|
console.log('[WebSocket] 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) {
|
||||||
|
console.log("[WebSocket] Websocket error - " + e.toString());
|
||||||
|
this.ws.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
ws_onMessage(e) {
|
||||||
|
this.onDataIn(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
ws_connect() {
|
||||||
|
this.ws = new WebSocket(this.serverAddr);
|
||||||
|
this.ws.binaryType = "arraybuffer";
|
||||||
|
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("[WebSocket] Starting...");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default { ReconnectingWebsocket }
|
||||||
359
photon-client/src/plugins/WebsocketVideoStream.js
Normal file
359
photon-client/src/plugins/WebsocketVideoStream.js
Normal file
@@ -0,0 +1,359 @@
|
|||||||
|
// 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}
|
||||||
@@ -1,121 +0,0 @@
|
|||||||
//https://gomakethings.com/getting-the-differences-between-two-objects-with-vanilla-js/
|
|
||||||
export const diff = function (obj1, obj2) {
|
|
||||||
|
|
||||||
// Make sure an object to compare is provided
|
|
||||||
if (!obj2 || Object.prototype.toString.call(obj2) !== '[object Object]') {
|
|
||||||
return obj1;
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// Variables
|
|
||||||
//
|
|
||||||
|
|
||||||
let diffs = {};
|
|
||||||
let key;
|
|
||||||
|
|
||||||
|
|
||||||
//
|
|
||||||
// Methods
|
|
||||||
//
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if two arrays are equal
|
|
||||||
* @param {Array} arr1 The first array
|
|
||||||
* @param {Array} arr2 The second array
|
|
||||||
* @return {Boolean} If true, both arrays are equal
|
|
||||||
*/
|
|
||||||
const arraysMatch = function (arr1, arr2) {
|
|
||||||
|
|
||||||
// Check if the arrays are the same length
|
|
||||||
if (arr1.length !== arr2.length) return false;
|
|
||||||
|
|
||||||
// Check if all items exist and are in the same order
|
|
||||||
for (let i = 0; i < arr1.length; i++) {
|
|
||||||
if (arr1[i] !== arr2[i]) return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Otherwise, return true
|
|
||||||
return true;
|
|
||||||
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Compare two items and push non-matches to object
|
|
||||||
* @param {*} item1 The first item
|
|
||||||
* @param {*} item2 The second item
|
|
||||||
* @param {String} key The key in our object
|
|
||||||
*/
|
|
||||||
const compare = function (item1, item2, key) {
|
|
||||||
|
|
||||||
// Get the object type
|
|
||||||
let type1 = Object.prototype.toString.call(item1);
|
|
||||||
let type2 = Object.prototype.toString.call(item2);
|
|
||||||
|
|
||||||
// If type2 is undefined it has been removed
|
|
||||||
if (type2 === '[object Undefined]') {
|
|
||||||
diffs[key] = null;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// If items are different types
|
|
||||||
if (type1 !== type2) {
|
|
||||||
diffs[key] = item2;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// If an object, compare recursively
|
|
||||||
if (type1 === '[object Object]') {
|
|
||||||
let objDiff = diff(item1, item2);
|
|
||||||
if (Object.keys(objDiff).length > 1) {
|
|
||||||
diffs[key] = objDiff;
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// If an array, compare
|
|
||||||
if (type1 === '[object Array]') {
|
|
||||||
if (!arraysMatch(item1, item2)) {
|
|
||||||
diffs[key] = item2;
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Else if it's a function, convert to a string and compare
|
|
||||||
// Otherwise, just compare
|
|
||||||
if (type1 === '[object Function]') {
|
|
||||||
if (item1.toString() !== item2.toString()) {
|
|
||||||
diffs[key] = item2;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (item1 !== item2) {
|
|
||||||
diffs[key] = item2;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
//
|
|
||||||
// Compare our objects
|
|
||||||
//
|
|
||||||
|
|
||||||
// Loop through the first object
|
|
||||||
for (key in obj1) {
|
|
||||||
if (obj1.hasOwnProperty(key)) {
|
|
||||||
compare(obj1[key], obj2[key], key);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Loop through the second object and find missing items
|
|
||||||
for (key in obj2) {
|
|
||||||
if (obj2.hasOwnProperty(key)) {
|
|
||||||
if (!obj1[key] && obj1[key] !== obj2[key] ) {
|
|
||||||
diffs[key] = obj2[key];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Return the object of differences
|
|
||||||
return diffs;
|
|
||||||
|
|
||||||
};
|
|
||||||
@@ -15,10 +15,13 @@ export default new Vuex.Store({
|
|||||||
},
|
},
|
||||||
state: {
|
state: {
|
||||||
backendConnected: false,
|
backendConnected: false,
|
||||||
|
websocket: null,
|
||||||
ntConnectionInfo: {
|
ntConnectionInfo: {
|
||||||
connected: false,
|
connected: false,
|
||||||
address: "",
|
address: "",
|
||||||
clients: 0,
|
clients: 0,
|
||||||
|
},
|
||||||
|
networkInfo: {
|
||||||
possibleRios: ["Loading..."],
|
possibleRios: ["Loading..."],
|
||||||
deviceips: ["Loading..."],
|
deviceips: ["Loading..."],
|
||||||
},
|
},
|
||||||
@@ -33,8 +36,8 @@ export default new Vuex.Store({
|
|||||||
tiltDegrees: 0.0,
|
tiltDegrees: 0.0,
|
||||||
currentPipelineIndex: 0,
|
currentPipelineIndex: 0,
|
||||||
pipelineNicknames: ["Unknown"],
|
pipelineNicknames: ["Unknown"],
|
||||||
outputStreamPort: 1181,
|
outputStreamPort: 0,
|
||||||
inputStreamPort: 1182,
|
inputStreamPort: 0,
|
||||||
nickname: "Unknown",
|
nickname: "Unknown",
|
||||||
videoFormatList: [
|
videoFormatList: [
|
||||||
{
|
{
|
||||||
@@ -49,13 +52,15 @@ export default new Vuex.Store({
|
|||||||
isFovConfigurable: true,
|
isFovConfigurable: true,
|
||||||
calibrated: false,
|
calibrated: false,
|
||||||
currentPipelineSettings: {
|
currentPipelineSettings: {
|
||||||
pipelineType: 2, // One of "calib", "driver", "reflective", "shape"
|
pipelineType: 5, // One of "calib", "driver", "reflective", "shape", "AprilTag"
|
||||||
// 2 is reflective
|
// 2 is reflective
|
||||||
|
|
||||||
// Settings that apply to all pipeline types
|
// Settings that apply to all pipeline types
|
||||||
cameraExposure: 1,
|
cameraExposure: 1,
|
||||||
cameraBrightness: 2,
|
cameraBrightness: 2,
|
||||||
cameraGain: 3,
|
cameraAutoExposure: false,
|
||||||
|
cameraRedGain: 3,
|
||||||
|
cameraBlueGain: 4,
|
||||||
inputImageRotationMode: 0,
|
inputImageRotationMode: 0,
|
||||||
cameraVideoModeIndex: 0,
|
cameraVideoModeIndex: 0,
|
||||||
streamingFrameDivisor: 0,
|
streamingFrameDivisor: 0,
|
||||||
@@ -64,10 +69,13 @@ export default new Vuex.Store({
|
|||||||
hsvHue: [0, 15],
|
hsvHue: [0, 15],
|
||||||
hsvSaturation: [0, 15],
|
hsvSaturation: [0, 15],
|
||||||
hsvValue: [0, 25],
|
hsvValue: [0, 25],
|
||||||
|
hueInverted: false,
|
||||||
contourArea: [0, 12],
|
contourArea: [0, 12],
|
||||||
contourRatio: [0, 12],
|
contourRatio: [0, 12],
|
||||||
contourFullness: [0, 12],
|
contourFullness: [0, 12],
|
||||||
contourSpecklePercentage: 5,
|
contourSpecklePercentage: 5,
|
||||||
|
contourFilterRangeX: 5,
|
||||||
|
contourFilterRangeY: 5,
|
||||||
contourGroupingMode: 0,
|
contourGroupingMode: 0,
|
||||||
contourIntersection: 0,
|
contourIntersection: 0,
|
||||||
contourSortMode: 0,
|
contourSortMode: 0,
|
||||||
@@ -82,7 +90,16 @@ export default new Vuex.Store({
|
|||||||
|
|
||||||
cornerDetectionAccuracyPercentage: 10,
|
cornerDetectionAccuracyPercentage: 10,
|
||||||
|
|
||||||
// Settings that apply to shape
|
// Settings that apply to AprilTag
|
||||||
|
tagFamily: 1,
|
||||||
|
decimate: 1.0,
|
||||||
|
blur: 0.0,
|
||||||
|
threads: 1,
|
||||||
|
debug: false,
|
||||||
|
refineEdges: true,
|
||||||
|
numIterations: 1,
|
||||||
|
decisionMargin: 0,
|
||||||
|
hammingDist: 0,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
@@ -96,9 +113,18 @@ export default new Vuex.Store({
|
|||||||
skew: 0,
|
skew: 0,
|
||||||
area: 0,
|
area: 0,
|
||||||
// 3D only
|
// 3D only
|
||||||
pose: {x: 0, y: 0, rot: 0},
|
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},
|
||||||
|
}]
|
||||||
|
},
|
||||||
settings: {
|
settings: {
|
||||||
general: {
|
general: {
|
||||||
version: "Unknown",
|
version: "Unknown",
|
||||||
@@ -144,6 +170,7 @@ export default new Vuex.Store({
|
|||||||
},
|
},
|
||||||
mutations: {
|
mutations: {
|
||||||
compactMode: set('compactMode'),
|
compactMode: set('compactMode'),
|
||||||
|
websocket: set('websocket'),
|
||||||
cameraSettings: set('cameraSettings'),
|
cameraSettings: set('cameraSettings'),
|
||||||
currentCameraIndex: set('currentCameraIndex'),
|
currentCameraIndex: set('currentCameraIndex'),
|
||||||
selectedOutputs: set('selectedOutputs'),
|
selectedOutputs: set('selectedOutputs'),
|
||||||
@@ -151,6 +178,7 @@ export default new Vuex.Store({
|
|||||||
calibrationData: set('calibrationData'),
|
calibrationData: set('calibrationData'),
|
||||||
metrics: set('metrics'),
|
metrics: set('metrics'),
|
||||||
ntConnectionInfo: set('ntConnectionInfo'),
|
ntConnectionInfo: set('ntConnectionInfo'),
|
||||||
|
networkInfo: set('networkInfo'),
|
||||||
backendConnected: set('backendConnected'),
|
backendConnected: set('backendConnected'),
|
||||||
logString: (state, newStr) => {
|
logString: (state, newStr) => {
|
||||||
const str = state.logMessages;
|
const str = state.logMessages;
|
||||||
|
|||||||
@@ -20,8 +20,8 @@
|
|||||||
v-model="currentCameraIndex"
|
v-model="currentCameraIndex"
|
||||||
name="Camera"
|
name="Camera"
|
||||||
:list="$store.getters.cameraList"
|
:list="$store.getters.cameraList"
|
||||||
@input="handleInput('currentCamera',currentCameraIndex)"
|
|
||||||
:select-cols="$vuetify.breakpoint.mdAndUp ? 10 : 7"
|
:select-cols="$vuetify.breakpoint.mdAndUp ? 10 : 7"
|
||||||
|
@input="handleInput('currentCamera',currentCameraIndex)"
|
||||||
/>
|
/>
|
||||||
<CVnumberinput
|
<CVnumberinput
|
||||||
v-model="cameraSettings.fov"
|
v-model="cameraSettings.fov"
|
||||||
@@ -31,14 +31,6 @@
|
|||||||
:label-cols="$vuetify.breakpoint.mdAndUp ? undefined : 7"
|
:label-cols="$vuetify.breakpoint.mdAndUp ? undefined : 7"
|
||||||
/>
|
/>
|
||||||
<br>
|
<br>
|
||||||
<CVnumberinput
|
|
||||||
v-model="cameraSettings.tiltDegrees"
|
|
||||||
name="Camera pitch"
|
|
||||||
tooltip="How many degrees above the horizontal the physical camera is tilted"
|
|
||||||
:step="0.01"
|
|
||||||
:label-cols="$vuetify.breakpoint.mdAndUp ? undefined : 7"
|
|
||||||
/>
|
|
||||||
<br>
|
|
||||||
<v-btn
|
<v-btn
|
||||||
style="margin-top:10px"
|
style="margin-top:10px"
|
||||||
small
|
small
|
||||||
@@ -80,6 +72,14 @@
|
|||||||
:disabled="isCalibrating"
|
:disabled="isCalibrating"
|
||||||
tooltip="Resolution to calibrate at (you will have to calibrate every resolution you use 3D mode on)"
|
tooltip="Resolution to calibrate at (you will have to calibrate every resolution you use 3D mode on)"
|
||||||
/>
|
/>
|
||||||
|
<CVselect
|
||||||
|
v-model="streamingFrameDivisor"
|
||||||
|
name="Decimation"
|
||||||
|
tooltip="Resolution to which camera frames are downscaled for detection. Calibration still uses full-res"
|
||||||
|
:list="calibrationDivisors"
|
||||||
|
select-cols="7"
|
||||||
|
@rollback="e => rollback('streamingFrameDivisor', e)"
|
||||||
|
/>
|
||||||
<CVselect
|
<CVselect
|
||||||
v-model="boardType"
|
v-model="boardType"
|
||||||
name="Board Type"
|
name="Board Type"
|
||||||
@@ -146,6 +146,24 @@
|
|||||||
text="Standard Deviation"
|
text="Standard Deviation"
|
||||||
/>
|
/>
|
||||||
</th>
|
</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>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
@@ -158,6 +176,9 @@
|
|||||||
{{ isCalibrated(value) ? value.mean.toFixed(2) + "px" : "—" }}
|
{{ isCalibrated(value) ? value.mean.toFixed(2) + "px" : "—" }}
|
||||||
</td>
|
</td>
|
||||||
<td> {{ isCalibrated(value) ? value.standardDeviation.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>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</v-simple-table>
|
</v-simple-table>
|
||||||
@@ -181,10 +202,13 @@
|
|||||||
>
|
>
|
||||||
<CVslider
|
<CVslider
|
||||||
v-model="$store.getters.currentPipelineSettings.cameraExposure"
|
v-model="$store.getters.currentPipelineSettings.cameraExposure"
|
||||||
|
:disabled="$store.getters.currentPipelineSettings.cameraAutoExposure"
|
||||||
name="Exposure"
|
name="Exposure"
|
||||||
:min="0"
|
:min="0"
|
||||||
:max="100"
|
:max="100"
|
||||||
slider-cols="8"
|
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)"
|
@input="e => handlePipelineUpdate('cameraExposure', e)"
|
||||||
/>
|
/>
|
||||||
<CVslider
|
<CVslider
|
||||||
@@ -195,14 +219,43 @@
|
|||||||
slider-cols="8"
|
slider-cols="8"
|
||||||
@input="e => handlePipelineUpdate('cameraBrightness', e)"
|
@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
|
<CVslider
|
||||||
v-if="$store.getters.currentPipelineSettings.cameraGain !== -1"
|
v-if="cameraGain >= 0"
|
||||||
v-model="$store.getters.currentPipelineSettings.cameraGain"
|
v-model="cameraGain"
|
||||||
name="Gain"
|
name="Camera Gain"
|
||||||
:min="0"
|
min="0"
|
||||||
:max="100"
|
max="100"
|
||||||
slider-cols="8"
|
tooltip="Controls camera gain, similar to brightness"
|
||||||
@input="e => handlePipelineUpdate('cameraGain', e)"
|
: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-col>
|
</v-col>
|
||||||
</v-row>
|
</v-row>
|
||||||
@@ -246,6 +299,19 @@
|
|||||||
Download Target
|
Download Target
|
||||||
</v-btn>
|
</v-btn>
|
||||||
</v-col>
|
</v-col>
|
||||||
|
<v-col>
|
||||||
|
<v-btn
|
||||||
|
color="secondary"
|
||||||
|
small
|
||||||
|
style="width: 100%;"
|
||||||
|
@click="$refs.importCalibrationFromCalibdb.click()"
|
||||||
|
>
|
||||||
|
<v-icon left>
|
||||||
|
mdi-upload
|
||||||
|
</v-icon>
|
||||||
|
Import From CalibDB
|
||||||
|
</v-btn>
|
||||||
|
</v-col>
|
||||||
</v-row>
|
</v-row>
|
||||||
</div>
|
</div>
|
||||||
</v-card>
|
</v-card>
|
||||||
@@ -257,7 +323,8 @@
|
|||||||
>
|
>
|
||||||
<template>
|
<template>
|
||||||
<CVimage
|
<CVimage
|
||||||
:address="$store.getters.streamAddress[1]"
|
:id="cameras-cal"
|
||||||
|
:idx=1
|
||||||
:disconnected="!$store.state.backendConnected"
|
:disconnected="!$store.state.backendConnected"
|
||||||
scale="100"
|
scale="100"
|
||||||
style="border-radius: 5px;"
|
style="border-radius: 5px;"
|
||||||
@@ -321,6 +388,20 @@
|
|||||||
</template>
|
</template>
|
||||||
</v-col>
|
</v-col>
|
||||||
</v-row>
|
</v-row>
|
||||||
|
|
||||||
|
<!-- Special hidden upload input that gets 'clicked' when the user imports settings -->
|
||||||
|
<input
|
||||||
|
ref="importCalibrationFromCalibdb"
|
||||||
|
type="file"
|
||||||
|
accept=".json"
|
||||||
|
style="display: none;"
|
||||||
|
@change="readImportedCalibration"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<v-snackbar v-model="uploadSnack" top :color="uploadSnackData.color" timeout="-1">
|
||||||
|
<span>{{ uploadSnackData.text }}</span>
|
||||||
|
</v-snackbar>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -328,6 +409,7 @@
|
|||||||
import CVselect from '../components/common/cv-select';
|
import CVselect from '../components/common/cv-select';
|
||||||
import CVnumberinput from '../components/common/cv-number-input';
|
import CVnumberinput from '../components/common/cv-number-input';
|
||||||
import CVslider from '../components/common/cv-slider';
|
import CVslider from '../components/common/cv-slider';
|
||||||
|
import CVswitch from '../components/common/cv-switch';
|
||||||
import CVimage from "../components/common/cv-image";
|
import CVimage from "../components/common/cv-image";
|
||||||
import TooltippedLabel from "../components/common/cv-tooltipped-label";
|
import TooltippedLabel from "../components/common/cv-tooltipped-label";
|
||||||
import jsPDF from "jspdf";
|
import jsPDF from "jspdf";
|
||||||
@@ -340,6 +422,7 @@ export default {
|
|||||||
CVselect,
|
CVselect,
|
||||||
CVnumberinput,
|
CVnumberinput,
|
||||||
CVslider,
|
CVslider,
|
||||||
|
CVswitch,
|
||||||
CVimage
|
CVimage
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
@@ -349,6 +432,12 @@ export default {
|
|||||||
calibrationFailed: false,
|
calibrationFailed: false,
|
||||||
filteredVideomodeIndex: 0,
|
filteredVideomodeIndex: 0,
|
||||||
settingsValid: true,
|
settingsValid: true,
|
||||||
|
unfilteredStreamDivisors: [1, 2, 4],
|
||||||
|
uploadSnackData: {
|
||||||
|
color: "success",
|
||||||
|
text: "",
|
||||||
|
},
|
||||||
|
uploadSnack: false,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
@@ -373,6 +462,31 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
cameraGain: {
|
||||||
|
get() {
|
||||||
|
return parseInt(this.$store.getters.currentPipelineSettings.cameraGain)
|
||||||
|
},
|
||||||
|
set(val) {
|
||||||
|
this.$store.commit("mutatePipeline", {"cameraGain": parseInt(val)});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
calibrationDivisors: {
|
||||||
|
get() {
|
||||||
|
return this.unfilteredStreamDivisors.filter(item => {
|
||||||
|
var res = this.stringResolutionList[this.selectedFilteredResIndex].split(" X ").map(it => parseInt(it));
|
||||||
|
console.log(res);
|
||||||
|
console.log(item);
|
||||||
|
// Realistically, we need more than 320x240, but lower than this is
|
||||||
|
// basically unusable. For now, don't allow decimations that take us
|
||||||
|
// below that
|
||||||
|
const ret = ((res[0] / item) >= 300 && (res[1] / item) >= 220) || (item === 1);
|
||||||
|
console.log(ret);
|
||||||
|
return ret;
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
// Makes sure there's only one entry per resolution
|
// Makes sure there's only one entry per resolution
|
||||||
filteredResolutionList: {
|
filteredResolutionList: {
|
||||||
get() {
|
get() {
|
||||||
@@ -385,6 +499,9 @@ export default {
|
|||||||
if (calib != null) {
|
if (calib != null) {
|
||||||
it['standardDeviation'] = calib.standardDeviation;
|
it['standardDeviation'] = calib.standardDeviation;
|
||||||
it['mean'] = calib.perViewErrors.reduce((a, b) => a + b) / calib.perViewErrors.length;
|
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);
|
filtered.push(it);
|
||||||
}
|
}
|
||||||
@@ -393,13 +510,11 @@ export default {
|
|||||||
return filtered
|
return filtered
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
stringResolutionList: {
|
stringResolutionList: {
|
||||||
get() {
|
get() {
|
||||||
return this.filteredResolutionList.map(res => `${res['width']} X ${res['height']}`);
|
return this.filteredResolutionList.map(res => `${res['width']} X ${res['height']}`);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
cameraSettings: {
|
cameraSettings: {
|
||||||
get() {
|
get() {
|
||||||
return this.$store.getters.currentCameraSettings;
|
return this.$store.getters.currentCameraSettings;
|
||||||
@@ -409,6 +524,16 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
streamingFrameDivisor: {
|
||||||
|
get() {
|
||||||
|
return this.$store.getters.currentPipelineSettings.streamingFrameDivisor;
|
||||||
|
},
|
||||||
|
set(val) {
|
||||||
|
this.$store.commit("mutatePipeline", {"streamingFrameDivisor": val});
|
||||||
|
this.handlePipelineUpdate("streamingFrameDivisor", val);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
boardType: {
|
boardType: {
|
||||||
get() {
|
get() {
|
||||||
return this.calibrationData.boardType
|
return this.calibrationData.boardType
|
||||||
@@ -478,6 +603,57 @@ export default {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
|
||||||
|
readImportedCalibration(event) {
|
||||||
|
// let formData = new FormData();
|
||||||
|
// formData.append("zipData", event.target.files[0]);
|
||||||
|
const filename = event.target.files[0].name;
|
||||||
|
|
||||||
|
event.target.files[0].text().then(fileText => {
|
||||||
|
const data = {
|
||||||
|
"cameraIndex": this.$store.getters.currentCameraIndex,
|
||||||
|
"payload": fileText,
|
||||||
|
"filename": filename,
|
||||||
|
};
|
||||||
|
|
||||||
|
this.axios
|
||||||
|
.post("http://" + this.$address + "/api/calibration/import", data, {
|
||||||
|
headers: { "Content-Type": "text/plain" },
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
this.uploadSnackData = {
|
||||||
|
color: "success",
|
||||||
|
text:
|
||||||
|
"Calibration imported successfully! PhotonVision will restart in the background...",
|
||||||
|
};
|
||||||
|
this.uploadSnack = true;
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
if (err.response) {
|
||||||
|
this.uploadSnackData = {
|
||||||
|
color: "error",
|
||||||
|
text:
|
||||||
|
"Error while uploading calibration file! Could not process provided file.",
|
||||||
|
};
|
||||||
|
} else if (err.request) {
|
||||||
|
this.uploadSnackData = {
|
||||||
|
color: "error",
|
||||||
|
text:
|
||||||
|
"Error while uploading calibration file! No respond to upload attempt.",
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
this.uploadSnackData = {
|
||||||
|
color: "error",
|
||||||
|
text: "Error while uploading calibration file!",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
this.uploadSnack = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
})
|
||||||
|
|
||||||
|
},
|
||||||
|
|
||||||
closeDialog() {
|
closeDialog() {
|
||||||
this.snack = false;
|
this.snack = false;
|
||||||
this.calibrationInProgress = false;
|
this.calibrationInProgress = false;
|
||||||
@@ -590,8 +766,7 @@ export default {
|
|||||||
this.axios.post("http://" + this.$address + "/api/settings/camera", {
|
this.axios.post("http://" + this.$address + "/api/settings/camera", {
|
||||||
"settings": this.cameraSettings,
|
"settings": this.cameraSettings,
|
||||||
"index": this.$store.state.currentCameraIndex
|
"index": this.$store.state.currentCameraIndex
|
||||||
}).then(
|
}).then(response => {
|
||||||
function (response) {
|
|
||||||
if (response.status === 200) {
|
if (response.status === 200) {
|
||||||
this.$store.state.saveBar = true;
|
this.$store.state.saveBar = true;
|
||||||
}
|
}
|
||||||
@@ -612,14 +787,15 @@ export default {
|
|||||||
if (this.isCalibrating === true) {
|
if (this.isCalibrating === true) {
|
||||||
data['takeCalibrationSnapshot'] = true
|
data['takeCalibrationSnapshot'] = true
|
||||||
} else {
|
} 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;
|
const calData = this.calibrationData;
|
||||||
calData.isCalibrating = true;
|
calData.isCalibrating = true;
|
||||||
data['startPnpCalibration'] = calData;
|
data['startPnpCalibration'] = calData;
|
||||||
|
|
||||||
console.log("starting calibration with index " + calData.videoModeIndex);
|
console.log("starting calibration with index " + calData.videoModeIndex);
|
||||||
}
|
}
|
||||||
|
this.$store.commit('currentPipelineIndex', -2);
|
||||||
this.$socket.send(this.$msgPack.encode(data));
|
this.$store.state.websocket.ws.send(this.$msgPack.encode(data));
|
||||||
},
|
},
|
||||||
sendCalibrationFinish() {
|
sendCalibrationFinish() {
|
||||||
console.log("finishing calibration for index " + this.$store.getters.currentCameraIndex);
|
console.log("finishing calibration for index " + this.$store.getters.currentCameraIndex);
|
||||||
|
|||||||
@@ -12,12 +12,21 @@
|
|||||||
color="secondary"
|
color="secondary"
|
||||||
style="margin-left: auto;"
|
style="margin-left: auto;"
|
||||||
depressed
|
depressed
|
||||||
@click="download('photonlog.log', rawLogs.map(it => it.message).join('\n'))"
|
@click="$refs.exportLogFile.click()"
|
||||||
>
|
>
|
||||||
<v-icon left>
|
<v-icon left>
|
||||||
mdi-download
|
mdi-download
|
||||||
</v-icon>
|
</v-icon>
|
||||||
Download Log
|
Download Log
|
||||||
|
|
||||||
|
<!-- Special hidden link that gets 'clicked' when the user exports journalctl logs -->
|
||||||
|
<a
|
||||||
|
ref="exportLogFile"
|
||||||
|
style="color: black; text-decoration: none; display: none"
|
||||||
|
:href="'http://' + this.$address + '/api/settings/photonvision-journalctl.txt'"
|
||||||
|
download="photonvision-journalctl.txt"
|
||||||
|
/>
|
||||||
|
|
||||||
</v-btn>
|
</v-btn>
|
||||||
</v-card-title>
|
</v-card-title>
|
||||||
<div class="pr-6 pl-6">
|
<div class="pr-6 pl-6">
|
||||||
|
|||||||
@@ -1,81 +1,75 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<v-container
|
<v-container
|
||||||
class="pa-3"
|
class="pa-3"
|
||||||
fluid
|
fluid
|
||||||
>
|
>
|
||||||
<v-row
|
<v-row
|
||||||
no-gutters
|
no-gutters
|
||||||
align="center"
|
align="center"
|
||||||
justify="center"
|
justify="center"
|
||||||
>
|
>
|
||||||
<v-col
|
<v-col
|
||||||
cols="12"
|
cols="12"
|
||||||
:class="['pb-3 ', 'pr-lg-3']"
|
:class="['pb-3 ', 'pr-lg-3']"
|
||||||
lg="8"
|
lg="8"
|
||||||
align-self="stretch"
|
align-self="stretch"
|
||||||
>
|
>
|
||||||
<v-card
|
<v-card
|
||||||
color="primary"
|
color="primary"
|
||||||
height="100%"
|
height="100%"
|
||||||
style="display: flex; flex-direction: column"
|
style="display: flex; flex-direction: column"
|
||||||
dark
|
dark
|
||||||
>
|
>
|
||||||
<v-card-title
|
<v-card-title
|
||||||
class="pb-0 mb-0 pl-4 pt-1"
|
class="pb-0 mb-0 pl-4 pt-1"
|
||||||
style="height: 15%; min-height: 50px;"
|
style="height: 15%; min-height: 50px;"
|
||||||
>
|
>
|
||||||
Cameras
|
Cameras
|
||||||
<v-chip
|
<v-chip
|
||||||
:class="fpsTooLow ? 'ml-2 mt-1' : 'mt-2'"
|
:class="fpsTooLow ? 'ml-2 mt-1' : 'mt-2'"
|
||||||
x-small
|
x-small
|
||||||
label
|
label
|
||||||
:color="fpsTooLow ? 'red' : 'transparent'"
|
:color="fpsTooLow ? 'error' : 'transparent'"
|
||||||
:text-color="fpsTooLow ? 'white' : 'grey'"
|
:text-color="fpsTooLow ? 'white' : 'grey'"
|
||||||
>
|
>
|
||||||
<span class="pr-1">{{ Math.round($store.state.pipelineResults.fps) }} FPS –</span>
|
<span class="pr-1">Processing @ {{ Math.round($store.state.pipelineResults.fps) }} FPS –</span>
|
||||||
<span v-if="!fpsTooLow">{{ Math.min(Math.round($store.state.pipelineResults.latency), 100) }} ms latency</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="!$store.getters.currentPipelineSettings.inputShouldShow">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>stop viewing the color stream for better performance</span>
|
<span v-else>{{ Math.min(Math.round($store.state.pipelineResults.latency), 9999) }} ms latency</span>
|
||||||
</v-chip>
|
|
||||||
<v-chip small label color="red" text-color="white" v-if="!$store.state.ntConnectionInfo.connected || $store.state.settings.networkSettings.runNTServer">
|
|
||||||
<span>
|
|
||||||
{{ $store.state.settings.networkSettings.runNTServer ?
|
|
||||||
"NetworkTables Server Enabled! Photonlib may not work" :
|
|
||||||
"NetworkTables not connected!" }}
|
|
||||||
</span>
|
|
||||||
</v-chip>
|
</v-chip>
|
||||||
<v-switch
|
<v-switch
|
||||||
v-model="driverMode"
|
v-model="driverMode"
|
||||||
label="Driver Mode"
|
label="Driver Mode"
|
||||||
style="margin-left: auto;"
|
style="margin-left: auto;"
|
||||||
color="accent"
|
color="accent"
|
||||||
/>
|
/>
|
||||||
</v-card-title>
|
</v-card-title>
|
||||||
<v-row
|
<v-row
|
||||||
align="center"
|
align="center"
|
||||||
>
|
>
|
||||||
<v-col
|
<v-col
|
||||||
v-for="idx in (selectedOutputs instanceof Array ? selectedOutputs : [selectedOutputs])"
|
v-for="idx in (selectedOutputs instanceof Array ? selectedOutputs : [selectedOutputs])"
|
||||||
:key="idx"
|
:key="idx"
|
||||||
cols="12"
|
cols="12"
|
||||||
:md="selectedOutputs.length === 1 ? 12 : Math.floor(12 / selectedOutputs.length)"
|
:md="selectedOutputs.length === 1 ? 12 : Math.floor(12 / selectedOutputs.length)"
|
||||||
class="pb-0 pt-0"
|
class="pb-0 pt-0"
|
||||||
style="height: 100%;"
|
style="height: 100%;"
|
||||||
>
|
>
|
||||||
<div style="position: relative; width: 100%; height: 100%;">
|
<div style="position: relative; width: 100%; height: 100%;">
|
||||||
<cv-image
|
<cv-image
|
||||||
:id="idx === 0 ? 'normal-stream' : ''"
|
:id="idx === 0 ? 'raw-stream' : 'processed-stream'"
|
||||||
ref="streams"
|
ref="streams"
|
||||||
:address="$store.getters.streamAddress[idx]"
|
:idx=idx
|
||||||
:disconnected="!$store.state.backendConnected"
|
:disconnected="!$store.state.backendConnected"
|
||||||
scale="100"
|
scale="100"
|
||||||
:max-height="$store.getters.isDriverMode ? '40vh' : '300px'"
|
:max-height="$store.getters.isDriverMode ? '40vh' : '300px'"
|
||||||
:max-height-md="$store.getters.isDriverMode ? '50vh' : '320px'"
|
:max-height-md="$store.getters.isDriverMode ? '50vh' : '380px'"
|
||||||
:max-height-xl="$store.getters.isDriverMode ? '60vh' : '450px'"
|
:max-height-lg="$store.getters.isDriverMode ? '55vh' : '390px'"
|
||||||
:alt="'Stream' + idx"
|
:max-height-xl="$store.getters.isDriverMode ? '60vh' : '450px'"
|
||||||
:color-picking="$store.state.colorPicking && idx === 0"
|
:alt="idx === 0 ? 'Raw stream' : 'Processed stream'"
|
||||||
@click="onImageClick"
|
:color-picking="$store.state.colorPicking && idx === 0"
|
||||||
|
@click="onImageClick"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</v-col>
|
</v-col>
|
||||||
@@ -83,49 +77,44 @@
|
|||||||
</v-card>
|
</v-card>
|
||||||
</v-col>
|
</v-col>
|
||||||
<v-col
|
<v-col
|
||||||
cols="12"
|
cols="12"
|
||||||
class="pb-3"
|
class="pb-3"
|
||||||
lg="4"
|
lg="4"
|
||||||
align-self="stretch"
|
align-self="stretch"
|
||||||
>
|
>
|
||||||
<v-card
|
<v-card
|
||||||
color="primary"
|
color="primary"
|
||||||
class="mt-3"
|
|
||||||
>
|
>
|
||||||
<!-- <v-btn @click="onCamNameChange">-->
|
<camera-and-pipeline-select />
|
||||||
<!-- Reload-->
|
|
||||||
<!-- </v-btn>-->
|
|
||||||
<camera-and-pipeline-select @camera-name-changed="reloadStreams" />
|
|
||||||
</v-card>
|
</v-card>
|
||||||
<v-card
|
<v-card
|
||||||
:disabled="$store.getters.isDriverMode || $store.state.colorPicking"
|
:disabled="$store.getters.isDriverMode || $store.state.colorPicking"
|
||||||
class="mt-6 mb-3"
|
class="mt-3"
|
||||||
color="primary"
|
color="primary"
|
||||||
>
|
>
|
||||||
<v-row
|
<v-row
|
||||||
align="center"
|
align="center"
|
||||||
class="pl-3 pr-3"
|
class="pl-3 pr-3"
|
||||||
>
|
>
|
||||||
<!-- -->
|
|
||||||
<v-col lg="12">
|
<v-col lg="12">
|
||||||
<p style="color: white;">
|
<p style="color: white;">
|
||||||
Processing mode:
|
Processing mode:
|
||||||
</p>
|
</p>
|
||||||
<v-btn-toggle
|
<v-btn-toggle
|
||||||
v-model="processingMode"
|
v-model="processingMode"
|
||||||
mandatory
|
mandatory
|
||||||
dark
|
dark
|
||||||
class="fill"
|
class="fill"
|
||||||
>
|
>
|
||||||
<v-btn
|
<v-btn
|
||||||
color="secondary"
|
color="secondary"
|
||||||
>
|
>
|
||||||
<v-icon>mdi-crop-square</v-icon>
|
<v-icon>mdi-crop-square</v-icon>
|
||||||
<span>2D</span>
|
<span>2D</span>
|
||||||
</v-btn>
|
</v-btn>
|
||||||
<v-btn
|
<v-btn
|
||||||
color="secondary"
|
color="secondary"
|
||||||
@click="on3DClick"
|
@click="on3DClick"
|
||||||
>
|
>
|
||||||
<v-icon>mdi-cube-outline</v-icon>
|
<v-icon>mdi-cube-outline</v-icon>
|
||||||
<span>3D</span>
|
<span>3D</span>
|
||||||
@@ -137,25 +126,25 @@
|
|||||||
Stream display:
|
Stream display:
|
||||||
</p>
|
</p>
|
||||||
<v-btn-toggle
|
<v-btn-toggle
|
||||||
v-model="selectedOutputs"
|
v-model="selectedOutputs"
|
||||||
:multiple="$vuetify.breakpoint.mdAndUp"
|
:multiple="$vuetify.breakpoint.mdAndUp"
|
||||||
mandatory
|
mandatory
|
||||||
dark
|
dark
|
||||||
class="fill"
|
class="fill"
|
||||||
>
|
>
|
||||||
<v-btn
|
<v-btn
|
||||||
color="secondary"
|
color="secondary"
|
||||||
class="fill"
|
class="fill"
|
||||||
>
|
>
|
||||||
<v-icon>mdi-palette</v-icon>
|
<v-icon>mdi-import</v-icon>
|
||||||
<span>Normal</span>
|
<span>Raw</span>
|
||||||
</v-btn>
|
</v-btn>
|
||||||
<v-btn
|
<v-btn
|
||||||
color="secondary"
|
color="secondary"
|
||||||
class="fill"
|
class="fill"
|
||||||
>
|
>
|
||||||
<v-icon>mdi-compare</v-icon>
|
<v-icon>mdi-export</v-icon>
|
||||||
<span>Threshold</span>
|
<span>Processed</span>
|
||||||
</v-btn>
|
</v-btn>
|
||||||
</v-btn-toggle>
|
</v-btn-toggle>
|
||||||
</v-col>
|
</v-col>
|
||||||
@@ -165,29 +154,29 @@
|
|||||||
</v-row>
|
</v-row>
|
||||||
<v-row no-gutters>
|
<v-row no-gutters>
|
||||||
<v-col
|
<v-col
|
||||||
v-for="(tabs, idx) in tabGroups"
|
v-for="(tabs, idx) in tabGroups"
|
||||||
:key="idx"
|
:key="idx"
|
||||||
:cols="Math.floor(12 / tabGroups.length)"
|
:cols="Math.floor(12 / tabGroups.length)"
|
||||||
:class="idx !== tabGroups.length - 1 ? 'pr-3' : ''"
|
:class="idx !== tabGroups.length - 1 ? 'pr-3' : ''"
|
||||||
align-self="stretch"
|
align-self="stretch"
|
||||||
>
|
>
|
||||||
<v-card
|
<v-card
|
||||||
color="primary"
|
color="primary"
|
||||||
height="100%"
|
height="100%"
|
||||||
class="pr-4 pl-4"
|
class="pr-4 pl-4"
|
||||||
>
|
>
|
||||||
<v-tabs
|
<v-tabs
|
||||||
v-if="!$store.getters.isDriverMode"
|
v-if="!$store.getters.isDriverMode"
|
||||||
v-model="selectedTabs[idx]"
|
v-model="selectedTabs[idx]"
|
||||||
grow
|
grow
|
||||||
background-color="primary"
|
background-color="primary"
|
||||||
dark
|
dark
|
||||||
height="48"
|
height="48"
|
||||||
slider-color="accent"
|
slider-color="accent"
|
||||||
>
|
>
|
||||||
<v-tab
|
<v-tab
|
||||||
v-for="(tab, i) in tabs.filter(it => it.name !== '3D' || $store.getters.currentPipelineSettings.solvePNPEnabled)"
|
v-for="(tab, i) in tabs"
|
||||||
:key="i"
|
:key="i"
|
||||||
>
|
>
|
||||||
{{ tab.name }}
|
{{ tab.name }}
|
||||||
</v-tab>
|
</v-tab>
|
||||||
@@ -195,10 +184,10 @@
|
|||||||
<div class="pl-4 pr-4 pt-2">
|
<div class="pl-4 pr-4 pt-2">
|
||||||
<keep-alive>
|
<keep-alive>
|
||||||
<component
|
<component
|
||||||
:is="(tabs[selectedTabs[idx]] || tabs[0]).component"
|
:is="(tabs[selectedTabs[idx]] || tabs[0]).component"
|
||||||
:ref="(tabs[selectedTabs[idx]] || tabs[0]).name"
|
:ref="(tabs[selectedTabs[idx]] || tabs[0]).name"
|
||||||
v-model="$store.getters.pipeline"
|
v-model="$store.getters.pipeline"
|
||||||
@update="$emit('save')"
|
@update="$emit('save')"
|
||||||
/>
|
/>
|
||||||
</keep-alive>
|
</keep-alive>
|
||||||
</div>
|
</div>
|
||||||
@@ -206,30 +195,33 @@
|
|||||||
</v-col>
|
</v-col>
|
||||||
</v-row>
|
</v-row>
|
||||||
</v-container>
|
</v-container>
|
||||||
<!-- snack bar and modal -->
|
|
||||||
<v-snackbar
|
<v-snackbar
|
||||||
v-model="snackbar"
|
v-model="showNTWarning"
|
||||||
:timeout="3000"
|
color="error"
|
||||||
top
|
timeout="-1"
|
||||||
color="error"
|
top
|
||||||
>
|
>
|
||||||
<span style="color:#000">Can not remove the only pipeline!</span>
|
{{ $store.state.settings.networkSettings.runNTServer ?
|
||||||
<v-btn
|
"NetworkTables server enabled! PhotonLib may not work." :
|
||||||
color="black"
|
"NetworkTables not connected! Are you on a network with a robot?" }}
|
||||||
text
|
<template v-slot:action>
|
||||||
@click="snackbar = false"
|
<v-btn
|
||||||
>
|
text
|
||||||
Close
|
@click="hideNTWarning = true"
|
||||||
</v-btn>
|
>
|
||||||
|
Hide
|
||||||
|
</v-btn>
|
||||||
|
</template>
|
||||||
</v-snackbar>
|
</v-snackbar>
|
||||||
|
|
||||||
<v-dialog
|
<v-dialog
|
||||||
v-model="dialog"
|
v-model="dialog"
|
||||||
width="500"
|
width="500"
|
||||||
>
|
>
|
||||||
<v-card
|
<v-card
|
||||||
color="primary"
|
color="primary"
|
||||||
dark
|
dark
|
||||||
>
|
>
|
||||||
<v-card-title>
|
<v-card-title>
|
||||||
Current resolution not calibrated
|
Current resolution not calibrated
|
||||||
@@ -238,9 +230,9 @@
|
|||||||
<v-card-text>
|
<v-card-text>
|
||||||
Because the current resolution {{ this.$store.getters.currentVideoFormat.width }} x {{ this.$store.getters.currentVideoFormat.height }} is not yet calibrated, 3D mode cannot be enabled. Please
|
Because the current resolution {{ this.$store.getters.currentVideoFormat.width }} x {{ this.$store.getters.currentVideoFormat.height }} is not yet calibrated, 3D mode cannot be enabled. Please
|
||||||
<a
|
<a
|
||||||
href="/#/cameras"
|
href="/#/cameras"
|
||||||
class="white--text"
|
class="white--text"
|
||||||
@click="$emit('switch-to-cameras')"
|
@click="$emit('switch-to-cameras')"
|
||||||
> visit the Cameras tab</a> to calibrate this resolution. For now, SolvePNP will do nothing.
|
> visit the Cameras tab</a> to calibrate this resolution. For now, SolvePNP will do nothing.
|
||||||
</v-card-text>
|
</v-card-text>
|
||||||
|
|
||||||
@@ -249,9 +241,9 @@
|
|||||||
<v-card-actions>
|
<v-card-actions>
|
||||||
<v-spacer />
|
<v-spacer />
|
||||||
<v-btn
|
<v-btn
|
||||||
color="white"
|
color="white"
|
||||||
text
|
text
|
||||||
@click="closeUncalibratedDialog"
|
@click="closeUncalibratedDialog"
|
||||||
>
|
>
|
||||||
OK
|
OK
|
||||||
</v-btn>
|
</v-btn>
|
||||||
@@ -269,218 +261,267 @@ import ThresholdTab from './PipelineViews/ThresholdTab';
|
|||||||
import ContoursTab from './PipelineViews/ContoursTab';
|
import ContoursTab from './PipelineViews/ContoursTab';
|
||||||
import OutputTab from './PipelineViews/OutputTab';
|
import OutputTab from './PipelineViews/OutputTab';
|
||||||
import TargetsTab from "./PipelineViews/TargetsTab";
|
import TargetsTab from "./PipelineViews/TargetsTab";
|
||||||
|
import Map3DTab from './PipelineViews/Map3DTab';
|
||||||
import PnPTab from './PipelineViews/PnPTab';
|
import PnPTab from './PipelineViews/PnPTab';
|
||||||
|
import AprilTagTab from './PipelineViews/AprilTagTab';
|
||||||
|
import ArucoTab from './PipelineViews/ArucoTab';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'Pipeline',
|
name: 'Pipeline',
|
||||||
components: {
|
components: {
|
||||||
CameraAndPipelineSelect,
|
CameraAndPipelineSelect,
|
||||||
cvImage,
|
cvImage,
|
||||||
InputTab,
|
InputTab,
|
||||||
ThresholdTab,
|
ThresholdTab,
|
||||||
ContoursTab,
|
ContoursTab,
|
||||||
OutputTab,
|
OutputTab,
|
||||||
TargetsTab,
|
TargetsTab,
|
||||||
PnPTab,
|
Map3DTab,
|
||||||
},
|
PnPTab,
|
||||||
data() {
|
AprilTagTab,
|
||||||
return {
|
ArucoTab,
|
||||||
selectedTabsData: [0, 0, 0, 0],
|
},
|
||||||
snackbar: false,
|
data() {
|
||||||
counterData: 0,
|
return {
|
||||||
dialog: false,
|
selectedTabsData: [0, 0, 0, 0],
|
||||||
processingModeOverride: false
|
counterData: 0,
|
||||||
}
|
dialog: false,
|
||||||
},
|
processingModeOverride: false,
|
||||||
computed: {
|
hideNTWarning: false,
|
||||||
selectedTabs: {
|
|
||||||
get() {
|
|
||||||
return this.$store.getters.isDriverMode ? [0] : this.selectedTabsData;
|
|
||||||
},
|
|
||||||
set(value) {
|
|
||||||
this.selectedTabsData = value;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
tabGroups: {
|
|
||||||
get() {
|
|
||||||
let tabs = {
|
|
||||||
input: {
|
|
||||||
name: "Input",
|
|
||||||
component: "InputTab",
|
|
||||||
},
|
|
||||||
threshold: {
|
|
||||||
name: "Threshold",
|
|
||||||
component: "ThresholdTab",
|
|
||||||
},
|
|
||||||
contours: {
|
|
||||||
name: "Contours",
|
|
||||||
component: "ContoursTab",
|
|
||||||
},
|
|
||||||
output: {
|
|
||||||
name: "Output",
|
|
||||||
component: "OutputTab",
|
|
||||||
},
|
|
||||||
targets: {
|
|
||||||
name: "Target Info",
|
|
||||||
component: "TargetsTab",
|
|
||||||
},
|
|
||||||
pnp: {
|
|
||||||
name: "3D",
|
|
||||||
component: "PnPTab",
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 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)) {
|
|
||||||
// One big tab group with all the tabs
|
|
||||||
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.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.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.output];
|
|
||||||
ret[3] = [tabs.targets, tabs.pnp];
|
|
||||||
}
|
|
||||||
|
|
||||||
return ret;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
processingMode: {
|
|
||||||
get() {
|
|
||||||
return (this.$store.getters.currentPipelineSettings.solvePNPEnabled || this.processingModeOverride) ? 1 : 0;
|
|
||||||
},
|
|
||||||
set(value) {
|
|
||||||
if (this.$store.getters.isCalibrated) {
|
|
||||||
this.$store.getters.currentPipelineSettings.solvePNPEnabled = value === 1;
|
|
||||||
this.handlePipelineUpdate("solvePNPEnabled", value === 1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
driverMode: {
|
|
||||||
get() {
|
|
||||||
return this.$store.getters.isDriverMode;
|
|
||||||
},
|
|
||||||
set(value) {
|
|
||||||
this.$store.getters.currentCameraSettings.currentPipelineIndex = value ? -1 : 0;
|
|
||||||
this.handleInputWithIndex('currentPipeline', value ? -1 : 0);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
selectedOutputs: {
|
|
||||||
// All this logic exists to deal with the reality that the output select buttons sometimes need an array and sometimes need a number (depending on whether or not they're exclusive)
|
|
||||||
get() {
|
|
||||||
// We switch the selector to single-select only on sm-and-down size devices, so we have to return a Number instead of an Array in that state
|
|
||||||
let ret = [];
|
|
||||||
if (this.$store.state.colorPicking) {
|
|
||||||
ret = [0]; // We want the input stream only while color picking
|
|
||||||
} else if (this.$store.getters.isDriverMode) {
|
|
||||||
ret = [1]; // We want only the output stream in driver mode
|
|
||||||
} else {
|
|
||||||
if (this.$store.getters.currentPipelineSettings.inputShouldShow) ret = ret.concat([0]);
|
|
||||||
if (this.$store.getters.currentPipelineSettings.outputShouldShow) ret = ret.concat([1]);
|
|
||||||
if (!ret.length) ret = [0];
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.$vuetify.breakpoint.mdAndUp) {
|
|
||||||
return ret;
|
|
||||||
} else {
|
|
||||||
return ret[0] || 0;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
set(value) {
|
|
||||||
let valToCommit = [0];
|
|
||||||
if (value instanceof Array) {
|
|
||||||
// Value is already an array, we don't need to do anything
|
|
||||||
valToCommit = value;
|
|
||||||
} else if (value) {
|
|
||||||
// Value is assumed to be a number, so we wrap it into an array
|
|
||||||
valToCommit = [value];
|
|
||||||
}
|
|
||||||
|
|
||||||
this.$store.commit("mutatePipeline", {"inputShouldShow": valToCommit.includes(0)});
|
|
||||||
this.$store.commit("mutatePipeline", {"outputShouldShow": valToCommit.includes(1)});
|
|
||||||
this.handlePipelineUpdate("inputShouldShow", valToCommit.includes(0));
|
|
||||||
}
|
|
||||||
},
|
|
||||||
fpsTooLow: {
|
|
||||||
get() {
|
|
||||||
// For now we only show the FPS is too low warning when GPU acceleration is enabled, because we don't really trust the presented video modes otherwise
|
|
||||||
return this.$store.state.pipelineResults.fps - this.$store.getters.currentVideoFormat.fps < -5 && this.$store.state.pipelineResults.fps !== 0 && !this.$store.getters.isDriverMode && this.$store.state.settings.general.gpuAcceleration;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
latency: {
|
|
||||||
get() {
|
|
||||||
return this.$store.getters.currentPipelineResults.latency;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
isCalibrated: {
|
|
||||||
get() {
|
|
||||||
const resolution = this.$store.getters.videoFormatList[this.$store.getters.currentPipelineSettings.cameraVideoModeIndex];
|
|
||||||
return this.$store.getters.currentCameraSettings.calibrations
|
|
||||||
.some(e => e.width === resolution.width && e.height === resolution.height)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
isRobotConnected: {
|
|
||||||
get() {
|
|
||||||
// return this.$store.state.ntConnectionInfo.connected && this.$store.state.backendConnected;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
created() {
|
|
||||||
this.$store.state.connectedCallbacks.push(this.reloadStreams)
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
reloadStreams() {
|
|
||||||
// Reload the streams as we technically close and reopen them
|
|
||||||
this.$refs.streams.forEach(it => it.reload())
|
|
||||||
},
|
|
||||||
onImageClick(event) {
|
|
||||||
// Only run on the input stream
|
|
||||||
if (event.target.alt !== "Stream0") return;
|
|
||||||
// Get a reference to the threshold tab (if it is shown) and call its "onClick" method
|
|
||||||
let ref = this.$refs["Threshold"];
|
|
||||||
if (ref && ref[0])
|
|
||||||
ref[0].onClick(event)
|
|
||||||
},
|
|
||||||
on3DClick() {
|
|
||||||
if (!this.$store.getters.isCalibrated) {
|
|
||||||
this.dialog = true;
|
|
||||||
this.processingModeOverride = true;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
closeUncalibratedDialog() {
|
|
||||||
this.dialog = false;
|
|
||||||
this.processingModeOverride = false;
|
|
||||||
// this.$store.getters.currentPipelineSettings.solvePNPEnabled = false;
|
|
||||||
this.handlePipelineUpdate("solvePNPEnabled", false);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
selectedTabs: {
|
||||||
|
get() {
|
||||||
|
return this.$store.getters.isDriverMode ? [0] : this.selectedTabsData;
|
||||||
|
},
|
||||||
|
set(value) {
|
||||||
|
this.selectedTabsData = value;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
tabGroups: {
|
||||||
|
get() {
|
||||||
|
let tabs = {
|
||||||
|
input: {
|
||||||
|
name: "Input",
|
||||||
|
component: "InputTab",
|
||||||
|
},
|
||||||
|
threshold: {
|
||||||
|
name: "Threshold",
|
||||||
|
component: "ThresholdTab",
|
||||||
|
},
|
||||||
|
contours: {
|
||||||
|
name: "Contours",
|
||||||
|
component: "ContoursTab",
|
||||||
|
},
|
||||||
|
apriltag: {
|
||||||
|
name: "AprilTag",
|
||||||
|
component: "AprilTagTab",
|
||||||
|
},
|
||||||
|
aruco: {
|
||||||
|
name: "Aruco",
|
||||||
|
component: "ArucoTab",
|
||||||
|
},
|
||||||
|
output: {
|
||||||
|
name: "Output",
|
||||||
|
component: "OutputTab",
|
||||||
|
},
|
||||||
|
targets: {
|
||||||
|
name: "Targets",
|
||||||
|
component: "TargetsTab",
|
||||||
|
},
|
||||||
|
pnp: {
|
||||||
|
name: "PnP",
|
||||||
|
component: "PnPTab",
|
||||||
|
},
|
||||||
|
map3d: {
|
||||||
|
name: "3D",
|
||||||
|
component: "Map3DTab",
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 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;
|
||||||
|
const isAruco = (this.$store.getters.currentPipelineSettings.pipelineType - 2) === 3;
|
||||||
|
|
||||||
|
// 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)) {
|
||||||
|
// One big tab group with all the tabs
|
||||||
|
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.aruco, tabs.output];
|
||||||
|
ret[1] = [tabs.targets, tabs.pnp, tabs.map3d];
|
||||||
|
} 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.aruco, tabs.output];
|
||||||
|
ret[2] = [tabs.targets, tabs.pnp, tabs.map3d];
|
||||||
|
} 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.aruco,tabs.output];
|
||||||
|
ret[3] = [tabs.targets, tabs.pnp, tabs.map3d];
|
||||||
|
}
|
||||||
|
|
||||||
|
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 || isAruco) && it.name === "PnP") //Filter out the PnP config tab if 3D isn't available, or we're doing Apriltags
|
||||||
|
&& !((isAprilTag || isAruco) && (it.name === "Threshold")) //Filter out threshold tab if we're doing apriltags
|
||||||
|
&& !((isAprilTag || isAruco)&& (it.name === "Contours")) //Filter out contours if we're doing Apriltag
|
||||||
|
&& !(!isAprilTag && it.name === "AprilTag") //Filter out apriltag unless we actually are doing Apriltags
|
||||||
|
&& !(!isAruco && it.name === "Aruco")
|
||||||
|
);
|
||||||
|
ret[i] = filteredGroup;
|
||||||
|
}
|
||||||
|
|
||||||
|
// One last filter to remove empty lists
|
||||||
|
return ret.filter(it => it !== undefined && it.length > 0);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
processingMode: {
|
||||||
|
get() {
|
||||||
|
return (this.$store.getters.currentPipelineSettings.solvePNPEnabled || this.processingModeOverride) ? 1 : 0;
|
||||||
|
},
|
||||||
|
set(value) {
|
||||||
|
if (this.$store.getters.isCalibrated) {
|
||||||
|
this.$store.getters.currentPipelineSettings.solvePNPEnabled = value === 1;
|
||||||
|
this.handlePipelineUpdate("solvePNPEnabled", value === 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
driverMode: {
|
||||||
|
get() {
|
||||||
|
return this.$store.getters.isDriverMode;
|
||||||
|
},
|
||||||
|
set(value) {
|
||||||
|
this.$store.getters.currentCameraSettings.currentPipelineIndex = value ? -1 : 0;
|
||||||
|
this.handleInputWithIndex('currentPipeline', value ? -1 : 0);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
selectedOutputs: {
|
||||||
|
// All this logic exists to deal with the reality that the output select buttons sometimes need an array and sometimes need a number (depending on whether or not they're exclusive)
|
||||||
|
get() {
|
||||||
|
// We switch the selector to single-select only on sm-and-down size devices, so we have to return a Number instead of an Array in that state
|
||||||
|
let ret = [];
|
||||||
|
if (this.$store.state.colorPicking) {
|
||||||
|
ret = [0]; // We want the input stream only while color picking
|
||||||
|
} else if (this.$store.getters.isDriverMode) {
|
||||||
|
ret = [1]; // We want only the output stream in driver mode
|
||||||
|
} else {
|
||||||
|
if (this.$store.getters.currentPipelineSettings.inputShouldShow) ret = ret.concat([0]);
|
||||||
|
if (this.$store.getters.currentPipelineSettings.outputShouldShow) ret = ret.concat([1]);
|
||||||
|
if (!ret.length) ret = [0];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.$vuetify.breakpoint.mdAndUp) {
|
||||||
|
return ret;
|
||||||
|
} else {
|
||||||
|
return ret[0] || 0;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
set(value) {
|
||||||
|
let valToCommit = [0];
|
||||||
|
if (value instanceof Array) {
|
||||||
|
// Value is already an array, we don't need to do anything
|
||||||
|
valToCommit = value;
|
||||||
|
} else if (value) {
|
||||||
|
// Value is assumed to be a number, so we wrap it into an array
|
||||||
|
valToCommit = [value];
|
||||||
|
}
|
||||||
|
|
||||||
|
this.$store.commit("mutatePipeline", {"inputShouldShow": valToCommit.includes(0)});
|
||||||
|
this.$store.commit("mutatePipeline", {"outputShouldShow": valToCommit.includes(1)});
|
||||||
|
this.handlePipelineUpdate("inputShouldShow", valToCommit.includes(0));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
fpsTooLow: {
|
||||||
|
get() {
|
||||||
|
// For now we only show the FPS is too low warning when GPU acceleration is enabled, because we don't really trust the presented video modes otherwise
|
||||||
|
const currFPS = this.$store.state.pipelineResults.fps;
|
||||||
|
const targetFPS = this.$store.getters.currentVideoFormat.fps;
|
||||||
|
const driverMode = this.$store.getters.isDriverMode;
|
||||||
|
const gpuAccel = this.$store.state.settings.general.gpuAcceleration === true;
|
||||||
|
const isReflective = this.$store.getters.pipelineType === 2;
|
||||||
|
|
||||||
|
return (currFPS - targetFPS) < -5 && this.$store.state.pipelineResults.fps !== 0 && !driverMode && gpuAccel && isReflective;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
latency: {
|
||||||
|
get() {
|
||||||
|
return this.$store.getters.currentPipelineResults.latency;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
isCalibrated: {
|
||||||
|
get() {
|
||||||
|
const resolution = this.$store.getters.videoFormatList[this.$store.getters.currentPipelineSettings.cameraVideoModeIndex];
|
||||||
|
return this.$store.getters.currentCameraSettings.calibrations
|
||||||
|
.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)
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
reloadStreams() {
|
||||||
|
// Reload the streams as we technically close and reopen them
|
||||||
|
this.$refs.streams.forEach(it => it.reload())
|
||||||
|
},
|
||||||
|
onImageClick(event) {
|
||||||
|
// Get a reference to the threshold tab (if it is shown) and call its "onClick" method
|
||||||
|
let ref = this.$refs["Threshold"];
|
||||||
|
if (ref && ref[0])
|
||||||
|
ref[0].onClick(event)
|
||||||
|
},
|
||||||
|
on3DClick() {
|
||||||
|
if (!this.$store.getters.isCalibrated) {
|
||||||
|
this.dialog = true;
|
||||||
|
this.processingModeOverride = true;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
closeUncalibratedDialog() {
|
||||||
|
this.dialog = false;
|
||||||
|
this.processingModeOverride = false;
|
||||||
|
// this.$store.getters.currentPipelineSettings.solvePNPEnabled = false;
|
||||||
|
this.handlePipelineUpdate("solvePNPEnabled", false);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.v-btn-toggle.fill {
|
.v-btn-toggle.fill {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.v-btn-toggle.fill > .v-btn {
|
.v-btn-toggle.fill > .v-btn {
|
||||||
width: 50%;
|
width: 50%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
th {
|
th {
|
||||||
width: 80px;
|
width: 80px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
154
photon-client/src/views/PipelineViews/AprilTagTab.vue
Normal file
154
photon-client/src/views/PipelineViews/AprilTagTab.vue
Normal file
@@ -0,0 +1,154 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<CVselect
|
||||||
|
v-model="selectedFamily"
|
||||||
|
name="Target family"
|
||||||
|
:list="['AprilTag family 36h11', 'AprilTag family 25h9', 'AprilTag family 16h5']"
|
||||||
|
select-cols="8"
|
||||||
|
@input="handlePipelineUpdate('tagFamily', selectedFamily)"
|
||||||
|
/>
|
||||||
|
<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="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'
|
||||||
|
import CVselect from '../../components/common/cv-select'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: "AprilTag",
|
||||||
|
components: {
|
||||||
|
CVslider,
|
||||||
|
CVswitch,
|
||||||
|
CVselect,
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
familyList: ["AprilTag family 36h11", "AprilTag family 25h9", "AprilTag family 16h5"],
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
selectedFamily: {
|
||||||
|
get() {
|
||||||
|
return this.$store.getters.currentPipelineSettings.tagFamily
|
||||||
|
},
|
||||||
|
set(val) {
|
||||||
|
this.$store.commit("mutatePipeline", {"tagFamily": val})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
decimate: {
|
||||||
|
get() {
|
||||||
|
return this.$store.getters.currentPipelineSettings.decimate
|
||||||
|
},
|
||||||
|
set(val) {
|
||||||
|
this.$store.commit("mutatePipeline", {"decimate": 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>
|
||||||
76
photon-client/src/views/PipelineViews/ArucoTab.vue
Normal file
76
photon-client/src/views/PipelineViews/ArucoTab.vue
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<CVslider
|
||||||
|
v-model="decimate"
|
||||||
|
class="pt-2"
|
||||||
|
slider-cols="8"
|
||||||
|
name="Decimate"
|
||||||
|
min="1"
|
||||||
|
max="8"
|
||||||
|
step=".5"
|
||||||
|
tooltip="Increases FPS at the expense of range by reducing image resolution initially"
|
||||||
|
@input="handlePipelineData('decimate')"
|
||||||
|
/>
|
||||||
|
<CVslider
|
||||||
|
v-model="numIterations"
|
||||||
|
class="pt-2"
|
||||||
|
slider-cols="8"
|
||||||
|
name="Corner Iterations"
|
||||||
|
min="30"
|
||||||
|
max="1000"
|
||||||
|
step="5"
|
||||||
|
tooltip="How many iterations are going to be used in order to refine corners. Higher values are lead to more accuracy at the cost of performance"
|
||||||
|
@input="handlePipelineData('numIterations')"
|
||||||
|
/>
|
||||||
|
<CVslider
|
||||||
|
v-model="cornerAccuracy"
|
||||||
|
class="pt-2"
|
||||||
|
slider-cols="8"
|
||||||
|
name="Corner Accuracy"
|
||||||
|
min=".01"
|
||||||
|
max="100"
|
||||||
|
step=".01"
|
||||||
|
tooltip="Minimum accuracy for the corners, lower is better but more performance intensive "
|
||||||
|
@input="handlePipelineData('cornerAccuracy')"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import CVslider from '../../components/common/cv-slider'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: "Aruco",
|
||||||
|
components: {
|
||||||
|
CVslider
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
decimate: {
|
||||||
|
get() {
|
||||||
|
return this.$store.getters.currentPipelineSettings.decimate
|
||||||
|
},
|
||||||
|
set(val) {
|
||||||
|
this.$store.commit("mutatePipeline", {"decimate": val});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
numIterations: {
|
||||||
|
get() {
|
||||||
|
return this.$store.getters.currentPipelineSettings.numIterations
|
||||||
|
},
|
||||||
|
set(val) {
|
||||||
|
this.$store.commit("mutatePipeline", {"numIterations": val});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
cornerAccuracy: {
|
||||||
|
get() {
|
||||||
|
return this.$store.getters.currentPipelineSettings.cornerAccuracy
|
||||||
|
},
|
||||||
|
set(val) {
|
||||||
|
this.$store.commit("mutatePipeline", {"cornerAccuracy": val});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -1,140 +1,169 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<CVrangeSlider
|
<CVrangeSlider
|
||||||
v-model="contourArea"
|
v-model="contourArea"
|
||||||
name="Area"
|
name="Area"
|
||||||
min="0"
|
min="0"
|
||||||
max="100"
|
max="100"
|
||||||
step="0.1"
|
step="0.01"
|
||||||
@input="handlePipelineData('contourArea')"
|
@input="handlePipelineData('contourArea')"
|
||||||
/>
|
/>
|
||||||
<CVrangeSlider
|
<CVrangeSlider
|
||||||
v-model="contourRatio"
|
v-if="currentPipelineType() !== 3"
|
||||||
v-if="currentPipelineType() !== 3"
|
v-model="contourRatio"
|
||||||
name="Ratio (W/H)"
|
name="Ratio (W/H)"
|
||||||
tooltip="Min and max ratio between the width and height of a contour's bounding rectangle"
|
tooltip="Min and max ratio between the width and height of a contour's bounding rectangle"
|
||||||
min="0"
|
min="0"
|
||||||
max="100"
|
max="100"
|
||||||
step="0.1"
|
step="0.1"
|
||||||
@input="handlePipelineData('contourRatio')"
|
@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)"
|
||||||
/>
|
/>
|
||||||
<CVrangeSlider
|
<CVrangeSlider
|
||||||
v-model="contourFullness"
|
v-if="currentPipelineType() !== 3"
|
||||||
v-if="currentPipelineType() !== 3"
|
v-model="contourFullness"
|
||||||
name="Fullness"
|
name="Fullness"
|
||||||
tooltip="Min and max ratio between a contour's area and its bounding rectangle"
|
tooltip="Min and max ratio between a contour's area and its bounding rectangle"
|
||||||
min="0"
|
min="0"
|
||||||
max="100"
|
max="100"
|
||||||
@input="handlePipelineData('contourFullness')"
|
@input="handlePipelineData('contourFullness')"
|
||||||
/>
|
/>
|
||||||
<CVrangeSlider
|
<CVrangeSlider
|
||||||
v-model="contourPerimeter"
|
v-if="currentPipelineType() === 3"
|
||||||
v-if="currentPipelineType() === 3"
|
v-model="contourPerimeter"
|
||||||
name="Perimeter"
|
name="Perimeter"
|
||||||
tooltip="Min and max perimeter of the shape, in pixels"
|
tooltip="Min and max perimeter of the shape, in pixels"
|
||||||
min="0"
|
min="0"
|
||||||
max="4000"
|
max="4000"
|
||||||
@input="handlePipelineData('contourPerimeter')"
|
@input="handlePipelineData('contourPerimeter')"
|
||||||
/>
|
/>
|
||||||
<CVslider
|
<CVslider
|
||||||
v-model="contourSpecklePercentage"
|
v-model="contourSpecklePercentage"
|
||||||
name="Speckle Rejection"
|
name="Speckle Rejection"
|
||||||
tooltip="Rejects contours whose average area is less than the given percentage of the average area of all the other contours"
|
tooltip="Rejects contours whose average area is less than the given percentage of the average area of all the other contours"
|
||||||
min="0"
|
min="0"
|
||||||
max="100"
|
max="100"
|
||||||
:slider-cols="largeBox"
|
:slider-cols="largeBox"
|
||||||
@input="handlePipelineData('contourSpecklePercentage')"
|
@input="handlePipelineData('contourSpecklePercentage')"
|
||||||
/>
|
/>
|
||||||
<template v-if="currentPipelineType() !== 3">
|
<template v-if="currentPipelineType() !== 3">
|
||||||
<CVselect
|
<CVslider
|
||||||
v-model="contourGroupingMode"
|
v-model="contourFilterRangeX"
|
||||||
name="Target Grouping"
|
name="X filter tightness"
|
||||||
tooltip="Whether or not every two targets are paired with each other (good for e.g. 2019 targets)"
|
tooltip="Rejects contours whose center X is further than X standard deviations above/below the mean X location"
|
||||||
:select-cols="largeBox"
|
min="0.1"
|
||||||
:list="['Single','Dual','2orMore']"
|
max="4"
|
||||||
@input="handlePipelineData('contourGroupingMode')"
|
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
|
<CVselect
|
||||||
v-model="contourIntersection"
|
v-model="contourGroupingMode"
|
||||||
name="Target Intersection"
|
name="Target Grouping"
|
||||||
tooltip="If target grouping is in dual mode it will use this dropdown to decide how targets are grouped with adjacent targets"
|
tooltip="Whether or not every two targets are paired with each other (good for e.g. 2019 targets)"
|
||||||
:select-cols="largeBox"
|
:select-cols="largeBox"
|
||||||
:list="['None','Up','Down','Left','Right']"
|
:list="['Single','Dual','2orMore']"
|
||||||
:disabled="contourGroupingMode === 0"
|
@input="handlePipelineData('contourGroupingMode')"
|
||||||
@input="handlePipelineData('contourIntersection')"
|
/>
|
||||||
|
<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')"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
<!-- If we arent not a shape, we are a shape-->
|
<!-- If we arent not a shape, we are a shape-->
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<v-divider class="mt-3"/>
|
<v-divider class="mt-3" />
|
||||||
<CVselect
|
<CVselect
|
||||||
v-model="contourShape"
|
v-model="contourShape"
|
||||||
name="Target Shape"
|
name="Target Shape"
|
||||||
tooltip="The shape of targets to look for"
|
tooltip="The shape of targets to look for"
|
||||||
:select-cols="largeBox"
|
:select-cols="largeBox"
|
||||||
:list="['Circle', 'Polygon', 'Triangle', 'Quadrilateral']"
|
:list="['Circle', 'Polygon', 'Triangle', 'Quadrilateral']"
|
||||||
@input="handlePipelineData('contourShape')"
|
@input="handlePipelineData('contourShape')"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<!-- Accuracy % is only for polygons-->
|
<!-- Accuracy % is only for polygons-->
|
||||||
<CVslider
|
<CVslider
|
||||||
v-model="accuracyPercentage"
|
v-model="accuracyPercentage"
|
||||||
:disabled="currentPipelineSettings().contourShape < 1"
|
:disabled="currentPipelineSettings().contourShape < 1"
|
||||||
name="Shape Simplification"
|
name="Shape Simplification"
|
||||||
tooltip="How much we should simply the input contour before checking how many sides it has"
|
tooltip="How much we should simply the input contour before checking how many sides it has"
|
||||||
min="0"
|
min="0"
|
||||||
max="100"
|
max="100"
|
||||||
:slider-cols="largeBox"
|
:slider-cols="largeBox"
|
||||||
@input="handlePipelineData('accuracyPercentage')"
|
@input="handlePipelineData('accuracyPercentage')"
|
||||||
/>
|
/>
|
||||||
<!-- Similarly, the threshold is only for circles -->
|
<!-- Similarly, the threshold is only for circles -->
|
||||||
<CVslider
|
<CVslider
|
||||||
v-model="circleDetectThreshold"
|
v-model="circleDetectThreshold"
|
||||||
:disabled="currentPipelineSettings().contourShape !== 0"
|
:disabled="currentPipelineSettings().contourShape !== 0"
|
||||||
name="Circle match distance"
|
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"
|
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"
|
min="1"
|
||||||
max="100"
|
max="100"
|
||||||
:slider-cols="largeBox"
|
:slider-cols="largeBox"
|
||||||
@input="handlePipelineData('circleDetectThreshold')"
|
@input="handlePipelineData('circleDetectThreshold')"
|
||||||
/>
|
/>
|
||||||
<CVrangeSlider
|
<CVrangeSlider
|
||||||
v-model="contourRadius"
|
v-model="contourRadius"
|
||||||
:disabled="currentPipelineSettings().contourShape !== 0"
|
:disabled="currentPipelineSettings().contourShape !== 0"
|
||||||
name="Radius"
|
name="Radius"
|
||||||
min="0"
|
min="0"
|
||||||
max="100"
|
max="100"
|
||||||
step="1"
|
step="1"
|
||||||
@input="handlePipelineData('contourRadius')"
|
label-cols="3"
|
||||||
|
@input="handlePipelineData('contourRadius')"
|
||||||
/>
|
/>
|
||||||
<CVslider
|
<CVslider
|
||||||
v-model="maxCannyThresh"
|
v-model="maxCannyThresh"
|
||||||
:disabled="currentPipelineSettings().contourShape !== 0"
|
:disabled="currentPipelineSettings().contourShape !== 0"
|
||||||
name="Max Canny Threshold"
|
name="Max Canny Threshold"
|
||||||
min="1"
|
min="1"
|
||||||
max="100"
|
max="100"
|
||||||
:slider-cols="largeBox"
|
:slider-cols="largeBox"
|
||||||
@input="handlePipelineData('maxCannyThresh')"
|
@input="handlePipelineData('maxCannyThresh')"
|
||||||
/>
|
/>
|
||||||
<CVslider
|
<CVslider
|
||||||
v-model="circleAccuracy"
|
v-model="circleAccuracy"
|
||||||
:disabled="currentPipelineSettings().contourShape !== 0"
|
:disabled="currentPipelineSettings().contourShape !== 0"
|
||||||
name="Circle Accuracy"
|
name="Circle Accuracy"
|
||||||
min="0"
|
min="1"
|
||||||
max="100"
|
max="100"
|
||||||
:slider-cols="largeBox"
|
:slider-cols="largeBox"
|
||||||
@input="handlePipelineData('circleAccuracy')"
|
@input="handlePipelineData('circleAccuracy')"
|
||||||
/>
|
/>
|
||||||
<v-divider class="mt-3"/>
|
<v-divider class="mt-3" />
|
||||||
</template>
|
</template>
|
||||||
<CVselect
|
<CVselect
|
||||||
v-model="contourSortMode"
|
v-model="contourSortMode"
|
||||||
name="Target Sort"
|
name="Target Sort"
|
||||||
tooltip="Chooses the sorting mode used to determine the 'best' targets to provide to user code"
|
tooltip="Chooses the sorting mode used to determine the 'best' targets to provide to user code"
|
||||||
:select-cols="largeBox"
|
:select-cols="largeBox"
|
||||||
:list="['Largest','Smallest','Highest','Lowest','Rightmost','Leftmost','Centermost']"
|
:list="['Largest','Smallest','Highest','Lowest','Rightmost','Leftmost','Centermost']"
|
||||||
@input="handlePipelineData('contourSortMode')"
|
@input="handlePipelineData('contourSortMode')"
|
||||||
@rollback="e => rollback('contourSortMode', e)"
|
@rollback="e => rollback('contourSortMode', e)"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -182,6 +211,14 @@ export default {
|
|||||||
this.$store.commit("mutatePipeline", {"contourRatio": val});
|
this.$store.commit("mutatePipeline", {"contourRatio": val});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
contourTargetOrientation: {
|
||||||
|
get() {
|
||||||
|
return this.$store.getters.currentPipelineSettings.contourTargetOrientation
|
||||||
|
},
|
||||||
|
set(val) {
|
||||||
|
this.$store.commit("mutatePipeline", {"contourTargetOrientation": val});
|
||||||
|
}
|
||||||
|
},
|
||||||
contourFullness: {
|
contourFullness: {
|
||||||
get() {
|
get() {
|
||||||
return this.$store.getters.currentPipelineSettings.contourFullness
|
return this.$store.getters.currentPipelineSettings.contourFullness
|
||||||
@@ -206,6 +243,25 @@ export default {
|
|||||||
this.$store.commit("mutatePipeline", {"contourSpecklePercentage": val});
|
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: {
|
contourGroupingMode: {
|
||||||
get() {
|
get() {
|
||||||
return this.$store.getters.currentPipelineSettings.contourGroupingMode
|
return this.$store.getters.currentPipelineSettings.contourGroupingMode
|
||||||
|
|||||||
@@ -2,11 +2,12 @@
|
|||||||
<div>
|
<div>
|
||||||
<CVslider
|
<CVslider
|
||||||
v-model="cameraExposure"
|
v-model="cameraExposure"
|
||||||
|
:disabled="cameraAutoExposure"
|
||||||
name="Exposure"
|
name="Exposure"
|
||||||
min="0"
|
min="0"
|
||||||
max="100"
|
max="100"
|
||||||
step="0.1"
|
step="0.1"
|
||||||
tooltip="Directly controls how much light is allowed to fall onto the sensor, which affects brightness"
|
tooltip="Directly controls how much light is allowed to fall onto the sensor, which affects apparent brightness"
|
||||||
:slider-cols="largeBox"
|
:slider-cols="largeBox"
|
||||||
@input="handlePipelineData('cameraExposure')"
|
@input="handlePipelineData('cameraExposure')"
|
||||||
@rollback="e => rollback('cameraExposure', e)"
|
@rollback="e => rollback('cameraExposure', e)"
|
||||||
@@ -21,22 +22,53 @@
|
|||||||
@input="handlePipelineData('cameraBrightness')"
|
@input="handlePipelineData('cameraBrightness')"
|
||||||
@rollback="e => rollback('cameraBrightness', e)"
|
@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
|
<CVslider
|
||||||
v-if="cameraGain !== -1"
|
v-if="cameraGain >= 0"
|
||||||
v-model="cameraGain"
|
v-model="cameraGain"
|
||||||
name="Gain"
|
name="Camera Gain"
|
||||||
min="0"
|
min="0"
|
||||||
max="100"
|
max="100"
|
||||||
tooltip="Controls automatic white balance gain, which affects how the camera captures colors in different conditions"
|
tooltip="Controls camera gain, similar to brightness"
|
||||||
:slider-cols="largeBox"
|
:slider-cols="largeBox"
|
||||||
@input="handlePipelineData('cameraGain')"
|
@input="handlePipelineData('cameraGain')"
|
||||||
@rollback="e => rollback('cameraGain', e)"
|
@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)"
|
||||||
|
/>
|
||||||
|
<!-- TODO: stop filtering out the 90 degree rotation modes when we fix those in libcamera -->
|
||||||
<CVselect
|
<CVselect
|
||||||
v-model="inputImageRotationMode"
|
v-model="inputImageRotationMode"
|
||||||
name="Orientation"
|
name="Orientation"
|
||||||
tooltip="Rotates the camera stream"
|
tooltip="Rotates the camera stream"
|
||||||
:list="['Normal','90° CW','180°','90° CCW']"
|
:list="['Normal','90° CW','180°','90° CCW']"
|
||||||
|
:filtered-indices="this.$store.state.settings.general.gpuAcceleration ? new Set([1, 3]) : undefined"
|
||||||
:select-cols="largeBox"
|
:select-cols="largeBox"
|
||||||
@input="handlePipelineData('inputImageRotationMode')"
|
@input="handlePipelineData('inputImageRotationMode')"
|
||||||
@rollback="e => rollback('inputImageRotationMode',e)"
|
@rollback="e => rollback('inputImageRotationMode',e)"
|
||||||
@@ -64,6 +96,7 @@
|
|||||||
<script>
|
<script>
|
||||||
import CVslider from '../../components/common/cv-slider'
|
import CVslider from '../../components/common/cv-slider'
|
||||||
import CVselect from '../../components/common/cv-select'
|
import CVselect from '../../components/common/cv-select'
|
||||||
|
import CVswitch from '../../components/common/cv-switch'
|
||||||
|
|
||||||
const unfilteredStreamDivisors = [1, 2, 4, 6];
|
const unfilteredStreamDivisors = [1, 2, 4, 6];
|
||||||
|
|
||||||
@@ -72,14 +105,10 @@
|
|||||||
components: {
|
components: {
|
||||||
CVslider,
|
CVslider,
|
||||||
CVselect,
|
CVselect,
|
||||||
|
CVswitch,
|
||||||
},
|
},
|
||||||
// eslint-disable-next-line vue/require-prop-types
|
// eslint-disable-next-line vue/require-prop-types
|
||||||
props: ['value'],
|
props: ['value'],
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
rawStreamDivisorIndex: 0,
|
|
||||||
}
|
|
||||||
},
|
|
||||||
computed: {
|
computed: {
|
||||||
largeBox: {
|
largeBox: {
|
||||||
get() {
|
get() {
|
||||||
@@ -97,6 +126,14 @@
|
|||||||
this.$store.commit("mutatePipeline", {"cameraExposure": parseFloat(val)});
|
this.$store.commit("mutatePipeline", {"cameraExposure": parseFloat(val)});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
cameraAutoExposure: {
|
||||||
|
get() {
|
||||||
|
return this.$store.getters.currentPipelineSettings.cameraAutoExposure;
|
||||||
|
},
|
||||||
|
set(val) {
|
||||||
|
this.$store.commit("mutatePipeline", {"cameraAutoExposure": val});
|
||||||
|
}
|
||||||
|
},
|
||||||
cameraBrightness: {
|
cameraBrightness: {
|
||||||
get() {
|
get() {
|
||||||
return parseInt(this.$store.getters.currentPipelineSettings.cameraBrightness)
|
return parseInt(this.$store.getters.currentPipelineSettings.cameraBrightness)
|
||||||
@@ -113,6 +150,22 @@
|
|||||||
this.$store.commit("mutatePipeline", {"cameraGain": parseInt(val)});
|
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: {
|
inputImageRotationMode: {
|
||||||
get() {
|
get() {
|
||||||
return this.$store.getters.currentPipelineSettings.inputImageRotationMode
|
return this.$store.getters.currentPipelineSettings.inputImageRotationMode
|
||||||
@@ -129,15 +182,22 @@
|
|||||||
this.$store.commit("mutatePipeline", {"cameraVideoModeIndex": val});
|
this.$store.commit("mutatePipeline", {"cameraVideoModeIndex": val});
|
||||||
|
|
||||||
this.handlePipelineUpdate("streamingFrameDivisor", this.getNumSkippedStreamDivisors());
|
this.handlePipelineUpdate("streamingFrameDivisor", this.getNumSkippedStreamDivisors());
|
||||||
this.rawStreamDivisorIndex = 0;
|
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});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
streamingFrameDivisor: {
|
streamingFrameDivisor: {
|
||||||
get() {
|
get() {
|
||||||
return this.rawStreamDivisorIndex;
|
return this.$store.getters.currentPipelineSettings.streamingFrameDivisor;
|
||||||
},
|
},
|
||||||
set(val) {
|
set(val) {
|
||||||
this.rawStreamDivisorIndex = val;
|
this.$store.commit("mutatePipeline", {"streamingFrameDivisor": val});
|
||||||
this.handlePipelineUpdate("streamingFrameDivisor", this.getNumSkippedStreamDivisors() + val);
|
this.handlePipelineUpdate("streamingFrameDivisor", this.getNumSkippedStreamDivisors() + val);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
53
photon-client/src/views/PipelineViews/Map3DTab.vue
Normal file
53
photon-client/src/views/PipelineViews/Map3DTab.vue
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
<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>
|
||||||
@@ -1,8 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<span class="white--text">Target Manipulation</span>
|
|
||||||
<v-divider class="mt-2" />
|
|
||||||
|
|
||||||
<CVselect
|
<CVselect
|
||||||
v-model="contourTargetOffsetPointEdge"
|
v-model="contourTargetOffsetPointEdge"
|
||||||
name="Target Offset Point"
|
name="Target Offset Point"
|
||||||
@@ -13,6 +10,7 @@
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<CVselect
|
<CVselect
|
||||||
|
v-if="!isTagPipeline"
|
||||||
v-model="contourTargetOrientation"
|
v-model="contourTargetOrientation"
|
||||||
name="Target Orientation"
|
name="Target Orientation"
|
||||||
tooltip="Used to determine how to calculate target landmarks (e.g. the top, left, or bottom of the target)"
|
tooltip="Used to determine how to calculate target landmarks (e.g. the top, left, or bottom of the target)"
|
||||||
@@ -24,15 +22,15 @@
|
|||||||
<CVswitch
|
<CVswitch
|
||||||
v-model="outputShowMultipleTargets"
|
v-model="outputShowMultipleTargets"
|
||||||
name="Show Multiple Targets"
|
name="Show Multiple Targets"
|
||||||
tooltip="If enabled, up to five targets will be displayed and sent to user code"
|
tooltip="If enabled, up to five targets will be displayed and sent to user code, instead of just one"
|
||||||
|
:disabled="isTagPipeline"
|
||||||
class="mb-4"
|
class="mb-4"
|
||||||
text-cols="3"
|
text-cols="3"
|
||||||
@input="handlePipelineData('outputShowMultipleTargets')"
|
@input="handlePipelineData('outputShowMultipleTargets')"
|
||||||
|
|
||||||
@rollback="e=> rollback('outputShowMultipleTargets', e)"
|
@rollback="e=> rollback('outputShowMultipleTargets', e)"
|
||||||
/>
|
/>
|
||||||
<span class="white--text">Robot Offset</span>
|
<v-divider />
|
||||||
<v-divider class="mt-2" />
|
|
||||||
<CVselect
|
<CVselect
|
||||||
v-model="offsetRobotOffsetMode"
|
v-model="offsetRobotOffsetMode"
|
||||||
name="Robot Offset Mode"
|
name="Robot Offset Mode"
|
||||||
@@ -141,6 +139,11 @@
|
|||||||
get() {
|
get() {
|
||||||
return undefined; // TODO fix
|
return undefined; // TODO fix
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
isTagPipeline: {
|
||||||
|
get() {
|
||||||
|
return this.$store.getters.currentPipelineSettings.pipelineType > 3;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
|||||||
@@ -6,7 +6,6 @@
|
|||||||
type="file"
|
type="file"
|
||||||
accept=".csv"
|
accept=".csv"
|
||||||
style="display: none;"
|
style="display: none;"
|
||||||
|
|
||||||
@change="readFile"
|
@change="readFile"
|
||||||
>
|
>
|
||||||
|
|
||||||
@@ -32,11 +31,7 @@
|
|||||||
@input="handlePipelineData('cornerDetectionAccuracyPercentage')"
|
@input="handlePipelineData('cornerDetectionAccuracyPercentage')"
|
||||||
@rollback="e => rollback('cornerDetectionAccuracyPercentage', e)"
|
@rollback="e => rollback('cornerDetectionAccuracyPercentage', e)"
|
||||||
/>
|
/>
|
||||||
<mini-map
|
|
||||||
class="miniMapClass"
|
|
||||||
:targets="targets"
|
|
||||||
:horizontal-f-o-v="horizontalFOV"
|
|
||||||
/>
|
|
||||||
<v-snackbar
|
<v-snackbar
|
||||||
v-model="snack"
|
v-model="snack"
|
||||||
top
|
top
|
||||||
@@ -49,18 +44,16 @@
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
import Papa from 'papaparse';
|
import Papa from 'papaparse';
|
||||||
import miniMap from '../../components/pipeline/3D/MiniMap';
|
|
||||||
import CVslider from '../../components/common/cv-slider'
|
import CVslider from '../../components/common/cv-slider'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: "PnP",
|
name: "PnP",
|
||||||
components: {
|
components: {
|
||||||
CVslider,
|
CVslider
|
||||||
miniMap
|
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
targetList: ['2020 High Goal Outer', '2020 High Goal Inner', '2019 Dual Target', 'Power Cell (7in)', '2016 High Goal'], //Keep in sync with TargetModel.java
|
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
|
||||||
snackbar: {
|
snackbar: {
|
||||||
color: "Success",
|
color: "Success",
|
||||||
text: ""
|
text: ""
|
||||||
@@ -72,7 +65,6 @@
|
|||||||
selectedModel: {
|
selectedModel: {
|
||||||
get() {
|
get() {
|
||||||
let ret = this.$store.getters.currentPipelineSettings.targetModel
|
let ret = this.$store.getters.currentPipelineSettings.targetModel
|
||||||
console.log(ret)
|
|
||||||
return this.targetList[ret];
|
return this.targetList[ret];
|
||||||
},
|
},
|
||||||
set(val) {
|
set(val) {
|
||||||
@@ -87,21 +79,6 @@
|
|||||||
this.$store.commit("mutatePipeline", {"cornerDetectionAccuracyPercentage": val});
|
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: {
|
methods: {
|
||||||
readFile(event) {
|
readFile(event) {
|
||||||
|
|||||||
@@ -18,29 +18,40 @@
|
|||||||
<th class="text-center">
|
<th class="text-center">
|
||||||
Target
|
Target
|
||||||
</th>
|
</th>
|
||||||
|
<th
|
||||||
|
v-if="$store.getters.pipelineType === 4 || (($store.getters.pipelineType - 2) === 3)"
|
||||||
|
class="text-center"
|
||||||
|
>
|
||||||
|
Fiducial ID
|
||||||
|
</th>
|
||||||
<template v-if="!$store.getters.currentPipelineSettings.solvePNPEnabled">
|
<template v-if="!$store.getters.currentPipelineSettings.solvePNPEnabled">
|
||||||
<th class="text-center">
|
<th class="text-center">
|
||||||
Pitch
|
Pitch, °
|
||||||
</th>
|
</th>
|
||||||
<th class="text-center">
|
<th class="text-center">
|
||||||
Yaw
|
Yaw, °
|
||||||
</th>
|
</th>
|
||||||
<th class="text-center">
|
<th class="text-center">
|
||||||
Skew
|
Skew, °
|
||||||
|
</th>
|
||||||
|
<th class="text-center">
|
||||||
|
Area, %
|
||||||
</th>
|
</th>
|
||||||
</template>
|
</template>
|
||||||
<th class="text-center">
|
<template v-else>
|
||||||
Area
|
|
||||||
</th>
|
|
||||||
<template v-if="$store.getters.currentPipelineSettings.solvePNPEnabled">
|
|
||||||
<th class="text-center">
|
<th class="text-center">
|
||||||
X
|
X, m
|
||||||
</th>
|
</th>
|
||||||
<th class="text-center">
|
<th class="text-center">
|
||||||
Y
|
Y, m
|
||||||
</th>
|
</th>
|
||||||
<th class="text-center">
|
<th class="text-center">
|
||||||
Angle
|
Z Angle, °
|
||||||
|
</th>
|
||||||
|
</template>
|
||||||
|
<template v-if="$store.getters.pipelineType === 4 && $store.getters.currentPipelineSettings.solvePNPEnabled">
|
||||||
|
<th class="text-center">
|
||||||
|
Ambiguity
|
||||||
</th>
|
</th>
|
||||||
</template>
|
</template>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -51,17 +62,29 @@
|
|||||||
:key="index"
|
:key="index"
|
||||||
>
|
>
|
||||||
<td>{{ index }}</td>
|
<td>{{ index }}</td>
|
||||||
|
<td v-if="$store.getters.pipelineType === 4 || (($store.getters.pipelineType - 2) === 3)">
|
||||||
|
{{ parseInt(value.fiducialId) }}
|
||||||
|
</td>
|
||||||
<template v-if="!$store.getters.currentPipelineSettings.solvePNPEnabled">
|
<template v-if="!$store.getters.currentPipelineSettings.solvePNPEnabled">
|
||||||
<td>{{ parseFloat(value.pitch).toFixed(2) }}</td>
|
<td>{{ parseFloat(value.pitch).toFixed(2) }}</td>
|
||||||
<td>{{ parseFloat(value.yaw).toFixed(2) }}</td>
|
<td>{{ parseFloat(value.yaw).toFixed(2) }}</td>
|
||||||
<td>{{ parseFloat(value.skew).toFixed(2) }}</td>
|
<td>{{ parseFloat(value.skew).toFixed(2) }}</td>
|
||||||
|
<td>{{ parseFloat(value.area).toFixed(2) }}</td>
|
||||||
</template>
|
</template>
|
||||||
<td>{{ parseFloat(value.area).toFixed(2) }}</td>
|
<template v-else-if="$store.getters.currentPipelineSettings.solvePNPEnabled && $store.getters.pipelineType === 4">
|
||||||
<template v-if="$store.getters.currentPipelineSettings.solvePNPEnabled">
|
|
||||||
<!-- TODO: Make sure that units are correct -->
|
|
||||||
<td>{{ parseFloat(value.pose.x).toFixed(2) }} m</td>
|
<td>{{ parseFloat(value.pose.x).toFixed(2) }} m</td>
|
||||||
<td>{{ parseFloat(value.pose.y).toFixed(2) }} m</td>
|
<td>{{ parseFloat(value.pose.y).toFixed(2) }} m</td>
|
||||||
<td>{{ parseFloat(value.pose.rot).toFixed(2) }}°</td>
|
<td>{{ (parseFloat(value.pose.angle_z) * 180 / Math.PI).toFixed(2) }}°</td>
|
||||||
|
</template>
|
||||||
|
<template v-else-if="$store.getters.currentPipelineSettings.solvePNPEnabled">
|
||||||
|
<td>{{ parseFloat(value.pose.x).toFixed(2) }} m</td>
|
||||||
|
<td>{{ parseFloat(value.pose.y).toFixed(2) }} m</td>
|
||||||
|
<td>{{ (parseFloat(value.pose.angle_z) * 180 / Math.PI).toFixed(2) }}°</td>
|
||||||
|
</template>
|
||||||
|
<template v-if="$store.getters.pipelineType === 4 && $store.getters.currentPipelineSettings.solvePNPEnabled">
|
||||||
|
<td>
|
||||||
|
{{ parseFloat(value.ambiguity).toFixed(2) }}
|
||||||
|
</td>
|
||||||
</template>
|
</template>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
|
|||||||
@@ -1,66 +1,79 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div :style="{'--averageHue': averageHue}">
|
||||||
<CVrangeSlider
|
<CVrangeSlider
|
||||||
v-model="hsvHue"
|
id="hue-slider"
|
||||||
name="Hue"
|
v-model="hsvHue"
|
||||||
tooltip="Describes color"
|
:class="hueInverted ? 'inverted-slider' : 'normal-slider'"
|
||||||
:min="0"
|
name="Hue"
|
||||||
:max="180"
|
tooltip="Describes color"
|
||||||
@input="handlePipelineData('hsvHue')"
|
:min="0"
|
||||||
@rollback="e => rollback('hue',e)"
|
:max="180"
|
||||||
|
:inverted="hueInverted"
|
||||||
|
@input="handlePipelineData('hsvHue')"
|
||||||
|
@rollback="e => rollback('hue',e)"
|
||||||
/>
|
/>
|
||||||
<CVrangeSlider
|
<CVrangeSlider
|
||||||
v-model="hsvSaturation"
|
id="sat-slider"
|
||||||
name="Saturation"
|
v-model="hsvSaturation"
|
||||||
tooltip="Describes colorfulness; the smaller this value the 'whiter' the color becomes"
|
class="normal-slider"
|
||||||
:min="0"
|
name="Saturation"
|
||||||
:max="255"
|
tooltip="Describes colorfulness; the smaller this value the 'whiter' the color becomes"
|
||||||
@input="handlePipelineData('hsvSaturation')"
|
:min="0"
|
||||||
@rollback="e => rollback('saturation',e)"
|
:max="255"
|
||||||
|
@input="handlePipelineData('hsvSaturation')"
|
||||||
|
@rollback="e => rollback('saturation',e)"
|
||||||
/>
|
/>
|
||||||
<CVrangeSlider
|
<CVrangeSlider
|
||||||
v-model="hsvValue"
|
id="value-slider"
|
||||||
name="Value"
|
v-model="hsvValue"
|
||||||
tooltip="Describes lightness; the smaller this value the 'blacker' the color becomes"
|
class="normal-slider"
|
||||||
:min="0"
|
name="Value"
|
||||||
:max="255"
|
tooltip="Describes lightness; the smaller this value the 'blacker' the color becomes"
|
||||||
@input="handlePipelineData('hsvValue')"
|
:min="0"
|
||||||
@rollback="e => rollback('value',e)"
|
:max="255"
|
||||||
|
@input="handlePipelineData('hsvValue')"
|
||||||
|
@rollback="e => rollback('value',e)"
|
||||||
/>
|
/>
|
||||||
<template v-if="this.currentPipelineType() === 3">
|
<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">
|
||||||
<CVSwitch
|
<CVSwitch
|
||||||
v-model="erode"
|
v-model="erode"
|
||||||
name="Erode"
|
name="Erode"
|
||||||
tooltip="Removes pixels around the edges of white areas in the thresholded image"
|
tooltip="Removes pixels around the edges of white areas in the thresholded image"
|
||||||
@input="handlePipelineData('erode')"
|
@input="handlePipelineData('erode')"
|
||||||
@rollback="e => rollback('erode',e)"
|
@rollback="e => rollback('erode',e)"
|
||||||
/>
|
/>
|
||||||
<CVSwitch
|
<CVSwitch
|
||||||
v-model="dilate"
|
v-model="dilate"
|
||||||
class="mb-0"
|
class="mb-0"
|
||||||
name="Dilate"
|
name="Dilate"
|
||||||
tooltip="Adds pixels around the edges of white areas in the thresholded image"
|
tooltip="Adds pixels around the edges of white areas in the thresholded image"
|
||||||
@input="handlePipelineData('dilate')"
|
@input="handlePipelineData('dilate')"
|
||||||
@rollback="e => rollback('dilate',e)"
|
@rollback="e => rollback('dilate',e)"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
<v-divider class="mb-3 mt-3"/>
|
|
||||||
<div class="pt-3 white--text">
|
<div class="pt-3 white--text">
|
||||||
Color Picker
|
Color Picker
|
||||||
</div>
|
</div>
|
||||||
<v-divider
|
<v-divider
|
||||||
class="mt-3"
|
class="mt-3"
|
||||||
/>
|
/>
|
||||||
<v-row
|
<v-row
|
||||||
justify="center"
|
justify="center"
|
||||||
class="mt-3 mb-3"
|
class="mt-3 mb-3"
|
||||||
>
|
>
|
||||||
<template v-if="!$store.state.colorPicking">
|
<template v-if="!$store.state.colorPicking">
|
||||||
<v-btn
|
<v-btn
|
||||||
color="accent"
|
color="accent"
|
||||||
class="ma-2 black--text"
|
class="ma-2 black--text"
|
||||||
small
|
small
|
||||||
@click="setFunction(3)"
|
@click="setFunction(hueInverted ? 2 : 3)"
|
||||||
>
|
>
|
||||||
<v-icon left>
|
<v-icon left>
|
||||||
mdi-minus
|
mdi-minus
|
||||||
@@ -68,21 +81,21 @@
|
|||||||
Shrink Range
|
Shrink Range
|
||||||
</v-btn>
|
</v-btn>
|
||||||
<v-btn
|
<v-btn
|
||||||
color="accent"
|
color="accent"
|
||||||
class="ma-2 black--text"
|
class="ma-2 black--text"
|
||||||
small
|
small
|
||||||
@click="setFunction(1)"
|
@click="setFunction(1)"
|
||||||
>
|
>
|
||||||
<v-icon left>
|
<v-icon left>
|
||||||
mdi-plus-minus
|
mdi-plus-minus
|
||||||
</v-icon>
|
</v-icon>
|
||||||
Set To Average
|
{{ hueInverted ? "Exclude" : "Set to" }} Average
|
||||||
</v-btn>
|
</v-btn>
|
||||||
<v-btn
|
<v-btn
|
||||||
color="accent"
|
color="accent"
|
||||||
class="ma-2 black--text"
|
class="ma-2 black--text"
|
||||||
small
|
small
|
||||||
@click="setFunction(2)"
|
@click="setFunction(hueInverted ? 3: 2)"
|
||||||
>
|
>
|
||||||
<v-icon left>
|
<v-icon left>
|
||||||
mdi-plus
|
mdi-plus
|
||||||
@@ -92,16 +105,17 @@
|
|||||||
</template>
|
</template>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<v-btn
|
<v-btn
|
||||||
color="accent"
|
color="accent"
|
||||||
class="ma-2 black--text"
|
class="ma-2 black--text"
|
||||||
style="width: 30%;"
|
style="width: 30%;"
|
||||||
small
|
small
|
||||||
@click="setFunction(0)"
|
@click="setFunction(0)"
|
||||||
>
|
>
|
||||||
Cancel
|
Cancel
|
||||||
</v-btn>
|
</v-btn>
|
||||||
</template>
|
</template>
|
||||||
</v-row>
|
</v-row>
|
||||||
|
<v-divider class="mb-3" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -133,9 +147,41 @@ export default {
|
|||||||
this.$store.commit("mutatePipeline", {"hsvHue": val})
|
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: {
|
hsvSaturation: {
|
||||||
get() {
|
get() {
|
||||||
return this.$store.getters.currentPipelineSettings.hsvSaturation
|
return this.$store.getters.currentPipelineSettings.hsvSaturation;
|
||||||
},
|
},
|
||||||
set(val) {
|
set(val) {
|
||||||
this.$store.commit("mutatePipeline", {"hsvSaturation": val})
|
this.$store.commit("mutatePipeline", {"hsvSaturation": val})
|
||||||
@@ -143,15 +189,15 @@ export default {
|
|||||||
},
|
},
|
||||||
hsvValue: {
|
hsvValue: {
|
||||||
get() {
|
get() {
|
||||||
return this.$store.getters.currentPipelineSettings.hsvValue
|
return this.$store.getters.currentPipelineSettings.hsvValue;
|
||||||
},
|
},
|
||||||
set(val) {
|
set(val) {
|
||||||
this.$store.commit("mutatePipeline", {"hsvValue": val})
|
this.$store.commit("mutatePipeline", {"hsvValue": val});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
erode: {
|
erode: {
|
||||||
get() {
|
get() {
|
||||||
return this.$store.getters.currentPipelineSettings.erode
|
return this.$store.getters.currentPipelineSettings.erode;
|
||||||
},
|
},
|
||||||
set(val) {
|
set(val) {
|
||||||
this.$store.commit("mutatePipeline", {"erode": val});
|
this.$store.commit("mutatePipeline", {"erode": val});
|
||||||
@@ -159,7 +205,7 @@ export default {
|
|||||||
},
|
},
|
||||||
dilate: {
|
dilate: {
|
||||||
get() {
|
get() {
|
||||||
return this.$store.getters.currentPipelineSettings.dilate
|
return this.$store.getters.currentPipelineSettings.dilate;
|
||||||
},
|
},
|
||||||
set(val) {
|
set(val) {
|
||||||
this.$store.commit("mutatePipeline", {"dilate": val});
|
this.$store.commit("mutatePipeline", {"dilate": val});
|
||||||
@@ -201,7 +247,7 @@ export default {
|
|||||||
'cameraIndex': this.$store.state.currentCameraIndex
|
'cameraIndex': this.$store.state.currentCameraIndex
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
this.$socket.send(msg);
|
this.$store.state.websocket.ws.send(msg);
|
||||||
this.$emit('update');
|
this.$emit('update');
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -233,3 +279,31 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
</script>
|
</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>
|
||||||
|
|||||||
@@ -37,7 +37,8 @@
|
|||||||
import Networking from './SettingsViews/Networking'
|
import Networking from './SettingsViews/Networking'
|
||||||
import Lighting from "./SettingsViews/Lighting";
|
import Lighting from "./SettingsViews/Lighting";
|
||||||
import cvImage from '../components/common/cv-image'
|
import cvImage from '../components/common/cv-image'
|
||||||
import General from "./SettingsViews/General";
|
import Stats from "./SettingsViews/Stats";
|
||||||
|
import DeviceControl from "./SettingsViews/DeviceControl";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'SettingsTab',
|
name: 'SettingsTab',
|
||||||
@@ -69,7 +70,7 @@
|
|||||||
},
|
},
|
||||||
tabList: {
|
tabList: {
|
||||||
get() {
|
get() {
|
||||||
return [General, Networking].concat(this.$store.state.settings.lighting.supported ? Lighting : []);
|
return [Stats, DeviceControl, Networking].concat(this.$store.state.settings.lighting.supported ? Lighting : []);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
289
photon-client/src/views/SettingsViews/DeviceControl.vue
Normal file
289
photon-client/src/views/SettingsViews/DeviceControl.vue
Normal file
@@ -0,0 +1,289 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<v-row>
|
||||||
|
<v-col cols="12" lg="4" md="6">
|
||||||
|
<v-btn color="red" @click="restartProgram()">
|
||||||
|
<v-icon left>
|
||||||
|
mdi-restart
|
||||||
|
</v-icon>
|
||||||
|
Restart PhotonVision
|
||||||
|
</v-btn>
|
||||||
|
</v-col>
|
||||||
|
<v-col cols="12" lg="4" md="6">
|
||||||
|
<v-btn color="red" @click="restartDevice()">
|
||||||
|
<v-icon left>
|
||||||
|
mdi-restart-alert
|
||||||
|
</v-icon>
|
||||||
|
Restart Device
|
||||||
|
</v-btn>
|
||||||
|
</v-col>
|
||||||
|
<v-col cols="12" lg="4">
|
||||||
|
<v-btn color="secondary" @click="$refs.offlineUpdate.click()">
|
||||||
|
<v-icon left>
|
||||||
|
mdi-update
|
||||||
|
</v-icon>
|
||||||
|
Offline Update
|
||||||
|
</v-btn>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
<v-divider />
|
||||||
|
<v-row>
|
||||||
|
<v-col cols="12" sm="6">
|
||||||
|
<v-btn color="secondary" @click="$refs.exportSettings.click()">
|
||||||
|
<v-icon left>
|
||||||
|
mdi-download
|
||||||
|
</v-icon>
|
||||||
|
Export Settings
|
||||||
|
</v-btn>
|
||||||
|
</v-col>
|
||||||
|
<v-col cols="12" sm="6">
|
||||||
|
<v-btn color="secondary" @click="$refs.importSettings.click()">
|
||||||
|
<v-icon left>
|
||||||
|
mdi-upload
|
||||||
|
</v-icon>
|
||||||
|
Import Settings
|
||||||
|
</v-btn>
|
||||||
|
</v-col>
|
||||||
|
|
||||||
|
<v-col cols="12" sm="6">
|
||||||
|
<v-btn color="secondary" @click="$refs.exportLogFile.click()">
|
||||||
|
<v-icon left>
|
||||||
|
mdi-file
|
||||||
|
</v-icon>
|
||||||
|
Export current log
|
||||||
|
|
||||||
|
<!-- Special hidden link that gets 'clicked' when the user exports journalctl logs -->
|
||||||
|
<a
|
||||||
|
ref="exportLogFile"
|
||||||
|
style="color: black; text-decoration: none; display: none"
|
||||||
|
:href="
|
||||||
|
'http://' +
|
||||||
|
this.$address +
|
||||||
|
'/api/settings/photonvision-journalctl.txt'
|
||||||
|
"
|
||||||
|
download="photonvision-journalctl.txt"
|
||||||
|
/>
|
||||||
|
</v-btn>
|
||||||
|
</v-col>
|
||||||
|
|
||||||
|
<v-col cols="12" sm="6">
|
||||||
|
<v-btn color="secondary" @click="showLogs()">
|
||||||
|
<v-icon left>
|
||||||
|
mdi-bug
|
||||||
|
</v-icon>
|
||||||
|
Show log viewer
|
||||||
|
</v-btn>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
<v-snackbar v-model="snack" top :color="snackbar.color" timeout="-1">
|
||||||
|
<span>{{ snackbar.text }}</span>
|
||||||
|
</v-snackbar>
|
||||||
|
|
||||||
|
<!-- Special hidden upload input that gets 'clicked' when the user imports settings -->
|
||||||
|
<input
|
||||||
|
ref="importSettings"
|
||||||
|
type="file"
|
||||||
|
accept=".zip, .json"
|
||||||
|
style="display: none;"
|
||||||
|
@change="readImportedSettings"
|
||||||
|
/>
|
||||||
|
<!-- Special hidden link that gets 'clicked' when the user exports settings -->
|
||||||
|
<a
|
||||||
|
ref="exportSettings"
|
||||||
|
style="color: black; text-decoration: none; display: none"
|
||||||
|
: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>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
name: "Device Control",
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
snack: false,
|
||||||
|
uploadPercentage: 0.0,
|
||||||
|
snackbar: {
|
||||||
|
color: "success",
|
||||||
|
text: "",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
settings() {
|
||||||
|
return this.$store.state.settings.general;
|
||||||
|
},
|
||||||
|
version() {
|
||||||
|
return `${this.settings.version}`;
|
||||||
|
},
|
||||||
|
hwModel() {
|
||||||
|
if (this.settings.hardwareModel !== "") {
|
||||||
|
return `${this.settings.hardwareModel}`;
|
||||||
|
} else {
|
||||||
|
return `Unknown`;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
platform() {
|
||||||
|
return `${this.settings.hardwarePlatform}`;
|
||||||
|
},
|
||||||
|
gpuAccel() {
|
||||||
|
return `${this.settings.gpuAcceleration ? "Enabled" : "Unsupported"} ${
|
||||||
|
this.settings.gpuAcceleration
|
||||||
|
? "(" + this.settings.gpuAcceleration + ")"
|
||||||
|
: ""
|
||||||
|
}`;
|
||||||
|
},
|
||||||
|
metrics() {
|
||||||
|
// console.log(this.$store.state.metrics);
|
||||||
|
return this.$store.state.metrics;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
restartProgram() {
|
||||||
|
this.axios.post("http://" + this.$address + "/api/restartProgram", {});
|
||||||
|
},
|
||||||
|
restartDevice() {
|
||||||
|
this.axios.post("http://" + this.$address + "/api/restartDevice", {});
|
||||||
|
},
|
||||||
|
readImportedSettings(event) {
|
||||||
|
let formData = new FormData();
|
||||||
|
formData.append("zipData", event.target.files[0]);
|
||||||
|
this.axios
|
||||||
|
.post("http://" + this.$address + "/api/settings/import", formData, {
|
||||||
|
headers: { "Content-Type": "multipart/form-data" },
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
this.snackbar = {
|
||||||
|
color: "success",
|
||||||
|
text:
|
||||||
|
"Settings imported successfully! PhotonVision will restart in the background...",
|
||||||
|
};
|
||||||
|
this.snack = true;
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
if (err.response) {
|
||||||
|
this.snackbar = {
|
||||||
|
color: "error",
|
||||||
|
text:
|
||||||
|
"Error while uploading settings file! Could not process provided file.",
|
||||||
|
};
|
||||||
|
} else if (err.request) {
|
||||||
|
this.snackbar = {
|
||||||
|
color: "error",
|
||||||
|
text:
|
||||||
|
"Error while uploading settings file! No respond to upload attempt.",
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
this.snackbar = {
|
||||||
|
color: "error",
|
||||||
|
text: "Error while uploading settings file!",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
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;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
showLogs(event) {
|
||||||
|
event;
|
||||||
|
this.$store.state.logsOverlay = true;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="css" scoped>
|
||||||
|
.v-btn {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.infoTable {
|
||||||
|
border: 1px solid;
|
||||||
|
border-collapse: separate;
|
||||||
|
border-spacing: 0px;
|
||||||
|
border-radius: 5px;
|
||||||
|
text-align: left;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
width: 100%;
|
||||||
|
display: block;
|
||||||
|
overflow-x: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.infoElem {
|
||||||
|
padding-right: 15px;
|
||||||
|
padding-bottom: 1px;
|
||||||
|
padding-top: 1px;
|
||||||
|
padding-left: 10px;
|
||||||
|
border-right: 1px solid;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -10,46 +10,52 @@
|
|||||||
name="Team Number"
|
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']"
|
: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"
|
class="mb-4"
|
||||||
:label-cols="$vuetify.breakpoint.mdAndUp ? undefined : 7"
|
:label-cols="$vuetify.breakpoint.mdAndUp ? undefined : 5"
|
||||||
/>
|
/>
|
||||||
<v-chip label color="red" v-bind:style="$vuetify.breakpoint.xsOnly ? 'height: auto;' : ''" text-color="white" v-if="parseInt(teamNumber) < 1 && !runNTServer">
|
<v-banner
|
||||||
<span class="text-wrap">
|
v-show="(teamNumber < 1 || teamNumber > 10000) && !runNTServer"
|
||||||
Team number not set! NetworkTables cannot connect.
|
rounded
|
||||||
</span>
|
color="red"
|
||||||
</v-chip>
|
text-color="white"
|
||||||
|
>
|
||||||
|
Team number is unset or invalid. NetworkTables will not be able to connect.
|
||||||
|
</v-banner>
|
||||||
<CVradio
|
<CVradio
|
||||||
v-model="connectionType"
|
v-show="$store.state.settings.networkSettings.shouldManage"
|
||||||
|
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']"
|
:list="['DHCP','Static']"
|
||||||
:disabled="!$store.state.settings.networkSettings.supported"
|
|
||||||
/>
|
/>
|
||||||
<template v-if="!isDHCP">
|
<CVinput
|
||||||
<CVinput
|
v-if="!isDHCP"
|
||||||
v-model="staticIp"
|
v-model="staticIp"
|
||||||
:input-cols="inputCols"
|
:input-cols="inputCols"
|
||||||
:rules="[v => isIPv4(v) || 'Invalid IPv4 address']"
|
:rules="[v => isIPv4(v) || 'Invalid IPv4 address']"
|
||||||
name="IP"
|
name="IP"
|
||||||
/>
|
/>
|
||||||
</template>
|
|
||||||
<CVinput
|
<CVinput
|
||||||
v-model="hostname"
|
v-model="hostname"
|
||||||
:input-cols="inputCols"
|
:input-cols="inputCols"
|
||||||
:rules="[v => isHostname(v) || 'Invalid hostname']"
|
:rules="[v => isHostname(v) || 'Invalid hostname']"
|
||||||
name="Hostname"
|
name="Hostname"
|
||||||
/>
|
/>
|
||||||
Advanced
|
|
||||||
<v-divider/>
|
|
||||||
<CVSwitch
|
<CVSwitch
|
||||||
v-model="runNTServer"
|
v-model="runNTServer"
|
||||||
name="Run NetworkTables Server (Debugging Only!)"
|
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."
|
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"
|
class="mt-3 mb-3"
|
||||||
:text-cols="$vuetify.breakpoint.mdAndUp ? undefined : 7"
|
:text-cols="$vuetify.breakpoint.mdAndUp ? undefined : 5"
|
||||||
/>
|
/>
|
||||||
<v-chip label color="red" text-color="white" v-if="runNTServer">
|
<v-banner
|
||||||
<span>
|
v-show="runNTServer"
|
||||||
Disable this switch if you're on a robot! Photonlib will NOT work.
|
rounded
|
||||||
</span>
|
color="red"
|
||||||
</v-chip>
|
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-form>
|
||||||
<v-btn
|
<v-btn
|
||||||
color="accent"
|
color="accent"
|
||||||
@@ -60,60 +66,101 @@
|
|||||||
>
|
>
|
||||||
Save
|
Save
|
||||||
</v-btn>
|
</v-btn>
|
||||||
<v-divider class="mt-4 mb-4"/>
|
<v-snackbar
|
||||||
<v-row>
|
v-model="snack"
|
||||||
<v-col cols="12" sm="6">
|
top
|
||||||
|
:color="snackbar.color"
|
||||||
|
timeout="5000"
|
||||||
|
>
|
||||||
|
<span>{{ snackbar.text }}</span>
|
||||||
|
</v-snackbar>
|
||||||
|
|
||||||
|
<template v-if="$store.state.settings.networkSettings.shouldManage && false">
|
||||||
|
|
||||||
|
<!-- Advanced controls for changing DHCP settings and stuff -->
|
||||||
|
<v-divider class="mt-4 mb-4" />
|
||||||
|
|
||||||
|
<v-title> Advanced </v-title>
|
||||||
|
|
||||||
|
<CVinput
|
||||||
|
:input-cols="inputCols"
|
||||||
|
name="Set DHCP command"
|
||||||
|
/>
|
||||||
|
<CVinput
|
||||||
|
:input-cols="inputCols"
|
||||||
|
name="Set static command"
|
||||||
|
/>
|
||||||
|
<CVinput
|
||||||
|
:input-cols="inputCols"
|
||||||
|
name="NetworkManager interface"
|
||||||
|
/>
|
||||||
|
<CVinput
|
||||||
|
:input-cols="inputCols"
|
||||||
|
name="Physical interface"
|
||||||
|
/>
|
||||||
|
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- TEMP - RIO finder is not currently enabled
|
||||||
|
<v-row>
|
||||||
|
<v-col
|
||||||
|
cols="12"
|
||||||
|
sm="6"
|
||||||
|
>
|
||||||
<v-simple-table
|
<v-simple-table
|
||||||
fixed-header
|
fixed-header
|
||||||
height="100%"
|
height="100%"
|
||||||
dense
|
dense
|
||||||
>
|
>
|
||||||
<template v-slot:default>
|
<template v-slot:default>
|
||||||
<thead style="font-size: 1.25rem;">
|
<thead style="font-size: 1.25rem;">
|
||||||
<tr>
|
<tr>
|
||||||
<th>
|
<th>
|
||||||
Device IPs
|
Device IPs
|
||||||
</th>
|
</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr
|
<tr
|
||||||
v-for="(value, index) in $store.state.ntConnectionInfo.deviceips"
|
v-for="(value, index) in $store.state.networkInfo.deviceips"
|
||||||
:key="index"
|
:key="index"
|
||||||
>
|
>
|
||||||
<td>{{ value }}</td>
|
<td>{{ value }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</template>
|
</template>
|
||||||
</v-simple-table>
|
</v-simple-table>
|
||||||
</v-col>
|
</v-col>
|
||||||
<v-col cols="12" sm="6">
|
<v-col
|
||||||
|
cols="12"
|
||||||
|
sm="6"
|
||||||
|
>
|
||||||
<v-simple-table
|
<v-simple-table
|
||||||
fixed-header
|
fixed-header
|
||||||
height="100%"
|
height="100%"
|
||||||
dense
|
dense
|
||||||
>
|
>
|
||||||
<template v-slot:default>
|
<template v-slot:default>
|
||||||
<thead style="font-size: 1.25rem;">
|
<thead style="font-size: 1.25rem;">
|
||||||
<tr>
|
<tr>
|
||||||
<th>
|
<th>
|
||||||
Possible RoboRIOs
|
Possible RoboRIOs
|
||||||
</th>
|
</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr
|
<tr
|
||||||
v-for="(value, index) in $store.state.ntConnectionInfo.possibleRios"
|
v-for="(value, index) in $store.state.networkInfo.possibleRios"
|
||||||
:key="index"
|
:key="index"
|
||||||
>
|
>
|
||||||
<td>{{ value }}</td>
|
<td>{{ value }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</template>
|
</template>
|
||||||
</v-simple-table>
|
</v-simple-table>
|
||||||
</v-col>
|
</v-col>
|
||||||
</v-row>
|
</v-row>
|
||||||
|
-->
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -150,7 +197,7 @@ export default {
|
|||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
inputCols() {
|
inputCols() {
|
||||||
return this.$vuetify.breakpoint.smAndUp ? 10 : 7;
|
return this.$vuetify.breakpoint.mdAndUp ? 10 : 7;
|
||||||
},
|
},
|
||||||
isDHCP() {
|
isDHCP() {
|
||||||
return this.settings.connectionType === 0;
|
return this.settings.connectionType === 0;
|
||||||
@@ -225,8 +272,16 @@ export default {
|
|||||||
return true;
|
return true;
|
||||||
},
|
},
|
||||||
sendGeneralSettings() {
|
sendGeneralSettings() {
|
||||||
|
const changingStaticIp = !this.isDHCP;
|
||||||
|
|
||||||
|
this.snackbar = {
|
||||||
|
color: "secondary",
|
||||||
|
text: "Updating settings..."
|
||||||
|
};
|
||||||
|
this.snack = true;
|
||||||
|
|
||||||
this.axios.post("http://" + this.$address + "/api/settings/general", this.settings).then(
|
this.axios.post("http://" + this.$address + "/api/settings/general", this.settings).then(
|
||||||
function (response) {
|
response => {
|
||||||
if (response.status === 200) {
|
if (response.status === 200) {
|
||||||
this.snackbar = {
|
this.snackbar = {
|
||||||
color: "success",
|
color: "success",
|
||||||
@@ -235,11 +290,18 @@ export default {
|
|||||||
this.snack = true;
|
this.snack = true;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
function (error) {
|
error => {
|
||||||
|
if (error.status === 504 || changingStaticIp) {
|
||||||
|
this.snackbar = {
|
||||||
|
color: "error",
|
||||||
|
text: (error.response || {data: `Connection lost! Try the new static IP at ${this.staticIp}:5800 or ${this.hostname}:5800 ?`}).data
|
||||||
|
};
|
||||||
|
} else {
|
||||||
this.snackbar = {
|
this.snackbar = {
|
||||||
color: "error",
|
color: "error",
|
||||||
text: (error.response || {data: "Couldn't save settings"}).data
|
text: (error.response || {data: "Couldn't save settings"}).data
|
||||||
};
|
};
|
||||||
|
}
|
||||||
this.snack = true;
|
this.snack = true;
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -49,22 +49,46 @@
|
|||||||
<th class="infoElem">
|
<th class="infoElem">
|
||||||
Disk Usage
|
Disk Usage
|
||||||
</th>
|
</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>
|
||||||
<tr v-if="metrics.cpuUtil !== 'N/A'">
|
<tr v-if="metrics.cpuUtil !== 'N/A'">
|
||||||
<td class="infoElem">
|
<td class="infoElem">
|
||||||
{{ metrics.cpuUtil.replace(" ", "") }}%
|
{{ metrics.cpuUtil }}%
|
||||||
</td>
|
</td>
|
||||||
<td class="infoElem">
|
<td class="infoElem">
|
||||||
{{ parseInt(metrics.cpuTemp) }}° C
|
{{ parseInt(metrics.cpuTemp) }}° C
|
||||||
</td>
|
</td>
|
||||||
<td class="infoElem">
|
<td class="infoElem">
|
||||||
{{ metrics.ramUtil.replace(" ", "") }}MB of {{ metrics.cpuMem }}MB
|
{{ metrics.ramUtil }}MB of {{ metrics.cpuMem }}MB
|
||||||
</td>
|
</td>
|
||||||
<td class="infoElem">
|
<td class="infoElem">
|
||||||
{{ metrics.gpuMemUtil.replace(" ", "") }}MB of {{ metrics.gpuMem }}MB
|
{{ metrics.gpuMemUtil }}MB of {{ metrics.gpuMem }}MB
|
||||||
</td>
|
</td>
|
||||||
<td class="infoElem">
|
<td class="infoElem">
|
||||||
{{ metrics.diskUtilPct.replace(" ", "") }}
|
{{ metrics.diskUtilPct }}
|
||||||
|
</td>
|
||||||
|
<td class="infoElem">
|
||||||
|
{{ metrics.cpuThr }}
|
||||||
|
</td>
|
||||||
|
<td class="infoElem">
|
||||||
|
{{ metrics.cpuUptime }}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr v-if="metrics.cpuUtil === 'N/A'">
|
<tr v-if="metrics.cpuUtil === 'N/A'">
|
||||||
@@ -83,89 +107,21 @@
|
|||||||
<td class="infoElem">
|
<td class="infoElem">
|
||||||
---
|
---
|
||||||
</td>
|
</td>
|
||||||
|
<td class="infoElem">
|
||||||
|
---
|
||||||
|
</td>
|
||||||
|
<td class="infoElem">
|
||||||
|
---
|
||||||
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
</v-row>
|
</v-row>
|
||||||
|
|
||||||
<v-row>
|
|
||||||
<v-col
|
|
||||||
cols="12"
|
|
||||||
sm="6"
|
|
||||||
md="4"
|
|
||||||
>
|
|
||||||
<v-btn
|
|
||||||
color="secondary"
|
|
||||||
@click="$refs.exportSettings.click()"
|
|
||||||
>
|
|
||||||
<v-icon left>
|
|
||||||
mdi-download
|
|
||||||
</v-icon>
|
|
||||||
Export Settings
|
|
||||||
</v-btn>
|
|
||||||
</v-col>
|
|
||||||
<v-col
|
|
||||||
cols="12"
|
|
||||||
sm="6"
|
|
||||||
md="4"
|
|
||||||
>
|
|
||||||
<v-btn
|
|
||||||
color="secondary"
|
|
||||||
@click="$refs.importSettings.click()"
|
|
||||||
>
|
|
||||||
<v-icon left>
|
|
||||||
mdi-upload
|
|
||||||
</v-icon>
|
|
||||||
Import Settings
|
|
||||||
</v-btn>
|
|
||||||
</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"
|
|
||||||
>
|
|
||||||
<v-btn
|
|
||||||
color="red"
|
|
||||||
@click="restartProgram()"
|
|
||||||
>
|
|
||||||
<v-icon left>
|
|
||||||
mdi-restart
|
|
||||||
</v-icon>
|
|
||||||
Restart Photon
|
|
||||||
</v-btn>
|
|
||||||
</v-col>
|
|
||||||
<v-col
|
|
||||||
cols="12"
|
|
||||||
lg="6"
|
|
||||||
>
|
|
||||||
<v-btn
|
|
||||||
color="red"
|
|
||||||
@click="restartDevice()"
|
|
||||||
>
|
|
||||||
<v-icon left>
|
|
||||||
mdi-restart
|
|
||||||
</v-icon>
|
|
||||||
Restart Device
|
|
||||||
</v-btn>
|
|
||||||
</v-col>
|
|
||||||
</v-row>
|
|
||||||
<v-snackbar
|
<v-snackbar
|
||||||
v-model="snack"
|
v-model="snack"
|
||||||
top
|
top
|
||||||
:color="snackbar.color"
|
:color="snackbar.color"
|
||||||
timeout="0"
|
timeout="-1"
|
||||||
>
|
>
|
||||||
<span>{{ snackbar.text }}</span>
|
<span>{{ snackbar.text }}</span>
|
||||||
</v-snackbar>
|
</v-snackbar>
|
||||||
@@ -200,7 +156,7 @@
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
export default {
|
export default {
|
||||||
name: 'General',
|
name: 'Stats',
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
snack: false,
|
snack: false,
|
||||||
@@ -232,8 +188,8 @@ export default {
|
|||||||
return `${this.settings.gpuAcceleration ? "Enabled" : "Unsupported"} ${this.settings.gpuAcceleration ? "(" + this.settings.gpuAcceleration + ")" : ""}`
|
return `${this.settings.gpuAcceleration ? "Enabled" : "Unsupported"} ${this.settings.gpuAcceleration ? "(" + this.settings.gpuAcceleration + ")" : ""}`
|
||||||
},
|
},
|
||||||
metrics() {
|
metrics() {
|
||||||
console.log(this.$store.state.metrics);
|
// console.log(this.$store.state.metrics);
|
||||||
return this.$store.state.metrics;
|
return this.$store.state.metrics;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
@@ -319,6 +275,10 @@ export default {
|
|||||||
this.snack = true;
|
this.snack = true;
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
showLogs(event) {
|
||||||
|
event;
|
||||||
|
this.$store.state.logsOverlay = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
2
photon-core/.gitignore
vendored
2
photon-core/.gitignore
vendored
@@ -9,5 +9,7 @@ build
|
|||||||
build/*
|
build/*
|
||||||
photonvision/*
|
photonvision/*
|
||||||
photonvision_config/*
|
photonvision_config/*
|
||||||
|
photon-server/lib/*
|
||||||
|
photon-server/package-lock.json
|
||||||
|
|
||||||
src/main/java/org/photonvision/PhotonVersion.java
|
src/main/java/org/photonvision/PhotonVersion.java
|
||||||
|
|||||||
@@ -1,3 +1,7 @@
|
|||||||
|
plugins {
|
||||||
|
id 'edu.wpi.first.WpilibTools' version '1.0.0'
|
||||||
|
}
|
||||||
|
|
||||||
import java.nio.file.Path
|
import java.nio.file.Path
|
||||||
|
|
||||||
apply from: "${rootDir}/shared/common.gradle"
|
apply from: "${rootDir}/shared/common.gradle"
|
||||||
@@ -10,9 +14,6 @@ dependencies {
|
|||||||
implementation 'org.msgpack:msgpack-core:0.9.0'
|
implementation 'org.msgpack:msgpack-core:0.9.0'
|
||||||
implementation 'org.msgpack:jackson-dataformat-msgpack:0.9.0'
|
implementation 'org.msgpack:jackson-dataformat-msgpack:0.9.0'
|
||||||
|
|
||||||
// wpiutil
|
|
||||||
jniPlatforms.each { implementation "edu.wpi.first.wpiutil:wpiutil-jni:$wpilibVersion:$it" }
|
|
||||||
|
|
||||||
// JOGL stuff (currently we only distribute for aarch64, which is Pi 4)
|
// JOGL stuff (currently we only distribute for aarch64, which is Pi 4)
|
||||||
implementation "org.jogamp.gluegen:gluegen-rt:$joglVersion"
|
implementation "org.jogamp.gluegen:gluegen-rt:$joglVersion"
|
||||||
implementation "org.jogamp.jogl:jogl-all:$joglVersion"
|
implementation "org.jogamp.jogl:jogl-all:$joglVersion"
|
||||||
@@ -22,11 +23,35 @@ dependencies {
|
|||||||
|
|
||||||
// Zip
|
// Zip
|
||||||
implementation 'org.zeroturnaround:zt-zip:1.14'
|
implementation 'org.zeroturnaround:zt-zip:1.14'
|
||||||
|
|
||||||
|
implementation wpilibTools.deps.wpilibJava("apriltag")
|
||||||
}
|
}
|
||||||
|
|
||||||
task writeCurrentVersionJava {
|
task writeCurrentVersionJava {
|
||||||
writePhotonVersionFile(Path.of("$projectDir", "src", "main", "java", "org", "photonvision", "PhotonVersion.java"),
|
def versionFileIn = file("${rootDir}/shared/PhotonVersion.java.in")
|
||||||
|
writePhotonVersionFile(versionFileIn, Path.of("$projectDir", "src", "main", "java", "org", "photonvision", "PhotonVersion.java"),
|
||||||
versionString)
|
versionString)
|
||||||
}
|
}
|
||||||
|
|
||||||
build.dependsOn writeCurrentVersionJava
|
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")
|
||||||
|
|||||||
@@ -14,6 +14,7 @@
|
|||||||
* You should have received a copy of the GNU General Public License
|
* You should have received a copy of the GNU General Public License
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package org.photonvision.common;
|
package org.photonvision.common;
|
||||||
|
|
||||||
public enum ProgramStatus {
|
public enum ProgramStatus {
|
||||||
|
|||||||
@@ -14,13 +14,14 @@
|
|||||||
* You should have received a copy of the GNU General Public License
|
* You should have received a copy of the GNU General Public License
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package org.photonvision.common.configuration;
|
package org.photonvision.common.configuration;
|
||||||
|
|
||||||
import com.fasterxml.jackson.annotation.JsonCreator;
|
import com.fasterxml.jackson.annotation.JsonCreator;
|
||||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||||
import edu.wpi.first.math.geometry.Rotation2d;
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
import java.util.Arrays;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import org.photonvision.common.logging.LogGroup;
|
import org.photonvision.common.logging.LogGroup;
|
||||||
import org.photonvision.common.logging.Logger;
|
import org.photonvision.common.logging.Logger;
|
||||||
@@ -45,11 +46,12 @@ public class CameraConfiguration {
|
|||||||
/** Can be either path (ex /dev/videoX) or index (ex 1). */
|
/** Can be either path (ex /dev/videoX) or index (ex 1). */
|
||||||
public String path = "";
|
public String path = "";
|
||||||
|
|
||||||
|
@JsonIgnore public String[] otherPaths = {};
|
||||||
|
|
||||||
public CameraType cameraType = CameraType.UsbCamera;
|
public CameraType cameraType = CameraType.UsbCamera;
|
||||||
public double FOV = 70;
|
public double FOV = 70;
|
||||||
public final List<CameraCalibrationCoefficients> calibrations;
|
public final List<CameraCalibrationCoefficients> calibrations;
|
||||||
public int currentPipelineIndex = 0;
|
public int currentPipelineIndex = 0;
|
||||||
public Rotation2d camPitch = new Rotation2d();
|
|
||||||
|
|
||||||
public int streamIndex = 0; // 0 index means ports [1181, 1182], 1 means [1183, 1184], etc...
|
public int streamIndex = 0; // 0 index means ports [1181, 1182], 1 means [1183, 1184], etc...
|
||||||
|
|
||||||
@@ -60,19 +62,22 @@ public class CameraConfiguration {
|
|||||||
public DriverModePipelineSettings driveModeSettings = new DriverModePipelineSettings();
|
public DriverModePipelineSettings driveModeSettings = new DriverModePipelineSettings();
|
||||||
|
|
||||||
public CameraConfiguration(String baseName, String path) {
|
public CameraConfiguration(String baseName, String path) {
|
||||||
this(baseName, baseName, baseName, path);
|
this(baseName, baseName, baseName, path, new String[0]);
|
||||||
}
|
}
|
||||||
|
|
||||||
public CameraConfiguration(String baseName, String uniqueName, String nickname, String path) {
|
public CameraConfiguration(
|
||||||
|
String baseName, String uniqueName, String nickname, String path, String[] alternates) {
|
||||||
this.baseName = baseName;
|
this.baseName = baseName;
|
||||||
this.uniqueName = uniqueName;
|
this.uniqueName = uniqueName;
|
||||||
this.nickname = nickname;
|
this.nickname = nickname;
|
||||||
this.path = path;
|
this.path = path;
|
||||||
this.calibrations = new ArrayList<>();
|
this.calibrations = new ArrayList<>();
|
||||||
|
this.otherPaths = alternates;
|
||||||
|
|
||||||
logger.debug(
|
logger.debug(
|
||||||
"Creating USB camera configuration for "
|
"Creating USB camera configuration for "
|
||||||
+ cameraType
|
+ cameraType
|
||||||
|
+ " "
|
||||||
+ baseName
|
+ baseName
|
||||||
+ " (AKA "
|
+ " (AKA "
|
||||||
+ nickname
|
+ nickname
|
||||||
@@ -89,8 +94,7 @@ public class CameraConfiguration {
|
|||||||
@JsonProperty("path") String path,
|
@JsonProperty("path") String path,
|
||||||
@JsonProperty("cameraType") CameraType cameraType,
|
@JsonProperty("cameraType") CameraType cameraType,
|
||||||
@JsonProperty("calibration") List<CameraCalibrationCoefficients> calibrations,
|
@JsonProperty("calibration") List<CameraCalibrationCoefficients> calibrations,
|
||||||
@JsonProperty("currentPipelineIndex") int currentPipelineIndex,
|
@JsonProperty("currentPipelineIndex") int currentPipelineIndex) {
|
||||||
@JsonProperty("camPitch") Rotation2d camPitch) {
|
|
||||||
this.baseName = baseName;
|
this.baseName = baseName;
|
||||||
this.uniqueName = uniqueName;
|
this.uniqueName = uniqueName;
|
||||||
this.nickname = nickname;
|
this.nickname = nickname;
|
||||||
@@ -99,11 +103,11 @@ public class CameraConfiguration {
|
|||||||
this.cameraType = cameraType;
|
this.cameraType = cameraType;
|
||||||
this.calibrations = calibrations != null ? calibrations : new ArrayList<>();
|
this.calibrations = calibrations != null ? calibrations : new ArrayList<>();
|
||||||
this.currentPipelineIndex = currentPipelineIndex;
|
this.currentPipelineIndex = currentPipelineIndex;
|
||||||
this.camPitch = camPitch;
|
|
||||||
|
|
||||||
logger.debug(
|
logger.debug(
|
||||||
"Creating camera configuration for "
|
"Creating camera configuration for "
|
||||||
+ cameraType
|
+ cameraType
|
||||||
|
+ " "
|
||||||
+ baseName
|
+ baseName
|
||||||
+ " (AKA "
|
+ " (AKA "
|
||||||
+ nickname
|
+ nickname
|
||||||
@@ -146,4 +150,33 @@ public class CameraConfiguration {
|
|||||||
.ifPresent(calibrations::remove);
|
.ifPresent(calibrations::remove);
|
||||||
calibrations.add(calibration);
|
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
|
||||||
|
+ "]";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,6 +14,7 @@
|
|||||||
* You should have received a copy of the GNU General Public License
|
* You should have received a copy of the GNU General Public License
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package org.photonvision.common.configuration;
|
package org.photonvision.common.configuration;
|
||||||
|
|
||||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||||
@@ -31,7 +32,6 @@ import java.util.*;
|
|||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
import org.photonvision.common.logging.LogGroup;
|
import org.photonvision.common.logging.LogGroup;
|
||||||
import org.photonvision.common.logging.Logger;
|
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.FileUtils;
|
||||||
import org.photonvision.common.util.file.JacksonUtils;
|
import org.photonvision.common.util.file.JacksonUtils;
|
||||||
import org.photonvision.vision.pipeline.CVPipelineSettings;
|
import org.photonvision.vision.pipeline.CVPipelineSettings;
|
||||||
@@ -56,6 +56,7 @@ public class ConfigManager {
|
|||||||
final File configDirectoryFile;
|
final File configDirectoryFile;
|
||||||
|
|
||||||
private long saveRequestTimestamp = -1;
|
private long saveRequestTimestamp = -1;
|
||||||
|
private Thread settingsSaveThread;
|
||||||
|
|
||||||
public static ConfigManager getInstance() {
|
public static ConfigManager getInstance() {
|
||||||
if (INSTANCE == null) {
|
if (INSTANCE == null) {
|
||||||
@@ -96,7 +97,8 @@ public class ConfigManager {
|
|||||||
new File(Path.of(configDirectoryFile.toString(), NET_SET_FNAME).toUri());
|
new File(Path.of(configDirectoryFile.toString(), NET_SET_FNAME).toUri());
|
||||||
this.camerasFolder = new File(Path.of(configDirectoryFile.toString(), "cameras").toUri());
|
this.camerasFolder = new File(Path.of(configDirectoryFile.toString(), "cameras").toUri());
|
||||||
|
|
||||||
TimedTaskManager.getInstance().addTask("ConfigManager", this::checkSaveAndWrite, 1000);
|
settingsSaveThread = new Thread(this::saveAndWriteTask);
|
||||||
|
settingsSaveThread.start();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void load() {
|
public void load() {
|
||||||
@@ -424,12 +426,24 @@ public class ConfigManager {
|
|||||||
saveRequestTimestamp = System.currentTimeMillis();
|
saveRequestTimestamp = System.currentTimeMillis();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void checkSaveAndWrite() {
|
private void saveAndWriteTask() {
|
||||||
// Only save if 1 second has past since the request was made
|
// Only save if 1 second has past since the request was made
|
||||||
if (saveRequestTimestamp > 0 && (System.currentTimeMillis() - saveRequestTimestamp) > 1000L) {
|
while (!Thread.currentThread().isInterrupted()) {
|
||||||
saveRequestTimestamp = -1;
|
if (saveRequestTimestamp > 0 && (System.currentTimeMillis() - saveRequestTimestamp) > 1000L) {
|
||||||
logger.debug("Saving to disk...");
|
saveRequestTimestamp = -1;
|
||||||
saveToDisk();
|
logger.debug("Saving to disk...");
|
||||||
|
saveToDisk();
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
Thread.sleep(1000);
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
logger.error("Exception waiting for settings semaphore", e);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void unloadCameraConfigs() {
|
||||||
|
this.config.getCameraConfigurations().clear();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,6 +14,7 @@
|
|||||||
* You should have received a copy of the GNU General Public License
|
* You should have received a copy of the GNU General Public License
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package org.photonvision.common.configuration;
|
package org.photonvision.common.configuration;
|
||||||
|
|
||||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||||
@@ -40,6 +41,8 @@ public class HardwareConfig {
|
|||||||
public final String cpuTempCommand;
|
public final String cpuTempCommand;
|
||||||
public final String cpuMemoryCommand;
|
public final String cpuMemoryCommand;
|
||||||
public final String cpuUtilCommand;
|
public final String cpuUtilCommand;
|
||||||
|
public final String cpuThrottleReasonCmd;
|
||||||
|
public final String cpuUptimeCommand;
|
||||||
public final String gpuMemoryCommand;
|
public final String gpuMemoryCommand;
|
||||||
public final String ramUtilCommand;
|
public final String ramUtilCommand;
|
||||||
public final String gpuMemUsageCommand;
|
public final String gpuMemUsageCommand;
|
||||||
@@ -64,6 +67,8 @@ public class HardwareConfig {
|
|||||||
cpuTempCommand = "";
|
cpuTempCommand = "";
|
||||||
cpuMemoryCommand = "";
|
cpuMemoryCommand = "";
|
||||||
cpuUtilCommand = "";
|
cpuUtilCommand = "";
|
||||||
|
cpuThrottleReasonCmd = "";
|
||||||
|
cpuUptimeCommand = "";
|
||||||
gpuMemoryCommand = "";
|
gpuMemoryCommand = "";
|
||||||
ramUtilCommand = "";
|
ramUtilCommand = "";
|
||||||
ledBlinkCommand = "";
|
ledBlinkCommand = "";
|
||||||
@@ -90,6 +95,8 @@ public class HardwareConfig {
|
|||||||
String cpuTempCommand,
|
String cpuTempCommand,
|
||||||
String cpuMemoryCommand,
|
String cpuMemoryCommand,
|
||||||
String cpuUtilCommand,
|
String cpuUtilCommand,
|
||||||
|
String cpuThrottleReasonCmd,
|
||||||
|
String cpuUptimeCommand,
|
||||||
String gpuMemoryCommand,
|
String gpuMemoryCommand,
|
||||||
String ramUtilCommand,
|
String ramUtilCommand,
|
||||||
String gpuMemUsageCommand,
|
String gpuMemUsageCommand,
|
||||||
@@ -110,6 +117,8 @@ public class HardwareConfig {
|
|||||||
this.cpuTempCommand = cpuTempCommand;
|
this.cpuTempCommand = cpuTempCommand;
|
||||||
this.cpuMemoryCommand = cpuMemoryCommand;
|
this.cpuMemoryCommand = cpuMemoryCommand;
|
||||||
this.cpuUtilCommand = cpuUtilCommand;
|
this.cpuUtilCommand = cpuUtilCommand;
|
||||||
|
this.cpuThrottleReasonCmd = cpuThrottleReasonCmd;
|
||||||
|
this.cpuUptimeCommand = cpuUptimeCommand;
|
||||||
this.gpuMemoryCommand = gpuMemoryCommand;
|
this.gpuMemoryCommand = gpuMemoryCommand;
|
||||||
this.ramUtilCommand = ramUtilCommand;
|
this.ramUtilCommand = ramUtilCommand;
|
||||||
this.gpuMemUsageCommand = gpuMemUsageCommand;
|
this.gpuMemUsageCommand = gpuMemUsageCommand;
|
||||||
@@ -119,7 +128,22 @@ public class HardwareConfig {
|
|||||||
this.blacklistedResIndices = blacklistedResIndices;
|
this.blacklistedResIndices = blacklistedResIndices;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** @return True if the FOV has been preset to a sane value, false otherwise */
|
||||||
public final boolean hasPresetFOV() {
|
public final boolean hasPresetFOV() {
|
||||||
return vendorFOV > 0;
|
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 != "";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,6 +14,7 @@
|
|||||||
* You should have received a copy of the GNU General Public License
|
* You should have received a copy of the GNU General Public License
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package org.photonvision.common.configuration;
|
package org.photonvision.common.configuration;
|
||||||
|
|
||||||
public class HardwareSettings {
|
public class HardwareSettings {
|
||||||
|
|||||||
@@ -14,16 +14,20 @@
|
|||||||
* You should have received a copy of the GNU General Public License
|
* You should have received a copy of the GNU General Public License
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package org.photonvision.common.configuration;
|
package org.photonvision.common.configuration;
|
||||||
|
|
||||||
import com.fasterxml.jackson.annotation.JsonCreator;
|
import com.fasterxml.jackson.annotation.JsonCreator;
|
||||||
import com.fasterxml.jackson.annotation.JsonGetter;
|
import com.fasterxml.jackson.annotation.JsonGetter;
|
||||||
|
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||||
import com.fasterxml.jackson.annotation.JsonSetter;
|
import com.fasterxml.jackson.annotation.JsonSetter;
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import org.photonvision.common.hardware.Platform;
|
import org.photonvision.common.hardware.Platform;
|
||||||
import org.photonvision.common.networking.NetworkMode;
|
import org.photonvision.common.networking.NetworkMode;
|
||||||
|
import org.photonvision.common.util.file.JacksonUtils;
|
||||||
|
|
||||||
public class NetworkConfig {
|
public class NetworkConfig {
|
||||||
public int teamNumber = 0;
|
public int teamNumber = 0;
|
||||||
@@ -32,6 +36,16 @@ public class NetworkConfig {
|
|||||||
public String hostname = "photonvision";
|
public String hostname = "photonvision";
|
||||||
public boolean runNTServer = false;
|
public boolean runNTServer = false;
|
||||||
|
|
||||||
|
@JsonIgnore public static final String NM_IFACE_STRING = "${interface}";
|
||||||
|
@JsonIgnore public static final String NM_IP_STRING = "${ipaddr}";
|
||||||
|
|
||||||
|
public String networkManagerIface = "Wired\\ connection\\ 1";
|
||||||
|
public String physicalInterface = "eth0";
|
||||||
|
public String setStaticCommand =
|
||||||
|
"nmcli con mod ${interface} ipv4.addresses ${ipaddr}/8 ipv4.method \"manual\" ipv6.method \"disabled\"";
|
||||||
|
public String setDHCPcommand =
|
||||||
|
"nmcli con mod ${interface} ipv4.method \"auto\" ipv6.method \"disabled\"";
|
||||||
|
|
||||||
private boolean shouldManage;
|
private boolean shouldManage;
|
||||||
|
|
||||||
public NetworkConfig() {
|
public NetworkConfig() {
|
||||||
@@ -45,46 +59,48 @@ public class NetworkConfig {
|
|||||||
@JsonProperty("staticIp") String staticIp,
|
@JsonProperty("staticIp") String staticIp,
|
||||||
@JsonProperty("hostname") String hostname,
|
@JsonProperty("hostname") String hostname,
|
||||||
@JsonProperty("runNTServer") boolean runNTServer,
|
@JsonProperty("runNTServer") boolean runNTServer,
|
||||||
@JsonProperty("shouldManage") boolean shouldManage) {
|
@JsonProperty("shouldManage") boolean shouldManage,
|
||||||
|
@JsonProperty("networkManagerIface") String networkManagerIface,
|
||||||
|
@JsonProperty("physicalInterface") String physicalInterface,
|
||||||
|
@JsonProperty("setStaticCommand") String setStaticCommand,
|
||||||
|
@JsonProperty("setDHCPcommand") String setDHCPcommand) {
|
||||||
this.teamNumber = teamNumber;
|
this.teamNumber = teamNumber;
|
||||||
this.connectionType = connectionType;
|
this.connectionType = connectionType;
|
||||||
this.staticIp = staticIp;
|
this.staticIp = staticIp;
|
||||||
this.hostname = hostname;
|
this.hostname = hostname;
|
||||||
this.runNTServer = runNTServer;
|
this.runNTServer = runNTServer;
|
||||||
|
this.networkManagerIface = networkManagerIface;
|
||||||
|
this.physicalInterface = physicalInterface;
|
||||||
|
this.setStaticCommand = setStaticCommand;
|
||||||
|
this.setDHCPcommand = setDHCPcommand;
|
||||||
setShouldManage(shouldManage);
|
setShouldManage(shouldManage);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static NetworkConfig fromHashMap(Map<String, Object> map) {
|
public static NetworkConfig fromHashMap(Map<String, Object> map) {
|
||||||
// teamNumber (int), supported (bool), connectionType (int),
|
try {
|
||||||
// staticIp (str), netmask (str), hostname (str)
|
return new ObjectMapper().convertValue(map, NetworkConfig.class);
|
||||||
var ret = new NetworkConfig();
|
} catch (Exception e) {
|
||||||
ret.teamNumber = Integer.parseInt(map.get("teamNumber").toString());
|
e.printStackTrace();
|
||||||
ret.connectionType = NetworkMode.values()[(Integer) map.get("connectionType")];
|
return new NetworkConfig();
|
||||||
ret.staticIp = (String) map.get("staticIp");
|
}
|
||||||
ret.hostname = (String) map.get("hostname");
|
|
||||||
ret.runNTServer = (Boolean) map.get("runNTServer");
|
|
||||||
ret.setShouldManage((Boolean) map.get("supported"));
|
|
||||||
return ret;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public HashMap<String, Object> toHashMap() {
|
public Map<String, Object> toHashMap() {
|
||||||
HashMap<String, Object> tmp = new HashMap<>();
|
try {
|
||||||
tmp.put("teamNumber", teamNumber);
|
return new ObjectMapper().convertValue(this, JacksonUtils.UIMap.class);
|
||||||
tmp.put("supported", shouldManage());
|
} catch (Exception e) {
|
||||||
tmp.put("connectionType", connectionType.ordinal());
|
e.printStackTrace();
|
||||||
tmp.put("staticIp", staticIp);
|
return new HashMap<>();
|
||||||
tmp.put("hostname", hostname);
|
}
|
||||||
tmp.put("runNTServer", runNTServer);
|
|
||||||
return tmp;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@JsonGetter("shouldManage")
|
@JsonGetter("shouldManage")
|
||||||
public boolean shouldManage() {
|
public boolean shouldManage() {
|
||||||
return this.shouldManage || Platform.isRaspberryPi();
|
return this.shouldManage || Platform.isLinux();
|
||||||
}
|
}
|
||||||
|
|
||||||
@JsonSetter("shouldManage")
|
@JsonSetter("shouldManage")
|
||||||
public void setShouldManage(boolean shouldManage) {
|
public void setShouldManage(boolean shouldManage) {
|
||||||
this.shouldManage = shouldManage || Platform.isRaspberryPi();
|
this.shouldManage = shouldManage || Platform.isLinux();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,6 +14,7 @@
|
|||||||
* You should have received a copy of the GNU General Public License
|
* You should have received a copy of the GNU General Public License
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package org.photonvision.common.configuration;
|
package org.photonvision.common.configuration;
|
||||||
|
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
@@ -24,7 +25,7 @@ import java.util.stream.Collectors;
|
|||||||
import org.photonvision.PhotonVersion;
|
import org.photonvision.PhotonVersion;
|
||||||
import org.photonvision.common.hardware.Platform;
|
import org.photonvision.common.hardware.Platform;
|
||||||
import org.photonvision.common.util.SerializationUtils;
|
import org.photonvision.common.util.SerializationUtils;
|
||||||
import org.photonvision.raspi.PicamJNI;
|
import org.photonvision.raspi.LibCameraJNI;
|
||||||
import org.photonvision.vision.processes.VisionModule;
|
import org.photonvision.vision.processes.VisionModule;
|
||||||
import org.photonvision.vision.processes.VisionModuleManager;
|
import org.photonvision.vision.processes.VisionModuleManager;
|
||||||
import org.photonvision.vision.processes.VisionSource;
|
import org.photonvision.vision.processes.VisionSource;
|
||||||
@@ -109,11 +110,11 @@ public class PhotonConfiguration {
|
|||||||
generalSubmap.put("version", PhotonVersion.versionString);
|
generalSubmap.put("version", PhotonVersion.versionString);
|
||||||
generalSubmap.put(
|
generalSubmap.put(
|
||||||
"gpuAcceleration",
|
"gpuAcceleration",
|
||||||
PicamJNI.isSupported()
|
LibCameraJNI.isSupported()
|
||||||
? "Zerocopy MMAL on " + PicamJNI.getSensorModel().getFriendlyName()
|
? "Zerocopy Libcamera on " + LibCameraJNI.getSensorModel().getFriendlyName()
|
||||||
: ""); // TODO add support for other types of GPU accel
|
: ""); // TODO add support for other types of GPU accel
|
||||||
generalSubmap.put("hardwareModel", hardwareConfig.deviceName);
|
generalSubmap.put("hardwareModel", hardwareConfig.deviceName);
|
||||||
generalSubmap.put("hardwarePlatform", Platform.getCurrentPlatform().toString());
|
generalSubmap.put("hardwarePlatform", Platform.getPlatformName());
|
||||||
settingsSubmap.put("general", generalSubmap);
|
settingsSubmap.put("general", generalSubmap);
|
||||||
|
|
||||||
map.put("settings", settingsSubmap);
|
map.put("settings", settingsSubmap);
|
||||||
@@ -127,7 +128,8 @@ public class PhotonConfiguration {
|
|||||||
|
|
||||||
public static class UICameraConfiguration {
|
public static class UICameraConfiguration {
|
||||||
@SuppressWarnings("unused")
|
@SuppressWarnings("unused")
|
||||||
public double fov, tiltDegrees;
|
public double fov;
|
||||||
|
|
||||||
public String nickname;
|
public String nickname;
|
||||||
public HashMap<String, Object> currentPipelineSettings;
|
public HashMap<String, Object> currentPipelineSettings;
|
||||||
public int currentPipelineIndex;
|
public int currentPipelineIndex;
|
||||||
|
|||||||
@@ -14,6 +14,7 @@
|
|||||||
* You should have received a copy of the GNU General Public License
|
* You should have received a copy of the GNU General Public License
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package org.photonvision.common.dataflow;
|
package org.photonvision.common.dataflow;
|
||||||
|
|
||||||
import java.util.function.Consumer;
|
import java.util.function.Consumer;
|
||||||
|
|||||||
@@ -14,6 +14,7 @@
|
|||||||
* You should have received a copy of the GNU General Public License
|
* You should have received a copy of the GNU General Public License
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package org.photonvision.common.dataflow;
|
package org.photonvision.common.dataflow;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
|||||||
@@ -14,6 +14,7 @@
|
|||||||
* You should have received a copy of the GNU General Public License
|
* You should have received a copy of the GNU General Public License
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package org.photonvision.common.dataflow;
|
package org.photonvision.common.dataflow;
|
||||||
|
|
||||||
import java.util.concurrent.BlockingQueue;
|
import java.util.concurrent.BlockingQueue;
|
||||||
|
|||||||
@@ -14,6 +14,7 @@
|
|||||||
* You should have received a copy of the GNU General Public License
|
* You should have received a copy of the GNU General Public License
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package org.photonvision.common.dataflow;
|
package org.photonvision.common.dataflow;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
|||||||
@@ -14,6 +14,7 @@
|
|||||||
* You should have received a copy of the GNU General Public License
|
* You should have received a copy of the GNU General Public License
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package org.photonvision.common.dataflow;
|
package org.photonvision.common.dataflow;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|||||||
@@ -14,6 +14,7 @@
|
|||||||
* You should have received a copy of the GNU General Public License
|
* You should have received a copy of the GNU General Public License
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package org.photonvision.common.dataflow.events;
|
package org.photonvision.common.dataflow.events;
|
||||||
|
|
||||||
import org.photonvision.common.dataflow.DataChangeDestination;
|
import org.photonvision.common.dataflow.DataChangeDestination;
|
||||||
|
|||||||
@@ -14,6 +14,7 @@
|
|||||||
* You should have received a copy of the GNU General Public License
|
* You should have received a copy of the GNU General Public License
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package org.photonvision.common.dataflow.events;
|
package org.photonvision.common.dataflow.events;
|
||||||
|
|
||||||
import org.photonvision.common.dataflow.DataChangeDestination;
|
import org.photonvision.common.dataflow.DataChangeDestination;
|
||||||
|
|||||||
@@ -14,6 +14,7 @@
|
|||||||
* You should have received a copy of the GNU General Public License
|
* You should have received a copy of the GNU General Public License
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package org.photonvision.common.dataflow.events;
|
package org.photonvision.common.dataflow.events;
|
||||||
|
|
||||||
import io.javalin.websocket.WsContext;
|
import io.javalin.websocket.WsContext;
|
||||||
|
|||||||
@@ -14,6 +14,7 @@
|
|||||||
* You should have received a copy of the GNU General Public License
|
* You should have received a copy of the GNU General Public License
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package org.photonvision.common.dataflow.events;
|
package org.photonvision.common.dataflow.events;
|
||||||
|
|
||||||
import io.javalin.websocket.WsContext;
|
import io.javalin.websocket.WsContext;
|
||||||
|
|||||||
@@ -14,24 +14,32 @@
|
|||||||
* You should have received a copy of the GNU General Public License
|
* You should have received a copy of the GNU General Public License
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package org.photonvision.common.dataflow.networktables;
|
package org.photonvision.common.dataflow.networktables;
|
||||||
|
|
||||||
import edu.wpi.first.networktables.EntryListenerFlags;
|
import edu.wpi.first.networktables.NetworkTableEvent;
|
||||||
import edu.wpi.first.networktables.EntryNotification;
|
import edu.wpi.first.networktables.NetworkTableInstance;
|
||||||
import edu.wpi.first.networktables.NetworkTableEntry;
|
import edu.wpi.first.networktables.Subscriber;
|
||||||
|
import java.util.EnumSet;
|
||||||
import java.util.function.Consumer;
|
import java.util.function.Consumer;
|
||||||
|
|
||||||
public class NTDataChangeListener {
|
public class NTDataChangeListener {
|
||||||
private final NetworkTableEntry watchedEntry;
|
private final NetworkTableInstance instance;
|
||||||
|
private final Subscriber watchedEntry;
|
||||||
private final int listenerID;
|
private final int listenerID;
|
||||||
|
|
||||||
public NTDataChangeListener(
|
public NTDataChangeListener(
|
||||||
NetworkTableEntry watchedEntry, Consumer<EntryNotification> dataChangeConsumer) {
|
NetworkTableInstance instance,
|
||||||
this.watchedEntry = watchedEntry;
|
Subscriber watchedSubscriber,
|
||||||
listenerID = watchedEntry.addListener(dataChangeConsumer, EntryListenerFlags.kUpdate);
|
Consumer<NetworkTableEvent> dataChangeConsumer) {
|
||||||
|
this.watchedEntry = watchedSubscriber;
|
||||||
|
this.instance = instance;
|
||||||
|
listenerID =
|
||||||
|
this.instance.addListener(
|
||||||
|
watchedEntry, EnumSet.of(NetworkTableEvent.Kind.kValueAll), dataChangeConsumer);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void remove() {
|
public void remove() {
|
||||||
watchedEntry.removeListener(listenerID);
|
this.instance.removeListener(listenerID);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,11 +14,11 @@
|
|||||||
* You should have received a copy of the GNU General Public License
|
* You should have received a copy of the GNU General Public License
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package org.photonvision.common.dataflow.networktables;
|
package org.photonvision.common.dataflow.networktables;
|
||||||
|
|
||||||
import edu.wpi.first.networktables.EntryNotification;
|
|
||||||
import edu.wpi.first.networktables.NetworkTable;
|
import edu.wpi.first.networktables.NetworkTable;
|
||||||
import edu.wpi.first.networktables.NetworkTableEntry;
|
import edu.wpi.first.networktables.NetworkTableEvent;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.function.BooleanSupplier;
|
import java.util.function.BooleanSupplier;
|
||||||
@@ -27,6 +27,9 @@ import java.util.function.Supplier;
|
|||||||
import org.opencv.core.Point;
|
import org.opencv.core.Point;
|
||||||
import org.photonvision.common.dataflow.CVPipelineResultConsumer;
|
import org.photonvision.common.dataflow.CVPipelineResultConsumer;
|
||||||
import org.photonvision.common.dataflow.structures.Packet;
|
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.PhotonPipelineResult;
|
||||||
import org.photonvision.targeting.PhotonTrackedTarget;
|
import org.photonvision.targeting.PhotonTrackedTarget;
|
||||||
import org.photonvision.targeting.TargetCorner;
|
import org.photonvision.targeting.TargetCorner;
|
||||||
@@ -34,31 +37,21 @@ import org.photonvision.vision.pipeline.result.CVPipelineResult;
|
|||||||
import org.photonvision.vision.target.TrackedTarget;
|
import org.photonvision.vision.target.TrackedTarget;
|
||||||
|
|
||||||
public class NTDataPublisher implements CVPipelineResultConsumer {
|
public class NTDataPublisher implements CVPipelineResultConsumer {
|
||||||
|
private final Logger logger = new Logger(NTDataPublisher.class, LogGroup.General);
|
||||||
|
|
||||||
private final NetworkTable rootTable = NetworkTablesManager.getInstance().kRootTable;
|
private final NetworkTable rootTable = NetworkTablesManager.getInstance().kRootTable;
|
||||||
private NetworkTable subTable;
|
|
||||||
private NetworkTableEntry rawBytesEntry;
|
|
||||||
|
|
||||||
private NetworkTableEntry pipelineIndexEntry;
|
private NTTopicSet ts = new NTTopicSet();
|
||||||
private final Consumer<Integer> pipelineIndexConsumer;
|
|
||||||
private NTDataChangeListener pipelineIndexListener;
|
|
||||||
private NetworkTableEntry driverModeEntry;
|
|
||||||
private final Consumer<Boolean> driverModeConsumer;
|
|
||||||
private NTDataChangeListener driverModeListener;
|
|
||||||
|
|
||||||
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;
|
|
||||||
|
|
||||||
|
NTDataChangeListener pipelineIndexListener;
|
||||||
private final Supplier<Integer> pipelineIndexSupplier;
|
private final Supplier<Integer> pipelineIndexSupplier;
|
||||||
|
private final Consumer<Integer> pipelineIndexConsumer;
|
||||||
|
|
||||||
|
NTDataChangeListener driverModeListener;
|
||||||
private final BooleanSupplier driverModeSupplier;
|
private final BooleanSupplier driverModeSupplier;
|
||||||
|
private final Consumer<Boolean> driverModeConsumer;
|
||||||
|
|
||||||
|
private long heartbeatCounter = 0;
|
||||||
|
|
||||||
public NTDataPublisher(
|
public NTDataPublisher(
|
||||||
String cameraNickname,
|
String cameraNickname,
|
||||||
@@ -75,93 +68,67 @@ public class NTDataPublisher implements CVPipelineResultConsumer {
|
|||||||
updateEntries();
|
updateEntries();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void onPipelineIndexChange(EntryNotification entryNotification) {
|
private void onPipelineIndexChange(NetworkTableEvent entryNotification) {
|
||||||
var newIndex = (int) entryNotification.value.getDouble();
|
var newIndex = (int) entryNotification.valueData.value.getInteger();
|
||||||
var originalIndex = pipelineIndexSupplier.get();
|
var originalIndex = pipelineIndexSupplier.get();
|
||||||
|
|
||||||
// ignore indexes below 0
|
// ignore indexes below 0
|
||||||
if (newIndex < 0) {
|
if (newIndex < 0) {
|
||||||
pipelineIndexEntry.forceSetNumber(originalIndex);
|
ts.pipelineIndexPublisher.set(originalIndex);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (newIndex == originalIndex) {
|
if (newIndex == originalIndex) {
|
||||||
// TODO: Log
|
logger.debug("Pipeline index is already " + newIndex);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
pipelineIndexConsumer.accept(newIndex);
|
pipelineIndexConsumer.accept(newIndex);
|
||||||
var setIndex = pipelineIndexSupplier.get();
|
var setIndex = pipelineIndexSupplier.get();
|
||||||
if (newIndex != setIndex) { // set failed
|
if (newIndex != setIndex) { // set failed
|
||||||
pipelineIndexEntry.forceSetNumber(setIndex);
|
ts.pipelineIndexPublisher.set(setIndex);
|
||||||
// TODO: Log
|
// TODO: Log
|
||||||
}
|
}
|
||||||
// TODO: Log
|
logger.debug("Successfully set pipeline index to " + newIndex);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void onDriverModeChange(EntryNotification entryNotification) {
|
private void onDriverModeChange(NetworkTableEvent entryNotification) {
|
||||||
var newDriverMode = entryNotification.value.getBoolean();
|
var newDriverMode = entryNotification.valueData.value.getBoolean();
|
||||||
var originalDriverMode = driverModeSupplier.getAsBoolean();
|
var originalDriverMode = driverModeSupplier.getAsBoolean();
|
||||||
|
|
||||||
if (newDriverMode == originalDriverMode) {
|
if (newDriverMode == originalDriverMode) {
|
||||||
// TODO: Log
|
logger.debug("Driver mode is already " + newDriverMode);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
driverModeConsumer.accept(newDriverMode);
|
driverModeConsumer.accept(newDriverMode);
|
||||||
// TODO: Log
|
logger.debug("Successfully set driver mode to " + newDriverMode);
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressWarnings("DuplicatedCode")
|
|
||||||
private void removeEntries() {
|
private void removeEntries() {
|
||||||
if (rawBytesEntry != null) rawBytesEntry.delete();
|
|
||||||
if (pipelineIndexListener != null) pipelineIndexListener.remove();
|
if (pipelineIndexListener != null) pipelineIndexListener.remove();
|
||||||
if (pipelineIndexEntry != null) pipelineIndexEntry.delete();
|
|
||||||
if (driverModeListener != null) driverModeListener.remove();
|
if (driverModeListener != null) driverModeListener.remove();
|
||||||
if (driverModeEntry != null) driverModeEntry.delete();
|
ts.removeEntries();
|
||||||
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() {
|
private void updateEntries() {
|
||||||
rawBytesEntry = subTable.getEntry("rawBytes");
|
if (pipelineIndexListener != null) pipelineIndexListener.remove();
|
||||||
|
if (driverModeListener != null) driverModeListener.remove();
|
||||||
|
|
||||||
|
ts.updateEntries();
|
||||||
|
|
||||||
if (pipelineIndexListener != null) {
|
|
||||||
pipelineIndexListener.remove();
|
|
||||||
}
|
|
||||||
pipelineIndexEntry = subTable.getEntry("pipelineIndex");
|
|
||||||
pipelineIndexListener =
|
pipelineIndexListener =
|
||||||
new NTDataChangeListener(pipelineIndexEntry, this::onPipelineIndexChange);
|
new NTDataChangeListener(
|
||||||
|
ts.subTable.getInstance(), ts.pipelineIndexSubscriber, this::onPipelineIndexChange);
|
||||||
|
|
||||||
if (driverModeListener != null) {
|
driverModeListener =
|
||||||
driverModeListener.remove();
|
new NTDataChangeListener(
|
||||||
}
|
ts.subTable.getInstance(), ts.driverModeSubscriber, this::onDriverModeChange);
|
||||||
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) {
|
public void updateCameraNickname(String newCameraNickname) {
|
||||||
removeEntries();
|
removeEntries();
|
||||||
subTable = rootTable.getSubTable(newCameraNickname);
|
ts.subTable = rootTable.getSubTable(newCameraNickname);
|
||||||
updateEntries();
|
updateEntries();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -173,49 +140,70 @@ public class NTDataPublisher implements CVPipelineResultConsumer {
|
|||||||
Packet packet = new Packet(simplified.getPacketSize());
|
Packet packet = new Packet(simplified.getPacketSize());
|
||||||
simplified.populatePacket(packet);
|
simplified.populatePacket(packet);
|
||||||
|
|
||||||
rawBytesEntry.forceSetRaw(packet.getData());
|
ts.rawBytesEntry.set(packet.getData());
|
||||||
|
|
||||||
pipelineIndexEntry.forceSetNumber(pipelineIndexSupplier.get());
|
ts.pipelineIndexPublisher.set(pipelineIndexSupplier.get());
|
||||||
driverModeEntry.forceSetBoolean(driverModeSupplier.getAsBoolean());
|
ts.driverModePublisher.set(driverModeSupplier.getAsBoolean());
|
||||||
latencyMillisEntry.forceSetDouble(result.getLatencyMillis());
|
ts.latencyMillisEntry.set(result.getLatencyMillis());
|
||||||
hasTargetEntry.forceSetBoolean(result.hasTargets());
|
ts.hasTargetEntry.set(result.hasTargets());
|
||||||
|
|
||||||
if (result.hasTargets()) {
|
if (result.hasTargets()) {
|
||||||
var bestTarget = result.targets.get(0);
|
var bestTarget = result.targets.get(0);
|
||||||
|
|
||||||
targetPitchEntry.forceSetDouble(bestTarget.getPitch());
|
ts.targetPitchEntry.set(bestTarget.getPitch());
|
||||||
targetYawEntry.forceSetDouble(bestTarget.getYaw());
|
ts.targetYawEntry.set(bestTarget.getYaw());
|
||||||
targetAreaEntry.forceSetDouble(bestTarget.getArea());
|
ts.targetAreaEntry.set(bestTarget.getArea());
|
||||||
targetSkewEntry.forceSetDouble(bestTarget.getSkew());
|
ts.targetSkewEntry.set(bestTarget.getSkew());
|
||||||
|
|
||||||
var poseX = bestTarget.getCameraToTarget().getTranslation().getX();
|
var pose = bestTarget.getBestCameraToTarget3d();
|
||||||
var poseY = bestTarget.getCameraToTarget().getTranslation().getY();
|
ts.targetPoseEntry.set(
|
||||||
var poseRot = bestTarget.getCameraToTarget().getRotation().getDegrees();
|
new double[] {
|
||||||
targetPoseEntry.forceSetDoubleArray(new double[] {poseX, poseY, poseRot});
|
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 targetOffsetPoint = bestTarget.getTargetOffsetPoint();
|
var targetOffsetPoint = bestTarget.getTargetOffsetPoint();
|
||||||
bestTargetPosX.forceSetDouble(targetOffsetPoint.x);
|
ts.bestTargetPosX.set(targetOffsetPoint.x);
|
||||||
bestTargetPosY.forceSetDouble(targetOffsetPoint.y);
|
ts.bestTargetPosY.set(targetOffsetPoint.y);
|
||||||
} else {
|
} else {
|
||||||
targetPitchEntry.forceSetDouble(0);
|
ts.targetPitchEntry.set(0);
|
||||||
targetYawEntry.forceSetDouble(0);
|
ts.targetYawEntry.set(0);
|
||||||
targetAreaEntry.forceSetDouble(0);
|
ts.targetAreaEntry.set(0);
|
||||||
targetSkewEntry.forceSetDouble(0);
|
ts.targetSkewEntry.set(0);
|
||||||
targetPoseEntry.forceSetDoubleArray(new double[] {0, 0, 0});
|
ts.targetPoseEntry.set(new double[] {0, 0, 0});
|
||||||
bestTargetPosX.forceSetDouble(0);
|
ts.bestTargetPosX.set(0);
|
||||||
bestTargetPosY.forceSetDouble(0);
|
ts.bestTargetPosY.set(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ts.heartbeatPublisher.set(heartbeatCounter++);
|
||||||
|
|
||||||
|
// TODO...nt4... is this needed?
|
||||||
rootTable.getInstance().flush();
|
rootTable.getInstance().flush();
|
||||||
}
|
}
|
||||||
|
|
||||||
public static List<PhotonTrackedTarget> simpleFromTrackedTargets(List<TrackedTarget> targets) {
|
public static List<PhotonTrackedTarget> simpleFromTrackedTargets(List<TrackedTarget> targets) {
|
||||||
var ret = new ArrayList<PhotonTrackedTarget>();
|
var ret = new ArrayList<PhotonTrackedTarget>();
|
||||||
for (var t : targets) {
|
for (var t : targets) {
|
||||||
var points = new Point[4];
|
var minAreaRectCorners = new ArrayList<TargetCorner>();
|
||||||
t.getMinAreaRect().points(points);
|
var detectedCorners = new ArrayList<TargetCorner>();
|
||||||
var cornerList = new ArrayList<TargetCorner>();
|
{
|
||||||
|
var points = new Point[4];
|
||||||
for (int i = 0; i < 4; i++) cornerList.add(new TargetCorner(points[i].x, points[i].y));
|
t.getMinAreaRect().points(points);
|
||||||
|
for (int i = 0; i < 4; i++) {
|
||||||
|
minAreaRectCorners.add(new TargetCorner(points[i].x, points[i].y));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
{
|
||||||
|
var points = t.getTargetCorners();
|
||||||
|
for (int i = 0; i < points.size(); i++) {
|
||||||
|
detectedCorners.add(new TargetCorner(points.get(i).x, points.get(i).y));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
ret.add(
|
ret.add(
|
||||||
new PhotonTrackedTarget(
|
new PhotonTrackedTarget(
|
||||||
@@ -223,8 +211,12 @@ public class NTDataPublisher implements CVPipelineResultConsumer {
|
|||||||
t.getPitch(),
|
t.getPitch(),
|
||||||
t.getArea(),
|
t.getArea(),
|
||||||
t.getSkew(),
|
t.getSkew(),
|
||||||
t.getCameraToTarget(),
|
t.getFiducialId(),
|
||||||
cornerList));
|
t.getBestCameraToTarget3d(),
|
||||||
|
t.getAltCameraToTarget3d(),
|
||||||
|
t.getPoseAmbiguity(),
|
||||||
|
minAreaRectCorners,
|
||||||
|
detectedCorners));
|
||||||
}
|
}
|
||||||
return ret;
|
return ret;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,19 +14,13 @@
|
|||||||
* You should have received a copy of the GNU General Public License
|
* You should have received a copy of the GNU General Public License
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package org.photonvision.common.dataflow.networktables;
|
package org.photonvision.common.dataflow.networktables;
|
||||||
|
|
||||||
import edu.wpi.first.cscore.CameraServerJNI;
|
|
||||||
import edu.wpi.first.networktables.LogMessage;
|
|
||||||
import edu.wpi.first.networktables.NetworkTable;
|
import edu.wpi.first.networktables.NetworkTable;
|
||||||
|
import edu.wpi.first.networktables.NetworkTableEvent;
|
||||||
import edu.wpi.first.networktables.NetworkTableInstance;
|
import edu.wpi.first.networktables.NetworkTableInstance;
|
||||||
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.HashMap;
|
||||||
import java.util.List;
|
|
||||||
import java.util.function.Consumer;
|
import java.util.function.Consumer;
|
||||||
import org.photonvision.PhotonVersion;
|
import org.photonvision.PhotonVersion;
|
||||||
import org.photonvision.common.configuration.ConfigManager;
|
import org.photonvision.common.configuration.ConfigManager;
|
||||||
@@ -44,8 +38,11 @@ public class NetworkTablesManager {
|
|||||||
private final String kRootTableName = "/photonvision";
|
private final String kRootTableName = "/photonvision";
|
||||||
public final NetworkTable kRootTable = ntInstance.getTable(kRootTableName);
|
public final NetworkTable kRootTable = ntInstance.getTable(kRootTableName);
|
||||||
|
|
||||||
|
private boolean isRetryingConnection = false;
|
||||||
|
|
||||||
private NetworkTablesManager() {
|
private NetworkTablesManager() {
|
||||||
ntInstance.addLogger(new NTLogger(), 0, 255); // to hide error messages
|
ntInstance.addLogger(0, 255, new NTLogger()); // to hide error messages
|
||||||
|
TimedTaskManager.getInstance().addTask("NTManager", this::ntTick, 5000);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static NetworkTablesManager INSTANCE;
|
private static NetworkTablesManager INSTANCE;
|
||||||
@@ -57,17 +54,17 @@ public class NetworkTablesManager {
|
|||||||
|
|
||||||
private static final Logger logger = new Logger(NetworkTablesManager.class, LogGroup.General);
|
private static final Logger logger = new Logger(NetworkTablesManager.class, LogGroup.General);
|
||||||
|
|
||||||
private static class NTLogger implements Consumer<LogMessage> {
|
private static class NTLogger implements Consumer<NetworkTableEvent> {
|
||||||
private boolean hasReportedConnectionFailure = false;
|
private boolean hasReportedConnectionFailure = false;
|
||||||
private long lastConnectMessageMillis = 0;
|
private long lastConnectMessageMillis = 0;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void accept(LogMessage logMessage) {
|
public void accept(NetworkTableEvent event) {
|
||||||
if (!hasReportedConnectionFailure && logMessage.message.contains("timed out")) {
|
if (!hasReportedConnectionFailure && event.logMessage.message.contains("timed out")) {
|
||||||
logger.error("NT Connection has failed! Will retry in background.");
|
logger.error("NT Connection has failed! Will retry in background.");
|
||||||
hasReportedConnectionFailure = true;
|
hasReportedConnectionFailure = true;
|
||||||
getInstance().broadcastConnectedStatus();
|
getInstance().broadcastConnectedStatus();
|
||||||
} else if (logMessage.message.contains("connected")
|
} else if (event.logMessage.message.contains("connected")
|
||||||
&& System.currentTimeMillis() - lastConnectMessageMillis > 125) {
|
&& System.currentTimeMillis() - lastConnectMessageMillis > 125) {
|
||||||
logger.info("NT Connected!");
|
logger.info("NT Connected!");
|
||||||
hasReportedConnectionFailure = false;
|
hasReportedConnectionFailure = false;
|
||||||
@@ -86,6 +83,7 @@ public class NetworkTablesManager {
|
|||||||
private void broadcastConnectedStatusImpl() {
|
private void broadcastConnectedStatusImpl() {
|
||||||
HashMap<String, Object> map = new HashMap<>();
|
HashMap<String, Object> map = new HashMap<>();
|
||||||
var subMap = new HashMap<String, Object>();
|
var subMap = new HashMap<String, Object>();
|
||||||
|
|
||||||
subMap.put("connected", ntInstance.isConnected());
|
subMap.put("connected", ntInstance.isConnected());
|
||||||
if (ntInstance.isConnected()) {
|
if (ntInstance.isConnected()) {
|
||||||
var connections = getInstance().ntInstance.getConnections();
|
var connections = getInstance().ntInstance.getConnections();
|
||||||
@@ -98,73 +96,6 @@ public class NetworkTablesManager {
|
|||||||
map.put("ntConnectionInfo", subMap);
|
map.put("ntConnectionInfo", subMap);
|
||||||
DataChangeService.getInstance()
|
DataChangeService.getInstance()
|
||||||
.publishEvent(new OutgoingUIEvent<>("networkTablesConnected", map));
|
.publishEvent(new OutgoingUIEvent<>("networkTablesConnected", map));
|
||||||
|
|
||||||
// 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());
|
|
||||||
DataChangeService.getInstance()
|
|
||||||
.publishEvent(new OutgoingUIEvent<>("networkTablesConnected", 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) {
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void broadcastVersion() {
|
private void broadcastVersion() {
|
||||||
@@ -182,17 +113,11 @@ public class NetworkTablesManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void setClientMode(int teamNumber) {
|
private void setClientMode(int teamNumber) {
|
||||||
logger.info("Starting NT Client");
|
if (!isRetryingConnection) logger.info("Starting NT Client");
|
||||||
ntInstance.stopServer();
|
ntInstance.stopServer();
|
||||||
|
ntInstance.startClient4("photonvision");
|
||||||
ntInstance.startClientTeam(teamNumber);
|
ntInstance.setServerTeam(teamNumber);
|
||||||
ntInstance.startDSClient();
|
ntInstance.startDSClient();
|
||||||
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...");
|
|
||||||
}
|
|
||||||
broadcastVersion();
|
broadcastVersion();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -202,4 +127,22 @@ public class NetworkTablesManager {
|
|||||||
ntInstance.startServer();
|
ntInstance.startServer();
|
||||||
broadcastVersion();
|
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...");
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,6 +14,7 @@
|
|||||||
* You should have received a copy of the GNU General Public License
|
* You should have received a copy of the GNU General Public License
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package org.photonvision.common.dataflow.websocket;
|
package org.photonvision.common.dataflow.websocket;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
|||||||
@@ -14,6 +14,7 @@
|
|||||||
* You should have received a copy of the GNU General Public License
|
* You should have received a copy of the GNU General Public License
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package org.photonvision.common.hardware.GPIO;
|
package org.photonvision.common.hardware.GPIO;
|
||||||
|
|
||||||
import org.photonvision.common.configuration.HardwareConfig;
|
import org.photonvision.common.configuration.HardwareConfig;
|
||||||
|
|||||||
@@ -14,6 +14,7 @@
|
|||||||
* You should have received a copy of the GNU General Public License
|
* You should have received a copy of the GNU General Public License
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package org.photonvision.common.hardware.GPIO;
|
package org.photonvision.common.hardware.GPIO;
|
||||||
|
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
|
|||||||
@@ -14,6 +14,7 @@
|
|||||||
* You should have received a copy of the GNU General Public License
|
* You should have received a copy of the GNU General Public License
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package org.photonvision.common.hardware.GPIO.pi;
|
package org.photonvision.common.hardware.GPIO.pi;
|
||||||
|
|
||||||
@SuppressWarnings("SpellCheckingInspection")
|
@SuppressWarnings("SpellCheckingInspection")
|
||||||
|
|||||||
@@ -14,6 +14,7 @@
|
|||||||
* You should have received a copy of the GNU General Public License
|
* You should have received a copy of the GNU General Public License
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package org.photonvision.common.hardware.GPIO.pi;
|
package org.photonvision.common.hardware.GPIO.pi;
|
||||||
|
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
|
|||||||
@@ -14,6 +14,7 @@
|
|||||||
* You should have received a copy of the GNU General Public License
|
* You should have received a copy of the GNU General Public License
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package org.photonvision.common.hardware.GPIO.pi;
|
package org.photonvision.common.hardware.GPIO.pi;
|
||||||
|
|
||||||
import static org.photonvision.common.hardware.GPIO.pi.PigpioException.*;
|
import static org.photonvision.common.hardware.GPIO.pi.PigpioException.*;
|
||||||
|
|||||||
@@ -14,6 +14,7 @@
|
|||||||
* You should have received a copy of the GNU General Public License
|
* You should have received a copy of the GNU General Public License
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package org.photonvision.common.hardware.GPIO.pi;
|
package org.photonvision.common.hardware.GPIO.pi;
|
||||||
|
|
||||||
public class PigpioPulse {
|
public class PigpioPulse {
|
||||||
|
|||||||
@@ -14,6 +14,7 @@
|
|||||||
* You should have received a copy of the GNU General Public License
|
* You should have received a copy of the GNU General Public License
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package org.photonvision.common.hardware.GPIO.pi;
|
package org.photonvision.common.hardware.GPIO.pi;
|
||||||
|
|
||||||
import static org.photonvision.common.hardware.GPIO.pi.PigpioException.*;
|
import static org.photonvision.common.hardware.GPIO.pi.PigpioException.*;
|
||||||
|
|||||||
@@ -14,6 +14,7 @@
|
|||||||
* You should have received a copy of the GNU General Public License
|
* You should have received a copy of the GNU General Public License
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package org.photonvision.common.hardware.GPIO.pi;
|
package org.photonvision.common.hardware.GPIO.pi;
|
||||||
|
|
||||||
import java.io.DataInputStream;
|
import java.io.DataInputStream;
|
||||||
|
|||||||
@@ -14,9 +14,10 @@
|
|||||||
* You should have received a copy of the GNU General Public License
|
* You should have received a copy of the GNU General Public License
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package org.photonvision.common.hardware;
|
package org.photonvision.common.hardware;
|
||||||
|
|
||||||
import edu.wpi.first.networktables.NetworkTableEntry;
|
import edu.wpi.first.networktables.IntegerEntry;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import org.photonvision.common.ProgramStatus;
|
import org.photonvision.common.ProgramStatus;
|
||||||
import org.photonvision.common.configuration.ConfigManager;
|
import org.photonvision.common.configuration.ConfigManager;
|
||||||
@@ -26,7 +27,7 @@ import org.photonvision.common.dataflow.networktables.NTDataChangeListener;
|
|||||||
import org.photonvision.common.dataflow.networktables.NetworkTablesManager;
|
import org.photonvision.common.dataflow.networktables.NetworkTablesManager;
|
||||||
import org.photonvision.common.hardware.GPIO.CustomGPIO;
|
import org.photonvision.common.hardware.GPIO.CustomGPIO;
|
||||||
import org.photonvision.common.hardware.GPIO.pi.PigpioSocket;
|
import org.photonvision.common.hardware.GPIO.pi.PigpioSocket;
|
||||||
import org.photonvision.common.hardware.metrics.MetricsBase;
|
import org.photonvision.common.hardware.metrics.MetricsManager;
|
||||||
import org.photonvision.common.logging.LogGroup;
|
import org.photonvision.common.logging.LogGroup;
|
||||||
import org.photonvision.common.logging.Logger;
|
import org.photonvision.common.logging.Logger;
|
||||||
import org.photonvision.common.util.ShellExec;
|
import org.photonvision.common.util.ShellExec;
|
||||||
@@ -40,11 +41,13 @@ public class HardwareManager {
|
|||||||
private final HardwareConfig hardwareConfig;
|
private final HardwareConfig hardwareConfig;
|
||||||
private final HardwareSettings hardwareSettings;
|
private final HardwareSettings hardwareSettings;
|
||||||
|
|
||||||
|
private final MetricsManager metricsManager;
|
||||||
|
|
||||||
@SuppressWarnings({"FieldCanBeLocal", "unused"})
|
@SuppressWarnings({"FieldCanBeLocal", "unused"})
|
||||||
private final StatusLED statusLED;
|
private final StatusLED statusLED;
|
||||||
|
|
||||||
@SuppressWarnings("FieldCanBeLocal")
|
@SuppressWarnings("FieldCanBeLocal")
|
||||||
private final NetworkTableEntry ledModeEntry;
|
private final IntegerEntry ledModeEntry;
|
||||||
|
|
||||||
@SuppressWarnings({"FieldCanBeLocal", "unused"})
|
@SuppressWarnings({"FieldCanBeLocal", "unused"})
|
||||||
private final NTDataChangeListener ledModeListener;
|
private final NTDataChangeListener ledModeListener;
|
||||||
@@ -64,8 +67,11 @@ public class HardwareManager {
|
|||||||
private HardwareManager(HardwareConfig hardwareConfig, HardwareSettings hardwareSettings) {
|
private HardwareManager(HardwareConfig hardwareConfig, HardwareSettings hardwareSettings) {
|
||||||
this.hardwareConfig = hardwareConfig;
|
this.hardwareConfig = hardwareConfig;
|
||||||
this.hardwareSettings = hardwareSettings;
|
this.hardwareSettings = hardwareSettings;
|
||||||
|
|
||||||
|
this.metricsManager = new MetricsManager();
|
||||||
|
this.metricsManager.setConfig(hardwareConfig);
|
||||||
|
|
||||||
CustomGPIO.setConfig(hardwareConfig);
|
CustomGPIO.setConfig(hardwareConfig);
|
||||||
MetricsBase.setConfig(hardwareConfig);
|
|
||||||
|
|
||||||
if (Platform.isRaspberryPi()) {
|
if (Platform.isRaspberryPi()) {
|
||||||
pigpioSocket = new PigpioSocket();
|
pigpioSocket = new PigpioSocket();
|
||||||
@@ -88,12 +94,16 @@ public class HardwareManager {
|
|||||||
hasBrightnessRange ? hardwareConfig.ledBrightnessRange.get(1) : 100,
|
hasBrightnessRange ? hardwareConfig.ledBrightnessRange.get(1) : 100,
|
||||||
pigpioSocket);
|
pigpioSocket);
|
||||||
|
|
||||||
ledModeEntry = NetworkTablesManager.getInstance().kRootTable.getEntry("ledMode");
|
ledModeEntry =
|
||||||
ledModeEntry.setNumber(VisionLEDMode.kDefault.value);
|
NetworkTablesManager.getInstance().kRootTable.getIntegerTopic("ledMode").getEntry(0);
|
||||||
|
ledModeEntry.set(VisionLEDMode.kDefault.value);
|
||||||
ledModeListener =
|
ledModeListener =
|
||||||
visionLED == null
|
visionLED == null
|
||||||
? null
|
? null
|
||||||
: new NTDataChangeListener(ledModeEntry, visionLED::onLedModeChange);
|
: new NTDataChangeListener(
|
||||||
|
NetworkTablesManager.getInstance().kRootTable.getInstance(),
|
||||||
|
ledModeEntry,
|
||||||
|
visionLED::onLedModeChange);
|
||||||
|
|
||||||
Runtime.getRuntime().addShutdownHook(new Thread(this::onJvmExit));
|
Runtime.getRuntime().addShutdownHook(new Thread(this::onJvmExit));
|
||||||
|
|
||||||
@@ -121,7 +131,7 @@ public class HardwareManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public boolean restartDevice() {
|
public boolean restartDevice() {
|
||||||
if (Platform.isRaspberryPi()) {
|
if (Platform.isLinux()) {
|
||||||
try {
|
try {
|
||||||
return shellExec.executeBashCommand("reboot now") == 0;
|
return shellExec.executeBashCommand("reboot now") == 0;
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
@@ -157,4 +167,8 @@ public class HardwareManager {
|
|||||||
public HardwareConfig getConfig() {
|
public HardwareConfig getConfig() {
|
||||||
return hardwareConfig;
|
return hardwareConfig;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void publishMetrics() {
|
||||||
|
metricsManager.publishMetrics();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,30 +14,58 @@
|
|||||||
* You should have received a copy of the GNU General Public License
|
* You should have received a copy of the GNU General Public License
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package org.photonvision.common.hardware;
|
package org.photonvision.common.hardware;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import org.photonvision.common.util.ShellExec;
|
||||||
|
|
||||||
public enum PiVersion {
|
public enum PiVersion {
|
||||||
PI_B("Pi Model B"),
|
PI_B("Pi Model B"),
|
||||||
COMPUTE_MODULE("Compute Module Rev"),
|
COMPUTE_MODULE("Compute Module Rev"),
|
||||||
ZERO_W("Pi Zero W Rev 1.1"),
|
ZERO_W("Pi Zero W Rev 1.1"),
|
||||||
ZERO_2_W("Raspberry Pi Zero 2 W"),
|
ZERO_2_W("Raspberry Pi Zero 2"),
|
||||||
PI_3("Pi 3"),
|
PI_3("Pi 3"),
|
||||||
PI_4("Pi 4"),
|
PI_4("Pi 4"),
|
||||||
COMPUTE_MODULE_3("Compute Module 3"),
|
COMPUTE_MODULE_3("Compute Module 3"),
|
||||||
UNKNOWN("UNKNOWN");
|
UNKNOWN("UNKNOWN");
|
||||||
|
|
||||||
private final String identifier;
|
private final String identifier;
|
||||||
|
private static final ShellExec shell = new ShellExec(true, false);
|
||||||
|
private static final PiVersion currentPiVersion = calcPiVersion();
|
||||||
|
|
||||||
PiVersion(String s) {
|
private PiVersion(String s) {
|
||||||
this.identifier = s.toLowerCase();
|
this.identifier = s.toLowerCase();
|
||||||
}
|
}
|
||||||
|
|
||||||
public static PiVersion getPiVersion() {
|
public static PiVersion getPiVersion() {
|
||||||
|
return currentPiVersion;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static PiVersion calcPiVersion() {
|
||||||
if (!Platform.isRaspberryPi()) return PiVersion.UNKNOWN;
|
if (!Platform.isRaspberryPi()) return PiVersion.UNKNOWN;
|
||||||
String piString = Platform.currentPiVersionStr;
|
String piString = getPiVersionString();
|
||||||
for (PiVersion p : PiVersion.values()) {
|
for (PiVersion p : PiVersion.values()) {
|
||||||
if (piString.toLowerCase().contains(p.identifier)) return p;
|
if (piString.toLowerCase().contains(p.identifier)) return p;
|
||||||
}
|
}
|
||||||
return UNKNOWN;
|
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 "";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,60 +14,94 @@
|
|||||||
* You should have received a copy of the GNU General Public License
|
* You should have received a copy of the GNU General Public License
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package org.photonvision.common.hardware;
|
package org.photonvision.common.hardware;
|
||||||
|
|
||||||
|
import com.jogamp.common.os.Platform.OSType;
|
||||||
import edu.wpi.first.util.RuntimeDetector;
|
import edu.wpi.first.util.RuntimeDetector;
|
||||||
|
import java.io.BufferedReader;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Paths;
|
||||||
import org.photonvision.common.util.ShellExec;
|
import org.photonvision.common.util.ShellExec;
|
||||||
|
|
||||||
@SuppressWarnings("unused")
|
@SuppressWarnings("unused")
|
||||||
public enum Platform {
|
public enum Platform {
|
||||||
// WPILib Supported (JNI)
|
// WPILib Supported (JNI)
|
||||||
WINDOWS_32("Windows x32"),
|
WINDOWS_64("Windows x64", false, OSType.WINDOWS, true),
|
||||||
WINDOWS_64("Windows x64"),
|
LINUX_32("Linux x86", false, OSType.LINUX, true),
|
||||||
LINUX_64("Linux x64"),
|
LINUX_64("Linux x64", false, OSType.LINUX, true),
|
||||||
LINUX_RASPBIAN("Linux Raspbian"), // Raspberry Pi 3/4
|
LINUX_RASPBIAN32(
|
||||||
LINUX_AARCH64BIONIC("Linux AARCH64 Bionic"), // Jetson Nano, Jetson TX2
|
"Linux Raspbian 32-bit", true, OSType.LINUX, true), // Raspberry Pi 3/4 with a 32-bit image
|
||||||
// PhotonVision Supported (Manual install)
|
LINUX_RASPBIAN64(
|
||||||
LINUX_ARM32("Linux ARM32"), // ODROID XU4, C1+
|
"Linux Raspbian 64-bit", true, OSType.LINUX, true), // Raspberry Pi 3/4 with a 64-bit image
|
||||||
LINUX_ARM64("Linux ARM64"), // ODROID C2, N2
|
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
|
||||||
|
|
||||||
// Completely unsupported
|
// Completely unsupported
|
||||||
UNSUPPORTED("Unsupported Platform");
|
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
|
||||||
|
}
|
||||||
|
|
||||||
private static final ShellExec shell = new ShellExec(true, false);
|
private static final ShellExec shell = new ShellExec(true, false);
|
||||||
public final String value;
|
public final String description;
|
||||||
public static final boolean isRoot = checkForRoot();
|
public final boolean isPi;
|
||||||
|
public final OSType osType;
|
||||||
|
public final boolean isSupported;
|
||||||
|
|
||||||
Platform(String value) {
|
// Set once at init, shouldn't be needed after.
|
||||||
this.value = value;
|
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static final String OS_NAME = System.getProperty("os.name");
|
//////////////////////////////////////////////////////
|
||||||
private static final String OS_ARCH = System.getProperty("os.arch");
|
// Public API
|
||||||
|
|
||||||
// These are queried on init and should never change after
|
|
||||||
public static final Platform currentPlatform = getCurrentPlatform();
|
|
||||||
protected static final String currentPiVersionStr = getPiVersionString();
|
|
||||||
public static final PiVersion currentPiVersion = PiVersion.getPiVersion();
|
|
||||||
|
|
||||||
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() {
|
public static boolean isLinux() {
|
||||||
return getCurrentPlatform() == LINUX_64
|
return currentPlatform.osType == OSType.LINUX;
|
||||||
|| getCurrentPlatform() == LINUX_RASPBIAN
|
|
||||||
|| getCurrentPlatform() == LINUX_ARM64;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static boolean isRaspberryPi() {
|
public static boolean isRaspberryPi() {
|
||||||
return currentPlatform.equals(LINUX_RASPBIAN);
|
return currentPlatform.isPi;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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")
|
@SuppressWarnings("StatementWithEmptyBody")
|
||||||
private static boolean checkForRoot() {
|
private static boolean checkForRoot() {
|
||||||
if (isLinux()) {
|
if (isLinux()) {
|
||||||
@@ -91,49 +125,92 @@ public enum Platform {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static Platform getCurrentPlatform() {
|
private static Platform getCurrentPlatform() {
|
||||||
if (RuntimeDetector.isWindows()) {
|
if (RuntimeDetector.isWindows()) {
|
||||||
if (RuntimeDetector.is32BitIntel()) return WINDOWS_32;
|
if (RuntimeDetector.is32BitIntel()) {
|
||||||
if (RuntimeDetector.is64BitIntel()) return WINDOWS_64;
|
return WINDOWS_32;
|
||||||
|
} else if (RuntimeDetector.is64BitIntel()) {
|
||||||
|
return WINDOWS_64;
|
||||||
|
} else {
|
||||||
|
// please don't try this
|
||||||
|
return UNKNOWN;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (RuntimeDetector.isMac()) {
|
if (RuntimeDetector.isMac()) {
|
||||||
if (RuntimeDetector.is32BitIntel()) return UNSUPPORTED;
|
// TODO - once we have real support, this might have to be more granular
|
||||||
|
return MACOS;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (RuntimeDetector.isLinux()) {
|
if (RuntimeDetector.isLinux()) {
|
||||||
if (RuntimeDetector.is32BitIntel()) return UNSUPPORTED;
|
if (isPiSBC()) {
|
||||||
if (RuntimeDetector.is64BitIntel()) return LINUX_64;
|
if (RuntimeDetector.isArm32()) {
|
||||||
if (RuntimeDetector.isRaspbian()) return LINUX_RASPBIAN;
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
System.out.println(UnknownPlatformString);
|
// If we fall through all the way to here,
|
||||||
return Platform.UNSUPPORTED;
|
return Platform.UNKNOWN;
|
||||||
}
|
}
|
||||||
|
|
||||||
public String toString() {
|
// Check for various known SBC types
|
||||||
if (this.equals(UNSUPPORTED)) {
|
private static boolean isPiSBC() {
|
||||||
return UnknownPlatformString;
|
return fileHasText("/proc/cpuinfo", "Raspberry Pi");
|
||||||
} else {
|
|
||||||
return this.value;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Querry /proc/device-tree/model. This should return the model of the pi
|
private static boolean isJetsonSBC() {
|
||||||
// Versions here:
|
// https://forums.developer.nvidia.com/t/how-to-recognize-jetson-nano-device/146624
|
||||||
// https://github.com/raspberrypi/linux/blob/rpi-5.10.y/arch/arm/boot/dts/bcm2710-rpi-cm3.dts
|
return fileHasText("/proc/device-tree/model", "NVIDIA Jetson");
|
||||||
private static String getPiVersionString() {
|
}
|
||||||
if (!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 "";
|
// 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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,6 +14,7 @@
|
|||||||
* You should have received a copy of the GNU General Public License
|
* You should have received a copy of the GNU General Public License
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package org.photonvision.common.hardware;
|
package org.photonvision.common.hardware;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|||||||
@@ -14,9 +14,10 @@
|
|||||||
* You should have received a copy of the GNU General Public License
|
* You should have received a copy of the GNU General Public License
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package org.photonvision.common.hardware;
|
package org.photonvision.common.hardware;
|
||||||
|
|
||||||
import edu.wpi.first.networktables.EntryNotification;
|
import edu.wpi.first.networktables.NetworkTableEvent;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.function.BooleanSupplier;
|
import java.util.function.BooleanSupplier;
|
||||||
@@ -84,6 +85,8 @@ public class VisionLED {
|
|||||||
pigpioSocket.generateAndSendWaveform(pulseLengthMillis, blinkCount, ledPins);
|
pigpioSocket.generateAndSendWaveform(pulseLengthMillis, blinkCount, ledPins);
|
||||||
} catch (PigpioException e) {
|
} catch (PigpioException e) {
|
||||||
logger.error("Failed to blink!", e);
|
logger.error("Failed to blink!", e);
|
||||||
|
} catch (NullPointerException e) {
|
||||||
|
logger.error("Failed to blink, pigpio internal issue!", e);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
for (GPIOBase led : visionLEDs) {
|
for (GPIOBase led : visionLEDs) {
|
||||||
@@ -99,13 +102,19 @@ public class VisionLED {
|
|||||||
pigpioSocket.waveTxStop();
|
pigpioSocket.waveTxStop();
|
||||||
} catch (PigpioException e) {
|
} catch (PigpioException e) {
|
||||||
logger.error("Failed to stop blink!", e);
|
logger.error("Failed to stop blink!", e);
|
||||||
|
} 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
|
try {
|
||||||
if (mappedBrightnessPercentage == 100 || !state) {
|
// if the user has set an LED brightness other than 100%, use that instead
|
||||||
visionLEDs.forEach((led) -> led.setState(state));
|
if (mappedBrightnessPercentage == 100 || !state) {
|
||||||
} else {
|
visionLEDs.forEach((led) -> led.setState(state));
|
||||||
visionLEDs.forEach((led) -> led.setBrightness(mappedBrightnessPercentage));
|
} else {
|
||||||
|
visionLEDs.forEach((led) -> led.setBrightness(mappedBrightnessPercentage));
|
||||||
|
}
|
||||||
|
} catch (NullPointerException e) {
|
||||||
|
logger.error("Failed to blink, pigpio internal issue!", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -113,8 +122,8 @@ public class VisionLED {
|
|||||||
setInternal(on ? VisionLEDMode.kOn : VisionLEDMode.kOff, false);
|
setInternal(on ? VisionLEDMode.kOn : VisionLEDMode.kOff, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
void onLedModeChange(EntryNotification entryNotification) {
|
void onLedModeChange(NetworkTableEvent entryNotification) {
|
||||||
var newLedModeRaw = (int) entryNotification.value.getDouble();
|
var newLedModeRaw = (int) entryNotification.valueData.value.getDouble();
|
||||||
if (newLedModeRaw != currentLedMode.value) {
|
if (newLedModeRaw != currentLedMode.value) {
|
||||||
VisionLEDMode newLedMode;
|
VisionLEDMode newLedMode;
|
||||||
switch (newLedModeRaw) {
|
switch (newLedModeRaw) {
|
||||||
@@ -176,6 +185,9 @@ public class VisionLED {
|
|||||||
case kOn:
|
case kOn:
|
||||||
setStateImpl(true);
|
setStateImpl(true);
|
||||||
break;
|
break;
|
||||||
|
case kBlink:
|
||||||
|
blinkImpl(85, -1);
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
logger.info("Changing LED internal state to " + newLedMode.toString());
|
logger.info("Changing LED internal state to " + newLedMode.toString());
|
||||||
|
|||||||
@@ -1,91 +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 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 "";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,161 @@
|
|||||||
|
/*
|
||||||
|
* 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 "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,73 +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.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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,25 +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;
|
|
||||||
|
|
||||||
public class RAMMetrics extends MetricsBase {
|
|
||||||
// TODO: Output in MBs for consistency
|
|
||||||
public String getUsedRam() {
|
|
||||||
if (ramUsageCommand.isEmpty()) return "";
|
|
||||||
return execute(ramUsageCommand);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
/*
|
||||||
|
* 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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
/*
|
||||||
|
* 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
/*
|
||||||
|
* 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";
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
/*
|
||||||
|
* 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]+'";
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -14,6 +14,7 @@
|
|||||||
* You should have received a copy of the GNU General Public License
|
* You should have received a copy of the GNU General Public License
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package org.photonvision.common.logging;
|
package org.photonvision.common.logging;
|
||||||
|
|
||||||
public enum LogGroup {
|
public enum LogGroup {
|
||||||
|
|||||||
@@ -14,6 +14,7 @@
|
|||||||
* You should have received a copy of the GNU General Public License
|
* You should have received a copy of the GNU General Public License
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package org.photonvision.common.logging;
|
package org.photonvision.common.logging;
|
||||||
|
|
||||||
public enum LogLevel {
|
public enum LogLevel {
|
||||||
|
|||||||
@@ -14,6 +14,7 @@
|
|||||||
* You should have received a copy of the GNU General Public License
|
* You should have received a copy of the GNU General Public License
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package org.photonvision.common.logging;
|
package org.photonvision.common.logging;
|
||||||
|
|
||||||
import java.io.*;
|
import java.io.*;
|
||||||
|
|||||||
@@ -14,6 +14,7 @@
|
|||||||
* You should have received a copy of the GNU General Public License
|
* You should have received a copy of the GNU General Public License
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package org.photonvision.common.networking;
|
package org.photonvision.common.networking;
|
||||||
|
|
||||||
import java.net.InterfaceAddress;
|
import java.net.InterfaceAddress;
|
||||||
|
|||||||
@@ -14,9 +14,11 @@
|
|||||||
* You should have received a copy of the GNU General Public License
|
* You should have received a copy of the GNU General Public License
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package org.photonvision.common.networking;
|
package org.photonvision.common.networking;
|
||||||
|
|
||||||
import org.photonvision.common.configuration.ConfigManager;
|
import org.photonvision.common.configuration.ConfigManager;
|
||||||
|
import org.photonvision.common.configuration.NetworkConfig;
|
||||||
import org.photonvision.common.hardware.Platform;
|
import org.photonvision.common.hardware.Platform;
|
||||||
import org.photonvision.common.logging.LogGroup;
|
import org.photonvision.common.logging.LogGroup;
|
||||||
import org.photonvision.common.logging.Logger;
|
import org.photonvision.common.logging.Logger;
|
||||||
@@ -46,9 +48,8 @@ public class NetworkManager {
|
|||||||
var config = ConfigManager.getInstance().getConfig().getNetworkConfig();
|
var config = ConfigManager.getInstance().getConfig().getNetworkConfig();
|
||||||
logger.info("Setting " + config.connectionType + " with team team " + config.teamNumber);
|
logger.info("Setting " + config.connectionType + " with team team " + config.teamNumber);
|
||||||
if (Platform.isLinux()) {
|
if (Platform.isLinux()) {
|
||||||
if (!Platform.isRoot) {
|
if (!Platform.isRoot()) {
|
||||||
logger.error("Cannot manage network without root!");
|
logger.error("Cannot manage hostname without root!");
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// always set hostname
|
// always set hostname
|
||||||
@@ -95,10 +96,11 @@ public class NetworkManager {
|
|||||||
if (config.connectionType == NetworkMode.DHCP) {
|
if (config.connectionType == NetworkMode.DHCP) {
|
||||||
var shell = new ShellExec();
|
var shell = new ShellExec();
|
||||||
try {
|
try {
|
||||||
if (!config.staticIp.equals("")) {
|
// set nmcli back to DHCP, and re-run dhclient -- this ought to grab a new IP address
|
||||||
shell.executeBashCommand("ip addr del " + config.staticIp + "/8 dev eth0");
|
shell.executeBashCommand(
|
||||||
}
|
config.setDHCPcommand.replace(
|
||||||
shell.executeBashCommand("dhclient eth0", false);
|
NetworkConfig.NM_IFACE_STRING, config.networkManagerIface));
|
||||||
|
shell.executeBashCommand("dhclient " + config.physicalInterface, false);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
logger.error("Exception while setting DHCP!");
|
logger.error("Exception while setting DHCP!");
|
||||||
}
|
}
|
||||||
@@ -106,7 +108,30 @@ public class NetworkManager {
|
|||||||
var shell = new ShellExec();
|
var shell = new ShellExec();
|
||||||
if (config.staticIp.length() > 0) {
|
if (config.staticIp.length() > 0) {
|
||||||
try {
|
try {
|
||||||
shell.executeBashCommand("ip addr add " + config.staticIp + "/8" + " dev eth0");
|
shell.executeBashCommand(
|
||||||
|
config
|
||||||
|
.setStaticCommand
|
||||||
|
.replace(NetworkConfig.NM_IFACE_STRING, config.networkManagerIface)
|
||||||
|
.replace(NetworkConfig.NM_IP_STRING, config.staticIp));
|
||||||
|
|
||||||
|
if (Platform.isRaspberryPi()) {
|
||||||
|
// Pi's need to manually have their interface adjusted?? and the 5 second sleep is
|
||||||
|
// integral in my testing (Matt)
|
||||||
|
shell.executeBashCommand(
|
||||||
|
"sh -c 'nmcli con down "
|
||||||
|
+ config.networkManagerIface
|
||||||
|
+ "; nmcli con up "
|
||||||
|
+ config.networkManagerIface
|
||||||
|
+ "'");
|
||||||
|
} else {
|
||||||
|
// for now just bring down /up -- more testing needed on beelink et al
|
||||||
|
shell.executeBashCommand(
|
||||||
|
"sh -c 'nmcli con down "
|
||||||
|
+ config.networkManagerIface
|
||||||
|
+ "; nmcli con up "
|
||||||
|
+ config.networkManagerIface
|
||||||
|
+ "'");
|
||||||
|
}
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
logger.error("Error while setting static IP!", e);
|
logger.error("Error while setting static IP!", e);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,9 +14,17 @@
|
|||||||
* You should have received a copy of the GNU General Public License
|
* You should have received a copy of the GNU General Public License
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package org.photonvision.common.networking;
|
package org.photonvision.common.networking;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonValue;
|
||||||
|
|
||||||
public enum NetworkMode {
|
public enum NetworkMode {
|
||||||
DHCP,
|
DHCP,
|
||||||
STATIC
|
STATIC;
|
||||||
|
|
||||||
|
@JsonValue
|
||||||
|
public int toValue() {
|
||||||
|
return ordinal();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,118 @@
|
|||||||
|
/*
|
||||||
|
* 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) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -14,6 +14,7 @@
|
|||||||
* You should have received a copy of the GNU General Public License
|
* You should have received a copy of the GNU General Public License
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package org.photonvision.common.scripting;
|
package org.photonvision.common.scripting;
|
||||||
|
|
||||||
public enum ScriptCommandType {
|
public enum ScriptCommandType {
|
||||||
|
|||||||
@@ -14,6 +14,7 @@
|
|||||||
* You should have received a copy of the GNU General Public License
|
* You should have received a copy of the GNU General Public License
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package org.photonvision.common.scripting;
|
package org.photonvision.common.scripting;
|
||||||
|
|
||||||
import com.fasterxml.jackson.annotation.JsonCreator;
|
import com.fasterxml.jackson.annotation.JsonCreator;
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user