Compare commits

...

93 Commits

Author SHA1 Message Date
Matt
7b6afd545b Pull thinclient into built JAR (#558) 2022-10-31 16:18:02 -04:00
Matt
0f99044468 Update pi image generation zip/xz confusion (#555)
* Add prints to image generation

* Make xz multithreaded

* More rename copypasta
2022-10-31 11:27:57 -04:00
sarah-e-c
1412155c50 Replace jcenter with MavenCentral (#554) 2022-10-31 08:32:49 -04:00
Andrew Gasser
b1280e49d5 Ignore cameras with no supported VideoModes (#550) 2022-10-30 22:58:22 -04:00
Chris Gerth
aaac6a4fbb Add Websocket Camera Streaming (#529)
* WIP adding second websocket handling for cameras

* just more WIP

* even more wip. Most java-side framework completed, but not yet debugged

* IT LIVES. Still needs lots of cleanup. But we're transferring and displaying data!

* moved down an architecture layer. Improved multiple-camera handling

* Additional WIP to help improve smoothness and performance, though not yet tested

* bugfixes galore

* tweak compression

* spotless

* more tweaks for handling slow/intermittent streams

* wpilibformat maybe?

* clang-format maybe?

* WIP - adding thinclient. I don't like it yet, it should be more auto-generated than it is.

* thinclient formatting fixups

* Reduced amount of empty send data by limiting to only one stream per client (which is all we really need). Framerate is up slightly, overhead is down.

* bugfixes, faster streaming, better mjpeg compression settings, thinclient working

* spotless and formatting

* cmon wpiformat....

* re-added mjpg streams

* added a loading GIF to imporve the feeling of responsiveness

* formatting

* urlparams and built-in thinclient

* wpiformat

* prevent wpiformat complaints

* Removed uint8 array and base64 conversion from client side

* Synced up js implementations for ws streaming

* formatting/spotless
2022-10-30 13:16:17 -05:00
laviRZ
b68b0ca5f6 Rename artifact to jars (#534) 2022-10-30 14:14:14 -04:00
Chris Gerth
45d99f1f6b Added camera quirek to account for Facetime HD Cameras, and fix logging message (#551) 2022-10-30 14:13:55 -04:00
Jack
a42fef67f2 Fix Camera Calibration Frontend (#542)
* Fix Start Calibration button requiring a page refresh

* Fix camera resolution selection

* Fix camera resolution selection so it works with the default selection
2022-10-29 06:57:32 -04:00
Jack
bd4d74c192 Fix missing and incorrectly bound snackbar (#539)
* Fix missing and incorrectly bound snackbar

* Add 5 second timeout
2022-10-29 06:52:59 -04:00
Chris Gerth
c4500ce12b Added throttling reasons and cpu uptime (#507)
* Added throttling reasons and cpu uptime

* spotless

* adding tooltips for the acronyms used

* Added icon for suggesting folks should attempt a hover-over for tooltip

* wip making the implementaiton more platform independent

* spotless

* wpiformat

* wpilibformat pt 2
2022-10-29 06:50:51 -04:00
Jack
81d19672d2 Change order of drawing to better show axes (#541) 2022-10-28 17:54:57 -05:00
Andrew Gasser
04bde1b230 Update sim pose estimator example to use 3d (#524) 2022-10-25 21:11:41 -04:00
Avery Black
4f355f2749 Fix photon-build-action versioning (#535)
* Describe tags (Do Not Merge)

* Try fetch depth 0

* Remove fetch tags

* Remove describe action

* Apparently more is broken than I thought (oops)
2022-10-24 15:56:49 -04:00
Avery Black
5e604cf98d Remove 90 degree offset from UI (#533)
Removes offset originally added to offset broken backend code
2022-10-24 15:18:46 -04:00
Matt
2d7a88e231 Expose both pose solutions (#521)
* Half-add second pose

* add c++

* run wpiformat

* Fix c++
2022-10-22 06:42:45 -05:00
amquake
27198a3e32 Don't spam log on client connection retry (#530)
* dont spam log on connection retry

* Move print into ntTick

Update NetworkTablesManager.java

Co-authored-by: Matt <matthew.morley.ca@gmail.com>
2022-10-21 23:37:22 -04:00
Chris Gerth
fbf6fb304e Add Auto-Exposure Switch to Calibration Window (#526) 2022-10-21 22:12:11 -04:00
Avery Black
d24a8d4188 Ci update (#518)
Update action versions so that github actions stop complaining about Node and set/get-ouput commands.
2022-10-21 20:56:08 -04:00
Matt
def40484e3 Add delay to version check (#466)
Rate limits version check spam print

Co-authored-by: Chris Gerth <gerth2@users.noreply.github.com>
2022-10-21 20:53:28 -04:00
Chris Gerth
aff163fc6a Pull latest pi image and updates for .xz (previously .zip) (#506) 2022-10-21 20:50:45 -04:00
Chris Gerth
c392d5fa4d Exclude more broken cameras (#527)
* Adding new broken cameras

* Fixed up snapcamera enumeration to actually detect snapcamera
2022-10-21 19:39:30 -04:00
Chris Gerth
8dbd428359 Temporarily remove RIO finder from UI (#525) 2022-10-21 19:36:30 -04:00
Chris Gerth
ccd3a512d6 Add additional try/catch to prevent pigpio communication issues from crashing the main thread (#511) 2022-10-21 18:10:32 -04:00
Matt
bfc5e45cd0 Restart NT client every 5 seconds if not connected (#467)
Fun hack to get around photonvision not connecting if it boots before robot code starts

Co-authored-by: shueja-personal <32416547+shueja-personal@users.noreply.github.com>
2022-10-18 23:52:13 -04:00
Jack
a1b09100e0 Remove pitch camera configuration (#492)
* Remove pitch configuration from camera view

* Remove pitch config from backend; fix 'this' binding bug

* Stylistic choice to remove excessive whitespace br

* Spotless apply

* Spotless apply 2
2022-10-17 12:41:57 -04:00
Avery Black
2bf7a77885 Update aarch64 apriltag build from CI (#497) 2022-10-17 07:12:29 -04:00
Andrew Gasser
d1bfb86ab4 Correct image capture time (#501)
* Correct image capture time

`Timer.getFPGATimesptamp()` returns the current time in _seconds_, but `res.getLatencyMillis()` is in _miliseconds_.

* Correct image capture time (correctly)

* Change double literal to not use suffix

Co-authored-by: shueja-personal <32416547+shueja-personal@users.noreply.github.com>
2022-10-16 20:51:48 -07:00
Matt
07904589df Rotate all solvePNP-ed poses to be 180 about Z facing camera (#500)
* Rotate all solvePNP-ed poses to be 180 about Z facing camera

* Run spotless

* Fix test coordinate systems
2022-10-16 17:48:30 -07:00
Jack
5540bbf115 [UI] Fix camera gain slider Vue errors (#493) 2022-10-12 15:51:53 -04:00
Chris Gerth
c827afb25f 3d viewer cleanup (#490)
* WIP fiddling with 3js stuff for different viewpoints

* more wip viewer cleanup

* More cleanups - split out minimap
2022-10-09 20:26:49 -07:00
Matt
87e7c3ca74 [Wip] Add auto exposure switch (#488)
* Add auto exposure switch

* Run wpiformat

* Update ZeroCopyPicamSource.java
2022-10-09 21:41:40 -05:00
Chris Gerth
4d5904dd6d Stream content reorg. (#489)
Revised stream and target draw logic to divide the streams by "Raw" and "Processed" and only draw the results on the "Processed" stream.

Should allow for input sterams to be recorded for raw camera input, and output for debug info.
2022-10-09 21:30:16 -04:00
Avery Black
9bf589ebc6 Disable auto focus on USB cameras by default (#487)
* Disable auto focus on USB cameras by default

* Remove extra log

* Implement camera quirk for auto focus

* Spotless apply
2022-10-09 17:49:58 -04:00
Σx
1e4a92c71f Calculate and Report FOV from Calibration Coefficients (#486) 2022-10-08 23:08:57 -04:00
Matt
4ad9d97508 Fix AprilTag rotation reversal bug (#482)
Applies base rotation to apriltags to match solvepnp base rotation
2022-10-08 09:27:27 -04:00
Matt
2c6b0ddac3 Expose pose ambiguity (#483)
* Expose pose ambiguity

* Run spotless

* Add tooltips and expose number of iterations
2022-10-08 09:27:00 -04:00
shueja-personal
dafee954e0 Draw3dTargetsPipe returns immediately if coeffs are null (previously NPE crashlooped) (#485)
* Draw3dTargetsPipe returns immediately if coeffs are null

* fix lint
2022-10-08 09:26:37 -04:00
shueja-personal
5ac541642e Remove extra distortion in Draw3dTargetsPipe (#479)
* Remove extra distortion in Draw3dTargetsPipe

* fix wpiformat
2022-09-29 10:47:00 -07:00
Matt
ad0474d42a Update aarch64 apriltag shared library (#477) 2022-09-29 09:28:39 -07:00
Matt
4b4a0a1cd9 [UI] Fix target tab under AprilTag (#478)
* Start addressing things

* Fix target tab table

* Update TrackedTarget.java
2022-09-29 09:28:11 -07:00
shueja-personal
a764ace7f2 Initial AprilTag support (#458)
(Very) beta AprilTag support in PhotonVision. Disables Picam GPU acceleration until we can debug auto exposure in the MMAL driver.

Co-authored-by: Banks Troutman <btrout.dhrs@gmail.com>
Co-authored-by: Matt <matthew.morley.ca@gmail.com>
Co-authored-by: Chris Gerth <gerth2@users.noreply.github.com>
Co-authored-by: Chris <chrisgerth010592@gmail.com>
Co-authored-by: mdurrani808 <mdurrani808@gmail.com>
2022-09-28 21:21:41 -04:00
shueja-personal
a3bcd3ac4f Fix #461 (pipeline type change index) (#462)
* Fix #461 (pipeline type change index)

* Reassign indexes after changing pipeline type
2022-05-08 17:09:52 -07:00
shueja-personal
661f8b2c04 Fix spelling on "set team #" popup (#459) 2022-04-27 11:15:03 -04:00
Matt
72717cecf0 Disable Roborio finder (#450)
Rio finder has been linked to weird crashes after Autonomous
2022-03-31 22:55:51 -04:00
Matt
971ff3ac40 Calculate aspect ratio using rotated rect (#447) 2022-03-31 22:51:14 -04:00
Banks T
b80e436f02 Force fs sync on all .json writes (#451) 2022-03-31 22:46:12 -04:00
Matt
be1a053cbe Fix PhotoVersion template typo (#446) 2022-03-16 21:39:02 -07:00
Matt
f4555dc545 Fix offset point bug (#445)
Fixes bug where offset point can be wrong
2022-03-16 21:38:47 -07:00
Matt
54fdd1db51 Add test mode from path (#440)
adds --path to --test-mode
2022-03-16 21:33:20 -07:00
Matt
1805785cc6 Rio discovery slowdown (#444)
* Only send rio IPs on settings button click

* Wpiformat
2022-03-14 20:44:14 -07:00
Matt
e62f6419b5 Move config saving to its own thread (#438)
* Move config saving to its own thread

RIO discovery can block

* Add sleep
2022-03-01 00:11:30 -05:00
Declan
fa7824c616 Fix 960x720 weirdness (#439)
* Update 960x720 FOV modifier to track video mode change

* Update native code to version that includes 960x720 fix
2022-02-28 07:42:26 -05:00
Matt
9090aa6bcc Add version verification disable switch in photonlib (#437) 2022-02-28 07:37:52 -05:00
Declan
5655ca6890 Separate AWB gain slider (#410)
Makes gain adjust digital gain, adds sliders for red/blue on picam

Co-authored-by: Chris Gerth <chrisgerth010592@gmail.com>
2022-02-28 00:45:29 -05:00
Matt
50fdfd8bce Add outlier rejection (#432)
Uses standard deviations from mean x/y location to reject outliers
2022-02-28 00:44:22 -05:00
Declan
3120a6439b Handle average hue inverted (#431)
Co-authored-by: Chris Gerth <chrisgerth010592@gmail.com>
2022-02-27 00:09:44 -05:00
Jason Daming
ab3e8c8db7 Add version string to NT in sim (#424) 2022-02-22 20:01:01 -05:00
Banks T
5144819ce2 Invertable hue (#428)
* Add UI-side changes for invertable hue slider

* Add hue inverted range

* Add new slider backgrounds to threshold tab

* Run spotless

* Updated libpicam.so to artifact built from commit c458bab87740 in that repo on gerth2's pi.

* undo the java-side hack since isVCSMSupported is fixed

* Hook up hue inversion frontend to backend and UI tweaks

* Remove unused .flipped class

* Fix hueInverted name in Vue.js store

Co-authored-by: Declan Freeman-Gleason <declanfreemangleason@gmail.com>
Co-authored-by: Matt <matthew.morley.ca@gmail.com>
Co-authored-by: Chris Gerth <chrisgerth010592@gmail.com>
2022-02-21 22:41:51 -05:00
Matt
d779fe23f0 Add disabled stream warning (#409) 2022-01-24 12:39:04 -05:00
Matt
b2a3f34433 Fix version verification with non-default networktable (#407)
Adds version verification to c++ too
2022-01-24 12:38:45 -05:00
drew-struensee
b09a6d6a2d Added Support for 3D tracking of the 2022 Cargo Balls (#408)
* added cargo ball 2022

* added cargoball2022. tested on pi.. it works

* spotlessapply

* made list more consistant
2022-01-20 22:36:54 -05:00
Tyler Veness
9893cf1f7e Update photonlib and photonlib example license headers to MIT (#395) 2022-01-20 22:35:28 -05:00
Matt
fc91daf397 Enable GPU acecel on any Pi Zeros, not just zero W (#405) 2022-01-20 21:59:29 -05:00
Matt
a3e205cb6f Limit circle accuracy to [1,100] (#406) 2022-01-20 21:57:41 -05:00
Vasista Vovveti
553bed32b5 [photonlib] Target macOS 10.14 (#402) 2022-01-16 15:04:03 -05:00
Declan
6c91feaf3f Make small cosmetic improvments across the user interface (#396) 2022-01-16 11:25:37 -05:00
Chris Gerth
4ddb9aa08f Create new pipelines with same type as current (#398) 2022-01-15 10:11:12 -05:00
Banks T
4aadebdbb2 Change PhotonLib License to MIT (#394)
* Change PhotonLib License to MIT

* Remove extraneous newline in license
2022-01-14 22:05:49 -05:00
Matt
da8d70f887 Ensure cameras with gain will always have it enabled (#388) 2022-01-11 17:12:30 -08:00
Tyler Veness
80a0d8de1c Fix test resources path (#386) 2022-01-10 21:40:43 -08:00
Matt
3ad476bc28 Send corners of min area rectangles (#382)
Adds a new corners entry to targets. Breaks byte-packed backwards compatibility. 

Co-authored-by: Tyler Veness <calcmogul@gmail.com>
2022-01-10 20:31:36 -08:00
Matt
e6d8e05b91 2022 grouping and test mode updates (#381)
Add 2022 images, "2orMore" grouping mode, and 2022 test mode
Test mode now preserves old settings
2022-01-10 16:51:06 -08:00
Tyler Veness
46fa17dfd8 Upgrade spotless and shadow (#385)
Fixes Log4J vulnerability
2022-01-10 11:56:45 -08:00
Matt
43c35286f3 Reword restart modal (#374)
Clarifies that photon will restart (when running as service)
2022-01-09 16:02:32 -08:00
Matt
3d317f7035 Switch to Releaser from eine/tip (#379) 2022-01-09 09:00:25 -08:00
Tyler Veness
a161bd5be9 Upgrade all maven dependencies for 2022 (#377)
This also fixes compilation with JDK 17.
2022-01-08 10:17:28 -08:00
Matt
0f730fc28d Update WPILib to 2022.1.1 rc 1 (#376) 2022-01-06 20:14:51 -08:00
Matt
12e06b09c3 Add Pi zero 2 W to PiVersion enum (#373)
This approach is quite brittle, but it's easy to get working and can ship in an initial 2022 release. It's necessary to prevent GPU acceleration from happening on Pi 4s though. Let's try to put together something better for future releases.
2022-01-05 20:34:42 -08:00
Matt
641101f574 Fix NetworkTables team connection bug (#375) 2021-12-31 22:55:32 -06:00
Matt
6a1201432c Hard-code picam resolutions (#366)
* Hard-code picam resolutions

* Address review comment
2021-12-30 23:19:25 -08:00
Matt
e77a06bfa6 Remove pipeline type select (#371) 2021-12-23 20:27:59 -08:00
dependabot[bot]
8b0b18bd07 Bump y18n from 4.0.0 to 4.0.3 in /photon-client (#372)
Bumps [y18n](https://github.com/yargs/y18n) from 4.0.0 to 4.0.3.
- [Release notes](https://github.com/yargs/y18n/releases)
- [Changelog](https://github.com/yargs/y18n/blob/y18n-v4.0.3/CHANGELOG.md)
- [Commits](https://github.com/yargs/y18n/compare/v4.0.0...y18n-v4.0.3)

---
updated-dependencies:
- dependency-name: y18n
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-12-20 10:39:21 -08:00
Matt
1766f3bf0f Add picam resolution type, remove k prefix (#365)
Changes "kUnknown" to "Picam" in resolution dropdown; similarly "kBGR" -> "BGR"
2021-12-20 10:08:17 -08:00
Matt
4578fa756c Null-check files to be cleaned in Logger (#367) 2021-12-19 06:52:13 -06:00
Matt
d6e1e28fc2 Add pi version check (#360)
Should prevent GPU acceleration on Pi 4
2021-12-18 13:12:53 -05:00
Matt
49048c3998 Don't limit divisors in driver mode (#363) 2021-12-18 12:56:52 -05:00
Matt
8b079d9b20 Add pi-only JAR createion (#362) 2021-12-18 12:53:08 -05:00
icemannie
1522adaa0e Remove pipeline index/driver mode/led mode caching in PhotonCamera 2021-12-15 12:16:09 -05:00
Matt
3cd57b8b43 Don't generate Pi images on PRs (#350) 2021-12-03 23:19:20 -05:00
Chris Gerth
c944967476 Offline Update (.jar replace) (#340)
Allows users to upload a new JAR to a Pi. Also bumps the pi image to increase the heap size.
2021-12-03 23:08:51 -05:00
Matt
dbd631da61 Suffix pi image zips with "-image.zip" (#339)
* Suffix pi image zips with "-image.zip"

* Update main.yml
2021-11-29 21:43:41 -06:00
mdurrani808
f731ae37d2 Updated README.md (#344) 2021-11-29 21:20:46 -06:00
Matt
0a8da1a0bd Fix image glob in dev releases (#338)
Should now upload pi images to dev releases
2021-11-28 18:32:33 -05:00
390 changed files with 27718 additions and 14303 deletions

View File

@@ -24,24 +24,20 @@ jobs:
# The type of runner that the job will run on.
runs-on: ubuntu-latest
# Grab the docker container.
container:
image: docker://node:10
steps:
# Checkout code.
- uses: actions/checkout@v1
- uses: actions/checkout@v3
# Setup Node.js
- name: Setup Node.js
uses: actions/setup-node@v1
uses: actions/setup-node@v3
with:
node-version: 10
node-version: 16
# Run npm
- run: |
npm ci
npm run build --if-present
- run: npm update -g npm
- run: npm ci
- run: npm run build --if-present
# Upload client artifact.
- uses: actions/upload-artifact@master
@@ -56,7 +52,9 @@ jobs:
steps:
# Checkout code.
- name: Checkout code
uses: actions/checkout@v1
uses: actions/checkout@v3
with:
fetch-depth: 0
# Fetch tags.
- name: Fetch tags
@@ -64,9 +62,10 @@ jobs:
# Install Java 11.
- name: Install Java 11
uses: actions/setup-java@v1
uses: actions/setup-java@v3
with:
java-version: 11
distribution: temurin
# Run Gradle build.
- name: Gradle Build
@@ -84,12 +83,12 @@ jobs:
# Publish Coverage Report.
- name: Publish Server Coverage Report
uses: codecov/codecov-action@v1
uses: codecov/codecov-action@v3
with:
file: ./photon-server/build/reports/jacoco/test/jacocoTestReport.xml
- name: Publish Core Coverage Report
uses: codecov/codecov-action@v1
uses: codecov/codecov-action@v3
with:
file: ./photon-core/build/reports/jacoco/test/jacocoTestReport.xml
@@ -98,13 +97,13 @@ jobs:
steps:
# Checkout docs.
- uses: actions/checkout@v2
- uses: actions/checkout@v3
with:
repository: 'PhotonVision/photonvision-docs.git'
ref: master
# Install Python.
- uses: actions/setup-python@v2
- uses: actions/setup-python@v4
with:
python-version: '3.6'
@@ -135,12 +134,15 @@ jobs:
steps:
# Checkout code.
- uses: actions/checkout@v1
- uses: actions/checkout@v3
with:
fetch-depth: 0
# Install Java 11.
- uses: actions/setup-java@v1
- uses: actions/setup-java@v3
with:
java-version: 11
distribution: temurin
# Check server code with Spotless.
- run: |
@@ -148,46 +150,11 @@ jobs:
./gradlew spotlessCheck
photon-release:
if: startsWith(github.ref, 'refs/tags/v')
needs: [photon-build-package]
runs-on: ubuntu-latest
steps:
- uses: actions/download-artifact@v2
with:
name: jar
- run: |
NEW_JAR=$(realpath $(find . -name photonvision\*.jar))
sudo apt install unzip zip
curl -sk https://api.github.com/repos/photonvision/photon-pi-gen/releases/latest | grep "browser_download_url.*zip" | cut -d : -f 2,3 | tr -d '"' | wget -qi -
FILE_NAME=$(ls | grep image_*.zip)
unzip $FILE_NAME
IMAGE_FILE=$(ls | grep *.img)
TMP=$(mktemp -d)
LOOP=$(sudo losetup --show -fP "${IMAGE_FILE}")
sudo mount ${LOOP}p2 $TMP
pushd .
cd $TMP/opt/photonvision
ls
sudo cp $NEW_JAR photonvision.jar
popd
sudo umount ${TMP}
sudo rmdir ${TMP}
rm $FILE_NAME
NEW_IMAGE=$(basename "${NEW_JAR/jar/img}")
mv $IMAGE_FILE $NEW_IMAGE
zip -r $(basename "${NEW_JAR/jar/zip}") $NEW_IMAGE
rm $NEW_IMAGE
- uses: softprops/action-gh-release@v1
with:
files: '**/*'
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# Building photonlib
photonlib-build-host:
env:
MACOSX_DEPLOYMENT_TARGET: 10.14
strategy:
fail-fast: false
matrix:
@@ -202,12 +169,13 @@ jobs:
runs-on: ${{ matrix.os }}
name: "Photonlib - Build - ${{ matrix.artifact-name }}"
steps:
- uses: actions/checkout@v2.3.4
- uses: actions/checkout@v3
with:
fetch-depth: 0
- uses: actions/setup-java@v1
- uses: actions/setup-java@v3
with:
java-version: 11
distribution: temurin
- run: git fetch --tags --force
- run: |
chmod +x gradlew
@@ -234,12 +202,13 @@ jobs:
container: ${{ matrix.container }}
name: "Photonlib - Build - ${{ matrix.artifact-name }}"
steps:
- uses: actions/checkout@v2.3.4
- uses: actions/checkout@v3
with:
fetch-depth: 0
- uses: actions/setup-java@v1
- uses: actions/setup-java@v3
with:
java-version: 11
distribution: temurin
- run: |
chmod +x gradlew
./gradlew photon-lib:build --max-workers 1
@@ -254,14 +223,14 @@ jobs:
name: "wpiformat"
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v3
- name: Fetch all history and metadata
run: |
git fetch --prune --unshallow
git checkout -b pr
git branch -f master origin/master
- name: Set up Python 3.8
uses: actions/setup-python@v2
uses: actions/setup-python@v4
with:
python-version: 3.8
- name: Install clang-format
@@ -278,7 +247,7 @@ jobs:
- name: Generate diff
run: git diff HEAD > wpiformat-fixes.patch
if: ${{ failure() }}
- uses: actions/upload-artifact@v2
- uses: actions/upload-artifact@v3
with:
name: wpiformat fixes
path: wpiformat-fixes.patch
@@ -292,12 +261,15 @@ jobs:
steps:
# Checkout code.
- uses: actions/checkout@v1
- uses: actions/checkout@v3
with:
fetch-depth: 0
# Install Java 11.
- uses: actions/setup-java@v1
- uses: actions/setup-java@v3
with:
java-version: 11
distribution: temurin
# Clear any existing web resources.
- run: |
@@ -305,62 +277,72 @@ jobs:
mkdir -p photon-server/src/main/resources/web/docs
# Download client artifact to resources folder.
- uses: actions/download-artifact@v2
- uses: actions/download-artifact@v3
with:
name: built-client
path: photon-server/src/main/resources/web/
# Copy thinclient in as well
- run: cp -r photon-thinclient/* photon-server/src/main/resources/web/
# Download docs artifact to resources folder.
- uses: actions/download-artifact@v2
- uses: actions/download-artifact@v3
with:
name: built-docs
path: photon-server/src/main/resources/web/docs
# Build fat jar.
# Build fat jar for both pi and everything
- run: |
chmod +x gradlew
./gradlew photon-server:shadowJar --max-workers 1
./gradlew photon-server:shadowJar --max-workers 1 -Ppionly
# The image will only pull the Pi JAR in
- name: Generate image
if: github.event_name != 'pull_request'
run: |
NEW_JAR=$(realpath $(find . -name photonvision\*.jar))
sudo apt install unzip zip
curl -sk https://api.github.com/repos/photonvision/photon-pi-gen/releases/latest | grep "browser_download_url.*zip" | cut -d : -f 2,3 | tr -d '"' | wget -qi -
FILE_NAME=$(ls | grep image_*.zip)
unzip $FILE_NAME
IMAGE_FILE=$(ls | grep *.img)
TMP=$(mktemp -d)
LOOP=$(sudo losetup --show -fP "${IMAGE_FILE}")
sudo mount ${LOOP}p2 $TMP
pushd .
cd $TMP/opt/photonvision
ls
sudo cp $NEW_JAR photonvision.jar
popd
sudo umount ${TMP}
sudo rmdir ${TMP}
rm $FILE_NAME
NEW_IMAGE=$(basename "${NEW_JAR/jar/img}")
mv $IMAGE_FILE $NEW_IMAGE
zip -r $(basename "${NEW_JAR/jar/zip}") $NEW_IMAGE
rm $NEW_IMAGE
chmod +x scripts/generatePiImage.sh
./scripts/generatePiImage.sh
# Upload final fat jar as artifact.
- uses: actions/upload-artifact@master
- uses: actions/upload-artifact@v3
with:
name: jar
name: jars
path: photon-server/build/libs
- uses: actions/upload-artifact@master
- uses: actions/upload-artifact@v3
if: github.event_name != 'pull_request'
with:
name: image
path: image*.zip
path: photonvision*.xz
- uses: eine/tip@master
- uses: pyTooling/Actions/releaser@r0
with:
token: ${{ secrets.GITHUB_TOKEN }}
tag: 'Dev'
rm: true
files: |
photon-server/build/libs/*.jar
image*.zip
photonvision*.xz
if: github.event_name == 'push'
photon-release:
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: jars
# 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
with:
files: '**/*'
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

7
.gitignore vendored
View File

@@ -30,6 +30,7 @@ backend/settings/
*.nar
*.ear
*.zip
*.xz
*.tar.gz
*.rar
@@ -143,3 +144,9 @@ build/*
build
photon-lib/src/main/java/org/photonvision/PhotonVersion.java
/photonlib-java-examples/bin/
photon-lib/src/generate/native/include/PhotonVersion.h
.gitattributes
lib/*
photon-server/lib/libapriltag.so
photon-server/bin/main/nativelibraries/apriltag/*
photon-server/src/main/resources/nativelibraries/apriltag/*

View File

@@ -11,8 +11,11 @@ cppSrcFileInclude {
modifiableFileExclude {
\.jpg$
\.jpeg$
\.png$
\.gif$
\.so$
\.dll$
}
includeProject {
@@ -25,7 +28,3 @@ includeOtherLibs {
^units/
^wpi/
}
licenseUpdateExclude {
\.java$
}

View File

@@ -10,8 +10,17 @@ If you are interested in contributing code or documentation to the project, plea
## Authors
A list of contributors is available in our documentation on ReadTheDocs.
<a href="https://github.com/PhotonVision/photonvision/graphs/contributors">
<img src="https://contrib.rocks/image?repo=PhotonVision/photonvision" />
</a>
## Gradle Arguments
Note that these are case sensitive!
* `-Ppionly`: only builds for `linuxraspbian`, which reduces JAR size. The JAR name will have "-raspi" appended.
- `-PtgtIp`: deploys (builds and copies the JAR) to the coprocessor at the specified IP
- `-Pprofile`: enables JVM profiling
## Acknowledgments
PhotonVision was forked from [Chameleon Vision](https://github.com/Chameleon-Vision/chameleon-vision/). Thank you to everyone who worked on the original project.
@@ -29,3 +38,9 @@ PhotonVision was forked from [Chameleon Vision](https://github.com/Chameleon-Vis
## License
PhotonVision is licensed under the [GNU General Public License](https://www.gnu.org/licenses/gpl-3.0.html)
## Meeting Notes
Our meeting notes can be found in the wiki section of this repository.
* [2020 Meeting Notes](https://github.com/PhotonVision/photonvision/wiki/2020-Meeting-Notes)
* [2021 Meeting Notes](https://github.com/PhotonVision/photonvision/wiki/2021-Meeting-Notes)

View File

@@ -1,17 +1,17 @@
plugins {
id "com.diffplug.gradle.spotless" version "3.28.0"
id "com.github.johnrengelman.shadow" version "5.2.0"
id "com.github.node-gradle.node" version "2.2.4" apply false
id "com.diffplug.spotless" version "6.1.2"
id "com.github.johnrengelman.shadow" version "7.1.2"
id "com.github.node-gradle.node" version "3.1.1" apply false
id "edu.wpi.first.GradleJni" version "1.0.0"
id "edu.wpi.first.GradleVsCode" version "1.0.0"
id "edu.wpi.first.NativeUtils" version "2022.4.4" apply false
id "edu.wpi.first.GradleVsCode" version "1.1.0"
id "edu.wpi.first.NativeUtils" version "2022.8.1" apply false
id "edu.wpi.first.wpilib.repositories.WPILibRepositoriesPlugin" version "2020.2"
id "org.hidetake.ssh" version "2.10.1"
}
allprojects {
repositories {
jcenter()
mavenCentral()
maven { url = "https://maven.photonvision.org/repository/internal/" }
}
wpilibRepositories.addAllReleaseRepositories(it)
@@ -22,24 +22,36 @@ allprojects {
apply from: "versioningHelper.gradle"
ext {
wpilibVersion = "2022.1.1-beta-3-1-g4ba80a3"
wpilibVersion = "2022.1.1"
opencvVersion = "4.5.2-1"
joglVersion = "2.4.0-rc-20200307"
pubVersion = versionString
isDev = pubVersion.startsWith("dev")
if(project.hasProperty('pionly')) {
jniPlatforms = ['linuxraspbian']
} else if(project.hasProperty('winonly')) {
jniPlatforms = ['windowsx86-64']
} else {
jniPlatforms = ['linuxaarch64bionic', 'linuxraspbian', 'linuxx86-64', 'osxx86-64', 'windowsx86-64']
}
println("Building for archs " + jniPlatforms)
}
spotless {
java {
toggleOffOn()
googleJavaFormat()
paddedCell()
indentWithTabs(2)
indentWithSpaces(4)
removeUnusedImports()
trimTrailingWhitespace()
endWithNewline()
}
java {
target "**/*.java"
licenseHeaderFile "$rootDir/LicenseHeader.txt"
targetExclude("photon-core/src/main/java/org/photonvision/PhotonVersion.java")
targetExclude("photon-lib/src/main/java/org/photonvision/PhotonVersion.java")
}

8
gradle.properties Normal file
View File

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

View File

@@ -1,5 +1,5 @@
distributionBase=GRADLE_USER_HOME
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
zipStorePath=wrapper/dists

227
gradlew vendored
View File

@@ -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");
# you may not use this file except in compliance with the License.
@@ -16,68 +16,58 @@
# limitations under the License.
#
##############################################################################
##
## Gradle start up script for UN*X
##
##############################################################################
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
PRG="$0"
# Need this for relative symlinks.
while [ -h "$PRG" ] ; do
ls=`ls -ld "$PRG"`
link=`expr "$ls" : '.*-> \(.*\)$'`
if expr "$link" : '/.*' > /dev/null; then
PRG="$link"
else
PRG=`dirname "$PRG"`"/$link"
fi
app_path=$0
# Need this for daisy-chained symlinks.
while
APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
[ -h "$app_path" ]
do
ls=$( ls -ld "$app_path" )
link=${ls#*' -> '}
case $link in #(
/*) app_path=$link ;; #(
*) app_path=$APP_HOME$link ;;
esac
done
SAVED="`pwd`"
cd "`dirname \"$PRG\"`/" >/dev/null
APP_HOME="`pwd -P`"
cd "$SAVED" >/dev/null
APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
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.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD="maximum"
MAX_FD=maximum
warn () {
echo "$*"
}
} >&2
die () {
echo
echo "$*"
echo
exit 1
}
} >&2
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
nonstop=false
case "`uname`" in
CYGWIN* )
cygwin=true
;;
Darwin* )
darwin=true
;;
MINGW* )
msys=true
;;
NONSTOP* )
nonstop=true
;;
case "$( uname )" in #(
CYGWIN* ) cygwin=true ;; #(
Darwin* ) darwin=true ;; #(
MSYS* | MINGW* ) msys=true ;; #(
NONSTOP* ) nonstop=true ;;
esac
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 [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD="$JAVA_HOME/jre/sh/java"
JAVACMD=$JAVA_HOME/jre/sh/java
else
JAVACMD="$JAVA_HOME/bin/java"
JAVACMD=$JAVA_HOME/bin/java
fi
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
@@ -98,7 +88,7 @@ Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
JAVACMD="java"
JAVACMD=java
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
@@ -106,80 +96,95 @@ location of your Java installation."
fi
# Increase the maximum file descriptors if we can.
if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
MAX_FD_LIMIT=`ulimit -H -n`
if [ $? -eq 0 ] ; then
if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
MAX_FD="$MAX_FD_LIMIT"
fi
ulimit -n $MAX_FD
if [ $? -ne 0 ] ; then
warn "Could not set maximum file descriptor limit: $MAX_FD"
fi
else
warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
fi
fi
# 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" ;;
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
case $MAX_FD in #(
max*)
MAX_FD=$( ulimit -H -n ) ||
warn "Could not query maximum file descriptor limit"
esac
case $MAX_FD in #(
'' | soft) :;; #(
*)
ulimit -n "$MAX_FD" ||
warn "Could not set maximum file descriptor limit to $MAX_FD"
esac
fi
# Escape application args
save () {
for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
echo " "
}
APP_ARGS=`save "$@"`
# Collect all arguments for the java command, stacking in reverse order:
# * args from the command line
# * the main class name
# * -classpath
# * -D...appname settings
# * --module-path (only if needed)
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
# Collect all arguments for the java command, following the shell quoting and substitution rules
eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
# For Cygwin or MSYS, switch paths to Windows format before running java
if "$cygwin" || "$msys" ; then
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
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" "$@"

178
gradlew.bat vendored
View File

@@ -1,89 +1,89 @@
@rem
@rem Copyright 2015 the original author or authors.
@rem
@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 obtain a copy of the License at
@rem
@rem https://www.apache.org/licenses/LICENSE-2.0
@rem
@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 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
@if "%DEBUG%" == "" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@rem
@rem ##########################################################################
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
if "%DIRNAME%" == "" set DIRNAME=.
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if "%ERRORLEVEL%" == "0" goto execute
echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto execute
echo.
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:execute
@rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
:end
@rem End local scope for the variables with windows NT shell
if "%ERRORLEVEL%"=="0" goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
exit /b 1
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega
@rem
@rem Copyright 2015 the original author or authors.
@rem
@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 obtain a copy of the License at
@rem
@rem https://www.apache.org/licenses/LICENSE-2.0
@rem
@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 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
@if "%DEBUG%" == "" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@rem
@rem ##########################################################################
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
if "%DIRNAME%" == "" set DIRNAME=.
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if "%ERRORLEVEL%" == "0" goto execute
echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto execute
echo.
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:execute
@rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
:end
@rem End local scope for the variables with windows NT shell
if "%ERRORLEVEL%"=="0" goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
exit /b 1
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega

File diff suppressed because it is too large Load Diff

View File

@@ -16,9 +16,10 @@
"jspdf": "^2.4.0",
"material-design-icons-iconfont": "^5.0.1",
"msgpack5": "^4.2.1",
"three-full": "^28.0.2",
"vue": "^2.6.12",
"vue-axios": "^2.1.5",
"vue-native-websocket": "git+https://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",
"vuetify": "^2.3.10",
"vuex": "^3.5.1"

View File

@@ -87,31 +87,56 @@
</v-icon>
</v-list-item-icon>
<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>
<div style="position: absolute; bottom: 0; left: 0;">
<v-list-item>
<v-list-item-icon>
<v-icon v-if="$store.state.settings.networkSettings.runNTServer">mdi-server</v-icon>
<img v-else-if="$store.state.ntConnectionInfo.connected" src="@/assets/robot.svg" alt="">
<img v-else class="pulse" style="border-radius: 100%" src="@/assets/robot-off.svg" alt="">
<v-icon v-if="$store.state.settings.networkSettings.runNTServer">
mdi-server
</v-icon>
<img
v-else-if="$store.state.ntConnectionInfo.connected"
src="@/assets/robot.svg"
alt=""
>
<img
v-else
class="pulse"
style="border-radius: 100%"
src="@/assets/robot-off.svg"
alt=""
>
</v-list-item-icon>
<v-list-item-content>
<v-list-item-title class="text-wrap" v-if="$store.state.settings.networkSettings.runNTServer">
NetworkTables server running for {{$store.state.ntConnectionInfo.clients ? $store.state.ntConnectionInfo.clients : 'zero'}} clients!
<v-list-item-title
v-if="$store.state.settings.networkSettings.runNTServer"
class="text-wrap"
>
NetworkTables server running for {{ $store.state.ntConnectionInfo.clients ? $store.state.ntConnectionInfo.clients : 'zero' }} clients!
</v-list-item-title>
<v-list-item-title class="text-wrap" v-else-if="$store.state.ntConnectionInfo.connected && $store.state.backendConnected">
Robot connected! {{$store.state.ntConnectionInfo.address}}
<v-list-item-title
v-else-if="$store.state.ntConnectionInfo.connected && $store.state.backendConnected"
class="text-wrap"
>
Robot connected! {{ $store.state.ntConnectionInfo.address }}
</v-list-item-title>
<v-list-item-title class="text-wrap" v-else>
<v-list-item-title
v-else
class="text-wrap"
>
Not connected to robot!
</v-list-item-title>
<a
href="/#/settings"
style="color:#FFD843"
>{{"Team: " + $store.state.settings.networkSettings.teamNumber}}</a>
<router-link
v-if="!$store.state.settings.networkSettings.runNTServer"
to="settings"
class="accent--text"
@click="switchToSettingsTab"
>
Team number is {{ $store.state.settings.networkSettings.teamNumber }}
</router-link>
</v-list-item-content>
</v-list-item>
@@ -121,9 +146,9 @@
mdi-wifi
</v-icon>
<v-icon
v-else
class="pulse"
style="border-radius: 100%;"
v-else
class="pulse"
style="border-radius: 100%;"
>
mdi-wifi-off
</v-icon>
@@ -135,7 +160,6 @@
</v-list-item-content>
</v-list-item>
</div>
</v-list>
</v-navigation-drawer>
<v-main>
@@ -159,23 +183,27 @@
<logs />
</v-dialog>
<v-dialog
v-model="needsTeamNumberSet"
width="500"
dark
persistent
v-model="needsTeamNumberSet"
width="500"
dark
persistent
>
<v-card
dark
color="primary"
flat
dark
color="primary"
flat
>
<v-card-title>No team number set!</v-card-title>
<v-card-text>
PhotonVision cannot connect to your robot! Please
<a
href="/#/settings"
style="color:#FFD843"
>head to the settings page</a> and set your team number.
<router-link
to="settings"
class="accent--text"
@click="switchToSettingsTab"
>
visit the settings tab
</router-link>
and set your team number.
</v-card-text>
</v-card>
</v-dialog>
@@ -219,12 +247,6 @@ export default {
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 => {
@@ -319,8 +341,7 @@ export default {
}
}
this.previouslySelectedIndices = null;
}
,
},
switchToSettingsTab() {
this.axios.post('http://' + this.$address + '/api/sendMetrics', {})
}

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

View File

@@ -5,7 +5,7 @@
:style="styleObject"
:src="src"
alt=""
@click="e => $emit('click', e)"
@click="e => {this.openThinclientStream(e)}"
>
</template>
@@ -13,7 +13,7 @@
export default {
name: "CvImage",
// eslint-disable-next-line vue/require-prop-types
props: ['address', 'scale', 'maxHeight', 'maxHeightMd', 'maxHeightXl', 'colorPicking', 'id', 'disconnected'],
props: ['idx', 'scale', 'maxHeight', 'maxHeightMd', 'maxHeightLg', 'maxHeightXl', 'colorPicking', 'id', 'disconnected'],
data() {
return {
seed: 1.0,
@@ -37,25 +37,57 @@
if (this.$vuetify.breakpoint.xl) {
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;
}
return ret;
}
},
src: {
port: {
get() {
return this.disconnected ? require("../../assets/noStream.jpg") : this.address + "?" + this.seed // This prevents caching
},
},
if(this.idx == 0){
return this.$store.state.cameraSettings[this.$store.state.currentCameraIndex].inputStreamPort;
} else {
return this.$store.state.cameraSettings[this.$store.state.currentCameraIndex].outputStreamPort;
}
}
}
},
watch : {
port(newPort, oldPort){
newPort;
oldPort;
this.reload();
},
disconnected(newVal, oldVal){
oldVal;
if(newVal){
this.wsStream.stopStream();
} else {
this.wsStream.startStream();
}
}
},
mounted() {
this.reload(); // Force reload image on creation
var wsvs = require('../../plugins/WebsocketVideoStream');
this.wsStream = new wsvs.WebsocketVideoStream(this.id, this.port, window.location.host);
},
unmounted() {
this.wsStream.stopStream();
this.wsStream.ws_close();
},
methods: {
reload() {
this.seed = new Date().getTime();
console.log("Reloading " + this.id + " with port " + String(this.port));
this.wsStream.setPort(this.port);
},
openThinclientStream(e){
e;
var URL = "/thinclient.html?port=" + String(this.port) + "&host=" + window.location.hostname;
window.open(URL, '_blank');
}
},
}

View File

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

View File

@@ -19,7 +19,9 @@
hide-details
class="align-center"
dark
color="accent"
:color="inverted ? 'rgba(255, 255, 255, 0.2)' : 'accent'"
:track-color="inverted ? 'accent' : undefined"
thumb-color="accent"
:step="step"
@input="handleInput"
@mousedown="$emit('rollback', localValue)"
@@ -76,7 +78,7 @@ export default {
TooltippedLabel,
},
// 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() {
return {
prependFocused: false,

View File

@@ -3,7 +3,7 @@
<v-tooltip
:disabled="tooltip === undefined"
right
open-delay="600"
open-delay="300"
>
<template v-slot:activator="{ on, attrs }">
<span

View File

@@ -1,154 +1,268 @@
<template>
<div>
<div
id="MapContainer"
style="flex-grow:1"
>
<v-row>
<v-col
align="center"
cols="12"
>
<span class="white--text">Target Location</span>
</v-col>
</v-row>
<v-row>
<v-col
align="center"
cols="12"
align-self="stretch"
>
<canvas
id="canvasId"
class="mt-2"
width="800"
height="800"
style="width:100%;height:100%"
/>
</v-col>
<v-row>
<v-col>
<v-btn
class="ml-10"
color="secondary"
@click="resetCamFirstPerson"
>
First Person
</v-btn>
</v-col>
<v-col>
<v-btn
class="ml-10"
color="secondary"
@click="resetCamThirdPerson"
>
Third Person
</v-btn>
</v-col>
</v-row>
</v-row>
</div>
</template>
<script>
import theme from "../../../theme";
export default {
name: "MiniMap",
props: {
// eslint-disable-next-line vue/require-default-prop
targets: Array,
// eslint-disable-next-line vue/require-default-prop
horizontalFOV: Number
},
data() {
return {
ctx: undefined,
canvas: undefined,
x: 0,
y: 0,
targetWidth: 40,
targetHeight: 6
}
},
computed: {
hLen: {
get() {
return Math.tan(this.horizontalFOV / 2 * Math.PI / 180) * 150;
}
}
},
watch: {
targets: {
deep: true,
handler() {
this.draw();
}
},
horizontalFOV() {
this.draw();
}
},
mounted: function () {
const canvas = document.getElementById("canvasId"); // getting the canvas element
const ctx = canvas.getContext("2d"); // getting the canvas context
this.canvas = canvas; // setting the canvas as a vue variable
this.ctx = ctx; // setting the canvas context as a vue variable
this.grad = this.ctx.createLinearGradient(400, 800, 400, 600);
this.grad.addColorStop(0, "rgb(119,119,119)");
this.grad.addColorStop(0.05, "rgba(14,92,22,0.96)");
this.grad.addColorStop(0.8, 'rgba(43,43,43,0.48)');
import {
ArrowHelper,
BoxGeometry,
ConeGeometry,
Mesh,
MeshNormalMaterial,
PerspectiveCamera,
Quaternion,
Scene,
TrackballControls,
Vector3,
Color,
WebGLRenderer
} from "three-full";
// 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>
<style scoped>
#canvasId {
width: 400px;
height: 400px;
background-color: #232C37;
border-radius: 5px;
border: 2px solid grey;
box-shadow: 0 0 5px 1px;
}
th {
width: 80px;
text-align: center;
}
</style>

View File

@@ -1,11 +1,15 @@
<template>
<div>
<v-row align="center">
<v-row
align="center"
class="pl-6"
>
<v-col
cols="10"
md="5"
lg="10"
class="pt-0 pb-0 pl-6"
no-gutters
class="pa-0"
>
<CVselect
v-if="isCameraNameEdit === false"
@@ -59,7 +63,8 @@
cols="10"
md="5"
lg="10"
class="pt-0 pb-0 pl-6"
no-gutters
class="pa-0"
>
<CVselect
v-model="currentPipelineIndex"
@@ -139,14 +144,16 @@
<v-col
v-if="currentPipelineType >= 0"
cols="10"
md="5"
md="11"
lg="10"
class="pt-0 pb-0 pl-6 ml-16"
no-gutters
class="pa-0"
>
<CVselect
v-model="currentPipelineType"
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)"
/>
</v-col>
@@ -173,15 +180,9 @@
<v-card-text>
<CVinput
v-model="newPipelineName"
name="Pipeline"
name="Name"
:error-message="checkPipelineName"
/>
<CVselect
v-model="newPipelineType"
name="Pipeline Type"
:list="['Reflective', 'Shape']"
:disabled="isPipelineNameEdit"
/>
</v-card-text>
<v-divider />
<v-card-actions>
@@ -263,7 +264,6 @@ export default {
isPipelineNameEdit: false,
namingDialog: false,
newPipelineName: "",
newPipelineType: 0,
duplicateDialog: false,
showPipeTypeDialog: false,
proposedPipelineType : 0,
@@ -277,12 +277,12 @@ export default {
for (let cam in this.cameraList) {
if (this.cameraList.hasOwnProperty(cam)) {
if (this.newCameraName === this.cameraList[cam]) {
return "A camera by that name already Exists"
return "A camera by that name already exists"
}
}
}
} else {
return "A camera name can only contain letters, numbers and spaces"
return "A camera name can only contain letters, numbers, and spaces"
}
}
return "";
@@ -385,7 +385,7 @@ export default {
if (this.isPipelineNameEdit) {
this.handleInputWithIndex("changePipelineName", this.newPipelineName);
} else {
this.handleInputWithIndex("addNewPipeline", [this.newPipelineName, this.newPipelineType]); // 0 for reflective, 1 for colored shpae
this.handleInputWithIndex("addNewPipeline", [this.newPipelineName, this.currentPipelineType]); // 0 for reflective, 1 for colored shpae
}
this.discardPipelineNameChange();
}

View File

@@ -15,11 +15,11 @@ if (process.env.NODE_ENV === "production") {
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, wsURL, {
Vue.use(VueNativeSock, wsDataURL, {
reconnection: true,
reconnectionDelay: 100,
connectManually: true,

View File

@@ -5,9 +5,11 @@ function initColorPicker() {
if (!canvas)
canvas = document.createElement('canvas');
image = document.querySelector('#normal-stream');
canvas.width = image.width;
canvas.height = image.height;
image = document.querySelector('#raw-stream');
if (image !== null) {
canvas.width = image.width;
canvas.height = image.height;
}
}
//Called on click of the image,

View File

@@ -0,0 +1,148 @@
export class WebsocketVideoStream{
constructor(drawDiv, streamPort, host) {
this.drawDiv = drawDiv;
this.image = document.getElementById(this.drawDiv);
this.streamPort = streamPort;
this.serverAddr = "ws://" + host + "/websocket_cameras";
this.noStream = false;
this.noStreamPrev = false;
this.setNoStream();
this.ws_connect();
this.imgData = null;
this.imgDataTime = -1;
this.imgObjURL = null;
this.frameRxCount = 0;
requestAnimationFrame(()=>this.animationLoop());
}
animationLoop(){
var now = window.performance.now();
if((now - this.imgDataTime) > 2500 && this.imgData != null){
//Handle websocket send timeouts by restarting
this.setNoStream();
this.stopStream();
setTimeout(this.startStream.bind(this), 1000); //restart stream one second later
} else {
if(this.streamPort == null){
this.setNoStream();
} else if (this.imgData != null) {
//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;
this.noStream = false;
} else {
//Nothing, hold previous image while waiting for next frame
}
}
requestAnimationFrame(()=>this.animationLoop());
}
setNoStream() {
this.noStreamPrev = this.noStream;
this.noStream = true;
if(this.noStreamPrev == false && this.noStream == true){
//One-shot background change to preserve animation
this.image.src = require("../assets/loading.gif");
}
}
startStream() {
if(this.serverConnectionActive == true && this.streamPort > 0){
this.ws.send(JSON.stringify({"cmd": "subscribe", "port":this.streamPort}));
this.noStream = false;
}
}
stopStream() {
if(this.serverConnectionActive == true && this.streamPort > 0){
this.ws.send(JSON.stringify({"cmd": "unsubscribe"}));
this.noStream = true;
}
}
setPort(streamPort){
this.stopStream();
this.frameRxCount = 0;
this.streamPort = streamPort;
this.startStream();
}
ws_onOpen() {
// Set the flag allowing general server communication
this.serverConnectionActive = true;
console.log("Connected!");
this.startStream();
}
ws_onClose(e) {
this.setNoStream();
//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 receive info here? Maybe "available 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.ws = new WebSocket(this.serverAddr);
this.ws.binaryType = "blob";
this.ws.onopen = this.ws_onOpen.bind(this);
this.ws.onmessage = this.ws_onMessage.bind(this);
this.ws.onclose = this.ws_onClose.bind(this);
this.ws.onerror = this.ws_onError.bind(this);
console.log("Connecting to server " + this.serverAddr);
}
ws_close(){
this.ws.close();
}
}
export default {WebsocketVideoStream}

View File

@@ -19,6 +19,8 @@ export default new Vuex.Store({
connected: false,
address: "",
clients: 0,
},
networkInfo: {
possibleRios: ["Loading..."],
deviceips: ["Loading..."],
},
@@ -33,8 +35,8 @@ export default new Vuex.Store({
tiltDegrees: 0.0,
currentPipelineIndex: 0,
pipelineNicknames: ["Unknown"],
outputStreamPort: 1181,
inputStreamPort: 1182,
outputStreamPort: 0,
inputStreamPort: 0,
nickname: "Unknown",
videoFormatList: [
{
@@ -49,13 +51,15 @@ export default new Vuex.Store({
isFovConfigurable: true,
calibrated: false,
currentPipelineSettings: {
pipelineType: 2, // One of "calib", "driver", "reflective", "shape"
pipelineType: 4, // One of "calib", "driver", "reflective", "shape", "AprilTag"
// 2 is reflective
// Settings that apply to all pipeline types
cameraExposure: 1,
cameraBrightness: 2,
cameraGain: 3,
cameraAutoExposure: false,
cameraRedGain: 3,
cameraBlueGain: 4,
inputImageRotationMode: 0,
cameraVideoModeIndex: 0,
streamingFrameDivisor: 0,
@@ -64,10 +68,13 @@ export default new Vuex.Store({
hsvHue: [0, 15],
hsvSaturation: [0, 15],
hsvValue: [0, 25],
hueInverted: false,
contourArea: [0, 12],
contourRatio: [0, 12],
contourFullness: [0, 12],
contourSpecklePercentage: 5,
contourFilterRangeX: 5,
contourFilterRangeY: 5,
contourGroupingMode: 0,
contourIntersection: 0,
contourSortMode: 0,
@@ -82,7 +89,14 @@ export default new Vuex.Store({
cornerDetectionAccuracyPercentage: 10,
// Settings that apply to shape
// Settings that apply to AprilTag
tagFamily: 0,
decimate: 1.0,
blur: 0.0,
threads: 1,
debug: false,
refineEdges: true,
numIterations: 1,
}
}
],
@@ -96,9 +110,18 @@ export default new Vuex.Store({
skew: 0,
area: 0,
// 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: {
general: {
version: "Unknown",
@@ -151,6 +174,7 @@ export default new Vuex.Store({
calibrationData: set('calibrationData'),
metrics: set('metrics'),
ntConnectionInfo: set('ntConnectionInfo'),
networkInfo: set('networkInfo'),
backendConnected: set('backendConnected'),
logString: (state, newStr) => {
const str = state.logMessages;

View File

@@ -19,8 +19,8 @@
<CVselect
v-model="currentCameraIndex"
name="Camera"
select-cols="10"
:list="$store.getters.cameraList"
:select-cols="$vuetify.breakpoint.mdAndUp ? 10 : 7"
@input="handleInput('currentCamera',currentCameraIndex)"
/>
<CVnumberinput
@@ -28,13 +28,7 @@
:tooltip="cameraSettings.isFovConfigurable ? 'Field of view (in degrees) of the camera measured across the diagonal of the frame, in a video mode which covers the whole sensor area.' : 'This setting is managed by a vendor'"
name="Maximum diagonal FOV"
:disabled="!cameraSettings.isFovConfigurable"
/>
<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
@@ -89,26 +83,26 @@
<CVnumberinput
v-model="squareSizeIn"
name="Pattern Spacing (in)"
label-cols="5"
tooltip="Spacing between pattern features in inches"
:disabled="isCalibrating"
:rules="[v => (v > 0) || 'Size must be positive']"
:label-cols="$vuetify.breakpoint.mdAndUp ? 5 : 7"
/>
<CVnumberinput
v-model="boardWidth"
name="Board width"
label-cols="5"
tooltip="Width of the board in dots or chessboard squares"
:disabled="isCalibrating"
:rules="[v => (v >= 4) || 'Width must be at least 4']"
:label-cols="$vuetify.breakpoint.mdAndUp ? 5 : 7"
/>
<CVnumberinput
v-model="boardHeight"
name="Board height"
label-cols="5"
tooltip="Height of the board in dots or chessboard squares"
:disabled="isCalibrating"
:rules="[v => (v >= 4) || 'Height must be at least 4']"
:label-cols="$vuetify.breakpoint.mdAndUp ? 5 : 7"
/>
</v-form>
</v-col>
@@ -144,6 +138,24 @@
text="Standard Deviation"
/>
</th>
<th class="text-center">
<tooltipped-label
tooltip="Estimated Horizontal FOV, in degrees"
text="Horizontal FOV"
/>
</th>
<th class="text-center">
<tooltipped-label
tooltip="Estimated Vertical FOV, in degrees"
text="Vertical FOV"
/>
</th>
<th class="text-center">
<tooltipped-label
tooltip="Estimated Diagonal FOV, in degrees"
text="Diagonal FOV"
/>
</th>
</tr>
</thead>
<tbody>
@@ -156,6 +168,9 @@
{{ isCalibrated(value) ? value.mean.toFixed(2) + "px" : "—" }}
</td>
<td> {{ isCalibrated(value) ? value.standardDeviation.toFixed(2) + "px" : "—" }} </td>
<td> {{ isCalibrated(value) ? value.horizontalFOV.toFixed(2) + "°" : "—" }} </td>
<td> {{ isCalibrated(value) ? value.verticalFOV.toFixed(2) + "°" : "—" }} </td>
<td> {{ isCalibrated(value) ? value.diagonalFOV.toFixed(2) + "°" : "—" }} </td>
</tr>
</tbody>
</v-simple-table>
@@ -179,10 +194,13 @@
>
<CVslider
v-model="$store.getters.currentPipelineSettings.cameraExposure"
:disabled="$store.getters.currentPipelineSettings.cameraAutoExposure"
name="Exposure"
:min="0"
:max="100"
slider-cols="8"
step="0.1"
tooltip="Directly controls how much light is allowed to fall onto the sensor, which affects apparent brightness"
@input="e => handlePipelineUpdate('cameraExposure', e)"
/>
<CVslider
@@ -193,14 +211,32 @@
slider-cols="8"
@input="e => handlePipelineUpdate('cameraBrightness', e)"
/>
<CVswitch
v-model="$store.getters.currentPipelineSettings.cameraAutoExposure"
class="pt-2"
name="Auto Exposure"
tooltip="Enables or Disables camera automatic adjustment for current lighting conditions"
@input="e => handlePipelineUpdate('cameraAutoExposure', e)"
/>
<CVslider
v-if="$store.getters.currentPipelineSettings.cameraGain !== -1"
v-model="$store.getters.currentPipelineSettings.cameraGain"
name="Gain"
:min="0"
:max="100"
slider-cols="8"
@input="e => handlePipelineUpdate('cameraGain', e)"
v-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-row>
@@ -255,7 +291,8 @@
>
<template>
<CVimage
:address="$store.getters.streamAddress[1]"
:id="cameras-cal"
:idx=1
:disconnected="!$store.state.backendConnected"
scale="100"
style="border-radius: 5px;"
@@ -326,6 +363,7 @@
import CVselect from '../components/common/cv-select';
import CVnumberinput from '../components/common/cv-number-input';
import CVslider from '../components/common/cv-slider';
import CVswitch from '../components/common/cv-switch';
import CVimage from "../components/common/cv-image";
import TooltippedLabel from "../components/common/cv-tooltipped-label";
import jsPDF from "jspdf";
@@ -338,6 +376,7 @@ export default {
CVselect,
CVnumberinput,
CVslider,
CVswitch,
CVimage
},
data() {
@@ -383,6 +422,9 @@ export default {
if (calib != null) {
it['standardDeviation'] = calib.standardDeviation;
it['mean'] = calib.perViewErrors.reduce((a, b) => a + b) / calib.perViewErrors.length;
it['horizontalFOV'] = 2 * Math.atan2(it.width/2,calib.intrinsics[0]) * (180/Math.PI);
it['verticalFOV'] = 2 * Math.atan2(it.height/2,calib.intrinsics[4]) * (180/Math.PI);
it['diagonalFOV'] = 2 * Math.atan2(Math.sqrt(it.width**2 + (it.height/(calib.intrinsics[4]/calib.intrinsics[0]))**2)/2,calib.intrinsics[0]) * (180/Math.PI);
}
filtered.push(it);
}
@@ -391,13 +433,11 @@ export default {
return filtered
}
},
stringResolutionList: {
get() {
return this.filteredResolutionList.map(res => `${res['width']} X ${res['height']}`);
}
},
cameraSettings: {
get() {
return this.$store.getters.currentCameraSettings;
@@ -406,7 +446,6 @@ export default {
this.$store.commit('cameraSettings', value);
}
},
boardType: {
get() {
return this.calibrationData.boardType
@@ -588,8 +627,7 @@ export default {
this.axios.post("http://" + this.$address + "/api/settings/camera", {
"settings": this.cameraSettings,
"index": this.$store.state.currentCameraIndex
}).then(
function (response) {
}).then(response => {
if (response.status === 200) {
this.$store.state.saveBar = true;
}
@@ -610,13 +648,14 @@ export default {
if (this.isCalibrating === true) {
data['takeCalibrationSnapshot'] = true
} else {
// This store prevents an edge case of a user not selecting a different resolution, which causes the set logic to not be called
this.$store.commit('mutateCalibrationState', {['videoModeIndex']: this.filteredResolutionList[this.selectedFilteredResIndex].index});
const calData = this.calibrationData;
calData.isCalibrating = true;
data['startPnpCalibration'] = calData;
console.log("starting calibration with index " + calData.videoModeIndex);
}
this.$store.commit('currentPipelineIndex', -2);
this.$socket.send(this.$msgPack.encode(data));
},
sendCalibrationFinish() {

View File

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

View File

@@ -0,0 +1,136 @@
<template>
<div>
<v-select
v-model="selectedFamily"
dark
color="accent"
item-color="secondary"
label="Select target family"
:items="familyList"
@input="handlePipelineUpdate('tagFamily', targetList.indexOf(selectedModel))"
/>
<CVslider
v-model="decimate"
class="pt-2"
slider-cols="8"
name="Decimate"
min="0"
max="3"
step=".5"
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="numIterations"
class="pt-2 pb-4"
slider-cols="8"
name="Pose Estimation Iterations"
min="0"
max="500"
step="1"
tooltip="Number of iterations the pose estimation algorithm will run, 50-100 is a good starting point"
@input="handlePipelineData('numIterations')"
/>
</div>
</template>
<script>
import CVslider from '../../components/common/cv-slider'
import CVswitch from '../../components/common/cv-switch'
export default {
name: "AprilTag",
components: {
CVslider,
CVswitch,
},
data() {
return {
familyList: ["tag36h11"],
}
},
computed: {
selectedFamily: {
get() {
let ret = this.$store.getters.currentPipelineSettings.tagFamily
return this.familyList[ret];
},
set(val) {
this.$store.commit("mutatePipeline", {"tagFamily": this.familyList.indexOf(val)})
}
},
decimate: {
get() {
return this.$store.getters.currentPipelineSettings.decimate
},
set(val) {
this.$store.commit("mutatePipeline", {"decimate": val});
}
},
numIterations: {
get() {
return this.$store.getters.currentPipelineSettings.numIterations
},
set(val) {
this.$store.commit("mutatePipeline", {"numIterations": val});
}
},
blur: {
get() {
return this.$store.getters.currentPipelineSettings.blur
},
set(val) {
this.$store.commit("mutatePipeline", {"blur": val});
}
},
threads: {
get() {
return this.$store.getters.currentPipelineSettings.threads
},
set(val) {
this.$store.commit("mutatePipeline", {"threads": val});
}
},
refineEdges: {
get() {
return this.$store.getters.currentPipelineSettings.refineEdges
},
set(val) {
this.$store.commit("mutatePipeline", {"refineEdges": val});
}
},
},
methods: {
}
}
</script>

View File

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

View File

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

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

View File

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

View File

@@ -6,7 +6,6 @@
type="file"
accept=".csv"
style="display: none;"
@change="readFile"
>
@@ -32,11 +31,7 @@
@input="handlePipelineData('cornerDetectionAccuracyPercentage')"
@rollback="e => rollback('cornerDetectionAccuracyPercentage', e)"
/>
<mini-map
class="miniMapClass"
:targets="targets"
:horizontal-f-o-v="horizontalFOV"
/>
<v-snackbar
v-model="snack"
top
@@ -49,18 +44,16 @@
<script>
import Papa from 'papaparse';
import miniMap from '../../components/pipeline/3D/MiniMap';
import CVslider from '../../components/common/cv-slider'
export default {
name: "PnP",
components: {
CVslider,
miniMap
CVslider
},
data() {
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'], //Keep in sync with TargetModel.java
snackbar: {
color: "Success",
text: ""
@@ -87,21 +80,6 @@
this.$store.commit("mutatePipeline", {"cornerDetectionAccuracyPercentage": val});
}
},
targets: {
get() {
return this.$store.getters.currentPipelineResults.targets;
}
},
horizontalFOV: {
get() {
let index = this.$store.getters.currentPipelineSettings.cameraVideoModeIndex;
let FOV = this.$store.getters.currentCameraSettings.fov;
let resolution = this.$store.getters.videoFormatList[index];
let diagonalView = FOV * (Math.PI / 180);
let diagonalAspect = Math.hypot(resolution.width, resolution.height);
return Math.atan(Math.tan(diagonalView / 2) * (resolution.width / diagonalAspect)) * 2 * (180 / Math.PI)
}
},
},
methods: {
readFile(event) {

View File

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

View File

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

View File

@@ -49,6 +49,7 @@
return {
selectedTab: 0,
snack: false,
calibrationInProgress: false,
snackbar: {
color: "accent",
text: ""

View File

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

View File

@@ -10,44 +10,52 @@
name="Team Number"
:rules="[v => (v > 0) || 'Team number must be greater than zero', v => (v < 10000) || 'Team number must have fewer than five digits']"
class="mb-4"
:label-cols="$vuetify.breakpoint.mdAndUp ? undefined : 5"
/>
<v-chip label color="red" text-color="white" v-if="parseInt(teamNumber) < 1 && !runNTServer">
<span>
Team number not set! NetworkTables cannot connect.
</span>
</v-chip>
<v-banner
v-show="(teamNumber < 1 || teamNumber > 10000) && !runNTServer"
rounded
color="red"
text-color="white"
>
Team number is unset or invalid. NetworkTables will not be able to connect.
</v-banner>
<CVradio
v-model="connectionType"
v-show="$store.state.settings.networkSettings.supported"
v-model="connectionType"
:input-cols="inputCols"
name="IP Assignment Mode"
tooltip="DHCP will make the radio (router) automatically assign an IP address; this may result in an IP address that changes across reboots. Static IP assignment means that you pick the IP address and it won't change."
:list="['DHCP','Static']"
:disabled="!$store.state.settings.networkSettings.supported"
/>
<template v-if="!isDHCP">
<CVinput
v-model="staticIp"
:input-cols="inputCols"
:rules="[v => isIPv4(v) || 'Invalid IPv4 address']"
name="IP"
/>
</template>
<CVinput
v-if="!isDHCP"
v-model="staticIp"
:input-cols="inputCols"
:rules="[v => isIPv4(v) || 'Invalid IPv4 address']"
name="IP"
/>
<CVinput
v-model="hostname"
:input-cols="inputCols"
:rules="[v => isHostname(v) || 'Invalid hostname']"
name="Hostname"
/>
Advanced
<v-divider/>
<CVSwitch
v-model="runNTServer"
name="Run NetworkTables Server (Debugging Only!)"
tooltip="If enabled, this device will create a NT server. This is useful for home debugging, but should be disabled on-robot."
class="mt-3 mb-3"
v-model="runNTServer"
name="Run NetworkTables Server (Debugging Only)"
tooltip="If enabled, this device will create a NT server. This is useful for home debugging, but should be disabled on-robot."
class="mt-3 mb-3"
:text-cols="$vuetify.breakpoint.mdAndUp ? undefined : 5"
/>
<v-chip label color="red" text-color="white" v-if="runNTServer">
<span>
Disable this switch if you're on a robot! Photonlib will NOT work.
</span>
</v-chip>
<v-banner
v-show="runNTServer"
rounded
color="red"
text-color="white"
>
This switch is intended for testing; it should be off on a robot. PhotonLib will NOT work!
</v-banner>
</v-form>
<v-btn
color="accent"
@@ -58,60 +66,75 @@
>
Save
</v-btn>
<v-divider class="mt-4 mb-4"/>
<v-snackbar
v-model="snack"
top
:color="snackbar.color"
timeout="5000"
>
<span>{{ snackbar.text }}</span>
</v-snackbar>
<v-divider class="mt-4 mb-4" />
<!-- TEMP - RIO finder is not currently enabled
<v-row>
<v-col cols="6">
<v-col
cols="12"
sm="6"
>
<v-simple-table
fixed-header
height="100%"
dense
fixed-header
height="100%"
dense
>
<template v-slot:default>
<thead style="font-size: 1.25rem;">
<tr>
<th>
Device IPs
</th>
</tr>
<tr>
<th>
Device IPs
</th>
</tr>
</thead>
<tbody>
<tr
v-for="(value, index) in $store.state.ntConnectionInfo.deviceips"
<tr
v-for="(value, index) in $store.state.networkInfo.deviceips"
:key="index"
>
<td>{{ value }}</td>
</tr>
>
<td>{{ value }}</td>
</tr>
</tbody>
</template>
</v-simple-table>
</v-col>
<v-col cols="6">
<v-col
cols="12"
sm="6"
>
<v-simple-table
fixed-header
height="100%"
dense
fixed-header
height="100%"
dense
>
<template v-slot:default>
<thead style="font-size: 1.25rem;">
<tr>
<th>
Possible RoboRIOs
</th>
</tr>
<tr>
<th>
Possible RoboRIOs
</th>
</tr>
</thead>
<tbody>
<tr
v-for="(value, index) in $store.state.ntConnectionInfo.possibleRios"
<tr
v-for="(value, index) in $store.state.networkInfo.possibleRios"
:key="index"
>
<td>{{ value }}</td>
</tr>
>
<td>{{ value }}</td>
</tr>
</tbody>
</template>
</v-simple-table>
</v-col>
</v-row>
-->
</div>
</template>
@@ -148,7 +171,7 @@ export default {
},
computed: {
inputCols() {
return this.$vuetify.breakpoint.smAndUp ? 10 : 7;
return this.$vuetify.breakpoint.mdAndUp ? 10 : 7;
},
isDHCP() {
return this.settings.connectionType === 0;
@@ -224,7 +247,7 @@ export default {
},
sendGeneralSettings() {
this.axios.post("http://" + this.$address + "/api/settings/general", this.settings).then(
function (response) {
response => {
if (response.status === 200) {
this.snackbar = {
color: "success",
@@ -233,7 +256,7 @@ export default {
this.snack = true;
}
},
function (error) {
error => {
this.snackbar = {
color: "error",
text: (error.response || {data: "Couldn't save settings"}).data

View File

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

View File

@@ -5,17 +5,13 @@ apply from: "${rootDir}/shared/common.gradle"
dependencies {
implementation project(':photon-targeting')
implementation 'io.javalin:javalin:3.7.0'
implementation 'io.javalin:javalin:4.2.0'
implementation 'org.msgpack:msgpack-core:0.8.20'
implementation 'org.msgpack:jackson-dataformat-msgpack:0.8.20'
implementation 'org.msgpack:msgpack-core:0.9.0'
implementation 'org.msgpack:jackson-dataformat-msgpack:0.9.0'
// wpiutil
implementation "edu.wpi.first.wpiutil:wpiutil-jni:$wpilibVersion:linuxaarch64bionic"
implementation "edu.wpi.first.wpiutil:wpiutil-jni:$wpilibVersion:linuxraspbian"
implementation "edu.wpi.first.wpiutil:wpiutil-jni:$wpilibVersion:linuxx86-64"
implementation "edu.wpi.first.wpiutil:wpiutil-jni:$wpilibVersion:osxx86-64"
implementation "edu.wpi.first.wpiutil:wpiutil-jni:$wpilibVersion:windowsx86-64"
jniPlatforms.each { implementation "edu.wpi.first.wpiutil:wpiutil-jni:$wpilibVersion:$it" }
// JOGL stuff (currently we only distribute for aarch64, which is Pi 4)
implementation "org.jogamp.gluegen:gluegen-rt:$joglVersion"
@@ -29,7 +25,8 @@ dependencies {
}
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)
}

View File

@@ -14,6 +14,7 @@
* 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;
public enum ProgramStatus {

View File

@@ -14,12 +14,12 @@
* 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.configuration;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonProperty;
import edu.wpi.first.math.geometry.Rotation2d;
import java.util.ArrayList;
import java.util.List;
import org.photonvision.common.logging.LogGroup;
@@ -49,7 +49,6 @@ public class CameraConfiguration {
public double FOV = 70;
public final List<CameraCalibrationCoefficients> calibrations;
public int currentPipelineIndex = 0;
public Rotation2d camPitch = new Rotation2d();
public int streamIndex = 0; // 0 index means ports [1181, 1182], 1 means [1183, 1184], etc...
@@ -73,6 +72,7 @@ public class CameraConfiguration {
logger.debug(
"Creating USB camera configuration for "
+ cameraType
+ " "
+ baseName
+ " (AKA "
+ nickname
@@ -89,8 +89,7 @@ public class CameraConfiguration {
@JsonProperty("path") String path,
@JsonProperty("cameraType") CameraType cameraType,
@JsonProperty("calibration") List<CameraCalibrationCoefficients> calibrations,
@JsonProperty("currentPipelineIndex") int currentPipelineIndex,
@JsonProperty("camPitch") Rotation2d camPitch) {
@JsonProperty("currentPipelineIndex") int currentPipelineIndex) {
this.baseName = baseName;
this.uniqueName = uniqueName;
this.nickname = nickname;
@@ -99,11 +98,11 @@ public class CameraConfiguration {
this.cameraType = cameraType;
this.calibrations = calibrations != null ? calibrations : new ArrayList<>();
this.currentPipelineIndex = currentPipelineIndex;
this.camPitch = camPitch;
logger.debug(
"Creating camera configuration for "
+ cameraType
+ " "
+ baseName
+ " (AKA "
+ nickname

View File

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

View File

@@ -14,6 +14,7 @@
* 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.configuration;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
@@ -40,6 +41,8 @@ public class HardwareConfig {
public final String cpuTempCommand;
public final String cpuMemoryCommand;
public final String cpuUtilCommand;
public final String cpuThrottleReasonCmd;
public final String cpuUptimeCommand;
public final String gpuMemoryCommand;
public final String ramUtilCommand;
public final String gpuMemUsageCommand;
@@ -64,6 +67,8 @@ public class HardwareConfig {
cpuTempCommand = "";
cpuMemoryCommand = "";
cpuUtilCommand = "";
cpuThrottleReasonCmd = "";
cpuUptimeCommand = "";
gpuMemoryCommand = "";
ramUtilCommand = "";
ledBlinkCommand = "";
@@ -90,6 +95,8 @@ public class HardwareConfig {
String cpuTempCommand,
String cpuMemoryCommand,
String cpuUtilCommand,
String cpuThrottleReasonCmd,
String cpuUptimeCommand,
String gpuMemoryCommand,
String ramUtilCommand,
String gpuMemUsageCommand,
@@ -110,6 +117,8 @@ public class HardwareConfig {
this.cpuTempCommand = cpuTempCommand;
this.cpuMemoryCommand = cpuMemoryCommand;
this.cpuUtilCommand = cpuUtilCommand;
this.cpuThrottleReasonCmd = cpuThrottleReasonCmd;
this.cpuUptimeCommand = cpuUptimeCommand;
this.gpuMemoryCommand = gpuMemoryCommand;
this.ramUtilCommand = ramUtilCommand;
this.gpuMemUsageCommand = gpuMemUsageCommand;

View File

@@ -14,6 +14,7 @@
* 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.configuration;
public class HardwareSettings {

View File

@@ -14,6 +14,7 @@
* 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.configuration;
import com.fasterxml.jackson.annotation.JsonCreator;

View File

@@ -14,6 +14,7 @@
* 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.configuration;
import java.util.Collection;
@@ -127,7 +128,8 @@ public class PhotonConfiguration {
public static class UICameraConfiguration {
@SuppressWarnings("unused")
public double fov, tiltDegrees;
public double fov;
public String nickname;
public HashMap<String, Object> currentPipelineSettings;
public int currentPipelineIndex;

View File

@@ -14,6 +14,7 @@
* 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.dataflow;
import java.util.function.Consumer;

View File

@@ -14,6 +14,7 @@
* 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.dataflow;
import java.util.ArrayList;

View File

@@ -14,6 +14,7 @@
* 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.dataflow;
import java.util.concurrent.BlockingQueue;

View File

@@ -14,6 +14,7 @@
* 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.dataflow;
import java.util.ArrayList;

View File

@@ -14,6 +14,7 @@
* 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.dataflow;
import java.util.List;

View File

@@ -14,6 +14,7 @@
* 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.dataflow.events;
import org.photonvision.common.dataflow.DataChangeDestination;

View File

@@ -14,6 +14,7 @@
* 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.dataflow.events;
import org.photonvision.common.dataflow.DataChangeDestination;

View File

@@ -14,6 +14,7 @@
* 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.dataflow.events;
import io.javalin.websocket.WsContext;

View File

@@ -14,6 +14,7 @@
* 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.dataflow.events;
import io.javalin.websocket.WsContext;

View File

@@ -14,6 +14,7 @@
* 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.dataflow.networktables;
import edu.wpi.first.networktables.EntryListenerFlags;

View File

@@ -14,6 +14,7 @@
* 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.dataflow.networktables;
import edu.wpi.first.networktables.EntryNotification;
@@ -24,10 +25,12 @@ import java.util.List;
import java.util.function.BooleanSupplier;
import java.util.function.Consumer;
import java.util.function.Supplier;
import org.opencv.core.Point;
import org.photonvision.common.dataflow.CVPipelineResultConsumer;
import org.photonvision.common.dataflow.structures.Packet;
import org.photonvision.targeting.PhotonPipelineResult;
import org.photonvision.targeting.PhotonTrackedTarget;
import org.photonvision.targeting.TargetCorner;
import org.photonvision.vision.pipeline.result.CVPipelineResult;
import org.photonvision.vision.target.TrackedTarget;
@@ -186,10 +189,17 @@ public class NTDataPublisher implements CVPipelineResultConsumer {
targetAreaEntry.forceSetDouble(bestTarget.getArea());
targetSkewEntry.forceSetDouble(bestTarget.getSkew());
var poseX = bestTarget.getCameraToTarget().getTranslation().getX();
var poseY = bestTarget.getCameraToTarget().getTranslation().getY();
var poseRot = bestTarget.getCameraToTarget().getRotation().getDegrees();
targetPoseEntry.forceSetDoubleArray(new double[] {poseX, poseY, poseRot});
var pose = bestTarget.getBestCameraToTarget3d();
targetPoseEntry.forceSetDoubleArray(
new double[] {
pose.getTranslation().getX(),
pose.getTranslation().getY(),
pose.getTranslation().getZ(),
pose.getRotation().getQuaternion().getW(),
pose.getRotation().getQuaternion().getX(),
pose.getRotation().getQuaternion().getY(),
pose.getRotation().getQuaternion().getZ()
});
var targetOffsetPoint = bestTarget.getTargetOffsetPoint();
bestTargetPosX.forceSetDouble(targetOffsetPoint.x);
@@ -209,9 +219,23 @@ public class NTDataPublisher implements CVPipelineResultConsumer {
public static List<PhotonTrackedTarget> simpleFromTrackedTargets(List<TrackedTarget> targets) {
var ret = new ArrayList<PhotonTrackedTarget>();
for (var t : targets) {
var points = new Point[4];
t.getMinAreaRect().points(points);
var cornerList = new ArrayList<TargetCorner>();
for (int i = 0; i < 4; i++) cornerList.add(new TargetCorner(points[i].x, points[i].y));
ret.add(
new PhotonTrackedTarget(
t.getYaw(), t.getPitch(), t.getArea(), t.getSkew(), t.getCameraToTarget()));
t.getYaw(),
t.getPitch(),
t.getArea(),
t.getSkew(),
t.getFiducialId(),
t.getBestCameraToTarget3d(),
t.getAltCameraToTarget3d(),
t.getPoseAmbiguity(),
cornerList));
}
return ret;
}

View File

@@ -14,19 +14,13 @@
* 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.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.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.List;
import java.util.function.Consumer;
import org.photonvision.PhotonVersion;
import org.photonvision.common.configuration.ConfigManager;
@@ -44,8 +38,11 @@ public class NetworkTablesManager {
private final String kRootTableName = "/photonvision";
public final NetworkTable kRootTable = ntInstance.getTable(kRootTableName);
private boolean isRetryingConnection = false;
private NetworkTablesManager() {
ntInstance.addLogger(new NTLogger(), 0, 255); // to hide error messages
TimedTaskManager.getInstance().addTask("NTManager", this::ntTick, 5000);
}
private static NetworkTablesManager INSTANCE;
@@ -86,6 +83,7 @@ public class NetworkTablesManager {
private void broadcastConnectedStatusImpl() {
HashMap<String, Object> map = new HashMap<>();
var subMap = new HashMap<String, Object>();
subMap.put("connected", ntInstance.isConnected());
if (ntInstance.isConnected()) {
var connections = getInstance().ntInstance.getConnections();
@@ -98,73 +96,6 @@ public class NetworkTablesManager {
map.put("ntConnectionInfo", subMap);
DataChangeService.getInstance()
.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() {
@@ -182,18 +113,11 @@ public class NetworkTablesManager {
}
private void setClientMode(int teamNumber) {
logger.info("Starting NT Client");
if (!isRetryingConnection) logger.info("Starting NT Client");
ntInstance.stopServer();
// ntInstance.startClientTeam(teamNumber);
ntInstance.startClient("localhost");
ntInstance.startClientTeam(teamNumber);
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();
}
@@ -203,4 +127,22 @@ public class NetworkTablesManager {
ntInstance.startServer();
broadcastVersion();
}
// So it seems like if Photon starts before the robot NT server does, and both aren't static IP,
// it'll never connect. This hack works around it by restarting the client/server while the nt
// instance
// isn't connected, same as clicking the save button in the settings menu (or restarting the
// service)
private void ntTick() {
if (!ntInstance.isConnected()
&& !ConfigManager.getInstance().getConfig().getNetworkConfig().runNTServer) {
setConfig(ConfigManager.getInstance().getConfig().getNetworkConfig());
}
if (!ntInstance.isConnected() && !isRetryingConnection) {
isRetryingConnection = true;
logger.error(
"[NetworkTablesManager] Could not connect to the robot! Will retry in the background...");
}
}
}

View File

@@ -14,6 +14,7 @@
* 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.dataflow.websocket;
import java.util.ArrayList;

View File

@@ -14,6 +14,7 @@
* 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.GPIO;
import org.photonvision.common.configuration.HardwareConfig;

View File

@@ -14,6 +14,7 @@
* 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.GPIO;
import java.util.Arrays;

View File

@@ -14,6 +14,7 @@
* 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.GPIO.pi;
@SuppressWarnings("SpellCheckingInspection")

View File

@@ -14,16 +14,17 @@
* 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.GPIO.pi;
import java.util.HashMap;
/**
* A class that defines the exceptions that can be thrown by Pigpio.
*
* <p>Credit to nkolban
* https://github.com/nkolban/jpigpio/blob/master/JPigpio/src/jpigpio/PigpioException.java
*/
* A class that defines the exceptions that can be thrown by Pigpio.
*
* <p>Credit to nkolban
* https://github.com/nkolban/jpigpio/blob/master/JPigpio/src/jpigpio/PigpioException.java
*/
@SuppressWarnings({"SpellCheckingInspection", "unused", "RedundantSuppression"})
public class PigpioException extends Exception {
private int rc = -99999999;
@@ -65,10 +66,10 @@ public class PigpioException extends Exception {
}
/**
* Retrieve the error code that was returned by the underlying Pigpio call.
*
* @return The error code that was returned by the underlying Pigpio call.
*/
* Retrieve the error code that was returned by the underlying Pigpio call.
*
* @return The error code that was returned by the underlying Pigpio call.
*/
public int getErrorCode() {
return rc;
} // End of getErrorCode

View File

@@ -14,6 +14,7 @@
* 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.GPIO.pi;
import static org.photonvision.common.hardware.GPIO.pi.PigpioException.*;

View File

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

View File

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

View File

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

View File

@@ -14,6 +14,7 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.photonvision.common.hardware;
import edu.wpi.first.networktables.NetworkTableEntry;

View File

@@ -0,0 +1,44 @@
/*
* 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;
public enum PiVersion {
PI_B("Pi Model B"),
COMPUTE_MODULE("Compute Module Rev"),
ZERO_W("Pi Zero W Rev 1.1"),
ZERO_2_W("Raspberry Pi Zero 2"),
PI_3("Pi 3"),
PI_4("Pi 4"),
COMPUTE_MODULE_3("Compute Module 3"),
UNKNOWN("UNKNOWN");
private final String identifier;
PiVersion(String s) {
this.identifier = s.toLowerCase();
}
public static PiVersion getPiVersion() {
if (!Platform.isRaspberryPi()) return PiVersion.UNKNOWN;
String piString = Platform.currentPiVersionStr;
for (PiVersion p : PiVersion.values()) {
if (piString.toLowerCase().contains(p.identifier)) return p;
}
return UNKNOWN;
}
}

View File

@@ -14,6 +14,7 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.photonvision.common.hardware;
import edu.wpi.first.util.RuntimeDetector;
@@ -45,23 +46,27 @@ public enum Platform {
private static final String OS_NAME = System.getProperty("os.name");
private static final String OS_ARCH = System.getProperty("os.arch");
public static final Platform CurrentPlatform = getCurrentPlatform();
private static String UnknownPlatformString =
// These are queried on init and should never change after
public static final Platform currentPlatform = getCurrentPlatform();
static final String currentPiVersionStr = getPiVersionString();
public static final PiVersion currentPiVersion = PiVersion.getPiVersion();
private static final String UnknownPlatformString =
String.format("Unknown Platform. OS: %s, Architecture: %s", OS_NAME, OS_ARCH);
public boolean isWindows() {
return this == WINDOWS_64 || this == WINDOWS_32;
public static boolean isWindows() {
return currentPlatform == WINDOWS_64 || currentPlatform == WINDOWS_32;
}
public static boolean isLinux() {
return getCurrentPlatform() == LINUX_64
|| getCurrentPlatform() == LINUX_RASPBIAN
|| getCurrentPlatform() == LINUX_ARM64;
return currentPlatform == LINUX_64
|| currentPlatform == LINUX_RASPBIAN
|| currentPlatform == LINUX_ARM64;
}
public static boolean isRaspberryPi() {
return CurrentPlatform.equals(LINUX_RASPBIAN);
return currentPlatform.equals(LINUX_RASPBIAN);
}
@SuppressWarnings("StatementWithEmptyBody")
@@ -114,4 +119,22 @@ public enum Platform {
return this.value;
}
}
// Querry /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 (!isRaspberryPi()) return "";
try {
shell.executeBashCommand("cat /proc/device-tree/model");
} catch (IOException e) {
e.printStackTrace();
}
if (shell.getExitCode() == 0) {
// We expect it to be in the format "raspberry pi X model X"
return shell.getOutput();
}
return "";
}
}

View File

@@ -14,6 +14,7 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.photonvision.common.hardware;
import java.util.List;

View File

@@ -14,6 +14,7 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.photonvision.common.hardware;
import edu.wpi.first.networktables.EntryNotification;
@@ -84,6 +85,8 @@ public class VisionLED {
pigpioSocket.generateAndSendWaveform(pulseLengthMillis, blinkCount, ledPins);
} catch (PigpioException e) {
logger.error("Failed to blink!", e);
} catch (NullPointerException e) {
logger.error("Failed to blink, pigpio internal issue!", e);
}
} else {
for (GPIOBase led : visionLEDs) {
@@ -99,13 +102,19 @@ public class VisionLED {
pigpioSocket.waveTxStop();
} catch (PigpioException e) {
logger.error("Failed to stop blink!", e);
} catch (NullPointerException e) {
logger.error("Failed to blink, pigpio internal issue!", e);
}
}
// if the user has set an LED brightness other than 100%, use that instead
if (mappedBrightnessPercentage == 100 || !state) {
visionLEDs.forEach((led) -> led.setState(state));
} else {
visionLEDs.forEach((led) -> led.setBrightness(mappedBrightnessPercentage));
try {
// if the user has set an LED brightness other than 100%, use that instead
if (mappedBrightnessPercentage == 100 || !state) {
visionLEDs.forEach((led) -> led.setState(state));
} else {
visionLEDs.forEach((led) -> led.setBrightness(mappedBrightnessPercentage));
}
} catch (NullPointerException e) {
logger.error("Failed to blink, pigpio internal issue!", e);
}
}

View File

@@ -14,6 +14,7 @@
* 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 CPUMetrics extends MetricsBase {
@@ -39,4 +40,12 @@ public class CPUMetrics extends MetricsBase {
public String getUtilization() {
return execute(cpuUtilizationCommand);
}
public String getUptime() {
return execute(cpuUptimeCommand);
}
public String getThrottleReason() {
return execute(cpuThrottleReasonCmd);
}
}

View File

@@ -14,6 +14,7 @@
* 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 DiskMetrics extends MetricsBase {

View File

@@ -14,6 +14,7 @@
* 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 GPUMetrics extends MetricsBase {

View File

@@ -14,6 +14,7 @@
* 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;
@@ -25,7 +26,7 @@ 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);
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 =
@@ -33,6 +34,15 @@ public abstract class MetricsBase {
public static String cpuUtilizationCommand =
"top -bn1 | grep \"Cpu(s)\" | sed \"s/.*, *\\([0-9.]*\\)%* id.*/\\1/\" | awk '{print 100 - $1}'";
public static String 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";
public static String cpuUptimeCommand = "uptime -p | cut -c 4-";
// 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]+'";
@@ -50,6 +60,8 @@ public abstract class MetricsBase {
cpuMemoryCommand = config.cpuMemoryCommand;
cpuTemperatureCommand = config.cpuTempCommand;
cpuUtilizationCommand = config.cpuUtilCommand;
cpuThrottleReasonCmd = config.cpuThrottleReasonCmd;
cpuUptimeCommand = config.cpuUptimeCommand;
gpuMemoryCommand = config.gpuMemoryCommand;
gpuMemUsageCommand = config.gpuMemUsageCommand;

View File

@@ -14,6 +14,7 @@
* 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;
@@ -59,6 +60,8 @@ public class MetricsPublisher {
metrics.put("cpuTemp", cpuMetrics.getTemp());
metrics.put("cpuUtil", cpuMetrics.getUtilization());
metrics.put("cpuMem", cpuMetrics.getMemory());
metrics.put("cpuThr", cpuMetrics.getThrottleReason());
metrics.put("cpuUptime", cpuMetrics.getUptime());
metrics.put("gpuMem", gpuMetrics.getGPUMemorySplit());
metrics.put("ramUtil", ramMetrics.getUsedRam());
metrics.put("gpuMemUtil", gpuMetrics.getMallocedMemory());

View File

@@ -14,6 +14,7 @@
* 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 {

View File

@@ -14,6 +14,7 @@
* 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.logging;
public enum LogGroup {

View File

@@ -14,6 +14,7 @@
* 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.logging;
public enum LogLevel {

View File

@@ -14,6 +14,7 @@
* 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.logging;
import java.io.*;
@@ -125,8 +126,9 @@ public class Logger {
}
public static void cleanLogs(Path folderToClean) {
LinkedList<File> logFileList =
new LinkedList<>(Arrays.asList(folderToClean.toFile().listFiles()));
File[] logs = folderToClean.toFile().listFiles();
if (logs == null) return;
LinkedList<File> logFileList = new LinkedList<>(Arrays.asList(logs));
HashMap<File, Date> logFileStartDateMap = new HashMap<>();
// Remove any files from the list for which we can't parse a start date from their name.
@@ -231,12 +233,12 @@ public class Logger {
}
/**
* Logs an error message with the stack trace of a Throwable. The stacktrace will only be printed
* if the current LogLevel is TRACE
*
* @param message
* @param t
*/
* Logs an error message with the stack trace of a Throwable. The stacktrace will only be printed
* if the current LogLevel is TRACE
*
* @param message
* @param t
*/
public void error(String message, Throwable t) {
log(message, LogLevel.ERROR);
log(convertStackTraceToString(t), LogLevel.ERROR, LogLevel.DEBUG);

View File

@@ -14,6 +14,7 @@
* 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 java.net.InterfaceAddress;

View File

@@ -14,6 +14,7 @@
* 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 org.photonvision.common.configuration.ConfigManager;
@@ -45,7 +46,7 @@ public class NetworkManager {
var config = ConfigManager.getInstance().getConfig().getNetworkConfig();
logger.info("Setting " + config.connectionType + " with team team " + config.teamNumber);
if (Platform.isLinux()) {
if (Platform.isRaspberryPi()) {
if (!Platform.isRoot) {
logger.error("Cannot manage network without root!");
return;

View File

@@ -14,6 +14,7 @@
* 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;
public enum NetworkMode {

View File

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

View File

@@ -14,6 +14,7 @@
* 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.scripting;
public enum ScriptCommandType {

View File

@@ -14,6 +14,7 @@
* 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.scripting;
import com.fasterxml.jackson.annotation.JsonCreator;

View File

@@ -14,6 +14,7 @@
* 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.scripting;
import java.io.IOException;

View File

@@ -14,6 +14,7 @@
* 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.scripting;
public enum ScriptEventType {

View File

@@ -14,6 +14,7 @@
* 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.scripting;
import java.io.IOException;
@@ -65,8 +66,7 @@ public class ScriptManager {
private void handleEvent(ScriptEventType eventType) {
var toRun =
events
.parallelStream()
events.parallelStream()
.filter(e -> e.config.eventType == eventType)
.findFirst()
.orElse(null);
@@ -128,7 +128,7 @@ public class ScriptManager {
}
public static void queueEvent(ScriptEventType eventType) {
if (!Platform.CurrentPlatform.isWindows()) {
if (!Platform.currentPlatform.isWindows()) {
try {
queuedEvents.putLast(eventType);
logger.info("Queued event: " + eventType.name());

View File

@@ -14,6 +14,7 @@
* 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.util;
import java.awt.*;

View File

@@ -14,6 +14,7 @@
* 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.util;
public class MemoryManager {

View File

@@ -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.util;
import java.nio.file.Path;
import java.nio.file.Paths;
public class NativeLibHelper {
private static NativeLibHelper INSTANCE;
public static NativeLibHelper getInstance() {
if (INSTANCE == null) {
INSTANCE = new NativeLibHelper();
}
return INSTANCE;
}
public final Path NativeLibPath;
private NativeLibHelper() {
String home = System.getProperty("user.home");
NativeLibPath = Paths.get(home, ".pvlibs", "nativecache");
}
}

View File

@@ -14,6 +14,7 @@
* 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.util;
public class ReflectionUtils {

View File

@@ -14,6 +14,7 @@
* 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.util;
import java.util.HashMap;

View File

@@ -14,6 +14,7 @@
* 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.util;
import java.io.*;
@@ -43,12 +44,12 @@ public class ShellExec {
}
/**
* Execute a bash command. We can handle complex bash commands including multiple executions (; |
* and ||), quotes, expansions ($), escapes (\), e.g.: "cd /abc/def; mv ghi 'older ghi '$(whoami)"
*
* @param command Bash command to execute
* @return true if bash got started, but your command may have failed.
*/
* Execute a bash command. We can handle complex bash commands including multiple executions (; |
* and ||), quotes, expansions ($), escapes (\), e.g.: "cd /abc/def; mv ghi 'older ghi '$(whoami)"
*
* @param command Bash command to execute
* @return true if bash got started, but your command may have failed.
*/
public int executeBashCommand(String command, boolean wait) throws IOException {
logger.debug("Executing \"" + command + "\"");
@@ -71,25 +72,25 @@ public class ShellExec {
}
/**
* Execute a command in current folder, and wait for process to end
*
* @param command command ("c:/some/folder/script.bat" or "some/folder/script.sh")
* @param args 0..n command line arguments
* @return process exit code
*/
* Execute a command in current folder, and wait for process to end
*
* @param command command ("c:/some/folder/script.bat" or "some/folder/script.sh")
* @param args 0..n command line arguments
* @return process exit code
*/
public int execute(String command, String... args) throws IOException {
return execute(command, null, true, args);
}
/**
* Execute a command.
*
* @param command command ("c:/some/folder/script.bat" or "some/folder/script.sh")
* @param workdir working directory or NULL to use command folder
* @param wait wait for process to end
* @param args 0..n command line arguments
* @return process exit code
*/
* Execute a command.
*
* @param command command ("c:/some/folder/script.bat" or "some/folder/script.sh")
* @param workdir working directory or NULL to use command folder
* @param wait wait for process to end
* @param args 0..n command line arguments
* @return process exit code
*/
public int execute(String command, String workdir, boolean wait, String... args)
throws IOException {
String[] cmdArr;
@@ -153,10 +154,10 @@ public class ShellExec {
// ********************************************
/**
* StreamGobbler reads inputstream to "gobble" it. This is used by Executor class when running a
* commandline applications. Gobblers must read/purge INSTR and ERRSTR process streams.
* http://www.javaworld.com/javaworld/jw-12-2000/jw-1229-traps.html?page=4
*/
* StreamGobbler reads inputstream to "gobble" it. This is used by Executor class when running a
* commandline applications. Gobblers must read/purge INSTR and ERRSTR process streams.
* http://www.javaworld.com/javaworld/jw-12-2000/jw-1229-traps.html?page=4
*/
@SuppressWarnings("WeakerAccess")
private static class StreamGobbler extends Thread {
private InputStream is;
@@ -186,19 +187,19 @@ public class ShellExec {
}
/**
* Get inputstream buffer or null if stream was not consumed.
*
* @return Output stream
*/
* Get inputstream buffer or null if stream was not consumed.
*
* @return Output stream
*/
public String getOutput() {
return (output != null ? output.toString() : null);
}
/**
* Is input stream completed.
*
* @return if input stream is completed
*/
* Is input stream completed.
*
* @return if input stream is completed
*/
public boolean isCompleted() {
return completed;
}

View File

@@ -14,10 +14,12 @@
* 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.util;
import com.fasterxml.jackson.databind.ObjectMapper;
import edu.wpi.first.cscore.CameraServerCvJNI;
import edu.wpi.first.math.util.Units;
import java.awt.*;
import java.io.File;
import java.io.IOException;
@@ -27,6 +29,15 @@ import org.opencv.highgui.HighGui;
import org.photonvision.vision.calibration.CameraCalibrationCoefficients;
public class TestUtils {
public static void loadLibraries() {
try {
CameraServerCvJNI.forceLoad();
// PicamJNI.forceLoad();
} catch (IOException ex) {
// ignored
}
}
@SuppressWarnings("unused")
public enum WPI2019Image {
kCargoAngledDark48in(1.2192),
@@ -97,6 +108,26 @@ public class TestUtils {
}
}
public enum WPI2022Image {
kTerminal12ft6in(Units.feetToMeters(12.5)),
kTerminal22ft6in(Units.feetToMeters(22.5));
public static double FOV = 68.5;
public final double distanceMeters;
public final Path path;
Path getPath() {
var filename = this.toString().substring(1).replace('_', '-');
return Path.of("2022", "WPI", filename + ".png");
}
WPI2022Image(double distanceMeters) {
this.distanceMeters = distanceMeters;
this.path = getPath();
}
}
public enum PolygonTestImages {
kPolygons;
@@ -132,8 +163,26 @@ public class TestUtils {
}
}
public enum ApriltagTestImages {
kRobots,
kTag1_640_480;
public final Path path;
Path getPath() {
// Strip leading k
var filename = this.toString().substring(1).toLowerCase();
return Path.of("apriltag", filename + ".jpg");
}
ApriltagTestImages() {
this.path = getPath();
}
}
private static Path getResourcesFolderPath(boolean testMode) {
return Path.of("../test-resources").toAbsolutePath();
System.out.println("CWD: " + Path.of("").toAbsolutePath().toString());
return Path.of("test-resources").toAbsolutePath();
}
public static Path getTestMode2019ImagePath() {
@@ -145,7 +194,19 @@ public class TestUtils {
public static Path getTestMode2020ImagePath() {
return getResourcesFolderPath(true)
.resolve("testimages")
.resolve(WPI2020Image.kBlueGoal_108in_Center.path);
.resolve(WPI2020Image.kBlueGoal_156in_Left.path);
}
public static Path getTestMode2022ImagePath() {
return getResourcesFolderPath(true)
.resolve("testimages")
.resolve(WPI2022Image.kTerminal22ft6in.path);
}
public static Path getTestModeApriltagPath() {
return getResourcesFolderPath(true)
.resolve("testimages")
.resolve(ApriltagTestImages.kRobots.path);
}
public static Path getTestImagesPath(boolean testMode) {
@@ -172,6 +233,10 @@ public class TestUtils {
return getTestImagesPath(testMode).resolve(image.path);
}
public static Path getApriltagImagePath(ApriltagTestImages image, boolean testMode) {
return getTestImagesPath(testMode).resolve(image.path);
}
public static Path getPowercellImagePath(PowercellTestImages image, boolean testMode) {
return getPowercellPath(testMode).resolve(image.path);
}
@@ -214,12 +279,8 @@ public class TestUtils {
return getCoeffs(LIFECAM_480P_CAL_FILE, testMode);
}
public static void loadLibraries() {
try {
CameraServerCvJNI.forceLoad();
} catch (IOException e) {
// ignored
}
public static CameraCalibrationCoefficients getLaptop() {
return getCoeffs("laptop.json", true);
}
private static int DefaultTimeoutMillis = 5000;

View File

@@ -14,6 +14,7 @@
* 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.util;
import java.util.concurrent.*;

View File

@@ -14,12 +14,14 @@
* 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.util.file;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.NoSuchFileException;
import java.nio.file.Path;
import java.nio.file.attribute.PosixFileAttributes;
import java.nio.file.attribute.PosixFilePermission;
@@ -60,7 +62,7 @@ public class FileUtils {
public static void deleteFile(Path path) {
try {
Files.delete(path);
} catch (FileNotFoundException fe) {
} catch (FileNotFoundException | NoSuchFileException fe) {
logger.warn("Tried to delete file \"" + path + "\" but it did not exist");
} catch (IOException e) {
logger.error("Exception deleting file \"" + path + "\"!", e);
@@ -76,7 +78,7 @@ public class FileUtils {
}
public static void setFilePerms(Path path) throws IOException {
if (!Platform.CurrentPlatform.isWindows()) {
if (!Platform.currentPlatform.isWindows()) {
File thisFile = path.toFile();
Set<PosixFilePermission> perms =
Files.readAttributes(path, PosixFileAttributes.class).permissions();
@@ -94,7 +96,7 @@ public class FileUtils {
}
public static void setAllPerms(Path path) {
if (!Platform.CurrentPlatform.isWindows()) {
if (!Platform.currentPlatform.isWindows()) {
String command = String.format("chmod 777 -R %s", path.toString());
try {
Process p = Runtime.getRuntime().exec(command);

View File

@@ -14,6 +14,7 @@
* 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.util.file;
import com.fasterxml.jackson.core.json.JsonReadFeature;
@@ -33,7 +34,7 @@ import java.nio.file.Path;
public class JacksonUtils {
public static <T> void serialize(Path path, T object) throws IOException {
serialize(path, object, false);
serialize(path, object, true);
}
public static <T> void serialize(Path path, T object, boolean forceSync) throws IOException {
@@ -79,7 +80,7 @@ public class JacksonUtils {
public static <T> void serialize(Path path, T object, Class<T> ref, StdSerializer<T> serializer)
throws IOException {
serialize(path, object, ref, serializer, false);
serialize(path, object, ref, serializer, true);
}
public static <T> void serialize(

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