Compare commits

...

193 Commits

Author SHA1 Message Date
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
Matt
be5f0880c8 Update WPILib to 2022.1.1-beta-3-1-g4ba80a3 (#337)
This fixes raspberry pi crashes
2021-11-28 07:34:13 -06:00
Banks T
a02cd4a50e Explicitly specify JDK11 for photon-targeting (#334) 2021-11-27 00:22:51 -05:00
Tyler Veness
efd31543f6 Upgrade athena Docker container from 2021 to 2022 (#332)
The proper compiler wasn't available for 2022 native-utils, so
linuxathena artifact builds were silently skipped.
2021-11-26 09:44:05 -05:00
Matt
a151f23319 Add team number dialog, NT connected chip (#313)
Makes NT connection status visible from the UI
2021-11-25 15:43:29 -05:00
Matt
822811c853 Upload a new image on releases (#329)
Uploads a new Pi image (without any hardware configs) on releases and pushes to dev
2021-11-25 15:42:58 -05:00
Matt
23834c87f4 Only let in tags starting with "v" (#328) 2021-11-23 20:22:54 -05:00
Tyler Veness
f103a6b712 Run wpiformat (#327) 2021-11-23 19:09:51 -05:00
Chris Gerth
3257736ffa Add .pdf taraget generation (#307)
* Added rectangle target generation

* added dot grid support

* dot grid functional. Added branding and ruler

* comments, rebuild, testing

* proper branding rework

* undo index.html commit
2021-11-21 20:34:06 -05:00
Tyler Veness
9df25eda88 Add clangd files to .gitignore (#318) 2021-11-21 20:30:10 -05:00
Tyler Veness
5a13739818 Remove unused private variable (#317) 2021-11-21 20:30:03 -05:00
Tyler Veness
5ca39e7f84 Upgrade to Gradle 7.2 and WPILib 2022 (#316) 2021-11-21 20:22:56 -05:00
Matt
ffe34f00fe Ensure skew is always in [-90, 90] (#319) 2021-11-21 18:50:15 -06:00
Matt
a5cc0808c4 Make photon targeting respect snapshot repo (#309) 2021-11-06 20:12:50 -04:00
Banks T
08fafe2607 Fix file delete, possible null on perms set (#303) 2021-11-06 19:58:07 -04:00
Matt
a2af7d9273 Add red warning text with server mode, team number not set (#308) 2021-11-03 09:12:29 -04:00
Matt
a731c7a8db Revert part of #288 (#306)
Fixes picam streaming/hangs
2021-10-30 12:27:41 -04:00
Matt
7e74da5cff Make photonlib complain if versions don't match (#302)
Adds messages if Photon isn't detected or major versions don't match
2021-10-18 22:31:18 -04:00
Matt
0977fd0dff Update PacketTest.java (#301)
Adds unit test to make sure the packet structure doesn't change
2021-10-16 09:42:21 -04:00
Matt
3241ef7b1b Update dev tag matcher (#300) 2021-10-07 11:13:11 -04:00
Matt
f922466d41 Fix contour grouping (#298)
Fixes bug where n+1 contours were needed to match a target of size n
2021-10-05 12:16:50 -04:00
Matt
243f06da2d Fix vision module stream index logic (#295)
Previous logic could cause stream index with multiple cameras to run away
2021-10-03 22:43:39 -04:00
Matt
44e91a184d Publish photon-targeting (#292)
Pushes photon-targeting to Maven
2021-10-03 10:58:35 -04:00
Matt
e6b0f398b6 Fix versioning picked up in CI (#291)
Fixes ignore blob in versioningHelper
2021-09-24 18:51:40 -04:00
Chris Gerth
5a475e1071 Fix crash in logging when log files are not writable (#286)
* Addresses null pointer crash in logging when log files are not writable

* One of these days, I'll be able to type code without spotless complaining.

But today is not that day.
2021-09-23 22:38:50 -04:00
Tyler Veness
f8def88e4d Rename tests to appease wpiformat (#290) 2021-09-23 21:13:04 -04:00
Chris Gerth
db09e5209f Colored shape fix (#288)
* Move test images out of resources folder

* Limit workers in CI

* Fix image area filtering bug in colored shape

* Add missing picam settings

* Swap to make blank/empty Mat when a picam doesn't supply a color image.

Co-authored-by: Matt <matthew.morley.ca@gmail.com>
2021-09-23 18:48:18 -04:00
Matt
9fdd945a52 Fix compilation with gradlew build (#284)
* Fix everything but test mode

* Update TestUtils.java

* Jank testutils fix

* Limit workers in CI
2021-09-07 06:49:07 -07:00
Matt
00b8e7d1c5 Add colored shape to the UI (#258)
* Fixup colored shape backend code

* More colored shape stuff

* Start adding shape change to drawing

* Mostly works!

* Add powercell image for shape test mode

* Make super-duper-sure to release stuff

Fixes colored shape leak

* Move approx poly dp into Contour

* Adjust epsilon threshold

* Add dialog to change pipeline type

* Run spotless

* Make yes red :>

* Move "no" button

* Fix duplication/deletion name logic and switching

* Fix compilation errors from rebase

* Update VisionSourceManager.java

* Update type dialog, remove duplicate popup

The dropdown still switches even if the user says "no" tho

Co-authored-by: Banks Troutman <btrout.dhrs@gmail.com>
2021-09-03 22:20:55 -04:00
Tyler Veness
798b8e398a Remove hasTargets member variable and fix docs warnings (#283)
hasTargets is redundant because the same information can be obtained by
checking the size of the targets array.
2021-09-03 22:19:38 -04:00
Tyler Veness
affb27038b Fix uninitialized variables in PhotonPipelineResult (#282)
If the default constructor is used, some member variables weren't properly initialized.
2021-09-02 20:48:05 -07:00
Tyler Veness
6767781a41 Update photonlib vendordep JSON URL (#281) 2021-09-01 05:11:03 -07:00
Matt
6007cc752d Add libpicam with gain slider bugfix (#278)
* Add libpicam with gain slider bugfix

* Patches to get zero-copy working with Pi3.

-- Success/Failure mistmatch assumptions - lots of functions in the JNI return true on failure (not true on success)
-- isVSCMSupported() is currently implemented to be "isVSCMNotSupported()"

Likely, we'll want at least some .so changes

Co-authored-by: Chris Gerth <chrisgerth010592@gmail.com>
2021-08-31 23:48:11 -04:00
Banks T
9cf5c77d69 CI test fix (#280)
Addresses OOM in CI
2021-08-31 20:27:51 -07:00
Matt
9dc5481d1c Push dev tag last 2021-04-02 17:04:22 -04:00
Matt
3948650e6c [photonlib] Fix C++ compilation errors (#266)
Properly builds linux athena artifacts and fixes vendor JSON bug
2021-03-28 14:36:03 -07:00
Chris Gerth
49fcdb64ed Update Sim Example Unit Conversions (#265)
Updated Java examples to fix radians/degrees mismatch.

Inspected c++ examples - they create a units::degree_t from the NT value, which should be handled properly inside the PhotonUtils methods.
2021-03-23 10:57:32 -07:00
Banks T
1e715ce4bb Sim target sorting and "easy" NT entries (#262)
* Typo fixes, add target sorting

* Add easy NT entries

* spotttttttttttttttttlessssssssssssss

* Run wpiformat

* Fix return on no targets

* formatting hell 2 electric boogaloo

Co-authored-by: Matt <matthew.morley.ca@gmail.com>
2021-03-23 12:47:26 -04:00
Vasista Vovveti
f9fd7a0b45 Handle GetLatestResult segfault (#259)
* Handle GetLatestResult segfault

* Update PhotonCamera.cpp

* Update PhotonCamera.cpp
2021-03-21 16:29:29 -04:00
Matt
e9a3c2d1b8 Pull tags in photon CI (#263) 2021-03-15 16:13:45 -07:00
Banks T
129575dd23 Fix Test Mode (#246)
* Fix test mode path and args

* spoooootless

* Fix unit test resources
2021-03-07 21:39:02 -05:00
Declan Freeman-Gleason
ba8d2691fc Add more GPU accel support detection and more libpicam error checks (#257) 2021-03-02 17:49:29 -05:00
Matt
f3d3a59ca0 Use snapshot repo for dev releases (#241) 2021-02-23 15:07:43 -05:00
Matt
b653fc7db1 deprecate hasTargets (#252)
hasTargets has the potential to allow users to inadvertantly create NPEs.
2021-02-23 15:07:31 -05:00
MarkGhebrial
71ee03a531 Draw 2D contours on top (#254)
Closes #102
2021-02-23 15:07:03 -05:00
Banks T
4a2493ff2e Remove spamy CSCore prints in unit tests (#255) 2021-02-23 11:37:01 -08:00
Banks T
0b20111824 Add update script (#248) 2021-02-01 11:00:29 -08:00
Banks T
449977e63b Update NetworkTablesManager.java (#250)
Allows NT connection on Romi
2021-01-29 12:20:16 -08:00
Banks T
b0504cbef5 Add VisionModule order determinism (#245)
This makes sure that VisionModules always appear in the same order.
2021-01-29 10:57:18 -08:00
Matt
fccb395564 Fix CVMat weirdness in calibration unit test (#240)
* Remove failing assertion

Cannot reproduce locally.

* comment out other test

* Update Calibrate3dPipeTest.java
2021-01-29 07:08:09 -05:00
Chris Gerth
b510417541 Fixup support for log messages in the client web interface (#247) 2021-01-26 23:48:14 -05:00
Chris Gerth
0330467874 Add Basic Sim Example (#237)
* WIP adding sim pose example

* WIP making examples buildable like WPI. Not quite there yet....

* Make examples runnable

* remove lock

* add lock

* WIP Adding a simpler example for simulation

* Spotless Apply

* Added simulation-supporting aim and range example

* Spotless, revised hand usage to be consistent across examples, and propagated required -1.0's to non-sim examples.

Co-authored-by: Prateek Machiraju <prateek.machiraju@gmail.com>
2021-01-26 23:26:15 -05:00
Banks T
4c15a46cda Move CameraConfiguration to VisionSource (#244) 2021-01-22 22:10:20 -08:00
Matt
d59f8d1227 Cleanup photonlib deps (#243)
Removes unnecessary photonlib dependencies and removes commons-math.
2021-01-22 22:05:39 -08:00
Matt
bbc8a3137b Fix photon-lib spelling in CI (#239) 2021-01-19 05:48:15 -08:00
Prateek Machiraju
951c038f19 Clean up buildscripts (#238)
Cleans up gradle buildscripts.
2021-01-18 19:12:57 -08:00
Prateek Machiraju
1b0c5af86e Make examples runnable, start work on examples (#235)
Co-authored-by: Chris <chrisgerth010592@gmail.com>
Co-authored-by: Matt <matthew.morley.ca@gmail.com>
2021-01-18 10:56:04 -05:00
Banks T
a113bd4192 Fix processing timings in CVPipelineResult (#236) 2021-01-17 23:28:00 -05:00
Matt
d3c23345da Use wpi::Now for image capture timestamp (#232)
This uses the same time source as CSCore does for image captures, and will make latency measurements more accurate.

Co-authored-by: Banks T <btrout.dhrs@gmail.com>
2021-01-17 14:57:21 -08:00
Matt
2e1b3d0f83 Add Photonlib (#231)
Merges Photonlib into Photonvision, along with the Photonlib code examples. Also creates a new PhotonTargeting library teams can depend on.
2021-01-16 20:41:47 -08:00
Banks T
58b39f47aa Split photon-server and photon-core (#211)
Uses multiple Gradle projects to support the split.
2021-01-14 18:45:26 -08:00
Declan Freeman-Gleason
dc0f8cf296 Fetch tags before building server so that the version string is correct (#230) 2021-01-12 12:37:41 -08:00
Declan Freeman-Gleason
3d34d1ca40 Remove 1280x720 on the Pi Camera v1 for now (#228) 2021-01-11 00:35:04 -05:00
Matt
5298de0f64 Run on tags starting with 'v' (#227) 2021-01-10 17:24:04 -08:00
Declan Freeman-Gleason
2330b72451 When describing the current commit, exclude the Dev tag (#226)
* When describing the current release, exclude the Dev tag

* Only run the release task on non-dev tags (i.e. real release tags)
2021-01-10 15:57:16 -08:00
Matt
69d2499e1a Upload release with artifact on tag (#225) 2021-01-10 11:35:10 -08:00
Declan Freeman-Gleason
0a4dcd17e0 Set inputShouldShow to false when all websockets disconnect (#224) 2021-01-09 16:35:42 -08:00
Declan Freeman-Gleason
15c687655a Use computed value to clear up TypeError in dialog persistient prop 2021-01-09 14:50:46 -08:00
Declan Freeman-Gleason
839f959681 Make CVRangeSlider gracefully accept undefined values 2021-01-09 14:50:46 -08:00
Declan Freeman-Gleason
0b2de7d9f1 Make pipeline index rollback work with multiple cameras 2021-01-09 14:50:46 -08:00
Declan Freeman-Gleason
5d139e0a4e Update and improve README (#223) 2021-01-09 11:14:14 -08:00
Matt
b2d939b3b5 Avoid implicit downcast in corner detection (#219) 2021-01-08 02:42:54 -05:00
Chris Gerth
f7e29a1992 Update which-cam-controls-LEDs logic (#220)
Modified VisionModule.java to cause any camera the either has a vendor-defined FOV or the PiCam quirks to control the LED state.

This is a bit of a patch, and directly assumes that piCam's are the devices primarily used for vision processing.

This fixes a tiny bit of pain experienced with a pi3b + picam v1 + usb Cam where not having the USB cam causes the LED's to be working normally, but plugging in the USB cam causes them to be always off.

Parallel issue entered in documentation to add this to known limitations of photon - if you had a set of all usb cameras, we wouldn't know which one is being used for vision processing, and would not be able to control the LED's accordingly. The ultimate solution is just to have teams control the NT entry themselves, which is easy enough. But, docs shouild be updated to reflect that.
2021-01-07 23:42:05 -08:00
Banks T
08a51fd237 Limit PS3Eye to 100FPS (#218)
This helps prevent running in to USB bandwidth issues with multiple PS3Eyes.
2021-01-06 17:27:01 -05:00
Declan Freeman-Gleason
b8bc65ec32 Fix undefined initial resolution in calibrator (#216) 2021-01-05 17:30:55 -08:00
Matt
8e190ce5f7 Save input image before resize or draw (#214) 2021-01-01 15:16:45 -08:00
Matt
f676023a5d Fix multicam and picam acceleration (#205)
Allows pipelines to access quirks
2020-12-31 22:41:57 -08:00
Banks T
d92595f622 Use pipeline members for setPipeParams (#208) 2020-12-31 20:52:34 -08:00
Matt
69142928b3 Only change LED state in the vendor camera vision module (#206) 2020-12-31 20:52:05 -08:00
Matt
28e3b510c7 Remove year from header (#207)
Yeet the year
2020-12-31 19:57:51 -08:00
Matt
fc05bcab2c Add best target raw X/Y position (#198) 2020-12-31 11:33:15 -08:00
Matt
abf5226405 Resize image in driver mode (#197) 2020-12-30 10:41:31 -08:00
Matt
d327428e1b Reduce max snapshots to 12 (#195)
This prevents OOMs on Gloworm, without sacrificing too much accuracy
2020-12-27 20:32:26 -08:00
Matt
79fc194575 Resize image before drawing (#193)
This helps line viability at high divisors
2020-12-27 14:07:53 -08:00
Declan Freeman-Gleason
7a9d999c15 Update libpicam to fix GPU OOM (#196) 2020-12-27 13:54:47 -08:00
Chris Gerth
2cf725876f Add Gloworm Images to Unit Tests for Calibration (#192)
* Incorporated new images from gloworm beta unit from Declan into unit test suite.

* Missed one unit test case - looks like the 1280x720 image set also returns wonky center values.
2020-12-27 10:30:32 -08:00
Declan Freeman-Gleason
2ca879c82d Add sensor model detection (#194) 2020-12-26 11:33:32 -08:00
Chris Gerth
be5d8f6518 Add Gloworm 320x240 9x7 Cal Unit Test (#189)
Unit tests non-square chessboards.
2020-12-23 09:31:12 -08:00
Matt
7a032cce6e [Calibration] Fix iterator bound while creating object points (#188)
Fixes mismatched iterator bounds while creating object points. This addresses calibration failing for non square boards.
2020-12-23 09:05:27 -08:00
Matt
f2f32da2f9 Compensate for binning in picam FOV (#186)
Compensates for picam binning in the OV sensor.
2020-12-23 00:42:39 -08:00
Matt
5768648cde Make dhclient not block (#187) 2020-12-22 21:16:06 -05:00
Chris Gerth
6856427f86 Calibration Checkerboard Corner Find Optimizations (#184)
* WIP monkeying around with adding new targets.

* Added testcase to replicate large extrinsics result from calibration.

* Tweaked calibration pipline to return image with chessboard corner detection overlaid.

* Removed "bad" images from the cal, but that didn't seem to help...

* Added test logic to output the undistorted Mat for evaluating the quality of the calibration.

* Tweaked generation of chessboard points to be in a square pattern, not parallelogram.

* Spotless and removed bespoke test target in prep for PR.

* Revised to a double-for loop for less complex-looking logic

* #thanks spotless

al;ksgfjh akljghf ;lakdfdhg ksadfgh klasdfjhg kasdfjghj aklsjg two spaces

* Boop

* Updated findBoardCorners to operate on the image at a fixed, small resolution.

This means the image is scaled down before passing to openCV for processing, then the returned point cloud is scaled back up by that same factor.
Added additional flags to findBoardCorners to "fail fast".
Revised subpixel optimization parameters to use a dynamically-sized window (based on the distance observed between board corners).

* Added additional unit testing on calibration at different resolutions

* Spotless cleanup

* Add Mat release calls

* One more missing release.

Also, tried to make spotless a bit happier

* Additional formatting and WIP tracking down t he memory leak

* Cleaned up `Point` allocation in utility functions that iterate over mats of points

* Maybe fixing this bugger????

* Indeed! We can now get through the image capture stage of calibration at full resolution and save off images. Still crashing on out of memory as expected.

Added additional unit sim pass/fail criteria to catch unreleased mat's and a new set of images for testing high-res cal.

* Letting spotless do its thing

* Undistort debug seems to be acting a bit wonky in CI, so just commenting out for now

* Guard against testcases bleeding state into each other.

In particular, at least in CI, it appears not all mats are getting released in previous testcases, which borks up a hard check against "0 mats allocated" in our calibration tests.

* Removed obsolete tests

* One of these days, I will indeed learn to run spotless before I push.

But today is (still) not that day.

Today we push. FOR ROHAN!
2020-12-20 22:06:14 -05:00
Chris Gerth
2a687a1db8 Calibration3D Pipeline Memory Leak (#185)
* Cherry-pick the extra debug info from Bank's patches.

* Updated Calibrate3d pipeline to release all unnedded mat's prior to returning.

Update a few raw mat operations to use CVMat for better traceability.

* spotless cleanup

* Added check to shouldPrint for optimization on cvmat deallocate

* Reworked stack trace printing to use lambdas for efficiency

* Missed an unneeded logger.trace

* Formatting improvements
2020-12-20 21:09:39 -05:00
Chris Gerth
771f7442c9 Cal checkerboard object coords update (#181)
* WIP monkeying around with adding new targets.

* Added testcase to replicate large extrinsics result from calibration.

* Tweaked calibration pipline to return image with chessboard corner detection overlaid.

* Removed "bad" images from the cal, but that didn't seem to help...

* Added test logic to output the undistorted Mat for evaluating the quality of the calibration.

* Tweaked generation of chessboard points to be in a square pattern, not parallelogram.

* Spotless and removed bespoke test target in prep for PR.

* Revised to a double-for loop for less complex-looking logic

* #thanks spotless

al;ksgfjh akljghf ;lakdfdhg ksadfgh klasdfjhg kasdfjghj aklsjg two spaces

* Boop

* Reverting my changes in Calibrate3dPipeline.java to make this merge better with other PR's

* Derp changed the wrong one
2020-12-20 20:39:21 -05:00
Matt
c7d092a775 Delete native library if it already exists (#179)
* Delete native library if it already exists

* Spotless
2020-12-13 19:32:00 -05:00
Declan Freeman-Gleason
11a66b15ed Statically link OpenCV and always extract shared object (#177) 2020-12-13 11:28:18 -08:00
Matt
0c89db421c Init image save command key with default value (#178)
This makes it show up in OutlineViewer
2020-12-13 10:29:52 -08:00
Chris Gerth
36de88f903 Added camera pitch (tiltDegrees) to the hunk of data sent to the UI on initial request. (#176) 2020-12-10 12:10:27 -08:00
Declan Freeman-Gleason
a49f3ac7f0 Don't show FPS tips in driver mode (#175) 2020-12-10 11:22:03 -05:00
Declan Freeman-Gleason
229570d522 Make libpicam only link with OpenCV core (#174) 2020-12-10 11:21:22 -05:00
Declan Freeman-Gleason
d346513ad7 Continually set Picam rotation (#173) 2020-12-10 11:20:59 -05:00
Matt
a5437f7215 Fix solvePNP draw bug (#172)
Removes duplicate members, try-catches streaming processing, and prevents drawing empty contours. The latter likely stems from some sort of use-after-free condition.
2020-12-08 17:54:02 -08:00
Matt
0c3aeb409b Address empty object points bug during calibration
Resolves failure mode where object points would be empty in all calibrations after the first.
2020-12-08 17:46:45 -08:00
Declan Freeman-Gleason
e4b6559b81 Fix driver mode memory leak with libpicam (#171) 2020-12-08 13:33:30 -05:00
Declan Freeman-Gleason
e608d073bd Lower reconnection delay to 100ms for more responsive reconnection (#169) 2020-12-08 13:33:03 -05:00
Declan Freeman-Gleason
663684fb10 Check if PicamJNI is supported in driver pipe (#170)
Fixes UnsatisfiedLinkErros when the JNI is not loaded.
2020-12-08 10:32:10 -08:00
Declan Freeman-Gleason
c3dbd45716 Add GPU Acceleration on the Raspberry Pi (#140)
* Add native stuff

* use runtimeloader

* add more native methods

* more stuff

* Switch JNI methods to static

* Remove non-java classes from the picam jni

* Add gradle task for JNI generation

* Migrate my previous GPU accel work

* Initial work on defining JNI interface

* Change libpicam to a symlink for now

* Initial work on adding no-copy OMX GPU accel on the pi

* Make DIRECT_OMX GPU accel mode not crash

* Clean up OMX changes (still not getting valid data back)

* Re-add GPU unit test

* A couple debugging tweaks/notes

* Add temporary special cases to get RGB out of ProcessingMode.NONE

* Code clarity improvements; fix possible VBO bug

* Get DIRECT_OMX working

* Remove some debugging switches in GPUAccelerator

* Pipe in VCSM stuff to read out pixels FAST

* Apply Spotless

* Revert versioningHelper changes

* Add missing import

* Convert to MMAL and move everything to native

* Re-add shared object

* Rework to use MMAL and do everything natively

* Condense pipeline settings classes

* Add OutputStreamPipeline

* Apply spotless

* Fix duplicate variable inits and add more video modes

* Integrate color frames and latency measurements for GPU

* Fix camera detection on pi and other platforms

* Add proper color copy disabling and camera settings calls

* Fix things that were broken by rebase

* Fix spotless issues and remove uneeded prints

* Remove libpicam symlink

* Fix stream resolution limiting

* Remove testing code in GPUAcceleratedHSVPipe

* Make profiling options general to all computers

* Make PicamJNI load from resources

* run spotlessApply

* Address review comments

* Update Maven repo for JOGL

* Fix release race condition

* Only run GPU accel test on the pi

* Lint fix and merge conflict accident fixes

* Make Jackson ignore extra fields when unmarshalling HardwareConfig

* Fix Mat releasing data race

* Spotless apply

* Remove broken header generation task

* Fix shared library loading typo

* Add a ZeroCopyPicam quirk to allow setting gain with the MMAL backend

* Make sure that exposure/brightness/gain get set after res changes

* Make rawInputMat properly local

* Remove bogus set of shouldRun flag

* Clean up small GPUHSVPipe print

* Add in some things that missed the ZeroCopyPiCameraSource rename

* Fix incorrect scoping introduced in past rebase

* Don't filter out too-low resolutions

* Only show latency when GPU accel is enabled

* Don't free Mats in stream thread before we use them

* Fix use-after-free and latency caluclation bugs on USB camera source

* Update libpicam

* Remove unwanted print

* Add libpicam forceLoad in unit test

* Fix streaming during camera calibration

* Fix zerocopy Picam calculation

* Use logger trace method instead of raw prints

* Fix calibration and driver mode pipes with the Picam

Co-authored-by: Matt <matthew.morley.ca@gmail.com>
Co-authored-by: Banks Troutman <btrout.dhrs@gmail.com>
2020-12-08 02:34:21 -05:00
Matt
15d21b7841 Fix calibration modal bug, save calibration images (#165)
Saves calibration images to photonvision_config/calibImgs
2020-12-05 12:18:07 -08:00
Matt
14ed7ce7a4 Ensure stream divisor respects pipeline changes (#163)
Ensure stream divisor respects pipeline changes. Previously this was only set on UI commands and on construction.
2020-12-04 19:38:23 -08:00
Matt
d2b0e465ff Restart avahi-daemon on hostname changes (#161)
* Restart avahi-daemon on hostname changes

* Update NetworkManager.java

* Spotless
2020-12-03 12:16:05 -08:00
Matt
5e385b5925 Set hostname in /etc/hosts (#160)
* Set hostname in /etc/hosts

* Run spotless
2020-12-03 10:40:53 -05:00
Matt
c11f1f3f28 Run apt update before installing LaTeX (#159) 2020-12-02 09:52:49 -08:00
Chris Gerth
308e108953 Update camera path if matched camera path doesn't match saved path
Improves stability when cameras change USB ports.
2020-11-28 06:24:30 -08:00
Matt
2ae750f00f Add modal and animations while calibration is running (#157)
* Add modal for calibration

* Run wpiformat
2020-11-21 21:14:27 -05:00
Chris Gerth
da2aaba033 Log file cleaner (#153)
Removes old log files to limit disk usage.
2020-11-11 11:09:31 -08:00
Chris Gerth
e74f750fc0 Leds active without vendor fov (#156)
* Changed out LED logic to always manipulate them if configured (rather than requiring vendor FOV)
2020-11-11 11:06:51 -08:00
Chris Gerth
3f48346557 Remove Erode/Dilate from backend and UI (#152)
Removing erode/dilate from Reflective pipeline in backend and UI
2020-11-07 17:23:27 -08:00
Chris Gerth
bf7c9fea44 Single Config-file Upload Support (#150)
Allow a single .json config file to be uploaded.
2020-11-07 17:21:07 -08:00
Chris Gerth
8109a2a437 Add Disk Usage Percentage Stat (#154)
Added disk usage percentage hardware stat

Updated Settings-General UI to have table-layout for metrics and other info

Added a `synchronized` modifier to the command runner due to some InterruptedThreadExceptions that were spuriously showing up while getting stats. Added additional stack trace logging when issues arise.
2020-11-07 17:13:45 -08:00
Chris Gerth
0ce49bd8f2 Improve Pi Non-GPU-accel exposure (#148)
Changes exposure setters to accept a floating point input (rather than integer)
Updates the UI to change exposure in increments of 0.1 (rather than 1.0)
Updates quirky PI camera logic to use the raw_ interface with scaling/offset logic matching the GPU-accelerated pi3 camera logic from Declan.
Adds logic to disable auto-white-balance in the PI camera, which should yield more consistent vision processing results.
2020-10-27 14:57:11 -07:00
Chris Gerth
33bbb4c69f Added Deploy task to build.gradle (#147)
Searches for an appropriate remote target or uses an address specified by -PtgtIP="X.X.X.X" on the build command.
Stops photonvision, copies the new jar to the correct location, and restarts photonvision.
2020-10-27 08:18:21 -07:00
Banks T
35a6c0bfa4 Disallow setting NT pipeline index below 0 (#145) 2020-10-19 10:29:25 -07:00
Matt
869e4628ce Fix image rotation in test mode (#144) 2020-10-19 13:26:11 -04:00
Matt
866ce2197e Alliow saving if running a NT server (#143) 2020-10-19 13:04:04 -04:00
Chris Gerth
3a78e23a55 Added input and output frame file save routines (#134)
* Added input and output frame file save routines

* First pass at review items

* Revised logic to not crash on start and pass tests

* Updated build.gradle to force line endings. Spotless passes now.

* Reverted lineEndings to not force Unix.

Gerth needs to fix up his dev pc.

Co-authored-by: Banks T <btrout.dhrs@gmail.com>
2020-10-16 21:49:50 -04:00
Matt
31013346c0 Rename MJPEG streams when camera name changes (#136)
* Rename MJPEG streams when camera name changes

* Change camera name to HTTP request

This allows us to wait for it to for sure be done

* Fix reload logic

* whee lnt

* Reload on backend connect too

* Update CameraAndPipelineSelect.vue
2020-10-16 19:48:24 -04:00
Banks T
e37fcdea98 Replace JPigpio, add hardware PWM support (#127)
* Send brightness properly to clients

* Refactor GPIO, add custom Pigpio socket wrapper

* Replace PiGPIO with PigpioPin, fix hardwarePWM

* Remove Jpigpio dependency

* Apply Spotless

* Warn user of Pigpio pin incapable of hardware PWM

* Begin rework of blinking and brightness control for vision leds

* Fixed blink, fixed brightness persistence, added bootup blink

* Cleanup HardwareManager access of config

* Apply spotless

* Skip hardware test on non-Pi

Co-authored-by: Matt <matthew.morley.ca@gmail.com>
2020-10-16 19:06:40 -04:00
Matt
23f3c0e6e1 Use chessboard squares (vs interior corners); fix resolution selector bug (#139)
* Change chessboard size to be squares not interior corners

This reduces ambiguity

* Force users to select resolution

This forces the correct video mode index to be selected. Otherwise the 0th camera videomode index will be used, as it's zero-inited. This is undesirable.

* Make target model an enum

This will allow the UI to remember the currently selected target.
2020-10-13 06:58:50 -07:00
Matt
6e0a6b804e Fix calibration slider not moving, hide gain slider when needed (#135) 2020-10-05 19:22:58 -07:00
Matt
2d7a4dd1b9 Add thread safety to Logger (#131) 2020-09-25 07:47:07 -07:00
Matt
28459704c6 Fix network config "Supported" bug (#130)
* Fix network config bug

* Use Jackson instead of hacky solution for management

* run spotless

* Add assertion
2020-09-24 18:34:47 -04:00
Banks T
125cd35557 Fix curl error, update service unit file (#129) 2020-09-18 21:37:56 -04:00
Matt
3305c6619d Fix pipeline duplication bug (#128) 2020-09-17 11:23:00 -07:00
Matt
24132555b8 Guarantee cameras always have the same ports (#123)
* Guarantee cameras always have the same ports

* clean up cscore stuff

* Fix stream api abuse
2020-09-15 19:00:26 -07:00
Matt
44bfc3ea6c Fix static IP and network settings on Pi 2020-09-15 11:34:27 -07:00
Matt
71fc8a7017 Metrics and lighting implementation (#116)
Implements metrics and lighting control.
2020-09-15 11:19:36 -07:00
Matt
b73c698e4d Never overwrite hardware config file (#122)
* Never overwrite hardware config file

* Remove unneeded assert
2020-09-13 12:58:07 -07:00
Matt
45686b7c9d Add NT servermode switch (#120) 2020-09-13 08:58:56 -07:00
Matt
90f8397688 Tag with dev if not right on release tag (#117) 2020-09-11 17:00:57 -04:00
Matt
2495d348ea Implement hostname, IP setting (#114) 2020-09-10 20:07:23 -07:00
Matt
a35f775b05 Fix driver mode settings, sort resolutions (#115)
* Fix DriverMode settings

* Update FileVisionSource.java

* Sort modes by resolution

* Filter duplicated modes

* run spotless

* Fix calibration bug

* run format

* aaaaa

* Add hardware and platform support

* decrease timing sensitivity

* Better handle jvm exitg

* Make reboot happen immediately

* Cleanup restart

* Remove debug print

* Fix Jackson exploding when deserializing old versions of configs

* Add unit test for old config versions

* Run format

* Add a comment

* remove isvendorcam from pipeline manager

* oops
2020-09-10 19:20:16 -07:00
Banks T
ddd15d362b PiCam Tweaks (#63)
* Update QuirkyCamera to do name-based matching

* Add pi-cam exposure set

* Refactor QuirkyCamera

* Add PiCam quirk test, fix tests for no-name quirky cameras

* Apply Spotless

* Add pinhole model unit test
2020-09-08 12:11:29 -04:00
Prateek Machiraju
7bf92a9db3 Fix spelling error in install script (#113) 2020-09-05 16:28:17 -04:00
Matt
73fc8e04ca Filter non-bgr modes on Pi (#112)
This can almost double performance in some cases.
2020-09-05 09:33:48 -07:00
739 changed files with 19095 additions and 6342 deletions

167
.clang-format Normal file
View File

@@ -0,0 +1,167 @@
---
Language: Cpp
BasedOnStyle: Google
AccessModifierOffset: -1
AlignAfterOpenBracket: Align
AlignConsecutiveMacros: false
AlignConsecutiveAssignments: false
AlignConsecutiveDeclarations: false
AlignEscapedNewlines: Left
AlignOperands: true
AlignTrailingComments: true
AllowAllArgumentsOnNextLine: true
AllowAllConstructorInitializersOnNextLine: true
AllowAllParametersOfDeclarationOnNextLine: true
AllowShortBlocksOnASingleLine: Never
AllowShortCaseLabelsOnASingleLine: false
AllowShortFunctionsOnASingleLine: All
AllowShortLambdasOnASingleLine: All
AllowShortIfStatementsOnASingleLine: WithoutElse
AllowShortLoopsOnASingleLine: true
AlwaysBreakAfterDefinitionReturnType: None
AlwaysBreakAfterReturnType: None
AlwaysBreakBeforeMultilineStrings: true
AlwaysBreakTemplateDeclarations: Yes
BinPackArguments: true
BinPackParameters: true
BraceWrapping:
AfterCaseLabel: false
AfterClass: false
AfterControlStatement: false
AfterEnum: false
AfterFunction: false
AfterNamespace: false
AfterObjCDeclaration: false
AfterStruct: false
AfterUnion: false
AfterExternBlock: false
BeforeCatch: false
BeforeElse: false
IndentBraces: false
SplitEmptyFunction: true
SplitEmptyRecord: true
SplitEmptyNamespace: true
BreakBeforeBinaryOperators: None
BreakBeforeBraces: Attach
BreakBeforeInheritanceComma: false
BreakInheritanceList: BeforeColon
BreakBeforeTernaryOperators: true
BreakConstructorInitializersBeforeComma: false
BreakConstructorInitializers: BeforeColon
BreakAfterJavaFieldAnnotations: false
BreakStringLiterals: true
ColumnLimit: 80
CommentPragmas: '^ IWYU pragma:'
CompactNamespaces: false
ConstructorInitializerAllOnOneLineOrOnePerLine: true
ConstructorInitializerIndentWidth: 4
ContinuationIndentWidth: 4
Cpp11BracedListStyle: true
DeriveLineEnding: false
DerivePointerAlignment: false
DisableFormat: false
ExperimentalAutoDetectBinPacking: false
FixNamespaceComments: true
ForEachMacros:
- foreach
- Q_FOREACH
- BOOST_FOREACH
IncludeBlocks: Regroup
IncludeCategories:
- Regex: '^<ext/.*\.h>'
Priority: 2
SortPriority: 0
- Regex: '^<.*\.h>'
Priority: 1
SortPriority: 0
- Regex: '^<.*'
Priority: 2
SortPriority: 0
- Regex: '.*'
Priority: 3
SortPriority: 0
IncludeIsMainRegex: '([-_](test|unittest))?$'
IncludeIsMainSourceRegex: ''
IndentCaseLabels: true
IndentGotoLabels: true
IndentPPDirectives: None
IndentWidth: 2
IndentWrappedFunctionNames: false
JavaScriptQuotes: Leave
JavaScriptWrapImports: true
KeepEmptyLinesAtTheStartOfBlocks: false
MacroBlockBegin: ''
MacroBlockEnd: ''
MaxEmptyLinesToKeep: 1
NamespaceIndentation: None
ObjCBinPackProtocolList: Never
ObjCBlockIndentWidth: 2
ObjCSpaceAfterProperty: false
ObjCSpaceBeforeProtocolList: true
PenaltyBreakAssignment: 2
PenaltyBreakBeforeFirstCallParameter: 1
PenaltyBreakComment: 300
PenaltyBreakFirstLessLess: 120
PenaltyBreakString: 1000
PenaltyBreakTemplateDeclaration: 10
PenaltyExcessCharacter: 1000000
PenaltyReturnTypeOnItsOwnLine: 200
PointerAlignment: Left
RawStringFormats:
- Language: Cpp
Delimiters:
- cc
- CC
- cpp
- Cpp
- CPP
- 'c++'
- 'C++'
CanonicalDelimiter: ''
BasedOnStyle: google
- Language: TextProto
Delimiters:
- pb
- PB
- proto
- PROTO
EnclosingFunctions:
- EqualsProto
- EquivToProto
- PARSE_PARTIAL_TEXT_PROTO
- PARSE_TEST_PROTO
- PARSE_TEXT_PROTO
- ParseTextOrDie
- ParseTextProtoOrDie
CanonicalDelimiter: ''
BasedOnStyle: google
ReflowComments: true
SortIncludes: false
SortUsingDeclarations: true
SpaceAfterCStyleCast: false
SpaceAfterLogicalNot: false
SpaceAfterTemplateKeyword: true
SpaceBeforeAssignmentOperators: true
SpaceBeforeCpp11BracedList: false
SpaceBeforeCtorInitializerColon: true
SpaceBeforeInheritanceColon: true
SpaceBeforeParens: ControlStatements
SpaceBeforeRangeBasedForLoopColon: true
SpaceInEmptyBlock: false
SpaceInEmptyParentheses: false
SpacesBeforeTrailingComments: 2
SpacesInAngles: false
SpacesInConditionalStatement: false
SpacesInContainerLiterals: true
SpacesInCStyleCastParentheses: false
SpacesInParentheses: false
SpacesInSquareBrackets: false
SpaceBeforeSquareBrackets: false
Standard: Auto
StatementMacros:
- Q_UNUSED
- QT_REQUIRE_VERSION
TabWidth: 8
UseCRLF: false
UseTab: Never
...

1
.github/CODEOWNERS vendored
View File

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

View File

@@ -7,12 +7,14 @@ name: CI
on:
push:
branches: [ master ]
tags:
- 'v*'
pull_request:
branches: [ master ]
jobs:
# This job builds the client (web view).
build-client:
photonclient-build:
# Let all steps run within the photon-client dir.
defaults:
@@ -29,7 +31,7 @@ jobs:
steps:
# Checkout code.
- uses: actions/checkout@v1
# Setup Node.js
- name: Setup Node.js
uses: actions/setup-node@v1
@@ -47,12 +49,7 @@ jobs:
name: built-client
path: photon-client/dist/
build-server:
# Let all steps run within the photon-server dir.
defaults:
run:
working-directory: photon-server
photon-build-all:
# The type of runner that the job will run on.
runs-on: ubuntu-latest
@@ -60,7 +57,11 @@ jobs:
# Checkout code.
- name: Checkout code
uses: actions/checkout@v1
# Fetch tags.
- name: Fetch tags
run: git fetch --tags --force
# Install Java 11.
- name: Install Java 11
uses: actions/setup-java@v1
@@ -71,19 +72,28 @@ jobs:
- name: Gradle Build
run: |
chmod +x gradlew
./gradlew build -x check
./gradlew build -x check --max-workers 1
# Run Gradle Tests.
- name: Gradle Tests
run: ./gradlew testHeadless -i --max-workers 1
# Generate Coverage Report.
- name: Gradle Coverage
run: ./gradlew jacocoTestReport --max-workers 1
# Run Tests Generate Coverage Report.
- name: Gradle Test and Coverage
run: ./gradlew jacocoTestReport
# Publish Coverage Report.
- name: Publish Coverage Report
- name: Publish Server Coverage Report
uses: codecov/codecov-action@v1
with:
file: ./photon-server/build/reports/jacoco/test/jacocoTestReport.xml
build-offline-docs:
- name: Publish Core Coverage Report
uses: codecov/codecov-action@v1
with:
file: ./photon-core/build/reports/jacoco/test/jacocoTestReport.xml
photonserver-build-offline-docs:
runs-on: ubuntu-latest
steps:
@@ -97,21 +107,18 @@ jobs:
- uses: actions/setup-python@v2
with:
python-version: '3.6'
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install sphinx sphinx_rtd_theme sphinx-tabs sphinxext-opengraph doc8
pip install -r requirements.txt
- name: Install LaTeX and other system dependencies
run: sudo apt install -y texlive-latex-recommended texlive-fonts-recommended texlive-latex-extra latexmk texlive-lang-greek texlive-luatex texlive-xetex texlive-fonts-extra dvipng graphviz
- name: Check the docs
run: |
make linkcheck
make lint
- name: Build the docs
run: |
make html
@@ -121,15 +128,129 @@ jobs:
with:
name: built-docs
path: build/html
build-package:
needs: [build-client, build-server, build-offline-docs]
photonserver-check-lint:
# The type of runner that the job will run on.
runs-on: ubuntu-latest
# Let all steps run within the photon-server dir.
defaults:
run:
working-directory: photon-server
steps:
# Checkout code.
- uses: actions/checkout@v1
# Install Java 11.
- uses: actions/setup-java@v1
with:
java-version: 11
# Check server code with Spotless.
- run: |
chmod +x gradlew
./gradlew spotlessCheck
# Building photonlib
photonlib-build-host:
env:
MACOSX_DEPLOYMENT_TARGET: 10.14
strategy:
fail-fast: false
matrix:
include:
- os: windows-latest
artifact-name: Win64
- os: macos-latest
artifact-name: macOS
- os: ubuntu-latest
artifact-name: Linux
runs-on: ${{ matrix.os }}
name: "Photonlib - Build - ${{ matrix.artifact-name }}"
steps:
- uses: actions/checkout@v2.3.4
with:
fetch-depth: 0
- uses: actions/setup-java@v1
with:
java-version: 11
- run: git fetch --tags --force
- run: |
chmod +x gradlew
./gradlew photon-lib:build --max-workers 1
- run: ./gradlew photon-lib:publish photon-targeting:publish
name: Publish
env:
ARTIFACTORY_API_KEY: ${{ secrets.ARTIFACTORY_API_KEY }}
if: github.event_name == 'push'
photonlib-build-docker:
strategy:
fail-fast: false
matrix:
include:
- container: wpilib/roborio-cross-ubuntu:2022-18.04
artifact-name: Athena
- container: wpilib/raspbian-cross-ubuntu:10-18.04
artifact-name: Raspbian
- container: wpilib/aarch64-cross-ubuntu:bionic-18.04
artifact-name: Aarch64
runs-on: ubuntu-latest
container: ${{ matrix.container }}
name: "Photonlib - Build - ${{ matrix.artifact-name }}"
steps:
- uses: actions/checkout@v2.3.4
with:
fetch-depth: 0
- uses: actions/setup-java@v1
with:
java-version: 11
- run: |
chmod +x gradlew
./gradlew photon-lib:build --max-workers 1
- run: |
chmod +x gradlew
./gradlew photon-lib:publish
env:
ARTIFACTORY_API_KEY: ${{ secrets.ARTIFACTORY_API_KEY }}
if: github.event_name == 'push'
photonlib-wpiformat:
name: "wpiformat"
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Fetch all history and metadata
run: |
git fetch --prune --unshallow
git checkout -b pr
git branch -f master origin/master
- name: Set up Python 3.8
uses: actions/setup-python@v2
with:
python-version: 3.8
- name: Install clang-format
run: |
sudo sh -c "echo 'deb http://archive.ubuntu.com/ubuntu/ $(lsb_release -cs)-proposed restricted main multiverse universe' >> /etc/apt/sources.list.d/proposed-repositories.list"
sudo apt-get update -q
sudo apt-get install -y clang-format-12
- name: Install wpiformat
run: pip3 install wpiformat
- name: Run
run: wpiformat -clang 12
- name: Check Output
run: git --no-pager diff --exit-code HEAD
- name: Generate diff
run: git diff HEAD > wpiformat-fixes.patch
if: ${{ failure() }}
- uses: actions/upload-artifact@v2
with:
name: wpiformat fixes
path: wpiformat-fixes.patch
if: ${{ failure() }}
photon-build-package:
needs: [photonclient-build, photon-build-all, photonserver-build-offline-docs, photonlib-build-host, photonlib-build-docker]
# The type of runner that the job will run on.
runs-on: ubuntu-latest
@@ -137,7 +258,7 @@ jobs:
steps:
# Checkout code.
- uses: actions/checkout@v1
# Install Java 11.
- uses: actions/setup-java@v1
with:
@@ -145,8 +266,8 @@ jobs:
# Clear any existing web resources.
- run: |
rm -rf src/main/resources/web/*
mkdir -p src/main/resources/web/docs
rm -rf photon-server/src/main/resources/web/*
mkdir -p photon-server/src/main/resources/web/docs
# Download client artifact to resources folder.
- uses: actions/download-artifact@v2
@@ -158,54 +279,59 @@ jobs:
- uses: actions/download-artifact@v2
with:
name: built-docs
path: photon-server/src/main/resources/web/docs
path: photon-server/src/main/resources/web/docs
# Print folder contents.
- run: ls
working-directory: photon-server/src/main/resources/web/
# Build fat jar.
# Build fat jar for both pi and everything
- run: |
chmod +x gradlew
./gradlew shadowJar
working-directory: photon-server
./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: |
chmod +x scripts/generatePiImage.sh
./scripts/generatePiImage.sh
# Upload final fat jar as artifact.
- uses: actions/upload-artifact@master
with:
name: jar
path: photon-server/build/libs
- uses: eine/tip@master
- uses: actions/upload-artifact@master
with:
name: image
path: photonvision*.zip
- uses: pyTooling/Actions/releaser@r0
with:
token: ${{ secrets.GITHUB_TOKEN }}
tag: 'Dev'
rm: true
files: |
photon-server/build/libs/*.jar
photonvision*.zip
if: github.event_name == 'push'
check-lint:
# Let all steps run within the photon-server dir.
defaults:
run:
working-directory: photon-server
# The type of runner that the job will run on.
photon-release:
if: startsWith(github.ref, 'refs/tags/v')
needs: [photon-build-package]
runs-on: ubuntu-latest
steps:
# Checkout code.
- uses: actions/checkout@v1
# Install Java 11.
- uses: actions/setup-java@v1
# This *should* pull in fat and pi-only jars
- uses: actions/download-artifact@v2
with:
java-version: 11
# Check server code with Spotless.
- run: |
chmod +x gradlew
./gradlew spotlessCheck
name: jar
# And the image we made previously
- uses: actions/download-artifact@v2
with:
name: image
# All we've downloaded (ideally) is the fat jar, pi jar, and image. So just upload it all
- uses: softprops/action-gh-release@v1
with:
files: '**/*'
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

37
.gitignore vendored
View File

@@ -104,17 +104,25 @@ fabric.properties
# Android studio 3.1+ serialized cache file
.idea/caches/build_file_checksums.ser
photon-server/.gradle
photon-server/target
photon-server/src/main/java/META-INF
photon-server/.settings
photon-server/.classpath
photon-server/.project
photon-server/settings
photon-server/dependency-reduced-pom.xml
# Temporary build files
**/.gradle
**/target
**/src/main/java/META-INF
**/.settings
**/.classpath
**/.project
**/settings
**/dependency-reduced-pom.xml
# photon-server/photon-vision.iml
# compile_commands
compile_commands.json
# clang configuration and clangd cache
.clang
.clangd/
.cache/
New client/photon-client/*
*.prefs
@@ -125,3 +133,14 @@ photon-server/build
photon-server/photon-vision
photon-server/src/main/resources/web
photon-server/src/main/java/org/photonvision/PhotonVersion.java
photon-server/src/main/generated/native/include/org_photonvision_raspi_PicamJNI.h
*.bin
.gradle
.gradle/*
photonvision_config
build/spotlessJava
build/*
build
photon-lib/src/main/java/org/photonvision/PhotonVersion.java
/photonlib-java-examples/bin/
photon-lib/src/generate/native/include/PhotonVersion.h

27
.styleguide Normal file
View File

@@ -0,0 +1,27 @@
cppHeaderFileInclude {
\.h$
\.hpp$
\.inc$
\.inl$
}
cppSrcFileInclude {
\.cpp$
}
modifiableFileExclude {
\.jpg$
\.png$
\.so$
}
includeProject {
^photonLib/
}
includeOtherLibs {
^frc/
^networktables/
^units/
^wpi/
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright (C) $YEAR Photon Vision.
* 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
@@ -14,4 +14,3 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/

View File

@@ -1,25 +1,32 @@
# Photon Vision
[![CI](https://github.com/PhotonVision/photonvision/workflows/CI/badge.svg)](https://github.com/PhotonVision/photonvision/actions?query=workflow%3ACI) [![codecov](https://codecov.io/gh/PhotonVision/photonvision/branch/master/graph/badge.svg)](https://codecov.io/gh/PhotonVision/photonvision)
[![CI](https://github.com/PhotonVision/photonvision/workflows/CI/badge.svg)](https://github.com/PhotonVision/photonvision/actions?query=workflow%3ACI) [![codecov](https://codecov.io/gh/PhotonVision/photonvision/branch/master/graph/badge.svg)](https://codecov.io/gh/PhotonVision/photonvision) [![Discord](https://img.shields.io/discord/725836368059826228?color=%23738ADB&label=Join%20our%20Discord&logo=discord&logoColor=white)](https://discord.gg/wYxTwym)
A copy of the latest development release is available [here](https://github.com/PhotonVision/photonvision/releases/tag/Dev).
PhotonVision is the free, fast, and easy-to-use computer vision solution for the *FIRST* Robotics Competition. You can read an overview of our features [on our website](https://photonvision.org). You can find our comprehensive documentation [here](https://docs.photonvision.org).
PhotonVision is a fork of [Chameleon Vision](https://github.com/Chameleon-Vision/chameleon-vision/), a free open-source software for FRC teams to use for vision processing on their robots. Thank you to everyone who worked on the original project.
A copy of the latest Raspberry Pi image is available [here](https://github.com/PhotonVision/photon-pi-gen/releases). A copy of the latest standalone JAR is available [here](https://github.com/PhotonVision/photonvision/releases). If you are a Gloworm user you can find the latest Gloworm image [here](https://github.com/gloworm-vision/pi-gen/releases).
For information on contributing or running PhotonVision, please read our documentation on ReadTheDocs.
# Roadmap
Our roadmap is publicly available on [Trello](https://trello.com/photonvision).
If you are interested in contributing code or documentation to the project, please [read our getting started page for contributors](https://docs.photonvision.org/en/latest/docs/other/contributing/index.html) and **[join the Discord](https://discord.gg/wYxTwym) to introduce yourself!** We hope to provide a welcoming community to anyone who is interested in helping.
## Authors
A 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.
* [WPILib](https://github.com/wpilibsuite) - Specifically [cscore](https://github.com/wpilibsuite/allwpilib/tree/master/cscore), [CameraServer](https://github.com/wpilibsuite/allwpilib/tree/master/cameraserver), [NTCore](https://github.com/wpilibsuite/allwpilib/tree/master/ntcore), and [OpenCV](https://github.com/wpilibsuite/thirdparty-opencv).
* [WPILib](https://github.com/wpilibsuite) - Specifically [cscore](https://github.com/wpilibsuite/allwpilib/tree/master/cscore), [CameraServer](https://github.com/wpilibsuite/allwpilib/tree/master/cameraserver), [NTCore](https://github.com/wpilibsuite/allwpilib/tree/master/ntcore), and [OpenCV](https://github.com/wpilibsuite/thirdparty-opencv).
* [Apache Commons](https://commons.apache.org/) - Specifically [Commons Math](https://commons.apache.org/proper/commons-math/), and [Commons Lang](https://commons.apache.org/proper/commons-lang/)
@@ -29,5 +36,11 @@ A list of contributors is available in our documentation on ReadTheDocs.
* [FasterXML](https://github.com/FasterXML) - Specifically [jackson](https://github.com/FasterXML/jackson)
## License
Usage of PhotonVision must fall under all terms of [GNU General Public License](https://www.gnu.org/licenses/gpl-3.0.html)
## 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)

53
build.gradle Normal file
View File

@@ -0,0 +1,53 @@
plugins {
id "com.diffplug.spotless" version "6.1.2"
id "com.github.johnrengelman.shadow" version "7.1.2"
id "com.github.node-gradle.node" version "3.1.1" apply false
id "edu.wpi.first.GradleJni" version "1.0.0"
id "edu.wpi.first.GradleVsCode" version "1.1.0"
id "edu.wpi.first.NativeUtils" version "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()
maven { url = "https://maven.photonvision.org/repository/internal/" }
}
wpilibRepositories.addAllReleaseRepositories(it)
wpilibRepositories.addAllDevelopmentRepositories(it)
}
// Configure the version number.
apply from: "versioningHelper.gradle"
ext {
wpilibVersion = "2022.1.1"
opencvVersion = "4.5.2-1"
joglVersion = "2.4.0-rc-20200307"
pubVersion = versionString
isDev = pubVersion.startsWith("dev")
jniPlatforms = project.hasProperty('pionly') ? ['linuxraspbian']
: ['linuxaarch64bionic', 'linuxraspbian', 'linuxx86-64', 'osxx86-64', 'windowsx86-64']
println("Building for archs " + jniPlatforms)
}
spotless {
java {
toggleOffOn()
googleJavaFormat()
indentWithTabs(2)
indentWithSpaces(4)
removeUnusedImports()
trimTrailingWhitespace()
endWithNewline()
}
java {
target "**/*.java"
targetExclude("photon-core/src/main/java/org/photonvision/PhotonVersion.java")
targetExclude("photon-lib/src/main/java/org/photonvision/PhotonVersion.java")
}
}

View File

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

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

View File

@@ -82,6 +82,7 @@ esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
@@ -129,6 +130,7 @@ fi
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

View File

@@ -1,100 +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 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 init
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 init
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
:init
@rem Get command-line arguments, handling Windows variants
if not "%OS%" == "Windows_NT" goto win9xME_args
:win9xME_args
@rem Slurp the command line arguments.
set CMD_LINE_ARGS=
set _SKIP=2
:win9xME_args_slurp
if "x%~1" == "x" goto execute
set CMD_LINE_ARGS=%*
:execute
@rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
: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

View File

@@ -2118,16 +2118,6 @@
"integrity": "sha512-Jus9s4CDbqwocc5pOAnh8ShfrnMcPHuJYzVcSUU7lrh8Ni5HuIqX3oilL86p3dlTrk0LzHRCgA/GQ7uNCw6l2Q==",
"dev": true
},
"@types/mini-css-extract-plugin": {
"version": "0.9.1",
"resolved": "https://registry.npmjs.org/@types/mini-css-extract-plugin/-/mini-css-extract-plugin-0.9.1.tgz",
"integrity": "sha512-+mN04Oszdz9tGjUP/c1ReVwJXxSniLd7lF++sv+8dkABxVNthg6uccei+4ssKxRHGoMmPxdn7uBdJWONSJGTGQ==",
"dev": true,
"optional": true,
"requires": {
"@types/webpack": "*"
}
},
"@types/minimatch": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.3.tgz",
@@ -2164,6 +2154,12 @@
"integrity": "sha512-+wYo+L6ZF6BMoEjtf8zB2esQsqdV6WsjRK/GP9WOgLPrq87PbNWgIxS76dS5uvl/QXtHGakZmwTznIfcPXcKlQ==",
"dev": true
},
"@types/raf": {
"version": "3.4.0",
"resolved": "https://registry.npmjs.org/@types/raf/-/raf-3.4.0.tgz",
"integrity": "sha512-taW5/WYqo36N7V39oYyHP9Ipfd5pNFvGTIQsNGj86xV88YQ7GnI30/yMfKDF7Zgin0m3e+ikX88FvImnK4RjGw==",
"optional": true
},
"@types/range-parser": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.3.tgz",
@@ -2679,17 +2675,6 @@
"integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==",
"dev": true
},
"ansi-styles": {
"version": "4.2.1",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.1.tgz",
"integrity": "sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA==",
"dev": true,
"optional": true,
"requires": {
"@types/color-name": "^1.1.1",
"color-convert": "^2.0.1"
}
},
"browserslist": {
"version": "4.14.0",
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.14.0.tgz",
@@ -2734,23 +2719,6 @@
"integrity": "sha512-Hpwa4obv7EGP+TjkCh/wVvbtNJewxmtg4yVJBLFnxo35vbPapBr138bUWENkb5j5L9JZJ9RXLn4OrXRG/cecPQ==",
"dev": true
},
"color-convert": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
"dev": true,
"optional": true,
"requires": {
"color-name": "~1.1.4"
}
},
"color-name": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
"dev": true,
"optional": true
},
"electron-to-chromium": {
"version": "1.3.554",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.554.tgz",
@@ -2784,13 +2752,6 @@
"path-exists": "^4.0.0"
}
},
"has-flag": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
"integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
"dev": true,
"optional": true
},
"locate-path": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz",
@@ -2924,16 +2885,6 @@
"ansi-regex": "^5.0.0"
}
},
"supports-color": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
"integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
"dev": true,
"optional": true,
"requires": {
"has-flag": "^4.0.0"
}
},
"terser-webpack-plugin": {
"version": "2.3.8",
"resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-2.3.8.tgz",
@@ -2960,34 +2911,6 @@
"psl": "^1.1.28",
"punycode": "^2.1.1"
}
},
"vue-loader-v16": {
"version": "npm:vue-loader@16.0.0-beta.5",
"resolved": "https://registry.npmjs.org/vue-loader/-/vue-loader-16.0.0-beta.5.tgz",
"integrity": "sha512-ciWfzNefqWlmzKznCWY9hl+fPP4KlQ0A9MtHbJ/8DpyY+dAM8gDrjufIdxwTgC4szE4EZC3A6ip/BbrqM84GqA==",
"dev": true,
"optional": true,
"requires": {
"@types/mini-css-extract-plugin": "^0.9.1",
"chalk": "^3.0.0",
"hash-sum": "^2.0.0",
"loader-utils": "^1.2.3",
"merge-source-map": "^1.1.0",
"source-map": "^0.6.1"
},
"dependencies": {
"chalk": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz",
"integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==",
"dev": true,
"optional": true,
"requires": {
"ansi-styles": "^4.1.0",
"supports-color": "^7.1.0"
}
}
}
}
}
},
@@ -3559,8 +3482,7 @@
"atob": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/atob/-/atob-2.1.2.tgz",
"integrity": "sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==",
"dev": true
"integrity": "sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg=="
},
"autoprefixer": {
"version": "9.8.6",
@@ -3819,6 +3741,12 @@
}
}
},
"base64-arraybuffer": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-0.2.0.tgz",
"integrity": "sha512-7emyCsu1/xiBXgQZrscw/8KPRT44I4Yq9Pe6EGs3aPRTsWuggML1/1DTuZUuIaJPIm1FTDUVXl4x/yW8s0kQDQ==",
"optional": true
},
"base64-js": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.3.1.tgz",
@@ -4110,6 +4038,11 @@
"node-releases": "^1.1.44"
}
},
"btoa": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/btoa/-/btoa-1.2.1.tgz",
"integrity": "sha512-SB4/MIGlsiVkMcHmT+pSmIPoNDoHg+7cMzmt3Uxt628MTz2487DKSqK/fuhFBrkuqrYv5UCEnACpF4dTFNKc/g=="
},
"buffer": {
"version": "4.9.2",
"resolved": "https://registry.npmjs.org/buffer/-/buffer-4.9.2.tgz",
@@ -4390,6 +4323,45 @@
"integrity": "sha512-6ljkLtF1KM5fQ+5ZN0wuyVvvebJxgJPTmScOMaFuQN2QuOzvRJnWSKfzQskQU5IOU4Gap3zasYPIinzwUjoj/g==",
"dev": true
},
"canvg": {
"version": "3.0.9",
"resolved": "https://registry.npmjs.org/canvg/-/canvg-3.0.9.tgz",
"integrity": "sha512-rDXcnRPuz4QHoCilMeoTxql+fvGqNAxp+qV/KHD8rOiJSAfVjFclbdUNHD2Uqfthr+VMg17bD2bVuk6F07oLGw==",
"optional": true,
"requires": {
"@babel/runtime": "^7.12.5",
"@types/raf": "^3.4.0",
"core-js": "^3.8.3",
"raf": "^3.4.1",
"regenerator-runtime": "^0.13.7",
"rgbcolor": "^1.0.1",
"stackblur-canvas": "^2.0.0",
"svg-pathdata": "^6.0.3"
},
"dependencies": {
"@babel/runtime": {
"version": "7.16.0",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.16.0.tgz",
"integrity": "sha512-Nht8L0O8YCktmsDV6FqFue7vQLRx3Hb0B37lS5y0jDRqRxlBG4wIJHnf9/bgSE2UyipKFA01YtS+npRdTWBUyw==",
"optional": true,
"requires": {
"regenerator-runtime": "^0.13.4"
}
},
"core-js": {
"version": "3.19.0",
"resolved": "https://registry.npmjs.org/core-js/-/core-js-3.19.0.tgz",
"integrity": "sha512-L1TpFRWXZ76vH1yLM+z6KssLZrP8Z6GxxW4auoCj+XiViOzNPJCAuTIkn03BGdFe6Z5clX5t64wRIRypsZQrUg==",
"optional": true
},
"regenerator-runtime": {
"version": "0.13.9",
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.9.tgz",
"integrity": "sha512-p3VT+cOEgxFsRRA9X4lkI1E+k2/CtnKtU4gcxyaCUreilL/vqI6CdZ3wxVUx3UOUg+gnUOQQcRI7BmSI656MYA==",
"optional": true
}
}
},
"case-sensitive-paths-webpack-plugin": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/case-sensitive-paths-webpack-plugin/-/case-sensitive-paths-webpack-plugin-2.3.0.tgz",
@@ -5172,6 +5144,15 @@
"timsort": "^0.3.0"
}
},
"css-line-break": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/css-line-break/-/css-line-break-2.0.1.tgz",
"integrity": "sha512-gwKYIMUn7xodIcb346wgUhE2Dt5O1Kmrc16PWi8sL4FTfyDj8P5095rzH7+O8CTZudJr+uw2GCI/hwEkDJFI2w==",
"optional": true,
"requires": {
"base64-arraybuffer": "^0.2.0"
}
},
"css-loader": {
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/css-loader/-/css-loader-3.6.0.tgz",
@@ -5809,6 +5790,12 @@
"domelementtype": "1"
}
},
"dompurify": {
"version": "2.3.3",
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-2.3.3.tgz",
"integrity": "sha512-dqnqRkPMAjOZE0FogZ+ceJNM2dZ3V/yNOuFB7+39qpO93hHhfRpHw3heYQC7DPK9FqbQTfBKUJhiSfz4MvXYwg==",
"optional": true
},
"domutils": {
"version": "1.7.0",
"resolved": "https://registry.npmjs.org/domutils/-/domutils-1.7.0.tgz",
@@ -6743,6 +6730,11 @@
"websocket-driver": ">=0.5.1"
}
},
"fflate": {
"version": "0.4.8",
"resolved": "https://registry.npmjs.org/fflate/-/fflate-0.4.8.tgz",
"integrity": "sha512-FJqqoDBR00Mdj9ppamLa/Y7vxm+PRmNWA67N846RvsoYVMKB4q3y/de5PA7gUmRMYK/8CMz2GDZQmCRN1wBcWA=="
},
"figgy-pudding": {
"version": "3.5.1",
"resolved": "https://registry.npmjs.org/figgy-pudding/-/figgy-pudding-3.5.1.tgz",
@@ -7440,6 +7432,16 @@
}
}
},
"html2canvas": {
"version": "1.3.2",
"resolved": "https://registry.npmjs.org/html2canvas/-/html2canvas-1.3.2.tgz",
"integrity": "sha512-4+zqv87/a1LsaCrINV69wVLGG8GBZcYBboz1JPWEgiXcWoD9kroLzccsBRU/L9UlfV2MAZ+3J92U9IQPVMDeSQ==",
"optional": true,
"requires": {
"css-line-break": "2.0.1",
"text-segmentation": "^1.0.2"
}
},
"htmlparser2": {
"version": "3.10.1",
"resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-3.10.1.tgz",
@@ -8369,6 +8371,42 @@
"graceful-fs": "^4.1.6"
}
},
"jspdf": {
"version": "2.4.0",
"resolved": "https://registry.npmjs.org/jspdf/-/jspdf-2.4.0.tgz",
"integrity": "sha512-nsZ92YfbNG/EimR1yqmOkxf2D4iJRypBsw7pvP1aPiIEnoGITaLl6XDR/GYA36/R29vMZSBedpEhBCzutSGytA==",
"requires": {
"@babel/runtime": "^7.14.0",
"atob": "^2.1.2",
"btoa": "^1.2.1",
"canvg": "^3.0.6",
"core-js": "^3.6.0",
"dompurify": "^2.2.0",
"fflate": "^0.4.8",
"html2canvas": "^1.0.0-rc.5"
},
"dependencies": {
"@babel/runtime": {
"version": "7.16.0",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.16.0.tgz",
"integrity": "sha512-Nht8L0O8YCktmsDV6FqFue7vQLRx3Hb0B37lS5y0jDRqRxlBG4wIJHnf9/bgSE2UyipKFA01YtS+npRdTWBUyw==",
"requires": {
"regenerator-runtime": "^0.13.4"
}
},
"core-js": {
"version": "3.19.0",
"resolved": "https://registry.npmjs.org/core-js/-/core-js-3.19.0.tgz",
"integrity": "sha512-L1TpFRWXZ76vH1yLM+z6KssLZrP8Z6GxxW4auoCj+XiViOzNPJCAuTIkn03BGdFe6Z5clX5t64wRIRypsZQrUg==",
"optional": true
},
"regenerator-runtime": {
"version": "0.13.9",
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.9.tgz",
"integrity": "sha512-p3VT+cOEgxFsRRA9X4lkI1E+k2/CtnKtU4gcxyaCUreilL/vqI6CdZ3wxVUx3UOUg+gnUOQQcRI7BmSI656MYA=="
}
}
},
"jsprim": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz",
@@ -9658,8 +9696,7 @@
"performance-now": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz",
"integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=",
"dev": true
"integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns="
},
"picomatch": {
"version": "2.2.2",
@@ -10546,6 +10583,15 @@
"integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==",
"dev": true
},
"raf": {
"version": "3.4.1",
"resolved": "https://registry.npmjs.org/raf/-/raf-3.4.1.tgz",
"integrity": "sha512-Sq4CW4QhwOHE8ucn6J34MqtZCeWFP2aQSmrlroYgqAV1PjStIhJXxYuTgUIfkEk7zTLjmIjLmU5q+fbD1NnOJA==",
"optional": true,
"requires": {
"performance-now": "^2.1.0"
}
},
"randombytes": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz",
@@ -10923,6 +10969,12 @@
"integrity": "sha1-QzdOLiyglosO8VI0YLfXMP8i7rM=",
"dev": true
},
"rgbcolor": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/rgbcolor/-/rgbcolor-1.0.1.tgz",
"integrity": "sha1-1lBezbMEplldom+ktDMHMGd1lF0=",
"optional": true
},
"rimraf": {
"version": "2.7.1",
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz",
@@ -11648,6 +11700,12 @@
"integrity": "sha512-ji9qxRnOVfcuLDySj9qzhGSEFVobyt1kIOSkj1qZzYLzq7Tos/oUUWvotUPQLlrsidqsK6tBH89Bc9kL5zHA6w==",
"dev": true
},
"stackblur-canvas": {
"version": "2.5.0",
"resolved": "https://registry.npmjs.org/stackblur-canvas/-/stackblur-canvas-2.5.0.tgz",
"integrity": "sha512-EeNzTVfj+1In7aSLPKDD03F/ly4RxEuF/EX0YcOG0cKoPXs+SLZxDawQbexQDBzwROs4VKLWTOaZQlZkGBFEIQ==",
"optional": true
},
"stackframe": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/stackframe/-/stackframe-1.2.0.tgz",
@@ -11955,6 +12013,12 @@
"has-flag": "^3.0.0"
}
},
"svg-pathdata": {
"version": "6.0.3",
"resolved": "https://registry.npmjs.org/svg-pathdata/-/svg-pathdata-6.0.3.tgz",
"integrity": "sha512-qsjeeq5YjBZ5eMdFuUa4ZosMLxgr5RZ+F+Y1OrDhuOCEInRMA3x74XdBtggJcj9kOeInz0WE+LgCPDkZFlBYJw==",
"optional": true
},
"svg-tags": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/svg-tags/-/svg-tags-1.0.0.tgz",
@@ -12041,6 +12105,15 @@
}
}
},
"text-segmentation": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/text-segmentation/-/text-segmentation-1.0.2.tgz",
"integrity": "sha512-uTqvLxdBrVnx/CFQOtnf8tfzSXFm+1Qxau7Xi54j4OPTZokuDOX8qncQzrg2G8ZicAMOM8TgzFAYTb+AqNO4Cw==",
"optional": true,
"requires": {
"utrie": "^1.0.1"
}
},
"text-table": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz",
@@ -12613,6 +12686,23 @@
"integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=",
"dev": true
},
"utrie": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/utrie/-/utrie-1.0.1.tgz",
"integrity": "sha512-JPaDXF3vzgZxfeEwutdGzlrNoVFL5UvZcbO6Qo9D4GoahrieUPoMU8GCpVpR7MQqcKhmShIh8VlbEN3PLM3EBg==",
"optional": true,
"requires": {
"base64-arraybuffer": "^1.0.1"
},
"dependencies": {
"base64-arraybuffer": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-1.0.1.tgz",
"integrity": "sha512-vFIUq7FdLtjZMhATwDul5RZWv2jpXQ09Pd6jcVEOvIsqCWTRFD/ONHNfyOS8dA/Ippi5dsIgpyKWKZaAKZltbA==",
"optional": true
}
}
},
"uuid": {
"version": "3.3.3",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.3.tgz",
@@ -12701,6 +12791,94 @@
}
}
},
"vue-loader-v16": {
"version": "npm:vue-loader@16.8.3",
"resolved": "https://registry.npmjs.org/vue-loader/-/vue-loader-16.8.3.tgz",
"integrity": "sha512-7vKN45IxsKxe5GcVCbc2qFU5aWzyiLrYJyUuMz4BQLKctCj/fmCa0w6fGiiQ2cLFetNcek1ppGJQDCup0c1hpA==",
"dev": true,
"requires": {
"chalk": "^4.1.0",
"hash-sum": "^2.0.0",
"loader-utils": "^2.0.0"
},
"dependencies": {
"ansi-styles": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
"dev": true,
"requires": {
"color-convert": "^2.0.1"
}
},
"chalk": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
"integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
"dev": true,
"requires": {
"ansi-styles": "^4.1.0",
"supports-color": "^7.1.0"
}
},
"color-convert": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
"dev": true,
"requires": {
"color-name": "~1.1.4"
}
},
"color-name": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
"dev": true
},
"emojis-list": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-3.0.0.tgz",
"integrity": "sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==",
"dev": true
},
"has-flag": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
"integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
"dev": true
},
"json5": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/json5/-/json5-2.2.0.tgz",
"integrity": "sha512-f+8cldu7X/y7RAJurMEJmdoKXGB/X550w2Nr3tTbezL6RwEE/iMcm+tZnXeoZtKuOq6ft8+CqzEkrIgx1fPoQA==",
"dev": true,
"requires": {
"minimist": "^1.2.5"
}
},
"loader-utils": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.2.tgz",
"integrity": "sha512-TM57VeHptv569d/GKh6TAYdzKblwDNiumOdkFnejjD0XwTH87K90w3O7AiJRqdQoXygvi1VQTJTLGhJl7WqA7A==",
"dev": true,
"requires": {
"big.js": "^5.2.2",
"emojis-list": "^3.0.0",
"json5": "^2.1.2"
}
},
"supports-color": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
"integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
"dev": true,
"requires": {
"has-flag": "^4.0.0"
}
}
}
},
"vue-native-websocket": {
"version": "git+https://github.com/PhotonVision/vue-native-websocket.git#7a327918e03b215b6899b0d648c5130ece1fa912",
"from": "git+https://github.com/PhotonVision/vue-native-websocket.git#7a32791",
@@ -12749,7 +12927,7 @@
"dependencies": {
"acorn": {
"version": "3.3.0",
"resolved": "http://registry.npmjs.org/acorn/-/acorn-3.3.0.tgz",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-3.3.0.tgz",
"integrity": "sha1-ReN/s56No/JbruP/U2niu18iAXo="
}
}
@@ -13653,7 +13831,7 @@
},
"browserify-aes": {
"version": "1.2.0",
"resolved": "http://registry.npmjs.org/browserify-aes/-/browserify-aes-1.2.0.tgz",
"resolved": "https://registry.npmjs.org/browserify-aes/-/browserify-aes-1.2.0.tgz",
"integrity": "sha512-+7CHXqGuspUn/Sl5aO7Ea0xWGAtETPXNSAjHo48JfLdPWcMng33Xe4znFvQweqc/uzk5zSOI3H52CYnjCfb5hA==",
"requires": {
"buffer-xor": "^1.0.3",
@@ -13687,7 +13865,7 @@
},
"browserify-rsa": {
"version": "4.0.1",
"resolved": "http://registry.npmjs.org/browserify-rsa/-/browserify-rsa-4.0.1.tgz",
"resolved": "https://registry.npmjs.org/browserify-rsa/-/browserify-rsa-4.0.1.tgz",
"integrity": "sha1-IeCr+vbyApzy+vsTNWenAdQTVSQ=",
"requires": {
"bn.js": "^4.1.0",
@@ -13718,7 +13896,7 @@
},
"buffer": {
"version": "4.9.1",
"resolved": "http://registry.npmjs.org/buffer/-/buffer-4.9.1.tgz",
"resolved": "https://registry.npmjs.org/buffer/-/buffer-4.9.1.tgz",
"integrity": "sha1-bRu2AbB6TvztlwlBMgkwJ8lbwpg=",
"requires": {
"base64-js": "^1.0.2",
@@ -13854,7 +14032,7 @@
},
"chalk": {
"version": "1.1.3",
"resolved": "http://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz",
"integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=",
"requires": {
"ansi-styles": "^2.2.1",
@@ -14101,7 +14279,7 @@
},
"create-hash": {
"version": "1.2.0",
"resolved": "http://registry.npmjs.org/create-hash/-/create-hash-1.2.0.tgz",
"resolved": "https://registry.npmjs.org/create-hash/-/create-hash-1.2.0.tgz",
"integrity": "sha512-z00bCGNHDG8mHAkP7CtT1qVu+bFQUPjYq/4Iv3C3kWjTFV10zIjfSoeqXo9Asws8gwSHDGj/hl2u4OGIjapeCg==",
"requires": {
"cipher-base": "^1.0.1",
@@ -14113,7 +14291,7 @@
},
"create-hmac": {
"version": "1.1.7",
"resolved": "http://registry.npmjs.org/create-hmac/-/create-hmac-1.1.7.tgz",
"resolved": "https://registry.npmjs.org/create-hmac/-/create-hmac-1.1.7.tgz",
"integrity": "sha512-MJG9liiZ+ogc4TzUwuvbER1JRdgvUFSB5+VR/g5h82fGaIRWMWddtKBHi7/sVhfjQZ6SehlyhvQYrcYkaUIpLg==",
"requires": {
"cipher-base": "^1.0.3",
@@ -14320,7 +14498,7 @@
},
"diffie-hellman": {
"version": "5.0.3",
"resolved": "http://registry.npmjs.org/diffie-hellman/-/diffie-hellman-5.0.3.tgz",
"resolved": "https://registry.npmjs.org/diffie-hellman/-/diffie-hellman-5.0.3.tgz",
"integrity": "sha512-kqag/Nl+f3GwyK25fhUMYj81BUOrZ9IuJsjIcDE5icNM9FJHAVm3VcUDxdLPoQtTuUylWm6ZIknYJwwaPxsUzg==",
"requires": {
"bn.js": "^4.1.0",
@@ -14396,7 +14574,7 @@
"dependencies": {
"debug": {
"version": "2.3.3",
"resolved": "http://registry.npmjs.org/debug/-/debug-2.3.3.tgz",
"resolved": "https://registry.npmjs.org/debug/-/debug-2.3.3.tgz",
"integrity": "sha1-QMRT5n5uE8kB3ewxeviYbNqe/4w=",
"requires": {
"ms": "0.7.2"
@@ -14435,7 +14613,7 @@
},
"debug": {
"version": "2.3.3",
"resolved": "http://registry.npmjs.org/debug/-/debug-2.3.3.tgz",
"resolved": "https://registry.npmjs.org/debug/-/debug-2.3.3.tgz",
"integrity": "sha1-QMRT5n5uE8kB3ewxeviYbNqe/4w=",
"requires": {
"ms": "0.7.2"
@@ -15886,7 +16064,7 @@
},
"http-errors": {
"version": "1.6.3",
"resolved": "http://registry.npmjs.org/http-errors/-/http-errors-1.6.3.tgz",
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.3.tgz",
"integrity": "sha1-i1VoC7S+KDoLW/TqLjhYC+HZMg0=",
"requires": {
"depd": "~1.1.2",
@@ -16372,7 +16550,7 @@
"dependencies": {
"lodash": {
"version": "3.10.1",
"resolved": "http://registry.npmjs.org/lodash/-/lodash-3.10.1.tgz",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-3.10.1.tgz",
"integrity": "sha1-W/Rejkm6QYnhfUgnid/RW9FAt7Y="
}
}
@@ -16408,7 +16586,7 @@
"dependencies": {
"minimist": {
"version": "1.2.0",
"resolved": "http://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz",
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz",
"integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ="
}
}
@@ -16617,7 +16795,7 @@
},
"readable-stream": {
"version": "1.0.34",
"resolved": "http://registry.npmjs.org/readable-stream/-/readable-stream-1.0.34.tgz",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.0.34.tgz",
"integrity": "sha1-Elgg40vIQtLyqq+v5MKRbuMsFXw=",
"requires": {
"core-util-is": "~1.0.0",
@@ -16776,7 +16954,7 @@
},
"minimist": {
"version": "1.2.0",
"resolved": "http://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz",
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz",
"integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ="
},
"path-exists": {
@@ -16903,7 +17081,7 @@
},
"minimist": {
"version": "0.0.8",
"resolved": "http://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz",
"resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz",
"integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0="
},
"mixin-deep": {
@@ -16927,7 +17105,7 @@
},
"mkdirp": {
"version": "0.5.1",
"resolved": "http://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz",
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz",
"integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=",
"requires": {
"minimist": "0.0.8"
@@ -16954,7 +17132,7 @@
"dependencies": {
"commander": {
"version": "2.9.0",
"resolved": "http://registry.npmjs.org/commander/-/commander-2.9.0.tgz",
"resolved": "https://registry.npmjs.org/commander/-/commander-2.9.0.tgz",
"integrity": "sha1-nJkJQXbhIkDLItbFFGCYQA/g99Q=",
"requires": {
"graceful-readlink": ">= 1.0.0"
@@ -17241,7 +17419,7 @@
},
"onetime": {
"version": "1.1.0",
"resolved": "http://registry.npmjs.org/onetime/-/onetime-1.1.0.tgz",
"resolved": "https://registry.npmjs.org/onetime/-/onetime-1.1.0.tgz",
"integrity": "sha1-ofeDj4MUxRbwXs78vEzP4EtO14k="
},
"optimist": {
@@ -17346,7 +17524,7 @@
},
"parse-asn1": {
"version": "5.1.1",
"resolved": "http://registry.npmjs.org/parse-asn1/-/parse-asn1-5.1.1.tgz",
"resolved": "https://registry.npmjs.org/parse-asn1/-/parse-asn1-5.1.1.tgz",
"integrity": "sha512-KPx7flKXg775zZpnp9SxJlz00gTd4BmJ2yJufSc44gMCRrRQ7NSzAcSJQfifuOLgW6bEi+ftrALtsgALeB2Adw==",
"requires": {
"asn1.js": "^4.0.0",
@@ -17569,7 +17747,7 @@
},
"public-encrypt": {
"version": "4.0.2",
"resolved": "http://registry.npmjs.org/public-encrypt/-/public-encrypt-4.0.2.tgz",
"resolved": "https://registry.npmjs.org/public-encrypt/-/public-encrypt-4.0.2.tgz",
"integrity": "sha512-4kJ5Esocg8X3h8YgJsKAuoesBgB7mqH3eowiDzMUPKiRDDE7E/BqqZD1hnTByIaAFiwAw246YEltSq7tdrOH0Q==",
"requires": {
"bn.js": "^4.1.0",
@@ -17685,7 +17863,7 @@
},
"readable-stream": {
"version": "2.3.6",
"resolved": "http://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz",
"integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==",
"requires": {
"core-util-is": "~1.0.0",
@@ -17998,7 +18176,7 @@
},
"sha.js": {
"version": "2.4.11",
"resolved": "http://registry.npmjs.org/sha.js/-/sha.js-2.4.11.tgz",
"resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.11.tgz",
"integrity": "sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ==",
"requires": {
"inherits": "^2.0.1",
@@ -18176,7 +18354,7 @@
"dependencies": {
"debug": {
"version": "2.3.3",
"resolved": "http://registry.npmjs.org/debug/-/debug-2.3.3.tgz",
"resolved": "https://registry.npmjs.org/debug/-/debug-2.3.3.tgz",
"integrity": "sha1-QMRT5n5uE8kB3ewxeviYbNqe/4w=",
"requires": {
"ms": "0.7.2"
@@ -18205,7 +18383,7 @@
"dependencies": {
"debug": {
"version": "2.3.3",
"resolved": "http://registry.npmjs.org/debug/-/debug-2.3.3.tgz",
"resolved": "https://registry.npmjs.org/debug/-/debug-2.3.3.tgz",
"integrity": "sha1-QMRT5n5uE8kB3ewxeviYbNqe/4w=",
"requires": {
"ms": "0.7.2"
@@ -18243,7 +18421,7 @@
},
"debug": {
"version": "2.3.3",
"resolved": "http://registry.npmjs.org/debug/-/debug-2.3.3.tgz",
"resolved": "https://registry.npmjs.org/debug/-/debug-2.3.3.tgz",
"integrity": "sha1-QMRT5n5uE8kB3ewxeviYbNqe/4w=",
"requires": {
"ms": "0.7.2"
@@ -18269,7 +18447,7 @@
"dependencies": {
"debug": {
"version": "2.2.0",
"resolved": "http://registry.npmjs.org/debug/-/debug-2.2.0.tgz",
"resolved": "https://registry.npmjs.org/debug/-/debug-2.2.0.tgz",
"integrity": "sha1-+HBX6ZWxofauaklgZkE3vFbwOdo=",
"requires": {
"ms": "0.7.1"
@@ -18464,7 +18642,7 @@
},
"table": {
"version": "3.8.3",
"resolved": "http://registry.npmjs.org/table/-/table-3.8.3.tgz",
"resolved": "https://registry.npmjs.org/table/-/table-3.8.3.tgz",
"integrity": "sha1-K7xULw/amGGnVdOUf+/Ys/UThV8=",
"requires": {
"ajv": "^4.7.0",
@@ -18521,7 +18699,7 @@
},
"through": {
"version": "2.3.8",
"resolved": "http://registry.npmjs.org/through/-/through-2.3.8.tgz",
"resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz",
"integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU="
},
"time-stamp": {
@@ -18697,7 +18875,7 @@
},
"yargs": {
"version": "3.10.0",
"resolved": "http://registry.npmjs.org/yargs/-/yargs-3.10.0.tgz",
"resolved": "https://registry.npmjs.org/yargs/-/yargs-3.10.0.tgz",
"integrity": "sha1-9+572FfdfB0tOMDnTvvWgdFDH9E=",
"requires": {
"camelcase": "^1.0.2",
@@ -19339,7 +19517,7 @@
},
"wrap-ansi": {
"version": "2.1.0",
"resolved": "http://registry.npmjs.org/wrap-ansi/-/wrap-ansi-2.1.0.tgz",
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-2.1.0.tgz",
"integrity": "sha1-2Pw9KE3QV5T+hJc8rs3Rz4JP3YU=",
"requires": {
"string-width": "^1.0.1",
@@ -20250,9 +20428,9 @@
"dev": true
},
"y18n": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.0.tgz",
"integrity": "sha512-r9S/ZyXu/Xu9q1tYlpsLIsa3EeLXXk0VwlxqTcFRfg9EhMW+17kbt9G0NrgCmhGb5vT2hyhJZLfDGx+7+5Uj/w==",
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz",
"integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==",
"dev": true
},
"yallist": {

View File

@@ -13,6 +13,7 @@
"axios": "^0.19.2",
"core-js": "^2.6.11",
"downloadjs": "^1.4.7",
"jspdf": "^2.4.0",
"material-design-icons-iconfont": "^5.0.1",
"msgpack5": "^4.2.1",
"vue": "^2.6.12",

View File

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

View File

@@ -38,9 +38,9 @@
</v-list-item-content>
</v-list-item>
<v-list-item
ref="camerasTabOpener"
link
to="cameras"
ref="camerasTabOpener"
@click="switchToDriverMode()"
>
<v-list-item-icon>
@@ -53,6 +53,7 @@
<v-list-item
link
to="settings"
@click="switchToSettingsTab()"
>
<v-list-item-icon>
<v-icon>mdi-settings</v-icon>
@@ -86,29 +87,79 @@
</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>
<v-list-item style="position: absolute; bottom: 0; left: 0;">
<v-list-item-icon>
<v-icon v-if="$store.state.backendConnected">
mdi-wifi
</v-icon>
<v-icon
v-else
class="pulse"
style="border-radius: 100%;"
>
mdi-wifi-off
</v-icon>
</v-list-item-icon>
<v-list-item-content>
<v-list-item-title>
{{ $store.state.backendConnected ? "Connected" : "Trying to connect..." }}
</v-list-item-title>
</v-list-item-content>
</v-list-item>
<div style="position: absolute; bottom: 0; left: 0;">
<v-list-item>
<v-list-item-icon>
<v-icon v-if="$store.state.settings.networkSettings.runNTServer">
mdi-server
</v-icon>
<img
v-else-if="$store.state.ntConnectionInfo.connected"
src="@/assets/robot.svg"
alt=""
>
<img
v-else
class="pulse"
style="border-radius: 100%"
src="@/assets/robot-off.svg"
alt=""
>
</v-list-item-icon>
<v-list-item-content>
<v-list-item-title
v-if="$store.state.settings.networkSettings.runNTServer"
class="text-wrap"
>
NetworkTables server running for {{ $store.state.ntConnectionInfo.clients ? $store.state.ntConnectionInfo.clients : 'zero' }} clients!
</v-list-item-title>
<v-list-item-title
v-else-if="$store.state.ntConnectionInfo.connected && $store.state.backendConnected"
class="text-wrap"
>
Robot connected! {{ $store.state.ntConnectionInfo.address }}
</v-list-item-title>
<v-list-item-title
v-else
class="text-wrap"
>
Not connected to robot!
</v-list-item-title>
<router-link
v-if="!$store.state.settings.networkSettings.runNTServer"
to="settings"
class="accent--text"
@click="switchToSettingsTab"
>
Team number is {{ $store.state.settings.networkSettings.teamNumber }}
</router-link>
</v-list-item-content>
</v-list-item>
<v-list-item>
<v-list-item-icon>
<v-icon v-if="$store.state.backendConnected">
mdi-wifi
</v-icon>
<v-icon
v-else
class="pulse"
style="border-radius: 100%;"
>
mdi-wifi-off
</v-icon>
</v-list-item-icon>
<v-list-item-content>
<v-list-item-title class="text-wrap">
{{ $store.state.backendConnected ? "Backend Connected" : "Trying to connect..." }}
</v-list-item-title>
</v-list-item-content>
</v-list-item>
</div>
</v-list>
</v-navigation-drawer>
<v-main>
@@ -118,7 +169,7 @@
>
<v-layout>
<v-flex>
<router-view v-on:switch-to-cameras="switchToDriverMode" />
<router-view @switch-to-cameras="switchToDriverMode" />
</v-flex>
</v-layout>
</v-container>
@@ -131,23 +182,57 @@
>
<logs />
</v-dialog>
<v-dialog
v-model="needsTeamNumberSet"
width="500"
dark
persistent
>
<v-card
dark
color="primary"
flat
>
<v-card-title>No team number set!</v-card-title>
<v-card-text>
PhotonVision cannot connect to your robot! Please
<router-link
to="settings"
class="accent--text"
@click="switchToSettingsTab"
>
vist the settings tab
</router-link>
and set your team number.
</v-card-text>
</v-card>
</v-dialog>
</v-app>
</template>
<script>
import Logs from "./views/LogsView"
// import {mapState} from "vuex";
export default {
export default {
name: 'App',
components: {
Logs
},
data: () => ({
// Used so that we can switch back to the previously selected pipeline after camera calibration
previouslySelectedIndex: undefined,
previouslySelectedIndices: [],
timer: undefined,
teamNumberDialog: true
}),
computed: {
needsTeamNumberSet: {
get() {
return this.$store.state.settings.networkSettings.teamNumber < 1
&& this.teamNumberDialog && this.$store.state.backendConnected
&& !this.$route.name.toLowerCase().includes("settings");
}
},
compact: {
get() {
if (this.$store.state.compactMode === undefined) {
@@ -196,11 +281,12 @@ import Logs from "./views/LogsView"
}
};
this.$options.sockets.onopen = () => {
this.$store.state.backendConnected = true;
this.$store.commit("backendConnected", true)
this.$store.state.connectedCallbacks.forEach(it => it())
};
let closed = () => {
this.$store.state.backendConnected = false;
this.$store.commit("backendConnected", false)
};
this.$options.sockets.onclose = closed;
this.$options.sockets.onerror = closed;
@@ -211,6 +297,8 @@ import Logs from "./views/LogsView"
handleMessage(key, value) {
if (key === "logMessage") {
this.logMessage(value["logMessage"], value["logLevel"]);
} else if(key === "log"){
this.logMessage(value["logMessage"]["logMessage"], value["logMessage"]["logLevel"]);
} else if (key === "updatePipelineResult") {
this.$store.commit('mutatePipelineResults', value)
} else if (this.$store.state.hasOwnProperty(key)) {
@@ -238,14 +326,24 @@ import Logs from "./views/LogsView"
})
},
switchToDriverMode() {
this.previouslySelectedIndex = this.$store.getters.currentPipelineIndex;
this.handleInputWithIndex('currentPipeline', -1)
},
rollbackPipelineIndex() {
if (this.previouslySelectedIndex !== null) {
this.handleInputWithIndex('currentPipeline', this.previouslySelectedIndex || 0);
if (!this.previouslySelectedIndices) this.previouslySelectedIndices = [];
for (const [i, cameraSettings] of this.$store.state.cameraSettings.entries()) {
this.previouslySelectedIndices[i] = cameraSettings.currentPipelineIndex;
this.handleInputWithIndex('currentPipeline', -1, i);
}
this.previouslySelectedIndex = null;
},
rollbackPipelineIndex()
{
if (this.previouslySelectedIndices !== null) {
for (const [i] of this.$store.state.cameraSettings.entries()) {
this.handleInputWithIndex('currentPipeline', this.previouslySelectedIndices[i] || 0, i);
}
}
this.previouslySelectedIndices = null;
},
switchToSettingsTab() {
this.axios.post('http://' + this.$address + '/api/sendMetrics', {})
}
}
};
@@ -328,4 +426,4 @@ import Logs from "./views/LogsView"
font-size: 14px !important;
}
}
</style>
</style>

View File

@@ -1,52 +0,0 @@
{
"2020 Hex Goal": {
"realWorldCoordinatesArray": [
{
"x": -0.49847498536109924,
"y": 0.0,
"z": 0.0
},
{
"x": -0.24942462146282196,
"y": -0.4318000078201294,
"z": 0.0
},
{
"x": 0.24942462146282196,
"y": -0.4318000078201294,
"z": 0.0
},
{
"x": 0.49847498536109924,
"y": 0.0,
"z": 0.0
}
],
"boxHeight": 0.30479999999999996
},
"2019 Dual Target": {
"realWorldCoordinatesArray": [
{
"x": -0.15077440440654755,
"y": 0.06761480122804642,
"z": 0.0
},
{
"x": -0.18575020134449005,
"y": -0.06761480122804642,
"z": 0.0
},
{
"x": 0.18575020134449005,
"y": -0.06761480122804642,
"z": 0.0
},
{
"x": 0.15077440440654755,
"y": 0.06761480122804642,
"z": 0.0
}
],
"boxHeight": 0.1
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

View File

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

After

Width:  |  Height:  |  Size: 928 B

View File

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

After

Width:  |  Height:  |  Size: 827 B

View File

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

View File

@@ -13,7 +13,12 @@
export default {
name: "CvImage",
// eslint-disable-next-line vue/require-prop-types
props: ['address', 'scale', 'maxHeight', 'maxHeightMd', 'maxHeightXl', 'colorPicking', 'id', 'disconnected'],
props: ['address', 'scale', 'maxHeight', 'maxHeightMd', 'maxHeightLg', 'maxHeightXl', 'colorPicking', 'id', 'disconnected'],
data() {
return {
seed: 1.0,
}
},
computed: {
styleObject: {
get() {
@@ -32,7 +37,9 @@
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;
}
@@ -41,9 +48,17 @@
},
src: {
get() {
return this.disconnected ? require("../../assets/noStream.jpg") : this.address;
return this.disconnected ? require("../../assets/noStream.jpg") : this.address + "?" + this.seed // This prevents caching
},
},
},
mounted() {
this.reload(); // Force reload image on creation
},
methods: {
reload() {
this.seed = new Date().getTime();
}
},
}
</script>
</script>

View File

@@ -61,4 +61,4 @@ s
</script>
<style lang="css" scoped>
</style>
</style>

View File

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

View File

@@ -1,27 +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"
/>
</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'],
props: ['name', 'value', 'list', 'disabled', 'inputCols', 'tooltip'],
data() {
return {}
},
@@ -40,4 +59,4 @@
<style lang="" scoped>
</style>
</style>

View File

@@ -15,10 +15,13 @@
:value="localValue"
:max="max"
:min="min"
:disabled="disabled"
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)"
@@ -34,7 +37,7 @@
hide-details
single-line
type="number"
style="width: 50px"
style="width: 60px"
:step="step"
@input="handleChange"
@focus="prependFocused = true"
@@ -53,7 +56,7 @@
hide-details
single-line
type="number"
style="width: 50px"
style="width: 60px"
:step="step"
@input="handleChange"
@focus="appendFocused = true"
@@ -75,7 +78,7 @@ export default {
TooltippedLabel,
},
// eslint-disable-next-line vue/require-prop-types
props: ["name", "min", "max", "value", "step", "tooltip"],
props: ["name", "min", "max", "value", "step", "tooltip", "disabled", "inverted"],
data() {
return {
prependFocused: false,
@@ -86,7 +89,7 @@ export default {
computed: {
localValue: {
get() {
return Object.values(this.value);
return Object.values(this.value || [0, 0]);
},
set(value) {
this.$emit("input", value);
@@ -128,4 +131,4 @@ export default {
</script>
<style lang="" scoped>
</style>
</style>

View File

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

View File

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

View File

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

View File

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

View File

@@ -151,4 +151,4 @@
width: 80px;
text-align: center;
}
</style>
</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"
@@ -34,7 +38,7 @@
:hover="true"
text="edit"
tooltip="Edit camera name"
@click="toCameraNameChange"
@click="changeCameraName"
/>
<div v-else>
<CVicon
@@ -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"
@@ -76,9 +81,9 @@
lg="2"
>
<v-menu
v-if="!$store.getters.isDriverMode"
offset-y
auto
v-if="!$store.getters.isDriverMode"
>
<template v-slot:activator="{ on }">
<v-icon
@@ -88,7 +93,11 @@
menu
</v-icon>
</template>
<v-list dense>
<v-list
dark
dense
color="primary"
>
<v-list-item @click="toPipelineNameChange">
<v-list-item-title>
<CVicon
@@ -119,7 +128,7 @@
/>
</v-list-item-title>
</v-list-item>
<v-list-item @click="openDuplicateDialog">
<v-list-item @click="duplicatePipeline">
<v-list-item-title>
<CVicon
color="#c5c5c5"
@@ -132,66 +141,47 @@
</v-list>
</v-menu>
</v-col>
<v-col
v-if="currentPipelineType >= 0"
cols="10"
md="11"
lg="10"
no-gutters
class="pa-0"
>
<CVselect
v-model="currentPipelineType"
name="Type"
tooltip="Changes the pipeline type, which changes the type of processing that will happen on input frames"
:list="['Reflective Tape', 'Colored Shape']"
@input="e => showTypeDialog(e)"
/>
</v-col>
</v-row>
<!--pipeline duplicate dialog-->
<v-dialog
v-model="duplicateDialog"
dark
width="500"
height="357"
>
<v-card dark>
<v-card-title
class="headline"
primary-title
>
Duplicate Pipeline
</v-card-title>
<v-card-text>
<CVselect
v-model="pipeIndexToDuplicate"
name="Pipeline"
:list="$store.getters.pipelineList"
/>
</v-card-text>
<v-divider />
<v-card-actions>
<v-spacer />
<v-btn
color="#ffd843"
@click="duplicatePipeline"
>
Duplicate
</v-btn>
<v-btn
color="error"
@click="closeDuplicateDialog"
>
Cancel
</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
<!--pipeline naming dialog-->
<v-dialog
v-model="namingDialog"
dark
persistent
width="500"
height="357"
>
<v-card dark>
<v-card
dark
color="primary"
>
<v-card-title
class="headline"
primary-title
>
Pipeline Name
{{ isPipelineNameEdit ? "Edit Pipeline Name" : "Create Pipeline" }}
</v-card-title>
<v-card-text>
<CVinput
v-model="newPipelineName"
name="Pipeline"
name="Name"
:error-message="checkPipelineName"
@Enter="savePipelineNameChange"
/>
</v-card-text>
<v-divider />
@@ -213,153 +203,206 @@
</v-card-actions>
</v-card>
</v-dialog>
<v-dialog
v-model="showPipeTypeDialog"
width="600"
>
<v-card
color="primary"
dark
>
<v-card-title>Change Pipeline Type</v-card-title>
<v-card-text>
Changing the type of this pipeline will erase the current pipeline's settings and replace it with a new {{ ['Reflective', 'Shape'][proposedPipelineType] }} pipeline. <b class="red--text format_bold">You will lose all settings for the pipeline
"{{ ($store.getters.isDriverMode ? ['Driver Mode'] : []).concat($store.getters.pipelineList)[currentPipelineIndex] }}."</b> Are you sure you want to do this?
<v-row
class="mt-6"
style="display: flex; align-items: center; justify-content: center"
align="center"
>
<v-btn
class="mr-3"
color="red"
width="250"
@click="e => changePipeType(true)"
>
Yes, replace this pipeline
</v-btn>
<v-btn
class="ml-10"
color="secondary"
width="250"
@click="e => changePipeType(false)"
>
No, take me back
</v-btn>
</v-row>
</v-card-text>
</v-card>
</v-dialog>
</div>
</template>
<script>
import CVicon from '../common/cv-icon'
import CVselect from '../common/cv-select'
import CVinput from '../common/cv-input'
export default {
name: "CameraAndPipelineSelect",
components: {
CVicon,
CVselect,
CVinput
},
data: () => {
return {
re: RegExp("^[A-Za-z0-9 \\-)(]*[A-Za-z0-9][A-Za-z0-9 \\-)(.]*$"),
isCameraNameEdit: false,
newCameraName: "",
cameraNameError: "",
isPipelineNameEdit: false,
namingDialog: false,
newPipelineName: "",
duplicateDialog: false,
pipeIndexToDuplicate: undefined
}
},
computed: {
checkCameraName() {
if (this.newCameraName !== this.$store.getters.cameraList[this.currentCameraIndex]) {
if (this.re.test(this.newCameraName)) {
for (let cam in this.cameraList) {
if (this.cameraList.hasOwnProperty(cam)) {
if (this.newCameraName === this.cameraList[cam]) {
return "A camera by that name already Exists"
}
}
}
} else {
return "A camera name can only contain letters, numbers and spaces"
}
}
return "";
},
checkPipelineName() {
if (this.newPipelineName !== this.$store.getters.pipelineList[this.currentPipelineIndex - 1] || this.isPipelineNameEdit === false) {
if (this.re.test(this.newPipelineName)) {
for (let pipe in this.$store.getters.pipelineList) {
if (this.$store.getters.pipelineList.hasOwnProperty(pipe)) {
if (this.newPipelineName === this.$store.getters.pipelineList[pipe]) {
return "A pipeline with this name already exists"
}
}
}
} else {
return "A pipeline name can only contain letters, numbers, and spaces"
}
}
return ""
},
currentCameraIndex: {
get() {
return this.$store.getters.currentCameraIndex;
},
set(value) {
this.$store.commit('currentCameraIndex', value);
}
},
currentPipelineIndex: {
get() {
return this.$store.getters.currentPipelineIndex + (this.$store.getters.isDriverMode ? 1 : 0);
},
set(value) {
this.$store.commit('currentPipelineIndex', value - (this.$store.getters.isDriverMode ? 1 : 0));
}
}
},
methods: {
toCameraNameChange() {
this.newCameraName = this.$store.getters.cameraList[this.currentCameraIndex];
this.isCameraNameEdit = true;
},
saveCameraNameChange() {
if (this.checkCameraName === "") {
this.handleInputWithIndex("changeCameraName", this.newCameraName);
this.discardCameraNameChange();
}
},
discardCameraNameChange() {
this.isCameraNameEdit = false;
this.newCameraName = "";
},
toPipelineNameChange() {
this.newPipelineName = this.$store.getters.pipelineList[this.currentPipelineIndex - 1];
this.isPipelineNameEdit = true;
this.namingDialog = true;
},
toCreatePipeline() {
this.newPipelineName = "New Pipeline";
this.isPipelineNameEdit = false;
this.namingDialog = true;
},
openDuplicateDialog() {
this.pipeIndexToDuplicate = this.currentPipelineIndex - 1;
this.duplicateDialog = true;
},
deleteCurrentPipeline() {
if (this.$store.getters.pipelineList.length > 1) {
this.handleInputWithIndex('deleteCurrentPipeline');
} else {
this.snackbar = true;
}
},
savePipelineNameChange() {
if (this.checkPipelineName === "") {
if (this.isPipelineNameEdit) {
this.handleInputWithIndex("changePipelineName", this.newPipelineName);
} else {
this.handleInputWithIndex("addNewPipeline", this.newPipelineName);
}
this.discardPipelineNameChange();
}
},
duplicatePipeline() {
// if (!this.anotherCamera) {
// this.pipelineDuplicate.camera = -1
// }
this.handleInputWithIndex("duplicatePipeline", this.pipeIndexToDuplicate);
// this.axios.post("http://" + this.$address + "/api/vision/duplicate", this.pipeIndexToDuplicate);
this.closeDuplicateDialog();
},
closeDuplicateDialog() {
this.duplicateDialog = false;
this.pipeIndexToDuplicate = undefined;
},
discardPipelineNameChange() {
this.namingDialog = false;
this.isPipelineNameEdit = false;
this.newPipelineName = "";
},
}
import CVicon from '../common/cv-icon'
import CVselect from '../common/cv-select'
import CVinput from '../common/cv-input'
export default {
name: "CameraAndPipelineSelect",
components: {
CVicon,
CVselect,
CVinput
},
data: () => {
return {
re: RegExp("^[A-Za-z0-9 \\-)(]*[A-Za-z0-9][A-Za-z0-9 \\-)(.]*$"),
isCameraNameEdit: false,
newCameraName: "",
cameraNameError: "",
isPipelineNameEdit: false,
namingDialog: false,
newPipelineName: "",
duplicateDialog: false,
showPipeTypeDialog: false,
proposedPipelineType : 0,
pipeIndexToDuplicate: undefined
}
},
computed: {
checkCameraName() {
if (this.newCameraName !== this.$store.getters.cameraList[this.currentCameraIndex]) {
if (this.re.test(this.newCameraName)) {
for (let cam in this.cameraList) {
if (this.cameraList.hasOwnProperty(cam)) {
if (this.newCameraName === this.cameraList[cam]) {
return "A camera by that name already exists"
}
}
}
} else {
return "A camera name can only contain letters, numbers, and spaces"
}
}
return "";
},
checkPipelineName() {
if (this.newPipelineName !== this.$store.getters.pipelineList[this.currentPipelineIndex - 1] || this.isPipelineNameEdit === false) {
if (this.re.test(this.newPipelineName)) {
for (let pipe in this.$store.getters.pipelineList) {
if (this.$store.getters.pipelineList.hasOwnProperty(pipe)) {
if (this.newPipelineName === this.$store.getters.pipelineList[pipe]) {
return "A pipeline with this name already exists"
}
}
}
} else {
return "A pipeline name can only contain letters, numbers, and spaces"
}
}
return ""
},
currentCameraIndex: {
get() {
return this.$store.getters.currentCameraIndex;
},
set(value) {
this.$store.commit('currentCameraIndex', value);
}
},
currentPipelineIndex: {
get() {
return this.$store.getters.currentPipelineIndex + (this.$store.getters.isDriverMode ? 1 : 0);
},
set(value) {
this.$store.commit('currentPipelineIndex', value - (this.$store.getters.isDriverMode ? 1 : 0));
}
},
currentPipelineType: {
get() {
return this.$store.getters.currentPipelineSettings.pipelineType - 2;
},
set(value) {
value; // nop, since we have the dialog for this
}
}
},
methods: {
showTypeDialog(idx) {
// Only show the dialog if it's a new type
this.showPipeTypeDialog = idx !== this.currentPipelineType;
this.proposedPipelineType = idx;
},
changePipeType(actuallyChange) {
const newIdx = actuallyChange ? this.proposedPipelineType : this.currentPipelineType
this.handleInputWithIndex('pipelineType', newIdx);
this.showPipeTypeDialog = false;
},
changeCameraName() {
this.newCameraName = this.$store.getters.cameraList[this.currentCameraIndex];
this.isCameraNameEdit = true;
},
saveCameraNameChange() {
if (this.checkCameraName === "") {
// this.handleInputWithIndex("changeCameraName", this.newCameraName);
this.axios.post('http://' + this.$address + '/api/setCameraNickname',
{name: this.newCameraName, cameraIndex: this.$store.getters.currentCameraIndex})
// eslint-disable-next-line
.then(r => {
this.$emit('camera-name-changed')
})
.catch(e => {
console.log("HTTP error while changing camera name " + e);
this.$emit('camera-name-changed')
})
this.discardCameraNameChange();
}
},
discardCameraNameChange() {
this.isCameraNameEdit = false;
this.newCameraName = "";
},
toPipelineNameChange() {
this.newPipelineName = this.$store.getters.pipelineList[this.currentPipelineIndex - 1];
this.isPipelineNameEdit = true;
this.namingDialog = true;
},
toCreatePipeline() {
this.newPipelineName = "New Pipeline";
this.isPipelineNameEdit = false;
this.namingDialog = true;
},
deleteCurrentPipeline() {
if (this.$store.getters.pipelineList.length > 1) {
this.handleInputWithIndex('deleteCurrentPipeline', {});
} else {
this.snackbar = true;
}
},
savePipelineNameChange() {
if (this.checkPipelineName === "") {
if (this.isPipelineNameEdit) {
this.handleInputWithIndex("changePipelineName", this.newPipelineName);
} else {
this.handleInputWithIndex("addNewPipeline", [this.newPipelineName, this.currentPipelineType]); // 0 for reflective, 1 for colored shpae
}
this.discardPipelineNameChange();
}
},
duplicatePipeline() {
this.handleInputWithIndex("duplicatePipeline", this.currentPipelineIndex);
},
discardPipelineNameChange() {
this.namingDialog = false;
this.isPipelineNameEdit = false;
this.newPipelineName = "";
},
}
}
</script>
<style scoped>
</style>
</style>

View File

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

View File

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

File diff suppressed because one or more lines are too long

View File

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

View File

@@ -21,6 +21,7 @@ import VueNativeSock from 'vue-native-websocket';
Vue.use(VueNativeSock, wsURL, {
reconnection: true,
reconnectionDelay: 100,
connectManually: true,
format: "arraybuffer",
});
@@ -28,9 +29,11 @@ Vue.use(VueAxios, axios);
Vue.prototype.$msgPack = msgPack(true);
import {dataHandleMixin} from './mixins/global/dataHandleMixin'
Vue.mixin(dataHandleMixin);
import {stateMixin} from './mixins/global/stateMixin'
Vue.mixin(stateMixin);
new Vue({
router,
store,

View File

@@ -4,10 +4,10 @@ export const dataHandleMixin = {
let msg = this.$msgPack.encode({[key]: value});
this.$socket.send(msg);
},
handleInputWithIndex(key, value) {
handleInputWithIndex(key, value, cameraIndex = this.$store.getters.currentCameraIndex) {
let msg = this.$msgPack.encode({
[key]: value,
["cameraIndex"]: this.$store.getters.currentCameraIndex
["cameraIndex"]: cameraIndex,
});
this.$socket.send(msg);
},

View File

@@ -0,0 +1,10 @@
export const stateMixin = {
methods: {
currentPipelineType() {
return this.$store.getters.pipelineType
},
currentPipelineSettings() {
return this.$store.getters.currentPipelineSettings
},
}
};

View File

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

View File

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

View File

@@ -15,12 +15,21 @@ export default new Vuex.Store({
},
state: {
backendConnected: false,
ntConnectionInfo: {
connected: false,
address: "",
clients: 0,
},
networkInfo: {
possibleRios: ["Loading..."],
deviceips: ["Loading..."],
},
connectedCallbacks: [],
colorPicking: false,
logsOverlay: false,
compactMode: localStorage.getItem("compactMode") === undefined ? undefined : localStorage.getItem("compactMode") === "true", // Compact mode is initially unset on purpose
logMessages: [],
currentCameraIndex: 0,
selectedOutputs: [0, 1], // 0 indicates normal, 1 indicates threshold
cameraSettings: [ // This is a list of objects representing the settings of all cameras
{
tiltDegrees: 0.0,
@@ -42,13 +51,14 @@ export default new Vuex.Store({
isFovConfigurable: true,
calibrated: false,
currentPipelineSettings: {
pipelineType: 2, // One of "driver", "reflective", "shape"
pipelineType: 2, // One of "calib", "driver", "reflective", "shape"
// 2 is reflective
// Settings that apply to all pipeline types
cameraExposure: 1,
cameraBrightness: 2,
cameraGain: 3,
cameraRedGain: 3,
cameraBlueGain: 4,
inputImageRotationMode: 0,
cameraVideoModeIndex: 0,
streamingFrameDivisor: 0,
@@ -57,15 +67,18 @@ export default new Vuex.Store({
hsvHue: [0, 15],
hsvSaturation: [0, 15],
hsvValue: [0, 25],
erode: false,
dilate: false,
hueInverted: false,
contourArea: [0, 12],
contourRatio: [0, 12],
contourFullness: [0, 12],
contourSpecklePercentage: 5,
contourFilterRangeX: 5,
contourFilterRangeY: 5,
contourGroupingMode: 0,
contourIntersection: 0,
contourSortMode: 0,
inputShouldShow: true,
outputShouldShow: true,
outputShouldDraw: true,
outputShowMultipleTargets: false,
offsetRobotOffsetMode: 0,
@@ -108,8 +121,8 @@ export default new Vuex.Store({
// Below options are only configurable if supported is true
connectionType: 0, // 0 = DHCP, 1 = Static
staticIp: "",
netmask: "",
hostname: "photonvision",
runNTServer: false,
},
lighting: {
supported: true,
@@ -119,13 +132,21 @@ export default new Vuex.Store({
calibrationData: {
count: 0,
videoModeIndex: 0,
minCount: 25,
minCount: 12, // Gets set by backend anyways, but we need a sane default
hasEnough: false,
squareSizeIn: 1.0,
patternWidth: 7,
patternHeight: 7,
patternWidth: 8,
patternHeight: 8,
boardType: 0, // Chessboard, dotboard
},
metrics: {
cpuTemp: "N/A",
cpuUtil: "N/A",
cpuMem: "N/A",
gpuMem: "N/A",
ramUtil: "N/A",
gpuMemUtil: "N/A",
}
},
mutations: {
compactMode: set('compactMode'),
@@ -134,9 +155,13 @@ export default new Vuex.Store({
selectedOutputs: set('selectedOutputs'),
settings: set('settings'),
calibrationData: set('calibrationData'),
metrics: set('metrics'),
ntConnectionInfo: set('ntConnectionInfo'),
networkInfo: set('networkInfo'),
backendConnected: set('backendConnected'),
logString: (state, newStr) => {
const str = state.logMessages;
str.push(newStr)
str.push(newStr);
Vue.set(state, 'logString', str)
},
@@ -172,6 +197,17 @@ export default new Vuex.Store({
}
},
mutateNetworkSettings: (state, payload) => {
for (let key in payload) {
if (!payload.hasOwnProperty(key)) continue;
const value = payload[key];
const settings = state.settings.networkSettings;
if (settings.hasOwnProperty(key)) {
Vue.set(settings, key, value);
}
}
},
mutatePipelineResults(state, payload) {
// Key: index, value: result
for (let key in payload) {
@@ -181,8 +217,12 @@ export default new Vuex.Store({
Vue.set(state, 'pipelineResults', payload[key])
}
}
},
mutateEnabledLEDPercentage(state, payload) {
const settings = state.settings;
settings.lighting.brightness = payload;
Vue.set(state, "settings", settings);
},
mutateCalibrationState: (state, payload) => {
@@ -215,10 +255,12 @@ export default new Vuex.Store({
currentCameraIndex: state => state.currentCameraIndex,
currentPipelineIndex: state => state.cameraSettings[state.currentCameraIndex].currentPipelineIndex,
currentPipelineSettings: state => state.cameraSettings[state.currentCameraIndex].currentPipelineSettings,
currentVideoFormat: state => state.cameraSettings[state.currentCameraIndex].videoFormatList[state.cameraSettings[state.currentCameraIndex].currentPipelineSettings.cameraVideoModeIndex],
videoFormatList: state => {
return Object.values(state.cameraSettings[state.currentCameraIndex].videoFormatList); // convert to a list
},
pipelineList: state => state.cameraSettings[state.currentCameraIndex].pipelineNicknames,
calibrationList: state => state.cameraSettings[state.currentCameraIndex].calibrations,
pipelineType: state => state.cameraSettings[state.currentCameraIndex].currentPipelineSettings.pipelineType
}
})
})

View File

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

View File

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

View File

@@ -19,15 +19,16 @@
<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
v-model="cameraSettings.fov"
:tooltip="cameraSettings.isFovConfigurable ? 'Field of view (in degrees) of the camera measured across the diagonal of the frame' : 'This setting is managed by a vendor'"
name="Diagonal FOV"
:tooltip="cameraSettings.isFovConfigurable ? 'Field of view (in degrees) of the camera measured across the diagonal of the frame, in a video mode which covers the whole sensor area.' : 'This setting is managed by a vendor'"
name="Maximum diagonal FOV"
:disabled="!cameraSettings.isFovConfigurable"
:label-cols="$vuetify.breakpoint.mdAndUp ? undefined : 7"
/>
<br>
<CVnumberinput
@@ -35,6 +36,7 @@
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
@@ -66,43 +68,51 @@
cols="12"
md="6"
>
<CVselect
v-model="selectedFilteredResIndex"
name="Resolution"
select-cols="7"
:list="stringResolutionList"
:disabled="isCalibrating"
tooltip="Resolution to calibrate at (you will have to calibrate every resolution you use 3D mode on)"
/>
<CVselect
v-model="boardType"
name="Board Type"
select-cols="7"
:list="['Chessboard', 'Dot Grid']"
:disabled="isCalibrating"
tooltip="Calibration board pattern to use"
/>
<CVnumberinput
v-model="squareSizeIn"
name="Pattern Spacing (in)"
label-cols="5"
tooltip="Spacing between pattern features in inches"
:disabled="isCalibrating"
/>
<CVnumberinput
v-model="boardWidth"
name="Board width"
label-cols="5"
tooltip="Width of the board in dots or corners; with the standard chessboard, this is usually 7"
:disabled="isCalibrating"
/>
<CVnumberinput
v-model="boardHeight"
name="Board height"
label-cols="5"
tooltip="Height of the board in dots or corners; with the standard chessboard, this is usually 7"
:disabled="isCalibrating"
/>
<v-form
ref="form"
v-model="settingsValid"
>
<CVselect
v-model="selectedFilteredResIndex"
name="Resolution"
select-cols="7"
:list="stringResolutionList"
:disabled="isCalibrating"
tooltip="Resolution to calibrate at (you will have to calibrate every resolution you use 3D mode on)"
/>
<CVselect
v-model="boardType"
name="Board Type"
select-cols="7"
:list="['Chessboard', 'Dot Grid']"
:disabled="isCalibrating"
tooltip="Calibration board pattern to use"
/>
<CVnumberinput
v-model="squareSizeIn"
name="Pattern Spacing (in)"
tooltip="Spacing between pattern features in inches"
:disabled="isCalibrating"
:rules="[v => (v > 0) || 'Size must be positive']"
:label-cols="$vuetify.breakpoint.mdAndUp ? 5 : 7"
/>
<CVnumberinput
v-model="boardWidth"
name="Board width"
tooltip="Width of the board in dots or chessboard squares"
:disabled="isCalibrating"
:rules="[v => (v >= 4) || 'Width must be at least 4']"
:label-cols="$vuetify.breakpoint.mdAndUp ? 5 : 7"
/>
<CVnumberinput
v-model="boardHeight"
name="Board height"
tooltip="Height of the board in dots or chessboard squares"
:disabled="isCalibrating"
:rules="[v => (v >= 4) || 'Height must be at least 4']"
:label-cols="$vuetify.breakpoint.mdAndUp ? 5 : 7"
/>
</v-form>
</v-col>
<!-- Calibrated table -->
@@ -143,7 +153,7 @@
v-for="(value, index) in filteredResolutionList"
:key="index"
>
<td> {{ value.width }} X {{ value.height }} </td>
<td> {{ value.width }} X {{ value.height }}</td>
<td>
{{ isCalibrated(value) ? value.mean.toFixed(2) + "px" : "—" }}
</td>
@@ -178,7 +188,7 @@
@input="e => handlePipelineUpdate('cameraExposure', e)"
/>
<CVslider
v-model="this.$store.getters.currentPipelineSettings.cameraBrightness"
v-model="$store.getters.currentPipelineSettings.cameraBrightness"
name="Brightness"
:min="0"
:max="100"
@@ -186,13 +196,24 @@
@input="e => handlePipelineUpdate('cameraBrightness', 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>
@@ -218,7 +239,7 @@
:disabled="checkCancellation"
@click="sendCalibrationFinish"
>
{{ hasEnough ? "End Calibration" : "Cancel Calibration" }}
{{ hasEnough ? "Finish Calibration" : "Cancel Calibration" }}
</v-btn>
</v-col>
<v-col>
@@ -227,19 +248,14 @@
small
outlined
style="width: 100%;"
:disabled="!settingsValid"
@click="downloadBoard"
>
<v-icon left>
mdi-download
</v-icon>
Download Chessboard
Download Target
</v-btn>
<a
ref="calibrationFile"
style="color: black; text-decoration: none; display: none"
:href="require('../assets/chessboard.png')"
download="chessboard.png"
/>
</v-col>
</v-row>
</div>
@@ -250,21 +266,72 @@
cols="12"
md="5"
>
<CVimage
:address="$store.getters.streamAddress[1]"
:disconnected="!$store.state.backendConnected"
scale="100"
style="border-radius: 5px;"
/>
<template>
<CVimage
:address="$store.getters.streamAddress[1]"
:disconnected="!$store.state.backendConnected"
scale="100"
style="border-radius: 5px;"
/>
<v-dialog
v-model="snack"
width="500px"
:persistent="true"
>
<v-card
color="primary"
dark
>
<v-card-title> Camera Calibration </v-card-title>
<div
class="ml-3"
>
<v-col align="center">
<template v-if="calibrationInProgress && !calibrationFailed">
<v-progress-circular
indeterminate
:size="70"
:width="8"
color="accent"
/>
<v-card-text>Camera is being calibrated. This process make take several minutes...</v-card-text>
</template>
<template v-else-if="!calibrationFailed">
<v-icon
color="green"
size="70"
>
mdi-check-bold
</v-icon>
<v-card-text>Camera has been successfully calibrated at {{ stringResolutionList[selectedFilteredResIndex] }}!</v-card-text>
</template>
<template v-else>
<v-icon
color="red"
size="70"
>
mdi-close
</v-icon>
<v-card-text>Camera calibration failed! Make sure that the photos are taken such that the rainbow grid circles align with the corners of the chessboard, and try again. More information is available in the program logs.</v-card-text>
</template>
</v-col>
</div>
<v-card-actions>
<v-spacer />
<v-btn
v-if="!calibrationInProgress || calibrationFailed"
color="white"
text
@click="closeDialog"
>
OK
</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
</template>
</v-col>
</v-row>
<v-snackbar
v-model="snack"
top
:color="snackbar.color"
>
<span>{{ snackbar.text }}</span>
</v-snackbar>
</div>
</template>
@@ -274,11 +341,13 @@ import CVnumberinput from '../components/common/cv-number-input';
import CVslider from '../components/common/cv-slider';
import CVimage from "../components/common/cv-image";
import TooltippedLabel from "../components/common/cv-tooltipped-label";
import jsPDF from "jspdf";
import "../jsPDFFonts/Prompt-Regular-normal.js";
export default {
name: 'Cameras',
components: {
TooltippedLabel,
TooltippedLabel,
CVselect,
CVnumberinput,
CVslider,
@@ -286,17 +355,16 @@ export default {
},
data() {
return {
snackbar: {
color: "success",
text: ""
},
snack: false,
filteredVideomodeIndex: 0
calibrationInProgress: false,
calibrationFailed: false,
filteredVideomodeIndex: 0,
settingsValid: true,
}
},
computed: {
disallowCalibration() {
return !(this.calibrationData.boardType === 0 || this.calibrationData.boardType === 1);
return !(this.calibrationData.boardType === 0 || this.calibrationData.boardType === 1) || !this.settingsValid;
},
checkCancellation() {
if (this.isCalibrating) {
@@ -325,7 +393,7 @@ export default {
if (!filtered.some(e => e.width === it.width && e.height === it.height)) {
it['index'] = i;
const calib = this.getCalibrationCoeffs(it);
if(calib != null) {
if (calib != null) {
it['standardDeviation'] = calib.standardDeviation;
it['mean'] = calib.perViewErrors.reduce((a, b) => a + b) / calib.perViewErrors.length;
}
@@ -409,7 +477,6 @@ export default {
return this.$store.getters.currentPipelineIndex === -2;
}
},
selectedFilteredResIndex: {
get() {
return this.filteredVideomodeIndex
@@ -422,21 +489,113 @@ export default {
},
},
methods: {
closeDialog() {
this.snack = false;
this.calibrationInProgress = false;
this.calibrationFailed = false;
},
getCalibrationCoeffs(resolution) {
const calList = this.$store.getters.calibrationList;
let ret = null;
calList.forEach(cal => {
if(cal.width === resolution.width && cal.height === resolution.height) {
if (cal.width === resolution.width && cal.height === resolution.height) {
ret = cal
}
});
return ret;
},
downloadBoard() {
this.axios.get("http://" + this.$address + require('../assets/chessboard.png'), {responseType: 'blob'}).then((response) => {
require('downloadjs')(response.data, "Calibration Board", "image/png");
});
// Generates a .pdf of a board for calibration and downloads it
//Murica paper.
var doc = new jsPDF({unit: 'in', format:'letter'});
var paper_x = 8.5;
var paper_y = 11.0;
//Load in custom fonts
console.log(doc.getFontList());
doc.setFont('Prompt-Regular');
doc.setFontSize(12);
// Common Parameters
var num_x = this.boardWidth;
var num_y = this.boardHeight;
var patternSize = this.squareSizeIn;
var isCheckerboard = (this.boardType==0);
var x_coord = 0.0;
var y_coord = 0.0;
var x_idx = 0;
var y_idx = 0;
var start_x = 0;
var start_y = 0;
var annotation = num_x + " x " + num_y + " | " + patternSize + "in "
if(isCheckerboard){
///////////////////////////////////////////
// Checkerboard Pattern
start_x = paper_x/2.0 - (num_x * patternSize)/2.0;
start_y = paper_y/2.0 - (num_y * patternSize)/2.0;
for(y_idx = 0; y_idx < num_y; y_idx++){
for(x_idx = 0; x_idx < num_x; x_idx++){
x_coord = start_x + x_idx * patternSize;
y_coord = start_y + y_idx * patternSize;
if((x_idx + y_idx) % 2 == 0){
doc.rect(x_coord, y_coord, patternSize, patternSize, "F");
}
}
}
} else {
///////////////////////////////////////////
// Assymetric Dot-Grid Pattern
// see https://github.com/opencv/opencv/blob/b450dd7a87bc69997a8417d94bdfb87427a9fe62/modules/calib3d/src/circlesgrid.cpp#L437
// as well as FindBoardCornersPipe.java's Dotboard implementation
start_x = paper_x/2.0 - ((2*(num_x-1) + (num_y-1) % 2) * patternSize)/2.0;
start_y = paper_y/2.0 - (num_y-1 * patternSize)/2.0;
// Dot Grid Pattern
for(y_idx = 0; y_idx < num_y; y_idx++){
for(x_idx = 0; x_idx < num_x; x_idx++){
x_coord = start_x + (2*x_idx + y_idx % 2) * patternSize;
y_coord = start_y + y_idx * patternSize;
doc.circle(x_coord, y_coord, patternSize/4.0, "F");
}
}
}
///////////////////////////////////////////
// Draw a fixed size inch ruler pattern to
// help users debug their printers
var lineStartX = 1.0;
var lineEndX = paper_x - lineStartX;
var lineY = paper_y - 1.0;
doc.setFont('Prompt-Regular');
doc.setLineWidth(0.01);
doc.line(lineStartX, lineY, lineEndX, lineY);
var segIdx = 0;
for(var tickX = lineStartX; tickX <= lineEndX; tickX += 1.0){
doc.line(tickX, lineY, tickX, lineY + 0.25);
doc.text(String(segIdx) + (segIdx == 0 ? " in" : ""), tickX + 0.1, lineY + 0.25);
segIdx++;
}
///////////////////////////////////////////
// Annotate what was drawn + branding
var img = new Image();
img.src = require('@/assets/logoMono.png');
doc.addImage(img, 'PNG', 1.0, 0.75, 1.4, 0.5 );
doc.setFont('Prompt-Regular');
doc.text(annotation, paper_x-1.0, 1.0, {maxWidth:(paper_x - 2.0)/2, align:"right"});
doc.save("calibrationTarget.pdf");
},
sendCameraSettings() {
this.axios.post("http://" + this.$address + "/api/settings/camera", {
@@ -476,34 +635,19 @@ export default {
sendCalibrationFinish() {
console.log("finishing calibration for index " + this.$store.getters.currentCameraIndex);
this.snackbar.text = "Calibrating...";
this.snackbar.color = "secondary";
this.snack = true;
this.calibrationInProgress = true;
this.axios.post("http://" + this.$address + "/api/settings/endCalibration", this.$store.getters.currentCameraIndex)
.then((response) => {
if (response.status === 200) {
this.snackbar = {
color: "success",
text: "Calibration successful! \n" +
"Standard deviation: " + response.data.toFixed(5)
};
this.snack = true;
if (response.status === 200) {
this.calibrationInProgress = false;
} else {
this.calibrationFailed = true;
}
}
else {
this.snackbar = {
color: "error",
text: "Calibration Failed!"
};
this.snack = true;
}
}
).catch(() => {
this.snackbar = {
color: "error",
text: "Calibration Failed!"
};
this.snack = true;
).catch(() => {
this.calibrationFailed = true;
});
}
}
@@ -511,18 +655,19 @@ export default {
</script>
<style scoped>
.v-data-table {
text-align: center;
background-color: transparent !important;
width: 100%;
height: 100%;
overflow-y: auto;
}
.v-data-table th {
background-color: #006492 !important;
}
.v-data-table {
text-align: center;
background-color: transparent !important;
width: 100%;
height: 100%;
overflow-y: auto;
}
.v-data-table th,td {
font-size: 1rem !important;
}
</style>
.v-data-table th {
background-color: #006492 !important;
}
.v-data-table th, td {
font-size: 1rem !important;
}
</style>

View File

@@ -1,67 +1,69 @@
<template>
<v-card
dark
class="pt-3"
color="primary"
flat
>
<v-card-title>
View Program Logs
<v-card
dark
class="pt-3"
color="primary"
flat
>
<v-card-title>
View Program Logs
<v-btn
color="secondary"
style="margin-left: auto;"
depressed
@click="download('photonlog.log', rawLogs.map(it => it.message).join('\n'))"
>
<v-icon left>
mdi-download
</v-icon>
Download Log
</v-btn>
</v-card-title>
<div class="pr-6 pl-6">
<v-btn-toggle
v-model="logLevel"
dark
multiple
class="fill mb-4"
>
<v-btn
color="secondary"
style="margin-left: auto;"
depressed
@click="download('photonlog.log', rawLogs.map(it => it.message).join('\n'))"
v-for="(level) in possibleLevelArray"
:key="level"
color="secondary"
class="fill"
>
<v-icon left>
mdi-download
</v-icon>
Download Log
{{ level }}
</v-btn>
</v-card-title>
<div class="pr-6 pl-6">
<v-btn-toggle
v-model="logLevel"
dark
multiple
class="fill mb-4"
>
<v-btn
v-for="(level) in possibleLevelArray"
:key="level"
color="secondary"
class="fill"
>
{{ level }}
</v-btn>
</v-btn-toggle>
<!-- Logs -->
</v-btn-toggle>
<!-- Logs -->
<v-virtual-scroll
:items="logMessageArray"
item-height="50"
height="600"
>
<template v-slot="{ item }">
<div :class="[getColor(item) + '--text', 'log-item']">{{ item.message }}</div>
</template>
</v-virtual-scroll>
</div>
<v-virtual-scroll
:items="logMessageArray"
item-height="50"
height="600"
>
<template v-slot="{ item }">
<div :class="[getColor(item) + '--text', 'log-item']">
{{ item.message }}
</div>
</template>
</v-virtual-scroll>
</div>
<v-divider />
<v-divider />
<v-card-actions>
<v-spacer />
<v-btn
color="white"
text
@click="$store.state.logsOverlay = false"
>
Close
</v-btn>
</v-card-actions>
</v-card>
<v-card-actions>
<v-spacer />
<v-btn
color="white"
text
@click="$store.state.logsOverlay = false"
>
Close
</v-btn>
</v-card-actions>
</v-card>
</template>
<script>

View File

@@ -26,6 +26,18 @@
style="height: 15%; min-height: 50px;"
>
Cameras
<v-chip
:class="fpsTooLow ? 'ml-2 mt-1' : 'mt-2'"
x-small
label
: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-else-if="!$store.getters.currentPipelineSettings.inputShouldShow">HSV thresholds are too broad; narrow them for better performance</span>
<span v-else>stop viewing the color stream for better performance</span>
</v-chip>
<v-switch
v-model="driverMode"
label="Driver Mode"
@@ -45,16 +57,18 @@
style="height: 100%;"
>
<div style="position: relative; width: 100%; height: 100%;">
<cvImage
<cv-image
:id="idx === 0 ? 'normal-stream' : ''"
ref="streams"
:address="$store.getters.streamAddress[idx]"
:disconnected="!$store.state.backendConnected"
scale="100"
:max-height="$store.getters.isDriverMode ? '40vh' : '300px'"
:max-height-md="$store.getters.isDriverMode ? '50vh' : '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"
:color-picking="$store.state.colorPicking && idx == 0"
:color-picking="$store.state.colorPicking && idx === 0"
@click="onImageClick"
/>
</div>
@@ -71,7 +85,7 @@
<v-card
color="primary"
>
<camera-and-pipeline-select />
<camera-and-pipeline-select @camera-name-changed="reloadStreams" />
</v-card>
<v-card
:disabled="$store.getters.isDriverMode || $store.state.colorPicking"
@@ -82,7 +96,6 @@
align="center"
class="pl-3 pr-3"
>
<!-- -->
<v-col lg="12">
<p style="color: white;">
Processing mode:
@@ -144,7 +157,7 @@
v-for="(tabs, idx) in tabGroups"
:key="idx"
:cols="Math.floor(12 / tabGroups.length)"
:class="idx != tabGroups.length - 1 ? 'pr-3' : ''"
:class="idx !== tabGroups.length - 1 ? 'pr-3' : ''"
align-self="stretch"
>
<v-card
@@ -182,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
@@ -212,9 +228,7 @@
</v-card-title>
<v-card-text>
Because the current resolution {{ this.$store.getters.videoFormatList[this.$store.getters.currentPipelineSettings.cameraVideoModeIndex].width }}
x {{ this.$store.getters.videoFormatList[this.$store.getters.currentPipelineSettings.cameraVideoModeIndex].height }}
is not yet calibrated, 3D mode cannot be enabled. Please
Because the current resolution {{ this.$store.getters.currentVideoFormat.width }} x {{ this.$store.getters.currentVideoFormat.height }} is not yet calibrated, 3D mode cannot be enabled. Please
<a
href="/#/cameras"
class="white--text"
@@ -240,202 +254,230 @@
</template>
<script>
import CameraAndPipelineSelect from "../components/pipeline/CameraAndPipelineSelect";
import cvImage from '../components/common/cv-image';
import InputTab from './PipelineViews/InputTab';
import ThresholdTab from './PipelineViews/ThresholdTab';
import ContoursTab from './PipelineViews/ContoursTab';
import OutputTab from './PipelineViews/OutputTab';
import TargetsTab from "./PipelineViews/TargetsTab";
import PnPTab from './PipelineViews/PnPTab';
import CameraAndPipelineSelect from "../components/pipeline/CameraAndPipelineSelect";
import cvImage from '../components/common/cv-image';
import InputTab from './PipelineViews/InputTab';
import ThresholdTab from './PipelineViews/ThresholdTab';
import ContoursTab from './PipelineViews/ContoursTab';
import OutputTab from './PipelineViews/OutputTab';
import TargetsTab from "./PipelineViews/TargetsTab";
import PnPTab from './PipelineViews/PnPTab';
export default {
name: 'CameraTab',
components: {
CameraAndPipelineSelect,
cvImage,
InputTab,
ThresholdTab,
ContoursTab,
OutputTab,
TargetsTab,
PnPTab,
},
data() {
return {
selectedTabsData: [0, 0, 0, 0],
snackbar: false,
counterData: 0,
dialog: false,
processingModeOverride: false
export default {
name: 'Pipeline',
components: {
CameraAndPipelineSelect,
cvImage,
InputTab,
ThresholdTab,
ContoursTab,
OutputTab,
TargetsTab,
PnPTab,
},
data() {
return {
selectedTabsData: [0, 0, 0, 0],
counterData: 0,
dialog: false,
processingModeOverride: false,
hideNTWarning: false,
}
},
computed: {
selectedTabs: {
get() {
return this.$store.getters.isDriverMode ? [0] : this.selectedTabsData;
},
set(value) {
this.selectedTabsData = value;
}
},
computed: {
selectedTabs: {
get() {
return this.$store.getters.isDriverMode ? [0] : this.selectedTabsData;
},
set(value) {
this.selectedTabsData = value;
}
},
tabGroups: {
get() {
let tabs = {
input: {
name: "Input",
component: "InputTab",
},
threshold: {
name: "Threshold",
component: "ThresholdTab",
},
contours: {
name: "Contours",
component: "ContoursTab",
},
output: {
name: "Output",
component: "OutputTab",
},
targets: {
name: "Target Info",
component: "TargetsTab",
},
pnp: {
name: "3D",
component: "PnPTab",
}
};
// 2D array of tab names and component names; each sub-array is a separate tab group
let ret = [];
if (this.$vuetify.breakpoint.smAndDown || this.$store.getters.isDriverMode || (this.$vuetify.breakpoint.mdAndDown && !this.$store.state.compactMode)) {
// One big tab group with all the tabs
ret[0] = Object.values(tabs);
} else if (this.$vuetify.breakpoint.mdAndDown || !this.$store.state.compactMode) {
// Two tab groups, one with "input, threshold, contours, output" and the other with "target info, 3D"
ret[0] = [tabs.input, tabs.threshold, tabs.contours, tabs.output];
ret[1] = [tabs.targets, tabs.pnp];
} else if (this.$vuetify.breakpoint.lgAndDown) {
// Three tab groups, one with "input", one with "threshold, contours, output", and the other with "target info, 3D"
ret[0] = [tabs.input];
ret[1] = [tabs.threshold, tabs.contours, tabs.output];
ret[2] = [tabs.targets, tabs.pnp];
} else if (this.$vuetify.breakpoint.xl) {
// Three tab groups, one with "input", one with "threshold, contours", and the other with "output, target info, 3D"
ret[0] = [tabs.input];
ret[1] = [tabs.threshold];
ret[2] = [tabs.contours, tabs.output]
ret[3] = [tabs.targets, tabs.pnp];
tabGroups: {
get() {
let tabs = {
input: {
name: "Input",
component: "InputTab",
},
threshold: {
name: "Threshold",
component: "ThresholdTab",
},
contours: {
name: "Contours",
component: "ContoursTab",
},
output: {
name: "Output",
component: "OutputTab",
},
targets: {
name: "Target Info",
component: "TargetsTab",
},
pnp: {
name: "3D",
component: "PnPTab",
}
};
// 2D array of tab names and component names; each sub-array is a separate tab group
let ret = [];
if (this.$vuetify.breakpoint.smAndDown || this.$store.getters.isDriverMode || (this.$vuetify.breakpoint.mdAndDown && !this.$store.state.compactMode)) {
// One big tab group with all the tabs
ret[0] = Object.values(tabs);
} else if (this.$vuetify.breakpoint.mdAndDown || !this.$store.state.compactMode) {
// Two tab groups, one with "input, threshold, contours, output" and the other with "target info, 3D"
ret[0] = [tabs.input, tabs.threshold, tabs.contours, tabs.output];
ret[1] = [tabs.targets, tabs.pnp];
} else if (this.$vuetify.breakpoint.lgAndDown) {
// Three tab groups, one with "input", one with "threshold, contours, output", and the other with "target info, 3D"
ret[0] = [tabs.input];
ret[1] = [tabs.threshold, tabs.contours, tabs.output];
ret[2] = [tabs.targets, tabs.pnp];
} else if (this.$vuetify.breakpoint.xl) {
// Three tab groups, one with "input", one with "threshold, contours", and the other with "output, target info, 3D"
ret[0] = [tabs.input];
ret[1] = [tabs.threshold];
ret[2] = [tabs.contours, tabs.output];
ret[3] = [tabs.targets, tabs.pnp];
}
return ret;
}
},
processingMode: {
get() {
return (this.$store.getters.currentPipelineSettings.solvePNPEnabled || this.processingModeOverride) ? 1 : 0;
},
set(value) {
if (this.$store.getters.isCalibrated) {
this.$store.getters.currentPipelineSettings.solvePNPEnabled = value === 1;
this.handlePipelineUpdate("solvePNPEnabled", value === 1);
}
}
},
driverMode: {
get() {
return this.$store.getters.isDriverMode;
},
set(value) {
this.$store.getters.currentCameraSettings.currentPipelineIndex = value ? -1 : 0;
this.handleInputWithIndex('currentPipeline', value ? -1 : 0);
}
},
selectedOutputs: {
// All this logic exists to deal with the reality that the output select buttons sometimes need an array and sometimes need a number (depending on whether or not they're exclusive)
get() {
// We switch the selector to single-select only on sm-and-down size devices, so we have to return a Number instead of an Array in that state
let ret = [];
if (this.$store.state.colorPicking) {
ret = [0]; // We want the input stream only while color picking
} else if (this.$store.getters.isDriverMode) {
ret = [1]; // We want only the output stream in driver mode
} else {
if (this.$store.getters.currentPipelineSettings.inputShouldShow) ret = ret.concat([0]);
if (this.$store.getters.currentPipelineSettings.outputShouldShow) ret = ret.concat([1]);
if (!ret.length) ret = [0];
}
if (this.$vuetify.breakpoint.mdAndUp) {
return ret;
} else {
return ret[0] || 0;
}
},
processingMode: {
get() {
return (this.$store.getters.currentPipelineSettings.solvePNPEnabled || this.processingModeOverride) ? 1 : 0;
},
set(value) {
if (this.$store.getters.isCalibrated) {
this.$store.getters.currentPipelineSettings.solvePNPEnabled = value === 1;
this.handlePipelineUpdate("solvePNPEnabled", value === 1);
}
set(value) {
let valToCommit = [0];
if (value instanceof Array) {
// Value is already an array, we don't need to do anything
valToCommit = value;
} else if (value) {
// Value is assumed to be a number, so we wrap it into an array
valToCommit = [value];
}
},
driverMode: {
get() {
return this.$store.getters.isDriverMode;
},
set(value) {
this.$store.getters.currentCameraSettings.currentPipelineIndex = value ? -1 : 0;
this.handleInputWithIndex('currentPipeline', value ? -1 : 0);
}
},
selectedOutputs: {
// All this logic exists to deal with the reality that the output select buttons sometimes need an array and sometimes need a number (depending on whether or not they're exclusive)
get() {
// We switch the selector to single-select only on sm-and-down size devices, so we have to return a Number instead of an Array in that state
let ret;
if (this.$store.state.colorPicking) {
ret = [0]; // We want the input stream only while color picking
} else if (!this.$store.getters.isDriverMode) {
ret = this.$store.state.selectedOutputs || [0];
} else {
ret = [1]; // We want the output stream in driver mode
}
if (this.$vuetify.breakpoint.mdAndUp) {
return ret;
} else {
return ret[0] || 0;
}
},
set(value) {
let valToCommit = [0];
if (value instanceof Array) {
// Value is already an array, we don't need to do anything
value.sort(); // Sort for visual consistency
valToCommit = value;
} else if (value) {
// Value is assumed to be a number, so we wrap it into an array
valToCommit = [value];
}
this.$store.commit("selectedOutputs", valToCommit);
// TODO: Currently the backend just sends both streams regardless of the selected outputs value, so we don't need to send anything
// this.handlePipelineUpdate('selectedOutputs', valToCommit);
}
},
latency: {
get() {
return this.$store.getters.currentPipelineResults.latency;
}
this.$store.commit("mutatePipeline", {"inputShouldShow": valToCommit.includes(0)});
this.$store.commit("mutatePipeline", {"outputShouldShow": valToCommit.includes(1)});
this.handlePipelineUpdate("inputShouldShow", valToCommit.includes(0));
}
},
methods: {
isCalibrated() {
const resolution = this.$store.getters.videoFormatList[this.$store.getters.currentPipelineSettings.cameraVideoModeIndex];
return this.$store.getters.currentCameraSettings.calibrations
.some(e => e.width === resolution.width && e.height === resolution.height)
},
onImageClick(event) {
// Only run on the input stream
if (event.target.alt !== "Stream0") return;
// Get a reference to the threshold tab (if it is shown) and call its "onClick" method
let ref = this.$refs["Threshold"];
if (ref && ref[0])
ref[0].onClick(event)
},
on3DClick() {
if (!this.$store.getters.isCalibrated) {
this.dialog = true;
this.processingModeOverride = true;
}
},
closeUncalibratedDialog() {
this.dialog = false;
this.processingModeOverride = false;
// this.$store.getters.currentPipelineSettings.solvePNPEnabled = false;
this.handlePipelineUpdate("solvePNPEnabled", false);
fpsTooLow: {
get() {
// For now we only show the FPS is too low warning when GPU acceleration is enabled, because we don't really trust the presented video modes otherwise
return this.$store.state.pipelineResults.fps - this.$store.getters.currentVideoFormat.fps < -5 && this.$store.state.pipelineResults.fps !== 0 && !this.$store.getters.isDriverMode && this.$store.state.settings.general.gpuAcceleration;
}
},
latency: {
get() {
return this.$store.getters.currentPipelineResults.latency;
}
},
isCalibrated: {
get() {
const resolution = this.$store.getters.videoFormatList[this.$store.getters.currentPipelineSettings.cameraVideoModeIndex];
return this.$store.getters.currentCameraSettings.calibrations
.some(e => e.width === resolution.width && e.height === resolution.height)
}
},
isRobotConnected: {
get() {
// return this.$store.state.ntConnectionInfo.connected && this.$store.state.backendConnected;
return true;
}
},
showNTWarning: {
get() {
return (!this.$store.state.ntConnectionInfo.connected || this.$store.state.settings.networkSettings.runNTServer) && this.$store.state.settings.networkSettings.teamNumber > 0 && this.$store.state.backendConnected && !this.hideNTWarning;
}
},
},
created() {
this.$store.state.connectedCallbacks.push(this.reloadStreams)
},
methods: {
reloadStreams() {
// Reload the streams as we technically close and reopen them
this.$refs.streams.forEach(it => it.reload())
},
onImageClick(event) {
// Only run on the input stream
if (event.target.alt !== "Stream0") return;
// Get a reference to the threshold tab (if it is shown) and call its "onClick" method
let ref = this.$refs["Threshold"];
if (ref && ref[0])
ref[0].onClick(event)
},
on3DClick() {
if (!this.$store.getters.isCalibrated) {
this.dialog = true;
this.processingModeOverride = true;
}
},
closeUncalibratedDialog() {
this.dialog = false;
this.processingModeOverride = false;
// this.$store.getters.currentPipelineSettings.solvePNPEnabled = false;
this.handlePipelineUpdate("solvePNPEnabled", false);
}
}
}
</script>
<style scoped>
.v-btn-toggle.fill {
width: 100%;
height: 100%;
}
.v-btn-toggle.fill {
width: 100%;
height: 100%;
}
.v-btn-toggle.fill > .v-btn {
width: 50%;
height: 100%;
}
.v-btn-toggle.fill > .v-btn {
width: 50%;
height: 100%;
}
th {
width: 80px;
text-align: center;
}
</style>
th {
width: 80px;
text-align: center;
}
</style>

View File

@@ -5,11 +5,11 @@
name="Area"
min="0"
max="100"
step="0.1"
step="0.01"
@input="handlePipelineData('contourArea')"
@rollback="e=> rollback('contourArea',e)"
/>
<CVrangeSlider
v-if="currentPipelineType() !== 3"
v-model="contourRatio"
name="Ratio (W/H)"
tooltip="Min and max ratio between the width and height of a contour's bounding rectangle"
@@ -17,16 +17,32 @@
max="100"
step="0.1"
@input="handlePipelineData('contourRatio')"
@rollback="e=> rollback('contourRatio',e)"
/>
<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-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')"
@rollback="e=> rollback('contourFullness',e)"
/>
<CVrangeSlider
v-if="currentPipelineType() === 3"
v-model="contourPerimeter"
name="Perimeter"
tooltip="Min and max perimeter of the shape, in pixels"
min="0"
max="4000"
@input="handlePipelineData('contourPerimeter')"
/>
<CVslider
v-model="contourSpecklePercentage"
@@ -36,27 +52,110 @@
max="100"
:slider-cols="largeBox"
@input="handlePipelineData('contourSpecklePercentage')"
@rollback="e=> rollback('contourSpecklePercentage',e)"
/>
<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')"
@rollback="e=> rollback('contourGroupingMode',e)"
/>
<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')"
@rollback="e=> rollback('contourIntersection',e)"
/>
<template v-if="currentPipelineType() !== 3">
<CVslider
v-model="contourFilterRangeX"
name="X filter tightness"
tooltip="Rejects contours whose center X is further than X standard deviations above/below the mean X location"
min="0.1"
max="4"
step="0.1"
:slider-cols="largeBox"
@input="handlePipelineData('contourFilterRangeX')"
/>
<CVslider
v-model="contourFilterRangeY"
name="Y filter tightness"
tooltip="Rejects contours whose center Y is further than X standard deviations above/below the mean Y location"
min="0.1"
max="4"
step="0.1"
:slider-cols="largeBox"
@input="handlePipelineData('contourFilterRangeY')"
/>
<CVselect
v-model="contourGroupingMode"
name="Target Grouping"
tooltip="Whether or not every two targets are paired with each other (good for e.g. 2019 targets)"
:select-cols="largeBox"
:list="['Single','Dual','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" />
<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')"
/>
<!-- 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')"
/>
<!-- 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')"
/>
<CVrangeSlider
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')"
/>
<CVslider
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" />
</template>
<CVselect
v-model="contourSortMode"
name="Target Sort"
@@ -70,93 +169,176 @@
</template>
<script>
import CVrangeSlider from '../../components/common/cv-range-slider'
import CVselect from '../../components/common/cv-select'
import CVslider from '../../components/common/cv-slider'
import CVrangeSlider from '../../components/common/cv-range-slider'
import CVselect from '../../components/common/cv-select'
import CVslider from '../../components/common/cv-slider'
export default {
name: 'Contours',
components: {
CVrangeSlider,
CVselect,
CVslider
},
// eslint-disable-next-line vue/require-prop-types
props: ['value'],
export default {
name: 'Contours',
components: {
CVrangeSlider,
CVselect,
CVslider
},
// eslint-disable-next-line vue/require-prop-types
props: ['value'],
data() {
return {}
},
computed: {
largeBox: {
get() {
// Sliders and selectors should be fuller width if we're on screen size medium and
// up and either not in compact mode (because the tab will be 100% screen width),
// or in driver mode (where the card will also be 100% screen width).
return this.$vuetify.breakpoint.mdAndUp && (!this.$store.state.compactMode || this.$store.getters.isDriverMode) ? 10 : 8;
}
},
contourArea: {
get() {
return this.$store.getters.currentPipelineSettings.contourArea
},
set(val) {
this.$store.commit("mutatePipeline", {"contourArea": val});
}
},
contourRatio: {
get() {
return this.$store.getters.currentPipelineSettings.contourRatio
},
set(val) {
this.$store.commit("mutatePipeline", {"contourRatio": val});
}
},
contourFullness: {
get() {
return this.$store.getters.currentPipelineSettings.contourFullness
},
set(val) {
this.$store.commit("mutatePipeline", {"contourFullness": val});
}
},
contourSpecklePercentage: {
get() {
return this.$store.getters.currentPipelineSettings.contourSpecklePercentage
},
set(val) {
this.$store.commit("mutatePipeline", {"contourSpecklePercentage": val});
}
},
contourGroupingMode: {
get() {
return this.$store.getters.currentPipelineSettings.contourGroupingMode
},
set(val) {
this.$store.commit("mutatePipeline", {"contourGroupingMode": val});
}
},
contourSortMode: {
get() {
return this.$store.getters.currentPipelineSettings.contourSortMode
},
set(val) {
this.$store.commit("mutatePipeline", {"contourSortMode": val});
}
},
contourIntersection: {
get() {
return this.$store.getters.currentPipelineSettings.contourIntersection
},
set(val) {
this.$store.commit("mutatePipeline", {"contourIntersection": val});
}
}
},
methods: {},
}
data() {
return {}
},
computed: {
largeBox: {
get() {
// Sliders and selectors should be fuller width if we're on screen size medium and
// up and either not in compact mode (because the tab will be 100% screen width),
// or in driver mode (where the card will also be 100% screen width).
return this.$vuetify.breakpoint.mdAndUp && (!this.$store.state.compactMode || this.$store.getters.isDriverMode) ? 10 : 8;
}
},
contourArea: {
get() {
return this.$store.getters.currentPipelineSettings.contourArea
},
set(val) {
this.$store.commit("mutatePipeline", {"contourArea": val});
}
},
contourRatio: {
get() {
return this.$store.getters.currentPipelineSettings.contourRatio
},
set(val) {
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
},
set(val) {
this.$store.commit("mutatePipeline", {"contourFullness": val});
}
},
contourPerimeter: {
get() {
return this.$store.getters.currentPipelineSettings.contourPerimeter
},
set(val) {
this.$store.commit("mutatePipeline", {"contourPerimeter": val});
}
},
contourSpecklePercentage: {
get() {
return this.$store.getters.currentPipelineSettings.contourSpecklePercentage
},
set(val) {
this.$store.commit("mutatePipeline", {"contourSpecklePercentage": val});
}
},
contourFilterRangeX: {
get() {
console.log(this.$store.getters.currentPipelineSettings.contourFilterRangeX)
return this.$store.getters.currentPipelineSettings.contourFilterRangeX
},
set(val) {
console.log("set")
console.log(val)
this.$store.commit("mutatePipeline", {"contourFilterRangeX": val});
}
},
contourFilterRangeY: {
get() {
return this.$store.getters.currentPipelineSettings.contourFilterRangeY
},
set(val) {
this.$store.commit("mutatePipeline", {"contourFilterRangeY": val});
}
},
contourGroupingMode: {
get() {
return this.$store.getters.currentPipelineSettings.contourGroupingMode
},
set(val) {
this.$store.commit("mutatePipeline", {"contourGroupingMode": val});
}
},
contourSortMode: {
get() {
return this.$store.getters.currentPipelineSettings.contourSortMode
},
set(val) {
this.$store.commit("mutatePipeline", {"contourSortMode": val});
}
},
contourShape: {
get() {
return this.$store.getters.currentPipelineSettings.contourShape
},
set(val) {
this.$store.commit("mutatePipeline", {"contourShape": val});
}
},
contourIntersection: {
get() {
return this.$store.getters.currentPipelineSettings.contourIntersection
},
set(val) {
this.$store.commit("mutatePipeline", {"contourIntersection": val});
}
},
accuracyPercentage: {
get() {
return this.$store.getters.currentPipelineSettings.accuracyPercentage
},
set(val) {
this.$store.commit("mutatePipeline", {"accuracyPercentage": val});
}
},
contourRadius: {
get() {
return this.$store.getters.currentPipelineSettings.contourRadius
},
set(val) {
this.$store.commit("mutatePipeline", {"contourRadius": val});
}
},
circleDetectThreshold: {
get() {
return this.$store.getters.currentPipelineSettings.circleDetectThreshold
},
set(val) {
this.$store.commit("mutatePipeline", {"circleDetectThreshold": val});
}
},
maxCannyThresh: {
get() {
return this.$store.getters.currentPipelineSettings.maxCannyThresh
},
set(val) {
this.$store.commit("mutatePipeline", {"maxCannyThresh": val});
}
},
circleAccuracy: {
get() {
return this.$store.getters.currentPipelineSettings.circleAccuracy
},
set(val) {
this.$store.commit("mutatePipeline", {"circleAccuracy": val});
}
},
},
methods: {},
}
</script>
<style lang="" scoped>
</style>
</style>

View File

@@ -5,6 +5,7 @@
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"
:slider-cols="largeBox"
@input="handlePipelineData('cameraExposure')"
@@ -21,15 +22,26 @@
@rollback="e => rollback('cameraBrightness', e)"
/>
<CVslider
v-if="cameraGain !== -1"
v-model="cameraGain"
name="Gain"
v-if="cameraRedGain !== -1"
v-model="cameraRedGain"
name="Red AWB Gain"
min="0"
max="100"
tooltip="Controls automatic white balance gain, which affects how the camera captures colors in different conditions"
tooltip="Controls red automatic white balance gain, which affects how the camera captures colors in different conditions"
:slider-cols="largeBox"
@input="handlePipelineData('cameraGain')"
@rollback="e => rollback('cameraGain', e)"
@input="handlePipelineData('cameraRedGain')"
@rollback="e => rollback('cameraRedGain', e)"
/>
<CVslider
v-if="cameraBlueGain !== -1"
v-model="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="largeBox"
@input="handlePipelineData('cameraBlueGain')"
@rollback="e => rollback('cameraBlueGain', e)"
/>
<CVselect
v-model="inputImageRotationMode"
@@ -55,7 +67,6 @@
tooltip="Resolution to which camera frames are downscaled for streaming to the dashboard"
:list="streamResolutionList"
:select-cols="largeBox"
@input="handlePipelineData('streamingFrameDivisor')"
@rollback="e => rollback('streamingFrameDivisor', e)"
/>
</div>
@@ -65,6 +76,8 @@
import CVslider from '../../components/common/cv-slider'
import CVselect from '../../components/common/cv-select'
const unfilteredStreamDivisors = [1, 2, 4, 6];
export default {
name: 'Input',
components: {
@@ -74,7 +87,9 @@
// eslint-disable-next-line vue/require-prop-types
props: ['value'],
data() {
return {}
return {
rawStreamDivisorIndex: 0,
}
},
computed: {
largeBox: {
@@ -87,10 +102,10 @@
},
cameraExposure: {
get() {
return parseInt(this.$store.getters.currentPipelineSettings.cameraExposure);
return parseFloat(this.$store.getters.currentPipelineSettings.cameraExposure);
},
set(val) {
this.$store.commit("mutatePipeline", {"cameraExposure": parseInt(val)});
this.$store.commit("mutatePipeline", {"cameraExposure": parseFloat(val)});
}
},
cameraBrightness: {
@@ -101,12 +116,20 @@
this.$store.commit("mutatePipeline", {"cameraBrightness": parseInt(val)});
}
},
cameraGain: {
cameraRedGain: {
get() {
return parseInt(this.$store.getters.currentPipelineSettings.cameraGain)
return parseInt(this.$store.getters.currentPipelineSettings.cameraRedGain)
},
set(val) {
this.$store.commit("mutatePipeline", {"cameraGain": parseInt(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: {
@@ -119,18 +142,22 @@
},
cameraVideoModeIndex: {
get() {
return this.$store.getters.currentPipelineSettings.cameraVideoModeIndex
return this.$store.getters.currentPipelineSettings.cameraVideoModeIndex;
},
set(val) {
this.$store.commit("mutatePipeline", {"cameraVideoModeIndex": val});
this.handlePipelineUpdate("streamingFrameDivisor", this.getNumSkippedStreamDivisors());
this.rawStreamDivisorIndex = 0;
}
},
streamingFrameDivisor: {
get() {
return this.$store.getters.currentPipelineSettings.streamingFrameDivisor
return this.rawStreamDivisorIndex;
},
set(val) {
this.$store.commit("mutatePipeline", {"streamingFrameDivisor": val});
this.rawStreamDivisorIndex = val;
this.handlePipelineUpdate("streamingFrameDivisor", this.getNumSkippedStreamDivisors() + val);
}
},
@@ -146,21 +173,35 @@
streamResolutionList: {
get() {
let cam_res = this.$store.getters.videoFormatList[
this.$store.getters.currentCameraSettings.currentPipelineSettings.cameraVideoModeIndex]
const cam_res = this.$store.getters.videoFormatList[
this.$store.getters.currentCameraSettings.currentPipelineSettings.cameraVideoModeIndex];
let tmp_list = [];
tmp_list.push(`${Math.floor(cam_res['width'])} X ${Math.floor(cam_res['height'])}`);
for (let x = 2; x <= 6; x += 2) {
for (const x of this.getRawStreamDivisors()) {
tmp_list.push(`${Math.floor(cam_res['width'] / x)} X ${Math.floor(cam_res['height'] / x)}`);
}
return tmp_list;
}
}
},
methods: {}
methods: {
getRawStreamDivisors() {
// Limit stream res when GPU acceleration is enabled because we *know* that we won't be able to get smooth streams above ~640x480
// It would probably be cleaner if this checked that we're on the Raspi 3 instead of checking for GPU accel status
const width = this.$store.getters.videoFormatList[
this.$store.getters.currentCameraSettings.currentPipelineSettings.cameraVideoModeIndex]['width'];
// If GPU acceleration is enabled, the downsized width must be below 400px
// This check should be skipped if we're currently in driver mode
return unfilteredStreamDivisors.filter((x) => this.$store.getters.isDriverMode
|| !this.$store.state.settings.general.gpuAcceleration || width / x < 400);
},
getNumSkippedStreamDivisors() {
return unfilteredStreamDivisors.length - this.getRawStreamDivisors().length;
}
}
}
</script>
<style scoped>
</style>
</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"
@@ -156,4 +152,4 @@
</script>
<style scoped>
</style>
</style>

View File

@@ -16,10 +16,10 @@
color="accent"
item-color="secondary"
label="Select a target model"
:items="FRCtargets"
:items="targetList"
item-text="name"
item-value="data"
@change="onModelSelect"
@input="handlePipelineUpdate('targetModel', targetList.indexOf(selectedModel))"
/>
<CVslider
v-model="cornerDetectionAccuracyPercentage"
@@ -51,28 +51,34 @@
import Papa from 'papaparse';
import miniMap from '../../components/pipeline/3D/MiniMap';
import CVslider from '../../components/common/cv-slider'
import FRCtargetsConfig from '../../assets/FRCtargets'
export default {
name: "SolvePNP",
name: "PnP",
components: {
CVslider,
miniMap
},
data() {
return {
FRCtargets: null,
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: ""
},
snack: false,
selectedModel: {
isCustom: false
}
}
},
computed: {
selectedModel: {
get() {
let ret = this.$store.getters.currentPipelineSettings.targetModel
console.log(ret)
return this.targetList[ret];
},
set(val) {
this.$store.commit("mutatePipeline", {"targetModel": this.targetList.indexOf(val)})
}
},
cornerDetectionAccuracyPercentage: {
get() {
return this.$store.getters.currentPipelineSettings.cornerDetectionAccuracyPercentage
@@ -97,20 +103,6 @@
}
},
},
mounted() {
let tmp = [];
for (let t in FRCtargetsConfig) {
if (FRCtargetsConfig.hasOwnProperty(t)) {
tmp.push({name: t, data: FRCtargetsConfig[t]});
}
}
// Special dropdown item for uploading your own model
// data is what gets put in selectedMode, so we add a special field
tmp.push({name: "Custom model", data: {isCustom: true}});
this.FRCtargets = tmp;
},
methods: {
readFile(event) {
let file = event.target.files[0];
@@ -119,13 +111,6 @@
skipEmptyLines: true
});
},
onModelSelect() {
if (this.selectedModel.isCustom) {
this.$refs.file.click();
} else {
this.uploadPremade();
}
},
onParse(result) {
if (result.data.length > 0) {
let data = [];
@@ -158,29 +143,6 @@
this.selectedModel = null;
}
},
uploadPremade() {
this.uploadModel(this.selectedModel, true);
},
uploadModel(model, premade = false) {
this.axios.post("http://" + this.$address + "/api/vision/pnpModel", {
['targetModel']: model,
['index']: this.$store.getters.currentCameraIndex
}).then(() => {
this.snackbar = {
color: "success",
text: premade ? "Target model changed successfully" : "Custom target model uploaded and selected successfully"
};
this.snack = true;
}).catch(() => {
this.snackbar = {
color: "error",
text: "An error occurred selecting a target model"
};
this.snack = true;
this.selectedModel = null;
});
}
}
}
</script>
@@ -193,4 +155,4 @@
margin-left: auto;
margin-right: auto;
}
</style>
</style>

View File

@@ -97,4 +97,4 @@
.v-data-table td {
font-family: monospace !important;
}
</style>
</style>

View File

@@ -1,16 +1,21 @@
<template>
<div>
<div :style="{'--averageHue': averageHue}">
<CVrangeSlider
id="hue-slider"
v-model="hsvHue"
:class="hueInverted ? 'inverted-slider' : 'normal-slider'"
name="Hue"
tooltip="Describes color"
:min="0"
:max="180"
:inverted="hueInverted"
@input="handlePipelineData('hsvHue')"
@rollback="e => rollback('hue',e)"
/>
<CVrangeSlider
id="sat-slider"
v-model="hsvSaturation"
class="normal-slider"
name="Saturation"
tooltip="Describes colorfulness; the smaller this value the 'whiter' the color becomes"
:min="0"
@@ -19,7 +24,9 @@
@rollback="e => rollback('saturation',e)"
/>
<CVrangeSlider
id="value-slider"
v-model="hsvValue"
class="normal-slider"
name="Value"
tooltip="Describes lightness; the smaller this value the 'blacker' the color becomes"
:min="0"
@@ -27,6 +34,30 @@
@input="handlePipelineData('hsvValue')"
@rollback="e => rollback('value',e)"
/>
<CVSwitch
v-model="hueInverted"
name="Invert hue"
tooltip="Selects the hue range outside of the hue slider bounds instead of inside"
@input="handlePipelineData('hueInverted')"
@rollback="e => rollback('hueInverted',e)"
/>
<template v-if="currentPipelineType() === 3">
<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)"
/>
<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)"
/>
</template>
<div class="pt-3 white--text">
Color Picker
</div>
@@ -42,7 +73,7 @@
color="accent"
class="ma-2 black--text"
small
@click="setFunction(3)"
@click="setFunction(hueInverted ? 2 : 3)"
>
<v-icon left>
mdi-minus
@@ -58,13 +89,13 @@
<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)"
@click="setFunction(hueInverted ? 3: 2)"
>
<v-icon left>
mdi-plus
@@ -85,144 +116,194 @@
</template>
</v-row>
<v-divider class="mb-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)"
/>
<CVswitch
v-model="dilate"
name="Dilate"
tooltip="Adds pixels around the edges of white areas in the thresholded image"
@input="handlePipelineData('dilate')"
@rollback="e => rollback('dilate',e)"
/>
</div>
</template>
<script>
import CVrangeSlider from '../../components/common/cv-range-slider'
import CVswitch from '../../components/common/cv-switch'
import CVrangeSlider from '../../components/common/cv-range-slider'
import CVSwitch from "@/components/common/cv-switch";
export default {
name: 'Threshold',
components: {
CVrangeSlider,
CVswitch
},
// eslint-disable-next-line vue/require-prop-types
props: ['value'],
data() {
return {
currentFunction: undefined,
colorPicker: undefined,
showThresholdState: 0
}
},
computed: {
hsvHue: {
get() {
return this.$store.getters.currentPipelineSettings.hsvHue
},
set(val) {
this.$store.commit("mutatePipeline", {"hsvHue": val})
}
},
hsvSaturation: {
get() {
return this.$store.getters.currentPipelineSettings.hsvSaturation
},
set(val) {
this.$store.commit("mutatePipeline", {"hsvSaturation": val})
}
},
hsvValue: {
get() {
return this.$store.getters.currentPipelineSettings.hsvValue
},
set(val) {
this.$store.commit("mutatePipeline", {"hsvValue": val})
}
},
erode: {
get() {
return this.$store.getters.currentPipelineSettings.erode
},
set(val) {
this.$store.commit("mutatePipeline", {"erode": val});
}
},
dilate: {
get() {
return this.$store.getters.currentPipelineSettings.dilate
},
set(val) {
this.$store.commit("mutatePipeline", {"dilate": val});
}
},
},
mounted: function () {
const self = this;
this.colorPicker = require('../../plugins/ColorPicker').default;
this.$nextTick(() => {
self.colorPicker.initColorPicker();
});
},
methods: {
onClick(event) {
if (this.currentFunction !== undefined) {
this.colorPicker.initColorPicker();
let s = this.$store.getters.currentPipelineSettings;
let hsvArray = this.colorPicker.colorPickerClick(event, this.currentFunction,
[
[s.hsvHue[0], s.hsvSaturation[0], s.hsvValue[0]],
[s.hsvHue[1], s.hsvSaturation[1], s.hsvValue[1]]
].map(hsv => hsv.map(it => it || 0)));
// That `map` calls are to make sure that we don't let any undefined/null values slip in
this.currentFunction = undefined;
this.$store.state.colorPicking = false;
this.handlePipelineUpdate("outputShouldDraw", true);
s.hsvHue = [hsvArray[0][0], hsvArray[1][0]];
s.hsvSaturation = [hsvArray[0][1], hsvArray[1][1]];
s.hsvValue = [hsvArray[0][2], hsvArray[1][2]];
let msg = this.$msgPack.encode({
"changePipelineSetting": {
'hsvHue': s.hsvHue,
'hsvSaturation': s.hsvSaturation,
'hsvValue': s.hsvValue,
'cameraIndex': this.$store.state.currentCameraIndex
}
});
this.$socket.send(msg);
this.$emit('update');
}
},
setFunction(index) {
switch (index) {
case 0:
this.currentFunction = undefined;
this.$store.state.colorPicking = false;
this.handlePipelineUpdate("outputShouldDraw", true);
return;
case 1:
this.currentFunction = this.colorPicker.eyeDrop;
break;
case 2:
this.currentFunction = this.colorPicker.expand;
break;
case 3:
this.currentFunction = this.colorPicker.shrink;
break;
}
this.$store.state.colorPicking = true;
this.handlePipelineUpdate("outputShouldDraw", false);
}
}
export default {
name: 'Threshold',
components: {
CVSwitch,
CVrangeSlider
},
// eslint-disable-next-line vue/require-prop-types
props: ['value'],
data() {
return {
currentFunction: undefined,
colorPicker: undefined,
showThresholdState: 0
}
},
computed: {
hsvHue: {
get() {
return this.$store.getters.currentPipelineSettings.hsvHue
},
set(val) {
this.$store.commit("mutatePipeline", {"hsvHue": val})
}
},
averageHue: {
get() {
var isInverted = this.$store.getters.currentPipelineSettings.hueInverted;
const arr = this.$store.getters.currentPipelineSettings.hsvHue;
var retVal = 0;
</script>
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;
},
set(val) {
this.$store.commit("mutatePipeline", {"hsvSaturation": val})
}
},
hsvValue: {
get() {
return this.$store.getters.currentPipelineSettings.hsvValue;
},
set(val) {
this.$store.commit("mutatePipeline", {"hsvValue": val});
}
},
erode: {
get() {
return this.$store.getters.currentPipelineSettings.erode;
},
set(val) {
this.$store.commit("mutatePipeline", {"erode": val});
}
},
dilate: {
get() {
return this.$store.getters.currentPipelineSettings.dilate;
},
set(val) {
this.$store.commit("mutatePipeline", {"dilate": val});
}
},
},
mounted: function () {
const self = this;
this.colorPicker = require('../../plugins/ColorPicker').default;
this.$nextTick(() => {
self.colorPicker.initColorPicker();
});
},
methods: {
onClick(event) {
if (this.currentFunction !== undefined) {
this.colorPicker.initColorPicker();
let s = this.$store.getters.currentPipelineSettings;
let hsvArray = this.colorPicker.colorPickerClick(event, this.currentFunction,
[
[s.hsvHue[0], s.hsvSaturation[0], s.hsvValue[0]],
[s.hsvHue[1], s.hsvSaturation[1], s.hsvValue[1]]
].map(hsv => hsv.map(it => it || 0)));
// That `map` calls are to make sure that we don't let any undefined/null values slip in
this.currentFunction = undefined;
this.$store.state.colorPicking = false;
this.handlePipelineUpdate("outputShouldDraw", true);
s.hsvHue = [hsvArray[0][0], hsvArray[1][0]];
s.hsvSaturation = [hsvArray[0][1], hsvArray[1][1]];
s.hsvValue = [hsvArray[0][2], hsvArray[1][2]];
let msg = this.$msgPack.encode({
"changePipelineSetting": {
'hsvHue': s.hsvHue,
'hsvSaturation': s.hsvSaturation,
'hsvValue': s.hsvValue,
'cameraIndex': this.$store.state.currentCameraIndex
}
});
this.$socket.send(msg);
this.$emit('update');
}
},
setFunction(index) {
switch (index) {
case 0:
this.currentFunction = undefined;
this.$store.state.colorPicking = false;
this.handlePipelineUpdate("outputShouldDraw", true);
return;
case 1:
this.currentFunction = this.colorPicker.eyeDrop;
break;
case 2:
this.currentFunction = this.colorPicker.expand;
break;
case 3:
this.currentFunction = this.colorPicker.shrink;
break;
}
this.$store.state.colorPicking = true;
this.handlePipelineUpdate("outputShouldDraw", false);
this.$store.commit("mutatePipeline", {"inputShouldShow": true});
this.handlePipelineUpdate("inputShouldShow", true);
}
}
}
</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

@@ -8,32 +8,19 @@
cols="12"
style="max-width: 1400px"
>
<v-form
ref="form"
v-model="valid"
<v-card
v-for="item in tabList"
:key="item.name"
dark
class="mb-3 pr-6 pb-3"
style="background-color: #006492;"
>
<v-card
v-for="item in tabList"
:key="item.name"
dark
class="mb-3 pr-6 pb-3"
style="background-color: #006492;"
>
<v-card-title>{{ item.name }}</v-card-title>
<component
:is="item"
class="ml-5"
/>
</v-card>
<v-btn
color="accent"
style="color: black; width: 100%;"
:disabled="!valid"
@click="sendGeneralSettings()"
>
Save
</v-btn>
</v-form>
<v-card-title>{{ item.name }}</v-card-title>
<component
:is="item"
class="ml-5"
/>
</v-card>
</v-col>
</v-row>
<v-snackbar
@@ -61,8 +48,8 @@
data() {
return {
selectedTab: 0,
valid: true, // Are all settings valid
snack: false,
calibrationInProgress: false,
snackbar: {
color: "accent",
text: ""
@@ -86,28 +73,6 @@
}
}
},
methods: {
sendGeneralSettings() {
this.axios.post("http://" + this.$address + "/api/settings/general", this.settings).then(
function (response) {
if (response.status === 200) {
this.snackbar = {
color: "success",
text: "Settings updated successfully"
};
this.snack = true;
}
},
function (error) {
this.snackbar = {
color: "error",
text: (error.response || {data: "Couldn't save settings"}).data
};
this.snack = true;
}
)
},
}
}
</script>
@@ -121,4 +86,4 @@
height: auto !important;
vertical-align: middle;
}
</style>
</style>

View File

@@ -1,17 +1,97 @@
<template>
<div>
<span>Version: {{ settings.version }}</span>
&mdash;
<span>Hardware model: {{ settings.hardwareModel }}</span>
&mdash;
<span>Platform: {{ settings.hardwarePlatform }}</span>
&mdash;
<span>GPU Acceleration: {{ settings.gpuAcceleration ? "Enabled" : "Unsupported" }}{{ settings.gpuAcceleration ? " (" + settings.gpuAcceleration + " mode)" : "" }}</span>
<v-row class="pa-4">
<table class="infoTable">
<tr>
<th class="infoElem">
Version
</th>
<th class="infoElem">
Hardware Model
</th>
<th class="infoElem">
Platform
</th>
<th class="infoElem">
GPU Acceleration
</th>
</tr>
<tr>
<td class="infoElem">
{{ version.replace(" ", "") }}
</td>
<td class="infoElem">
{{ hwModel.replace(" ", "") }}
</td>
<td class="infoElem">
{{ platform.replace(" ", "") }}
</td>
<td class="infoElem">
{{ gpuAccel.replace(" ", "") }}
</td>
</tr>
</table>
<table class="infoTable">
<tr>
<th class="infoElem">
CPU Usage
</th>
<th class="infoElem">
CPU Temp
</th>
<th class="infoElem">
CPU Memory Usage
</th>
<th class="infoElem">
GPU Memory Usage
</th>
<th class="infoElem">
Disk Usage
</th>
</tr>
<tr v-if="metrics.cpuUtil !== 'N/A'">
<td class="infoElem">
{{ metrics.cpuUtil.replace(" ", "") }}%
</td>
<td class="infoElem">
{{ parseInt(metrics.cpuTemp) }}&deg;&nbsp;C
</td>
<td class="infoElem">
{{ metrics.ramUtil.replace(" ", "") }}MB of {{ metrics.cpuMem }}MB
</td>
<td class="infoElem">
{{ metrics.gpuMemUtil.replace(" ", "") }}MB of {{ metrics.gpuMem }}MB
</td>
<td class="infoElem">
{{ metrics.diskUtilPct.replace(" ", "") }}
</td>
</tr>
<tr v-if="metrics.cpuUtil === 'N/A'">
<td class="infoElem">
---
</td>
<td class="infoElem">
---
</td>
<td class="infoElem">
---
</td>
<td class="infoElem">
---
</td>
<td class="infoElem">
---
</td>
</tr>
</table>
</v-row>
<v-row>
<v-col
cols="12"
sm="6"
lg="3"
md="4"
>
<v-btn
color="secondary"
@@ -19,13 +99,14 @@
>
<v-icon left>
mdi-download
</v-icon> Export Settings
</v-icon>
Export Settings
</v-btn>
</v-col>
<v-col
cols="12"
sm="6"
lg="3"
md="4"
>
<v-btn
color="secondary"
@@ -33,33 +114,50 @@
>
<v-icon left>
mdi-upload
</v-icon> Import Settings
</v-icon>
Import Settings
</v-btn>
</v-col>
<v-col
cols="12"
lg="3"
md="4"
>
<v-btn
color="red"
@click="axios.post('http://' + this.$address + '/api/restartProgram')"
color="secondary"
@click="$refs.offlineUpdate.click()"
>
<v-icon left>
mdi-restart
</v-icon> Restart Photon
mdi-update
</v-icon>
Offline Update
</v-btn>
</v-col>
<v-col
cols="12"
lg="3"
lg="6"
>
<v-btn
color="red"
@click="axios.post('http://' + this.$address + '/api/restartDevice')"
@click="restartProgram()"
>
<v-icon left>
mdi-restart
</v-icon> Restart Device
</v-icon>
Restart PhotonVision
</v-btn>
</v-col>
<v-col
cols="12"
lg="6"
>
<v-btn
color="red"
@click="restartDevice()"
>
<v-icon left>
mdi-restart-alert
</v-icon>
Restart Device
</v-btn>
</v-col>
</v-row>
@@ -76,7 +174,7 @@
<input
ref="importSettings"
type="file"
accept=".zip"
accept=".zip, .json"
style="display: none;"
@change="readImportedSettings"
@@ -88,6 +186,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>
@@ -97,6 +204,7 @@ export default {
data() {
return {
snack: false,
uploadPercentage: 0.0,
snackbar: {
color: "success",
text: ""
@@ -106,9 +214,35 @@ export default {
computed: {
settings() {
return this.$store.state.settings.general;
},
version() {
return `${this.settings.version}`;
},
hwModel() {
if (this.settings.hardwareModel !== '') {
return `${this.settings.hardwareModel}`;
} else {
return `Unknown`;
}
},
platform() {
return `${this.settings.hardwarePlatform}`;
},
gpuAccel() {
return `${this.settings.gpuAcceleration ? "Enabled" : "Unsupported"} ${this.settings.gpuAcceleration ? "(" + this.settings.gpuAcceleration + ")" : ""}`
},
metrics() {
console.log(this.$store.state.metrics);
return this.$store.state.metrics;
}
},
methods: {
restartProgram() {
this.axios.post('http://' + this.$address + '/api/restartProgram', {});
},
restartDevice() {
this.axios.post('http://' + this.$address + '/api/restartDevice', {});
},
readImportedSettings(event) {
let formData = new FormData();
formData.append("zipData", event.target.files[0]);
@@ -116,15 +250,73 @@ 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(() => {
}).catch(err => {
if (err.response) {
this.snackbar = {
color: "error",
text: "Error while uploading settings file! Could not process provided file.",
};
} else if (err.request) {
this.snackbar = {
color: "error",
text: "Error while uploading settings file! No respond to upload attempt.",
};
} else {
this.snackbar = {
color: "error",
text: "Error while uploading settings file!",
};
}
this.snack = true;
});
},
doOfflineUpdate(event) {
this.snackbar = {
color: "secondary",
text: "New Software Upload in Process..."
};
this.snack = true;
let formData = new FormData();
formData.append("jarData", event.target.files[0]);
this.axios.post("http://" + this.$address + "/api/settings/offlineUpdate", formData,
{headers: {"Content-Type": "multipart/form-data"},
onUploadProgress: function( progressEvent ) {
this.uploadPercentage = parseInt( Math.round( ( progressEvent.loaded / progressEvent.total ) * 100 ) );
if(this.uploadPercentage < 99.5){
this.snackbar.text = "New Software Upload in Process, " + this.uploadPercentage + "% complete";
} else {
this.snackbar.text = "Installing uploaded software...";
}
}.bind(this)
}).then(() => {
this.snackbar = {
color: "success",
text: "Settings imported successfully! Program will now exit...",
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;
});
},
}
@@ -135,4 +327,25 @@ export default {
.v-btn {
width: 100%;
}
</style>
.infoTable{
border: 1px solid;
border-collapse: separate;
border-spacing: 0px;
border-radius: 5px;
text-align: left;
margin-bottom: 10px;
width: 100%;
display: block;
overflow-x: auto;
}
.infoElem {
padding-right: 15px;
padding-bottom: 1px;
padding-top: 1px;
padding-left: 10px;
border-right: 1px solid;
}
</style>

View File

@@ -1,14 +1,13 @@
<template>
<div>
<CVslider
v-model="settings.brightness"
v-model="enabledLEDPercentage"
class="pt-2"
slider-cols="12"
name="Brightness"
min="0"
max="100"
@input="handleData('accuracy')"
@rollback="e => rollback('accuracy', e)"
@input="handleData('enabledLEDPercentage')"
/>
</div>
</template>
@@ -22,6 +21,14 @@
CVslider,
},
computed: {
enabledLEDPercentage: {
get() {
return this.settings.brightness
},
set(value) {
this.$store.commit("mutateEnabledLEDPercentage", value)
}
},
isDHCP() {
return this.settings.connectionType === 0;
},
@@ -34,4 +41,4 @@
<style lang="" scoped>
</style>
</style>

View File

@@ -1,101 +1,283 @@
<template>
<div>
<CVnumberinput
v-model="settings.teamNumber"
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']"
/>
<template v-if="$store.state.settings.networkSettings.supported">
<v-form
ref="form"
v-model="valid"
>
<CVnumberinput
v-model="teamNumber"
:disabled="settings.runNTServer"
name="Team Number"
:rules="[v => (v > 0) || 'Team number must be greater than zero', v => (v < 10000) || 'Team number must have fewer than five digits']"
class="mb-4"
:label-cols="$vuetify.breakpoint.mdAndUp ? undefined : 5"
/>
<v-banner
v-show="(teamNumber < 1 || teamNumber > 10000) && !runNTServer"
rounded
color="red"
text-color="white"
>
Team number is unset or invalid. NetworkTables will not be able to connect.
</v-banner>
<CVradio
v-model="settings.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']"
/>
<template v-if="!isDHCP">
<CVinput
v-model="settings.ip"
:input-cols="inputCols"
:rules="[v => isIPv4(v) || 'Invalid IPv4 address']"
name="IP"
/>
</template>
</template>
<CVinput
v-model="settings.hostname"
:input-cols="inputCols"
:rules="[v => isHostname(v) || 'Invalid hostname']"
name="Hostname"
/>
<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"
/>
<CVSwitch
v-model="runNTServer"
name="Run NetworkTables Server (Debugging Only)"
tooltip="If enabled, this device will create a NT server. This is useful for home debugging, but should be disabled on-robot."
class="mt-3 mb-3"
:text-cols="$vuetify.breakpoint.mdAndUp ? undefined : 5"
/>
<v-banner
v-show="runNTServer"
rounded
color="red"
text-color="white"
>
This switch is intended for testing; it should be off on a robot. PhotonLib will NOT work!
</v-banner>
</v-form>
<v-btn
color="accent"
:class="runNTServer ? 'mt-3' : ''"
style="color: black; width: 100%;"
:disabled="!valid && !runNTServer"
@click="sendGeneralSettings()"
>
Save
</v-btn>
<v-divider class="mt-4 mb-4" />
<v-row>
<v-col
cols="12"
sm="6"
>
<v-simple-table
fixed-header
height="100%"
dense
>
<template v-slot:default>
<thead style="font-size: 1.25rem;">
<tr>
<th>
Device IPs
</th>
</tr>
</thead>
<tbody>
<tr
v-for="(value, index) in $store.state.networkInfo.deviceips"
:key="index"
>
<td>{{ value }}</td>
</tr>
</tbody>
</template>
</v-simple-table>
</v-col>
<v-col
cols="12"
sm="6"
>
<v-simple-table
fixed-header
height="100%"
dense
>
<template v-slot:default>
<thead style="font-size: 1.25rem;">
<tr>
<th>
Possible RoboRIOs
</th>
</tr>
</thead>
<tbody>
<tr
v-for="(value, index) in $store.state.networkInfo.possibleRios"
:key="index"
>
<td>{{ value }}</td>
</tr>
</tbody>
</template>
</v-simple-table>
</v-col>
</v-row>
</div>
</template>
<script>
import CVnumberinput from '../../components/common/cv-number-input'
import CVradio from '../../components/common/cv-radio'
import CVinput from '../../components/common/cv-input'
import CVnumberinput from '../../components/common/cv-number-input'
import CVradio from '../../components/common/cv-radio'
import CVinput from '../../components/common/cv-input'
import CVSwitch from "@/components/common/cv-switch";
// https://stackoverflow.com/a/17871737
const ipv4Regex = /^((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])$/;
// https://stackoverflow.com/a/18494710
const hostnameRegex = /^([a-zA-Z0-9]+(-[a-zA-Z0-9]+)*)+(\.([a-zA-Z0-9]+(-[a-zA-Z0-9]+)*))*$/;
// https://stackoverflow.com/a/17871737
const ipv4Regex = /^((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])$/;
// https://stackoverflow.com/a/18494710
const hostnameRegex = /^([a-zA-Z0-9]+(-[a-zA-Z0-9]+)*)+(\.([a-zA-Z0-9]+(-[a-zA-Z0-9]+)*))*$/;
export default {
name: 'Networking',
components: {
CVnumberinput,
CVradio,
CVinput
export default {
name: 'Networking',
components: {
CVSwitch,
CVnumberinput,
CVradio,
CVinput
},
data() {
return {
file: undefined,
snackbar: {
color: "success",
text: ""
},
snack: false,
isLoading: false,
valid: true, // Are all settings valid
}
},
computed: {
inputCols() {
return this.$vuetify.breakpoint.mdAndUp ? 10 : 7;
},
data() {
return {
file: undefined,
snackbar: {
color: "success",
text: ""
},
snack: false,
isLoading: false
isDHCP() {
return this.settings.connectionType === 0;
},
settings() {
return this.$store.state.settings.networkSettings;
},
teamNumber: {
get() {
return this.settings.teamNumber
},
set(value) {
this.$store.commit('mutateNetworkSettings', {['teamNumber']: value || 0});
}
},
runNTServer: {
get() {
return this.settings.runNTServer
},
set(value) {
this.$store.commit('mutateNetworkSettings', {['runNTServer']: value});
}
},
computed: {
inputCols() {
return this.$vuetify.breakpoint.smAndUp ? 10 : 7;
connectionType: {
get() {
return this.settings.connectionType
},
isDHCP() {
return this.settings.connectionType === 0;
},
settings() {
return this.$store.state.settings.networkSettings;
set(value) {
this.$store.commit('mutateNetworkSettings', {['connectionType']: value});
}
},
methods: {
isIPv4(v) {
return ipv4Regex.test(v);
staticIp: {
get() {
return this.settings.staticIp
},
isHostname(v) {
return hostnameRegex.test(v);
set(value) {
this.$store.commit('mutateNetworkSettings', {['staticIp']: value});
}
},
hostname: {
get() {
return this.settings.hostname
},
// https://www.freesoft.org/CIE/Course/Subnet/6.htm
// https://stackoverflow.com/a/13957228
isSubnetMask(v) {
// Has to be valid IPv4 so we'll start here
if (!this.isIPv4(v)) return false;
set(value) {
this.$store.commit('mutateNetworkSettings', {['hostname']: value});
}
},
},
methods: {
isIPv4(v) {
return ipv4Regex.test(v);
},
isHostname(v) {
return hostnameRegex.test(v);
},
// https://www.freesoft.org/CIE/Course/Subnet/6.htm
// https://stackoverflow.com/a/13957228
isSubnetMask(v) {
// Has to be valid IPv4 so we'll start here
if (!this.isIPv4(v)) return false;
let octets = v.split(".").map(it => Number(it));
let restAreOnes = false;
for (let i = 3; i >= 0; i--) {
let octets = v.split(".").map(it => Number(it));
let restAreOnes = false;
for (let i = 3; i >= 0; i--) {
for (let j = 0; j < 8; j++) {
let bitValue = (octets[i] >>> j & 1) == 1;
if (restAreOnes && !bitValue)
return false;
restAreOnes = bitValue;
let bitValue = (octets[i] >>> j & 1) == 1;
if (restAreOnes && !bitValue)
return false;
restAreOnes = bitValue;
}
}
return true;
}
return true;
},
}
sendGeneralSettings() {
this.axios.post("http://" + this.$address + "/api/settings/general", this.settings).then(
function (response) {
if (response.status === 200) {
this.snackbar = {
color: "success",
text: "Settings updated successfully"
};
this.snack = true;
}
},
function (error) {
this.snackbar = {
color: "error",
text: (error.response || {data: "Couldn't save settings"}).data
};
this.snack = true;
}
)
},
},
}
</script>
<style lang="" scoped>
<style scoped>
.v-data-table {
/*text-align: center;*/
background-color: transparent !important;
width: 100%;
height: 100%;
overflow-y: auto;
}
.v-data-table th {
background-color: #006492 !important;
}
.v-data-table th, td {
font-size: 1rem !important;
}
.v-data-table td {
font-family: monospace !important;
}
</style>

13
photon-core/.gitignore vendored Normal file
View File

@@ -0,0 +1,13 @@
bin/*
.settings/*
.project
.classpath
*.prefs
.gradle
.gradle/*
build
build/*
photonvision/*
photonvision_config/*
src/main/java/org/photonvision/PhotonVersion.java

33
photon-core/build.gradle Normal file
View File

@@ -0,0 +1,33 @@
import java.nio.file.Path
apply from: "${rootDir}/shared/common.gradle"
dependencies {
implementation project(':photon-targeting')
implementation 'io.javalin:javalin:4.2.0'
implementation 'org.msgpack:msgpack-core:0.9.0'
implementation 'org.msgpack:jackson-dataformat-msgpack:0.9.0'
// wpiutil
jniPlatforms.each { implementation "edu.wpi.first.wpiutil:wpiutil-jni:$wpilibVersion:$it" }
// JOGL stuff (currently we only distribute for aarch64, which is Pi 4)
implementation "org.jogamp.gluegen:gluegen-rt:$joglVersion"
implementation "org.jogamp.jogl:jogl-all:$joglVersion"
implementation "org.jogamp.gluegen:gluegen-rt:$joglVersion:natives-linux-aarch64"
implementation "org.jogamp.jogl:jogl-all:$joglVersion:natives-linux-aarch64"
// Zip
implementation 'org.zeroturnaround:zt-zip:1.14'
}
task writeCurrentVersionJava {
def versionFileIn = file("${rootDir}/shared/PhotonVersion.java.in")
writePhotonVersionFile(versionFileIn, Path.of("$projectDir", "src", "main", "java", "org", "photonvision", "PhotonVersion.java"),
versionString)
}
build.dependsOn writeCurrentVersionJava

View File

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

View File

@@ -1,5 +1,5 @@
/*
* Copyright (C) 2020 Photon Vision.
* 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

View File

@@ -1,5 +1,5 @@
/*
* Copyright (C) 2020 Photon Vision.
* 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
@@ -20,7 +20,7 @@ package org.photonvision.common.configuration;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonProperty;
import edu.wpi.first.wpilibj.geometry.Rotation2d;
import edu.wpi.first.math.geometry.Rotation2d;
import java.util.ArrayList;
import java.util.List;
import org.photonvision.common.logging.LogGroup;
@@ -49,10 +49,11 @@ public class CameraConfiguration {
public CameraType cameraType = CameraType.UsbCamera;
public double FOV = 70;
public final List<CameraCalibrationCoefficients> calibrations;
public List<Integer> cameraLeds = new ArrayList<>();
public int currentPipelineIndex = 0;
public Rotation2d camPitch = new Rotation2d();
public int streamIndex = 0; // 0 index means ports [1181, 1182], 1 means [1183, 1184], etc...
@JsonIgnore // this ignores the pipes as we serialize them to their own subfolder
public List<CVPipelineSettings> pipelineSettings = new ArrayList<>();
@@ -89,7 +90,6 @@ public class CameraConfiguration {
@JsonProperty("path") String path,
@JsonProperty("cameraType") CameraType cameraType,
@JsonProperty("calibration") List<CameraCalibrationCoefficients> calibrations,
@JsonProperty("cameraLEDs") List<Integer> cameraLeds,
@JsonProperty("currentPipelineIndex") int currentPipelineIndex,
@JsonProperty("camPitch") Rotation2d camPitch) {
this.baseName = baseName;
@@ -99,7 +99,6 @@ public class CameraConfiguration {
this.path = path;
this.cameraType = cameraType;
this.calibrations = calibrations != null ? calibrations : new ArrayList<>();
this.cameraLeds = cameraLeds;
this.currentPipelineIndex = currentPipelineIndex;
this.camPitch = camPitch;

View File

@@ -1,5 +1,5 @@
/*
* Copyright (C) 2020 Photon Vision.
* 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
@@ -22,13 +22,16 @@ import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.text.DateFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.time.temporal.TemporalAccessor;
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;
@@ -40,14 +43,20 @@ public class ConfigManager {
private static final Logger logger = new Logger(ConfigManager.class, LogGroup.General);
private static ConfigManager INSTANCE;
public static final String HW_CFG_FNAME = "hardwareConfig.json";
public static final String HW_SET_FNAME = "hardwareSettings.json";
public static final String NET_SET_FNAME = "networkSettings.json";
private PhotonConfiguration config;
private final File hardwareConfigFile;
private final File hardwareSettingsFile;
private final File networkConfigFile;
private final File camerasFolder;
final File configDirectoryFile;
private long saveRequestTimestamp = -1;
private Thread settingsSaveThread;
public static ConfigManager getInstance() {
if (INSTANCE == null) {
@@ -81,12 +90,15 @@ public class ConfigManager {
ConfigManager(Path configDirectoryFile) {
this.configDirectoryFile = new File(configDirectoryFile.toUri());
this.hardwareConfigFile =
new File(Path.of(configDirectoryFile.toString(), "hardwareConfig.json").toUri());
new File(Path.of(configDirectoryFile.toString(), HW_CFG_FNAME).toUri());
this.hardwareSettingsFile =
new File(Path.of(configDirectoryFile.toString(), HW_SET_FNAME).toUri());
this.networkConfigFile =
new File(Path.of(configDirectoryFile.toString(), "networkSettings.json").toUri());
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() {
@@ -110,6 +122,7 @@ public class ConfigManager {
}
HardwareConfig hardwareConfig;
HardwareSettings hardwareSettings;
NetworkConfig networkConfig;
if (hardwareConfigFile.exists()) {
@@ -129,6 +142,23 @@ public class ConfigManager {
hardwareConfig = new HardwareConfig();
}
if (hardwareSettingsFile.exists()) {
try {
hardwareSettings =
JacksonUtils.deserialize(hardwareSettingsFile.toPath(), HardwareSettings.class);
if (hardwareSettings == null) {
logger.error("Could not deserialize hardware settings! Loading defaults");
hardwareSettings = new HardwareSettings();
}
} catch (IOException e) {
logger.error("Could not deserialize hardware settings! Loading defaults");
hardwareSettings = new HardwareSettings();
}
} else {
logger.info("Hardware settings does not exist! Loading defaults");
hardwareSettings = new HardwareSettings();
}
if (networkConfigFile.exists()) {
try {
networkConfig = JacksonUtils.deserialize(networkConfigFile.toPath(), NetworkConfig.class);
@@ -155,23 +185,25 @@ public class ConfigManager {
HashMap<String, CameraConfiguration> cameraConfigurations = loadCameraConfigs();
this.config = new PhotonConfiguration(hardwareConfig, networkConfig, cameraConfigurations);
this.config =
new PhotonConfiguration(
hardwareConfig, hardwareSettings, networkConfig, cameraConfigurations);
}
public void saveToDisk() {
// Delete old configs
FileUtils.deleteDirectory(camerasFolder.toPath());
try {
JacksonUtils.serialize(hardwareConfigFile.toPath(), config.getHardwareConfig());
} catch (IOException e) {
logger.error("Could not save hardware config!", e);
}
try {
JacksonUtils.serialize(networkConfigFile.toPath(), config.getNetworkConfig());
} catch (IOException e) {
logger.error("Could not save network config!", e);
}
try {
JacksonUtils.serialize(hardwareSettingsFile.toPath(), config.getHardwareSettings());
} catch (IOException e) {
logger.error("Could not save hardware config!", e);
}
// save all of our cameras
var cameraConfigMap = config.getCameraConfigurations();
@@ -301,12 +333,8 @@ public class ConfigManager {
return loadedConfigurations;
}
public void addCameraConfigurations(HashMap<VisionSource, List<CVPipelineSettings>> sources) {
List<CameraConfiguration> list =
sources.keySet().stream()
.map(it -> it.getSettables().getConfiguration())
.collect(Collectors.toList());
getConfig().addCameraConfigs(list);
public void addCameraConfigurations(List<VisionSource> sources) {
getConfig().addCameraConfigs(sources);
requestSave();
}
@@ -330,26 +358,92 @@ public class ConfigManager {
requestSave();
}
public Path getLogsDir() {
return Path.of(configDirectoryFile.toString(), "logs");
}
public Path getCalibDir() {
return Path.of(configDirectoryFile.toString(), "calibImgs");
}
public static final String LOG_PREFIX = "photonvision-";
public static final String LOG_EXT = ".log";
public static final String LOG_DATE_TIME_FORMAT = "yyyy-M-d_hh-mm-ss";
public String taToLogFname(TemporalAccessor date) {
var dateString = DateTimeFormatter.ofPattern(LOG_DATE_TIME_FORMAT).format(date);
return LOG_PREFIX + dateString + LOG_EXT;
}
public Date logFnameToDate(String fname) throws ParseException {
// Strip away known unneded portions of the log file name
fname = fname.replace(LOG_PREFIX, "").replace(LOG_EXT, "");
DateFormat format = new SimpleDateFormat(LOG_DATE_TIME_FORMAT);
return format.parse(fname);
}
public Path getLogPath() {
var dateString = DateTimeFormatter.ofPattern("yyyy-M-d_hh-mm-ss").format(LocalDateTime.now());
var logFile =
Path.of(configDirectoryFile.toString(), "logs", "photonvision-" + dateString + ".log")
.toFile();
var logFile = Path.of(this.getLogsDir().toString(), taToLogFname(LocalDateTime.now())).toFile();
if (!logFile.getParentFile().exists()) logFile.getParentFile().mkdirs();
return logFile.toPath();
}
public Path getImageSavePath() {
var imgFilePath = Path.of(configDirectoryFile.toString(), "imgSaves").toFile();
if (!imgFilePath.exists()) imgFilePath.mkdirs();
return imgFilePath.toPath();
}
public Path getHardwareConfigFile() {
return this.hardwareConfigFile.toPath();
}
public Path getHardwareSettingsFile() {
return this.hardwareSettingsFile.toPath();
}
public Path getNetworkConfigFile() {
return this.networkConfigFile.toPath();
}
public void saveUploadedHardwareConfig(Path uploadPath) {
FileUtils.deleteFile(this.getHardwareConfigFile());
FileUtils.copyFile(uploadPath, this.getHardwareConfigFile());
}
public void saveUploadedHardwareSettings(Path uploadPath) {
FileUtils.deleteFile(this.getHardwareSettingsFile());
FileUtils.copyFile(uploadPath, this.getHardwareSettingsFile());
}
public void saveUploadedNetworkConfig(Path uploadPath) {
FileUtils.deleteFile(this.getNetworkConfigFile());
FileUtils.copyFile(uploadPath, this.getNetworkConfigFile());
}
public void requestSave() {
logger.trace("Requesting save...");
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 semaphor", e);
}
}
}
public void unloadCameraConfigs() {
this.config.getCameraConfigurations().clear();
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright (C) 2020 Photon Vision.
* 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
@@ -17,10 +17,13 @@
package org.photonvision.common.configuration;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
@JsonIgnoreProperties(ignoreUnknown = true)
public class HardwareConfig {
public final String deviceName;
public final String deviceLogoPath;
public final String supportURL;
@@ -29,9 +32,7 @@ public class HardwareConfig {
public final ArrayList<Integer> ledPins;
public final String ledSetCommand;
public final boolean ledsCanDim;
public final ArrayList<Integer> ledPWMRange;
public final String ledPWMSetRange;
public final int ledPWMFrequency;
public final ArrayList<Integer> ledBrightnessRange;
public final String ledDimCommand;
public final String ledBlinkCommand;
public final ArrayList<Integer> statusRGBPins;
@@ -41,12 +42,14 @@ public class HardwareConfig {
public final String cpuMemoryCommand;
public final String cpuUtilCommand;
public final String gpuMemoryCommand;
public final String gpuTempCommand;
public final String ramUtilCommand;
public final String gpuMemUsageCommand;
public final String diskUsageCommand;
// Device stuff
public final String restartHardwareCommand;
public final double vendorFOV; // -1 for unmanaged
public final List<Integer> blacklistedResIndices; // this happens before the defaults are applied
public HardwareConfig() {
deviceName = "";
@@ -55,22 +58,22 @@ public class HardwareConfig {
ledPins = new ArrayList<>();
ledSetCommand = "";
ledsCanDim = false;
ledPWMRange = new ArrayList<>();
ledBrightnessRange = new ArrayList<>();
statusRGBPins = new ArrayList<>();
ledPWMFrequency = 0;
ledPWMSetRange = "";
ledDimCommand = "";
cpuTempCommand = "";
cpuMemoryCommand = "";
cpuUtilCommand = "";
gpuMemoryCommand = "";
gpuTempCommand = "";
ramUtilCommand = "";
ledBlinkCommand = "";
gpuMemUsageCommand = "";
diskUsageCommand = "";
restartHardwareCommand = "";
vendorFOV = -1;
blacklistedResIndices = Collections.emptyList();
}
@SuppressWarnings("unused")
@@ -81,29 +84,27 @@ public class HardwareConfig {
ArrayList<Integer> ledPins,
String ledSetCommand,
boolean ledsCanDim,
ArrayList<Integer> statusRGBPins,
String ledPWMSetRange,
int ledPWMFrequency,
ArrayList<Integer> ledBrightnessRange,
String ledDimCommand,
String ledBlinkCommand,
ArrayList<Integer> ledPWMRange,
ArrayList<Integer> statusRGBPins,
String cpuTempCommand,
String cpuMemoryCommand,
String cpuUtilCommand,
String gpuMemoryCommand,
String gpuTempCommand,
String ramUtilCommand,
String gpuMemUsageCommand,
String diskUsageCommand,
String restartHardwareCommand,
double vendorFOV) {
double vendorFOV,
List<Integer> blacklistedResIndices) {
this.deviceName = deviceName;
this.deviceLogoPath = deviceLogoPath;
this.supportURL = supportURL;
this.ledPins = ledPins;
this.ledSetCommand = ledSetCommand;
this.ledsCanDim = ledsCanDim;
this.ledPWMRange = ledPWMRange;
this.ledPWMSetRange = ledPWMSetRange;
this.ledPWMFrequency = ledPWMFrequency;
this.ledBrightnessRange = ledBrightnessRange;
this.ledDimCommand = ledDimCommand;
this.ledBlinkCommand = ledBlinkCommand;
this.statusRGBPins = statusRGBPins;
@@ -111,10 +112,12 @@ public class HardwareConfig {
this.cpuMemoryCommand = cpuMemoryCommand;
this.cpuUtilCommand = cpuUtilCommand;
this.gpuMemoryCommand = gpuMemoryCommand;
this.gpuTempCommand = gpuTempCommand;
this.ramUtilCommand = ramUtilCommand;
this.gpuMemUsageCommand = gpuMemUsageCommand;
this.diskUsageCommand = diskUsageCommand;
this.restartHardwareCommand = restartHardwareCommand;
this.vendorFOV = vendorFOV;
this.blacklistedResIndices = blacklistedResIndices;
}
public final boolean hasPresetFOV() {

View File

@@ -1,5 +1,5 @@
/*
* Copyright (C) 2020 Photon Vision.
* 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
@@ -17,15 +17,6 @@
package org.photonvision.common.configuration;
public enum StreamDivisor {
NONE(1),
HALF(2),
QUARTER(4),
SIXTH(6);
public final Integer value;
StreamDivisor(int value) {
this.value = value;
}
public class HardwareSettings {
public int ledBrightnessPercentage = 100;
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright (C) 2020 Photon Vision.
* 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
@@ -17,35 +17,42 @@
package org.photonvision.common.configuration;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonGetter;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonSetter;
import java.util.HashMap;
import java.util.Map;
import org.photonvision.common.hardware.Platform;
import org.photonvision.common.networking.NetworkMode;
public class NetworkConfig {
public int teamNumber = -1;
public int teamNumber = 0;
public NetworkMode connectionType = NetworkMode.DHCP;
public String staticIp = "";
public String netmask = "";
public String hostname = "photonvision";
public boolean runNTServer = false;
// TODO implement networking
public boolean shouldManage;
private boolean shouldManage;
public NetworkConfig() {}
public NetworkConfig() {
setShouldManage(false);
}
@JsonCreator
public NetworkConfig(
int teamNumber,
NetworkMode connectionType,
String staticIp,
String netmask,
String hostname,
boolean shouldManage) {
@JsonProperty("teamNumber") int teamNumber,
@JsonProperty("connectionType") NetworkMode connectionType,
@JsonProperty("staticIp") String staticIp,
@JsonProperty("hostname") String hostname,
@JsonProperty("runNTServer") boolean runNTServer,
@JsonProperty("shouldManage") boolean shouldManage) {
this.teamNumber = teamNumber;
this.connectionType = connectionType;
this.staticIp = staticIp;
this.netmask = netmask;
this.hostname = hostname;
this.shouldManage = shouldManage;
this.runNTServer = runNTServer;
setShouldManage(shouldManage);
}
public static NetworkConfig fromHashMap(Map<String, Object> map) {
@@ -53,22 +60,32 @@ public class NetworkConfig {
// staticIp (str), netmask (str), hostname (str)
var ret = new NetworkConfig();
ret.teamNumber = Integer.parseInt(map.get("teamNumber").toString());
ret.shouldManage = (Boolean) map.get("supported");
ret.connectionType = NetworkMode.values()[(Integer) map.get("connectionType")];
ret.staticIp = (String) map.get("staticIp");
ret.netmask = (String) map.get("netmask");
ret.hostname = (String) map.get("hostname");
ret.runNTServer = (Boolean) map.get("runNTServer");
ret.setShouldManage((Boolean) map.get("supported"));
return ret;
}
public HashMap<String, Object> toHashMap() {
HashMap<String, Object> tmp = new HashMap<>();
tmp.put("teamNumber", teamNumber);
tmp.put("supported", shouldManage);
tmp.put("supported", shouldManage());
tmp.put("connectionType", connectionType.ordinal());
tmp.put("staticIp", staticIp);
tmp.put("netmask", netmask);
tmp.put("hostname", hostname);
tmp.put("runNTServer", runNTServer);
return tmp;
}
@JsonGetter("shouldManage")
public boolean shouldManage() {
return this.shouldManage || Platform.isRaspberryPi();
}
@JsonSetter("shouldManage")
public void setShouldManage(boolean shouldManage) {
this.shouldManage = shouldManage || Platform.isRaspberryPi();
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright (C) 2020 Photon Vision.
* 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
@@ -17,17 +17,44 @@
package org.photonvision.common.configuration;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import org.photonvision.PhotonVersion;
import org.photonvision.common.hardware.Platform;
import org.photonvision.common.util.SerializationUtils;
import org.photonvision.raspi.PicamJNI;
import org.photonvision.vision.processes.VisionModule;
import org.photonvision.vision.processes.VisionModuleManager;
import org.photonvision.vision.processes.VisionSource;
// TODO rename this class
public class PhotonConfiguration {
private HardwareConfig hardwareConfig;
private HardwareSettings hardwareSettings;
private NetworkConfig networkConfig;
private HashMap<String, CameraConfiguration> cameraConfigurations;
public PhotonConfiguration(
HardwareConfig hardwareConfig,
HardwareSettings hardwareSettings,
NetworkConfig networkConfig) {
this(hardwareConfig, hardwareSettings, networkConfig, new HashMap<>());
}
public PhotonConfiguration(
HardwareConfig hardwareConfig,
HardwareSettings hardwareSettings,
NetworkConfig networkConfig,
HashMap<String, CameraConfiguration> cameraConfigurations) {
this.hardwareConfig = hardwareConfig;
this.hardwareSettings = hardwareSettings;
this.networkConfig = networkConfig;
this.cameraConfigurations = cameraConfigurations;
}
public HardwareConfig getHardwareConfig() {
return hardwareConfig;
}
@@ -36,6 +63,10 @@ public class PhotonConfiguration {
return networkConfig;
}
public HardwareSettings getHardwareSettings() {
return hardwareSettings;
}
public void setNetworkConfig(NetworkConfig networkConfig) {
this.networkConfig = networkConfig;
}
@@ -44,9 +75,9 @@ public class PhotonConfiguration {
return cameraConfigurations;
}
public void addCameraConfigs(List<CameraConfiguration> config) {
for (var c : config) {
addCameraConfig(c);
public void addCameraConfigs(Collection<VisionSource> sources) {
for (var s : sources) {
addCameraConfig(s.getCameraConfiguration());
}
}
@@ -58,25 +89,6 @@ public class PhotonConfiguration {
cameraConfigurations.put(name, config);
}
private HardwareConfig hardwareConfig;
private NetworkConfig networkConfig;
private HashMap<String, CameraConfiguration> cameraConfigurations;
public PhotonConfiguration(HardwareConfig hardwareConfig, NetworkConfig networkConfig) {
this(hardwareConfig, networkConfig, new HashMap<>());
}
public PhotonConfiguration(
HardwareConfig hardwareConfig,
NetworkConfig networkConfig,
HashMap<String, CameraConfiguration> cameraConfigurations) {
this.hardwareConfig = hardwareConfig;
this.networkConfig = networkConfig;
this.cameraConfigurations = cameraConfigurations;
}
public Map<String, Object> toHashMap() {
Map<String, Object> map = new HashMap<>();
var settingsSubmap = new HashMap<String, Object>();
@@ -89,20 +101,31 @@ public class PhotonConfiguration {
.map(SerializationUtils::objectToHashMap)
.collect(Collectors.toList()));
settingsSubmap.put("lighting", SerializationUtils.objectToHashMap(hardwareConfig));
var lightingConfig = new UILightingConfig();
lightingConfig.brightness = hardwareSettings.ledBrightnessPercentage;
lightingConfig.supported = (hardwareConfig.ledPins.size() != 0);
settingsSubmap.put("lighting", SerializationUtils.objectToHashMap(lightingConfig));
var generalSubmap = new HashMap<String, Object>();
generalSubmap.put("version", PhotonVersion.versionString);
generalSubmap.put("gpuAcceleration", false); // TODO gpu accel and accel type
generalSubmap.put("gpuAccelerationType", "Unknown");
generalSubmap.put("hardwareModel", "Unknown"); // TODO hardware model and platform
generalSubmap.put("hardwarePlatform", "Unknown");
generalSubmap.put(
"gpuAcceleration",
PicamJNI.isSupported()
? "Zerocopy MMAL on " + PicamJNI.getSensorModel().getFriendlyName()
: ""); // TODO add support for other types of GPU accel
generalSubmap.put("hardwareModel", hardwareConfig.deviceName);
generalSubmap.put("hardwarePlatform", Platform.getCurrentPlatform().toString());
settingsSubmap.put("general", generalSubmap);
map.put("settings", settingsSubmap);
return map;
}
public static class UILightingConfig {
public int brightness = 0;
public boolean supported = true;
}
public static class UICameraConfiguration {
@SuppressWarnings("unused")
public double fov, tiltDegrees;

View File

@@ -1,5 +1,5 @@
/*
* Copyright (C) 2020 Photon Vision.
* 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

View File

@@ -1,5 +1,5 @@
/*
* Copyright (C) 2020 Photon Vision.
* 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

View File

@@ -1,5 +1,5 @@
/*
* Copyright (C) 2020 Photon Vision.
* 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
@@ -27,7 +27,6 @@ import org.photonvision.common.logging.Logger;
@SuppressWarnings("rawtypes")
public class DataChangeService {
private static final Logger logger = new Logger(DataChangeService.class, LogGroup.WebServer);
private static class ThreadSafeSingleton {

View File

@@ -1,5 +1,5 @@
/*
* Copyright (C) 2020 Photon Vision.
* 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

View File

@@ -1,5 +1,5 @@
/*
* Copyright (C) 2020 Photon Vision.
* 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

View File

@@ -1,5 +1,5 @@
/*
* Copyright (C) 2020 Photon Vision.
* 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

View File

@@ -1,5 +1,5 @@
/*
* Copyright (C) 2020 Photon Vision.
* 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

View File

@@ -1,5 +1,5 @@
/*
* Copyright (C) 2020 Photon Vision.
* 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
@@ -23,7 +23,6 @@ import org.photonvision.common.dataflow.DataChangeDestination;
import org.photonvision.common.dataflow.DataChangeSource;
public class IncomingWebSocketEvent<T> extends DataChangeEvent<T> {
public final Integer cameraIndex;
public final WsContext originContext;

View File

@@ -1,5 +1,5 @@
/*
* Copyright (C) 2020 Photon Vision.
* 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
@@ -34,6 +34,13 @@ public class OutgoingUIEvent<T> extends DataChangeEvent<T> {
this.originContext = originContext;
}
public static OutgoingUIEvent<HashMap<String, Object>> wrappedOf(
String commandName, Object value) {
HashMap<String, Object> data = new HashMap<>();
data.put(commandName, value);
return new OutgoingUIEvent<>(commandName, data);
}
public static OutgoingUIEvent<HashMap<String, Object>> wrappedOf(
String commandName, String propertyName, Object value, WsContext originContext) {
HashMap<String, Object> data = new HashMap<>();

View File

@@ -1,5 +1,5 @@
/*
* Copyright (C) 2020 Photon Vision.
* 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
@@ -23,7 +23,6 @@ import edu.wpi.first.networktables.NetworkTableEntry;
import java.util.function.Consumer;
public class NTDataChangeListener {
private final NetworkTableEntry watchedEntry;
private final int listenerID;

View File

@@ -1,5 +1,5 @@
/*
* Copyright (C) 2020 Photon Vision.
* 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
@@ -20,16 +20,21 @@ package org.photonvision.common.dataflow.networktables;
import edu.wpi.first.networktables.EntryNotification;
import edu.wpi.first.networktables.NetworkTable;
import edu.wpi.first.networktables.NetworkTableEntry;
import java.util.ArrayList;
import java.util.List;
import java.util.function.BooleanSupplier;
import java.util.function.Consumer;
import java.util.function.Supplier;
import org.opencv.core.Point;
import org.photonvision.common.dataflow.CVPipelineResultConsumer;
import org.photonvision.common.dataflow.structures.Packet;
import org.photonvision.targeting.PhotonPipelineResult;
import org.photonvision.targeting.PhotonTrackedTarget;
import org.photonvision.targeting.TargetCorner;
import org.photonvision.vision.pipeline.result.CVPipelineResult;
import org.photonvision.vision.pipeline.result.SimplePipelineResult;
import org.photonvision.vision.target.TrackedTarget;
public class NTDataPublisher implements CVPipelineResultConsumer {
private final NetworkTable rootTable = NetworkTablesManager.getInstance().kRootTable;
private NetworkTable subTable;
private NetworkTableEntry rawBytesEntry;
@@ -49,6 +54,10 @@ public class NTDataPublisher implements CVPipelineResultConsumer {
private NetworkTableEntry targetPoseEntry;
private NetworkTableEntry targetSkewEntry;
// The raw position of the best target, in pixels.
private NetworkTableEntry bestTargetPosX;
private NetworkTableEntry bestTargetPosY;
private final Supplier<Integer> pipelineIndexSupplier;
private final BooleanSupplier driverModeSupplier;
@@ -71,6 +80,12 @@ public class NTDataPublisher implements CVPipelineResultConsumer {
var newIndex = (int) entryNotification.value.getDouble();
var originalIndex = pipelineIndexSupplier.get();
// ignore indexes below 0
if (newIndex < 0) {
pipelineIndexEntry.forceSetNumber(originalIndex);
return;
}
if (newIndex == originalIndex) {
// TODO: Log
return;
@@ -98,6 +113,7 @@ public class NTDataPublisher implements CVPipelineResultConsumer {
// TODO: Log
}
@SuppressWarnings("DuplicatedCode")
private void removeEntries() {
if (rawBytesEntry != null) rawBytesEntry.delete();
if (pipelineIndexListener != null) pipelineIndexListener.remove();
@@ -111,6 +127,8 @@ public class NTDataPublisher implements CVPipelineResultConsumer {
if (targetYawEntry != null) targetYawEntry.delete();
if (targetPoseEntry != null) targetPoseEntry.delete();
if (targetSkewEntry != null) targetSkewEntry.delete();
if (bestTargetPosX != null) bestTargetPosX.delete();
if (bestTargetPosY != null) bestTargetPosY.delete();
}
private void updateEntries() {
@@ -137,6 +155,9 @@ public class NTDataPublisher implements CVPipelineResultConsumer {
targetYawEntry = subTable.getEntry("targetYaw");
targetPoseEntry = subTable.getEntry("targetPose");
targetSkewEntry = subTable.getEntry("targetSkew");
bestTargetPosX = subTable.getEntry("targetPixelsX");
bestTargetPosY = subTable.getEntry("targetPixelsY");
}
public void updateCameraNickname(String newCameraNickname) {
@@ -147,7 +168,9 @@ public class NTDataPublisher implements CVPipelineResultConsumer {
@Override
public void accept(CVPipelineResult result) {
var simplified = new SimplePipelineResult(result);
var simplified =
new PhotonPipelineResult(
result.getLatencyMillis(), simpleFromTrackedTargets(result.targets));
Packet packet = new Packet(simplified.getPacketSize());
simplified.populatePacket(packet);
@@ -170,13 +193,40 @@ public class NTDataPublisher implements CVPipelineResultConsumer {
var poseY = bestTarget.getCameraToTarget().getTranslation().getY();
var poseRot = bestTarget.getCameraToTarget().getRotation().getDegrees();
targetPoseEntry.forceSetDoubleArray(new double[] {poseX, poseY, poseRot});
var targetOffsetPoint = bestTarget.getTargetOffsetPoint();
bestTargetPosX.forceSetDouble(targetOffsetPoint.x);
bestTargetPosY.forceSetDouble(targetOffsetPoint.y);
} else {
targetPitchEntry.forceSetDouble(0);
targetYawEntry.forceSetDouble(0);
targetAreaEntry.forceSetDouble(0);
targetSkewEntry.forceSetDouble(0);
targetPoseEntry.forceSetDoubleArray(new double[] {0, 0, 0});
bestTargetPosX.forceSetDouble(0);
bestTargetPosY.forceSetDouble(0);
}
rootTable.getInstance().flush();
}
public static List<PhotonTrackedTarget> simpleFromTrackedTargets(List<TrackedTarget> targets) {
var ret = new ArrayList<PhotonTrackedTarget>();
for (var t : targets) {
var points = new Point[4];
t.getMinAreaRect().points(points);
var cornerList = new ArrayList<TargetCorner>();
for (int i = 0; i < 4; i++) cornerList.add(new TargetCorner(points[i].x, points[i].y));
ret.add(
new PhotonTrackedTarget(
t.getYaw(),
t.getPitch(),
t.getArea(),
t.getSkew(),
t.getCameraToTarget(),
cornerList));
}
return ret;
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright (C) 2020 Photon Vision.
* 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
@@ -20,15 +20,19 @@ package org.photonvision.common.dataflow.networktables;
import edu.wpi.first.networktables.LogMessage;
import edu.wpi.first.networktables.NetworkTable;
import edu.wpi.first.networktables.NetworkTableInstance;
import java.util.HashMap;
import java.util.function.Consumer;
import org.photonvision.PhotonVersion;
import org.photonvision.common.configuration.NetworkConfig;
import org.photonvision.common.dataflow.DataChangeService;
import org.photonvision.common.dataflow.events.OutgoingUIEvent;
import org.photonvision.common.logging.LogGroup;
import org.photonvision.common.logging.Logger;
import org.photonvision.common.scripting.ScriptEventType;
import org.photonvision.common.scripting.ScriptManager;
import org.photonvision.common.util.TimedTaskManager;
public class NetworkTablesManager {
private final NetworkTableInstance ntInstance = NetworkTableInstance.getDefault();
private final String kRootTableName = "/photonvision";
public final NetworkTable kRootTable = ntInstance.getTable(kRootTableName);
@@ -46,10 +50,7 @@ public class NetworkTablesManager {
private static final Logger logger = new Logger(NetworkTablesManager.class, LogGroup.General);
public boolean isServer = false;
private static class NTLogger implements Consumer<LogMessage> {
private boolean hasReportedConnectionFailure = false;
private long lastConnectMessageMillis = 0;
@@ -58,42 +59,74 @@ public class NetworkTablesManager {
if (!hasReportedConnectionFailure && logMessage.message.contains("timed out")) {
logger.error("NT Connection has failed! Will retry in background.");
hasReportedConnectionFailure = true;
getInstance().broadcastConnectedStatus();
} else if (logMessage.message.contains("connected")
&& System.currentTimeMillis() - lastConnectMessageMillis > 125) {
logger.info("NT Connected!");
hasReportedConnectionFailure = false;
lastConnectMessageMillis = System.currentTimeMillis();
ScriptManager.queueEvent(ScriptEventType.kNTConnected);
getInstance().broadcastVersion();
getInstance().broadcastConnectedStatus();
}
}
}
public void setConfig(NetworkConfig config) {
if (config.teamNumber > 0) {
setClientMode(config.teamNumber);
} else {
setServerMode();
public void broadcastConnectedStatus() {
TimedTaskManager.getInstance().addOneShotTask(this::broadcastConnectedStatusImpl, 1000L);
}
private void broadcastConnectedStatusImpl() {
HashMap<String, Object> map = new HashMap<>();
var subMap = new HashMap<String, Object>();
subMap.put("connected", ntInstance.isConnected());
if (ntInstance.isConnected()) {
var connections = getInstance().ntInstance.getConnections();
if (connections.length > 0) {
subMap.put("address", connections[0].remote_ip + ":" + connections[0].remote_port);
}
subMap.put("clients", connections.length);
}
map.put("ntConnectionInfo", subMap);
DataChangeService.getInstance()
.publishEvent(new OutgoingUIEvent<>("networkTablesConnected", map));
}
private void broadcastVersion() {
kRootTable.getEntry("version").setString(PhotonVersion.versionString);
kRootTable.getEntry("buildDate").setString(PhotonVersion.buildDate);
}
public void setConfig(NetworkConfig config) {
if (config.runNTServer) {
setServerMode();
} else {
setClientMode(config.teamNumber);
}
broadcastVersion();
}
private void setClientMode(int teamNumber) {
isServer = false;
logger.info("Starting NT Client");
ntInstance.stopServer();
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();
}
private void setServerMode() {
isServer = true;
logger.info("Starting NT Server");
ntInstance.stopClient();
ntInstance.startServer();
broadcastVersion();
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright (C) 2020 Photon Vision.
* 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
@@ -17,14 +17,13 @@
package org.photonvision.common.dataflow.websocket;
import com.fasterxml.jackson.core.JsonProcessingException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import org.photonvision.common.dataflow.CVPipelineResultConsumer;
import org.photonvision.common.dataflow.DataChangeService;
import org.photonvision.common.dataflow.events.OutgoingUIEvent;
import org.photonvision.common.logging.LogGroup;
import org.photonvision.common.logging.Logger;
import org.photonvision.server.SocketHandler;
import org.photonvision.vision.pipeline.result.CVPipelineResult;
public class UIDataPublisher implements CVPipelineResultConsumer {
@@ -39,15 +38,17 @@ public class UIDataPublisher implements CVPipelineResultConsumer {
@Override
public void accept(CVPipelineResult result) {
var now = System.currentTimeMillis();
long now = System.currentTimeMillis();
var dataMap = new HashMap<String, Object>();
dataMap.put("latency", result.getLatencyMillis());
// only update the UI at 15hz
if (lastUIResultUpdateTime + 1000.0 / 10.0 > now) return;
var uiMap = new HashMap<Integer, HashMap<String, Object>>();
var dataMap = new HashMap<String, Object>();
dataMap.put("latency", result.getLatencyMillis());
dataMap.put("fps", result.fps);
var targets = result.targets;
@@ -56,18 +57,10 @@ public class UIDataPublisher implements CVPipelineResultConsumer {
uiTargets.add(t.toHashMap());
}
dataMap.put("targets", uiTargets);
uiMap.put(index, dataMap);
var retMap = new HashMap<String, Object>();
retMap.put("updatePipelineResult", uiMap);
try {
SocketHandler.getInstance().broadcastMessage(retMap, null);
} catch (JsonProcessingException e) {
logger.error(e.getMessage());
logger.error(Arrays.toString(e.getStackTrace()));
}
DataChangeService.getInstance()
.publishEvent(OutgoingUIEvent.wrappedOf("updatePipelineResult", uiMap));
lastUIResultUpdateTime = now;
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright (C) 2020 Photon Vision.
* 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
@@ -17,15 +17,11 @@
package org.photonvision.common.hardware.GPIO;
import java.util.ArrayList;
import java.util.List;
import org.photonvision.common.configuration.HardwareConfig;
import org.photonvision.common.hardware.Platform;
public class CustomGPIO extends GPIOBase {
private boolean currentState;
private List<Integer> pwmRange = new ArrayList<>();
private final int port;
public CustomGPIO(int port) {
@@ -75,22 +71,6 @@ public class CustomGPIO extends GPIOBase {
return currentState;
}
@Override
public void setPwmRangeImpl(List<Integer> range) {
execute(
commands
.get("setRange")
.replace("{lower_range}", String.valueOf(range.get(0)))
.replace("{upper_range}", String.valueOf(range.get(1)))
.replace("{p}", String.valueOf(port)));
pwmRange = range;
}
@Override
public List<Integer> getPwmRangeImpl() {
return pwmRange;
}
@Override
public void blinkImpl(int pulseTimeMillis, int blinks) {
execute(
@@ -103,8 +83,6 @@ public class CustomGPIO extends GPIOBase {
@Override
public void setBrightnessImpl(int brightness) {
// Check to see if dimValue is within the range
if (brightness < pwmRange.get(0) || brightness > pwmRange.get(1)) return;
execute(
commands
.get("dim")
@@ -115,7 +93,6 @@ public class CustomGPIO extends GPIOBase {
public static void setConfig(HardwareConfig config) {
if (Platform.isRaspberryPi()) return;
commands.replace("setState", config.ledSetCommand);
commands.replace("setRange", config.ledPWMSetRange);
commands.replace("dim", config.ledDimCommand);
commands.replace("blink", config.ledBlinkCommand);
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright (C) 2020 Photon Vision.
* 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
@@ -19,7 +19,6 @@ package org.photonvision.common.hardware.GPIO;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import org.photonvision.common.logging.LogGroup;
import org.photonvision.common.logging.Logger;
import org.photonvision.common.util.ShellExec;
@@ -32,7 +31,6 @@ public abstract class GPIOBase {
new HashMap<>() {
{
put("setState", "");
put("setRange", "");
put("shutdown", "");
put("dim", "");
put("blink", "");
@@ -81,22 +79,6 @@ public abstract class GPIOBase {
public abstract boolean getStateImpl();
public final void setPwmRange(List<Integer> range) {
if (getPinNumber() != -1) {
setPwmRangeImpl(range);
}
}
protected abstract void setPwmRangeImpl(List<Integer> range);
public final List<Integer> getPwmRange() {
if (getPinNumber() != -1) {
return getPwmRangeImpl();
} else return List.of(0, 255);
}
protected abstract List<Integer> getPwmRangeImpl();
public final void blink(int pulseTimeMillis, int blinks) {
if (getPinNumber() != -1) {
blinkImpl(pulseTimeMillis, blinks);
@@ -107,6 +89,8 @@ public abstract class GPIOBase {
public final void setBrightness(int brightness) {
if (getPinNumber() != -1) {
if (brightness > 100) brightness = 100;
if (brightness < 0) brightness = 0;
setBrightnessImpl(brightness);
}
}

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.hardware.GPIO.pi;
@SuppressWarnings("SpellCheckingInspection")
public enum PigpioCommand {
PCMD_READ(3), // int gpio_read(unsigned gpio)
PCMD_WRITE(4), // int gpio_write(unsigned gpio, unsigned level)
PCMD_WVCLR(27), // int wave_clear(void)
PCMD_WVAG(28), // int wave_add_generic(unsigned numPulses, gpioPulse_t *pulses)
PCMD_WVHLT(33), // int wave_tx_stop(void)
PCMD_WVCRE(49), // int wave_create(void)
PCMD_WVDEL(50), // int wave_delete(unsigned wave_id)
PCMD_WVTX(51), // int wave_tx_send(unsigned wave_id) (once)
PCMD_WVTXR(52), // int wave_tx_send(unsigned wave_id) (repeat)
PCMD_GDC(83), // int get_duty_cyle(unsigned user_gpio)
PCMD_HP(86), // int hardware_pwm(unsigned gpio, unsigned PWMfreq, unsigned PWMduty)
PCMD_WVTXM(100); // int wave_tx_send(unsigned wave_id, unsigned wave_mode)
public final int value;
PigpioCommand(int value) {
this.value = value;
}
}

View File

@@ -0,0 +1,343 @@
/*
* 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.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
*/
@SuppressWarnings({"SpellCheckingInspection", "unused", "RedundantSuppression"})
public class PigpioException extends Exception {
private int rc = -99999999;
private static final long serialVersionUID = 443595760654129068L;
public PigpioException() {
super();
}
public PigpioException(int rc) {
super();
this.rc = rc;
}
public PigpioException(int rc, String msg) {
super(msg);
this.rc = rc;
}
public PigpioException(String arg0, Throwable arg1, boolean arg2, boolean arg3) {
super(arg0, arg1, arg2, arg3);
}
public PigpioException(String arg0, Throwable arg1) {
super(arg0, arg1);
}
public PigpioException(String arg0) {
super(arg0);
}
public PigpioException(Throwable arg0) {
super(arg0);
}
@Override
public String getMessage() {
return "(" + rc + ") " + getMessageForError(rc);
}
/**
* 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
// Public constants for the error codes that can be thrown by Pigpio
public static final int PI_INIT_FAILED = -1; // gpioInitialise failed
public static final int PI_BAD_USER_GPIO = -2; // gpio not 0-31
public static final int PI_BAD_GPIO = -3; // gpio not 0-53
public static final int PI_BAD_MODE = -4; // mode not 0-7
public static final int PI_BAD_LEVEL = -5; // level not 0-1
public static final int PI_BAD_PUD = -6; // pud not 0-2
public static final int PI_BAD_PULSEWIDTH = -7; // pulsewidth not 0 or 500-2500
public static final int PI_BAD_DUTYCYCLE = -8; // dutycycle outside set range
public static final int PI_BAD_TIMER = -9; // timer not 0-9
public static final int PI_BAD_MS = -10; // ms not 10-60000
public static final int PI_BAD_TIMETYPE = -11; // timetype not 0-1
public static final int PI_BAD_SECONDS = -12; // seconds < 0
public static final int PI_BAD_MICROS = -13; // micros not 0-999999
public static final int PI_TIMER_FAILED = -14; // gpioSetTimerFunc failed
public static final int PI_BAD_WDOG_TIMEOUT = -15; // timeout not 0-60000
public static final int PI_NO_ALERT_FUNC = -16; // DEPRECATED
public static final int PI_BAD_CLK_PERIPH = -17; // clock peripheral not 0-1
public static final int PI_BAD_CLK_SOURCE = -18; // DEPRECATED
public static final int PI_BAD_CLK_MICROS = -19; // clock micros not 1, 2, 4, 5, 8, or 10
public static final int PI_BAD_BUF_MILLIS = -20; // buf millis not 100-10000
public static final int PI_BAD_DUTYRANGE = -21; // dutycycle range not 25-40000
public static final int PI_BAD_DUTY_RANGE = -21; // DEPRECATED (use PI_BAD_DUTYRANGE)
public static final int PI_BAD_SIGNUM = -22; // signum not 0-63
public static final int PI_BAD_PATHNAME = -23; // can't open pathname
public static final int PI_NO_HANDLE = -24; // no handle available
public static final int PI_BAD_HANDLE = -25; // unknown handle
public static final int PI_BAD_IF_FLAGS = -26; // ifFlags > 3
public static final int PI_BAD_CHANNEL = -27; // DMA channel not 0-14
public static final int PI_BAD_PRIM_CHANNEL = -27; // DMA primary channel not 0-14
public static final int PI_BAD_SOCKET_PORT = -28; // socket port not 1024-32000
public static final int PI_BAD_FIFO_COMMAND = -29; // unrecognized fifo command
public static final int PI_BAD_SECO_CHANNEL = -30; // DMA secondary channel not 0-6
public static final int PI_NOT_INITIALISED = -31; // function called before gpioInitialise
public static final int PI_INITIALISED = -32; // function called after gpioInitialise
public static final int PI_BAD_WAVE_MODE = -33; // waveform mode not 0-1
public static final int PI_BAD_CFG_INTERNAL = -34; // bad parameter in gpioCfgInternals call
public static final int PI_BAD_WAVE_BAUD = -35; // baud rate not 50-250K(RX)/50-1M(TX)
public static final int PI_TOO_MANY_PULSES = -36; // waveform has too many pulses
public static final int PI_TOO_MANY_CHARS = -37; // waveform has too many chars
public static final int PI_NOT_SERIAL_GPIO = -38; // no serial read in progress on gpio
public static final int PI_BAD_SERIAL_STRUC = -39; // bad (null) serial structure parameter
public static final int PI_BAD_SERIAL_BUF = -40; // bad (null) serial buf parameter
public static final int PI_NOT_PERMITTED = -41; // gpio operation not permitted
public static final int PI_SOME_PERMITTED = -42; // one or more gpios not permitted
public static final int PI_BAD_WVSC_COMMND = -43; // bad WVSC subcommand
public static final int PI_BAD_WVSM_COMMND = -44; // bad WVSM subcommand
public static final int PI_BAD_WVSP_COMMND = -45; // bad WVSP subcommand
public static final int PI_BAD_PULSELEN = -46; // trigger pulse length not 1-100
public static final int PI_BAD_SCRIPT = -47; // invalid script
public static final int PI_BAD_SCRIPT_ID = -48; // unknown script id
public static final int PI_BAD_SER_OFFSET = -49; // add serial data offset > 30 minutes
public static final int PI_GPIO_IN_USE = -50; // gpio already in use
public static final int PI_BAD_SERIAL_COUNT = -51; // must read at least a byte at a time
public static final int PI_BAD_PARAM_NUM = -52; // script parameter id not 0-9
public static final int PI_DUP_TAG = -53; // script has duplicate tag
public static final int PI_TOO_MANY_TAGS = -54; // script has too many tags
public static final int PI_BAD_SCRIPT_CMD = -55; // illegal script command
public static final int PI_BAD_VAR_NUM = -56; // script variable id not 0-149
public static final int PI_NO_SCRIPT_ROOM = -57; // no more room for scripts
public static final int PI_NO_MEMORY = -58; // can't allocate temporary memory
public static final int PI_SOCK_READ_FAILED = -59; // socket read failed
public static final int PI_SOCK_WRIT_FAILED = -60; // socket write failed
public static final int PI_TOO_MANY_PARAM = -61; // too many script parameters (> 10)
public static final int PI_NOT_HALTED = -62; // script already running or failed
public static final int PI_BAD_TAG = -63; // script has unresolved tag
public static final int PI_BAD_MICS_DELAY = -64; // bad MICS delay (too large)
public static final int PI_BAD_MILS_DELAY = -65; // bad MILS delay (too large)
public static final int PI_BAD_WAVE_ID = -66; // non existent wave id
public static final int PI_TOO_MANY_CBS = -67; // No more CBs for waveform
public static final int PI_TOO_MANY_OOL = -68; // No more OOL for waveform
public static final int PI_EMPTY_WAVEFORM = -69; // attempt to create an empty waveform
public static final int PI_NO_WAVEFORM_ID = -70; // no more waveforms
public static final int PI_I2C_OPEN_FAILED = -71; // can't open I2C device
public static final int PI_SER_OPEN_FAILED = -72; // can't open serial device
public static final int PI_SPI_OPEN_FAILED = -73; // can't open SPI device
public static final int PI_BAD_I2C_BUS = -74; // bad I2C bus
public static final int PI_BAD_I2C_ADDR = -75; // bad I2C address
public static final int PI_BAD_SPI_CHANNEL = -76; // bad SPI channel
public static final int PI_BAD_FLAGS = -77; // bad i2c/spi/ser open flags
public static final int PI_BAD_SPI_SPEED = -78; // bad SPI speed
public static final int PI_BAD_SER_DEVICE = -79; // bad serial device name
public static final int PI_BAD_SER_SPEED = -80; // bad serial baud rate
public static final int PI_BAD_PARAM = -81; // bad i2c/spi/ser parameter
public static final int PI_I2C_WRITE_FAILED = -82; // i2c write failed
public static final int PI_I2C_READ_FAILED = -83; // i2c read failed
public static final int PI_BAD_SPI_COUNT = -84; // bad SPI count
public static final int PI_SER_WRITE_FAILED = -85; // ser write failed
public static final int PI_SER_READ_FAILED = -86; // ser read failed
public static final int PI_SER_READ_NO_DATA = -87; // ser read no data available
public static final int PI_UNKNOWN_COMMAND = -88; // unknown command
public static final int PI_SPI_XFER_FAILED = -89; // spi xfer/read/write failed
public static final int PI_BAD_POINTER = -90; // bad (NULL) pointer
public static final int PI_NO_AUX_SPI = -91; // need a A+/B+/Pi2 for auxiliary SPI
public static final int PI_NOT_PWM_GPIO = -92; // gpio is not in use for PWM
public static final int PI_NOT_SERVO_GPIO = -93; // gpio is not in use for servo pulses
public static final int PI_NOT_HCLK_GPIO = -94; // gpio has no hardware clock
public static final int PI_NOT_HPWM_GPIO = -95; // gpio has no hardware PWM
public static final int PI_BAD_HPWM_FREQ = -96; // hardware PWM frequency not 1-125M
public static final int PI_BAD_HPWM_DUTY = -97; // hardware PWM dutycycle not 0-1M
public static final int PI_BAD_HCLK_FREQ = -98; // hardware clock frequency not 4689-250M
public static final int PI_BAD_HCLK_PASS = -99; // need password to use hardware clock 1
public static final int PI_HPWM_ILLEGAL = -100; // illegal, PWM in use for main clock
public static final int PI_BAD_DATABITS = -101; // serial data bits not 1-32
public static final int PI_BAD_STOPBITS = -102; // serial (half) stop bits not 2-8
public static final int PI_MSG_TOOBIG = -103; // socket/pipe message too big
public static final int PI_BAD_MALLOC_MODE = -104; // bad memory allocation mode
public static final int PI_TOO_MANY_SEGS = -105; // too many I2C transaction parts
public static final int PI_BAD_I2C_SEG = -106; // a combined I2C transaction failed
public static final int PI_BAD_SMBUS_CMD = -107;
public static final int PI_NOT_I2C_GPIO = -108;
public static final int PI_BAD_I2C_WLEN = -109;
public static final int PI_BAD_I2C_RLEN = -110;
public static final int PI_BAD_I2C_CMD = -111;
public static final int PI_BAD_I2C_BAUD = -112;
public static final int PI_CHAIN_LOOP_CNT = -113;
public static final int PI_BAD_CHAIN_LOOP = -114;
public static final int PI_CHAIN_COUNTER = -115;
public static final int PI_BAD_CHAIN_CMD = -116;
public static final int PI_BAD_CHAIN_DELAY = -117;
public static final int PI_CHAIN_NESTING = -118;
public static final int PI_CHAIN_TOO_BIG = -119;
public static final int PI_DEPRECATED = -120;
public static final int PI_BAD_SER_INVERT = -121;
public static final int PI_BAD_EDGE = -122;
public static final int PI_BAD_ISR_INIT = -123;
public static final int PI_BAD_FOREVER = -124;
public static final int PI_BAD_FILTER = -125;
public static final int PI_PIGIF_ERR_0 = -2000;
public static final int PI_PIGIF_ERR_99 = -2099;
public static final int PI_CUSTOM_ERR_0 = -3000;
public static final int PI_CUSTOM_ERR_999 = -3999;
private static final HashMap<Integer, String> errorMessages = new HashMap<>();
static {
errorMessages.put(PI_INIT_FAILED, "pigpio initialisation failed");
errorMessages.put(PI_BAD_USER_GPIO, "GPIO not 0-31");
errorMessages.put(PI_BAD_GPIO, "GPIO not 0-53");
errorMessages.put(PI_BAD_MODE, "mode not 0-7");
errorMessages.put(PI_BAD_LEVEL, "level not 0-1");
errorMessages.put(PI_BAD_PUD, "pud not 0-2");
errorMessages.put(PI_BAD_PULSEWIDTH, "pulsewidth not 0 or 500-2500");
errorMessages.put(PI_BAD_DUTYCYCLE, "dutycycle not 0-range (default 255)");
errorMessages.put(PI_BAD_TIMER, "timer not 0-9");
errorMessages.put(PI_BAD_MS, "ms not 10-60000");
errorMessages.put(PI_BAD_TIMETYPE, "timetype not 0-1");
errorMessages.put(PI_BAD_SECONDS, "seconds < 0");
errorMessages.put(PI_BAD_MICROS, "micros not 0-999999");
errorMessages.put(PI_TIMER_FAILED, "gpioSetTimerFunc failed");
errorMessages.put(PI_BAD_WDOG_TIMEOUT, "timeout not 0-60000");
errorMessages.put(PI_NO_ALERT_FUNC, "DEPRECATED");
errorMessages.put(PI_BAD_CLK_PERIPH, "clock peripheral not 0-1");
errorMessages.put(PI_BAD_CLK_SOURCE, "DEPRECATED");
errorMessages.put(PI_BAD_CLK_MICROS, "clock micros not 1, 2, 4, 5, 8, or 10");
errorMessages.put(PI_BAD_BUF_MILLIS, "buf millis not 100-10000");
errorMessages.put(PI_BAD_DUTYRANGE, "dutycycle range not 25-40000");
errorMessages.put(PI_BAD_SIGNUM, "signum not 0-63");
errorMessages.put(PI_BAD_PATHNAME, "can't open pathname");
errorMessages.put(PI_NO_HANDLE, "no handle available");
errorMessages.put(PI_BAD_HANDLE, "unknown handle");
errorMessages.put(PI_BAD_IF_FLAGS, "ifFlags > 3");
errorMessages.put(PI_BAD_CHANNEL, "DMA channel not 0-14");
errorMessages.put(PI_BAD_SOCKET_PORT, "socket port not 1024-30000");
errorMessages.put(PI_BAD_FIFO_COMMAND, "unknown fifo command");
errorMessages.put(PI_BAD_SECO_CHANNEL, "DMA secondary channel not 0-14");
errorMessages.put(PI_NOT_INITIALISED, "function called before gpioInitialise");
errorMessages.put(PI_INITIALISED, "function called after gpioInitialise");
errorMessages.put(PI_BAD_WAVE_MODE, "waveform mode not 0-1");
errorMessages.put(PI_BAD_CFG_INTERNAL, "bad parameter in gpioCfgInternals call");
errorMessages.put(PI_BAD_WAVE_BAUD, "baud rate not 50-250000(RX)/1000000(TX)");
errorMessages.put(PI_TOO_MANY_PULSES, "waveform has too many pulses");
errorMessages.put(PI_TOO_MANY_CHARS, "waveform has too many chars");
errorMessages.put(PI_NOT_SERIAL_GPIO, "no bit bang serial read in progress on GPIO");
errorMessages.put(PI_NOT_PERMITTED, "no permission to update GPIO");
errorMessages.put(PI_SOME_PERMITTED, "no permission to update one or more GPIO");
errorMessages.put(PI_BAD_WVSC_COMMND, "bad WVSC subcommand");
errorMessages.put(PI_BAD_WVSM_COMMND, "bad WVSM subcommand");
errorMessages.put(PI_BAD_WVSP_COMMND, "bad WVSP subcommand");
errorMessages.put(PI_BAD_PULSELEN, "trigger pulse length not 1-100");
errorMessages.put(PI_BAD_SCRIPT, "invalid script");
errorMessages.put(PI_BAD_SCRIPT_ID, "unknown script id");
errorMessages.put(PI_BAD_SER_OFFSET, "add serial data offset > 30 minute");
errorMessages.put(PI_GPIO_IN_USE, "GPIO already in use");
errorMessages.put(PI_BAD_SERIAL_COUNT, "must read at least a byte at a time");
errorMessages.put(PI_BAD_PARAM_NUM, "script parameter id not 0-9");
errorMessages.put(PI_DUP_TAG, "script has duplicate tag");
errorMessages.put(PI_TOO_MANY_TAGS, "script has too many tags");
errorMessages.put(PI_BAD_SCRIPT_CMD, "illegal script command");
errorMessages.put(PI_BAD_VAR_NUM, "script variable id not 0-149");
errorMessages.put(PI_NO_SCRIPT_ROOM, "no more room for scripts");
errorMessages.put(PI_NO_MEMORY, "can't allocate temporary memory");
errorMessages.put(PI_SOCK_READ_FAILED, "socket read failed");
errorMessages.put(PI_SOCK_WRIT_FAILED, "socket write failed");
errorMessages.put(PI_TOO_MANY_PARAM, "too many script parameters (> 10)");
errorMessages.put(PI_NOT_HALTED, "script already running or failed");
errorMessages.put(PI_BAD_TAG, "script has unresolved tag");
errorMessages.put(PI_BAD_MICS_DELAY, "bad MICS delay (too large)");
errorMessages.put(PI_BAD_MILS_DELAY, "bad MILS delay (too large)");
errorMessages.put(PI_BAD_WAVE_ID, "non existent wave id");
errorMessages.put(PI_TOO_MANY_CBS, "No more CBs for waveform");
errorMessages.put(PI_TOO_MANY_OOL, "No more OOL for waveform");
errorMessages.put(PI_EMPTY_WAVEFORM, "attempt to create an empty waveform");
errorMessages.put(PI_NO_WAVEFORM_ID, "No more waveform ids");
errorMessages.put(PI_I2C_OPEN_FAILED, "can't open I2C device");
errorMessages.put(PI_SER_OPEN_FAILED, "can't open serial device");
errorMessages.put(PI_SPI_OPEN_FAILED, "can't open SPI device");
errorMessages.put(PI_BAD_I2C_BUS, "bad I2C bus");
errorMessages.put(PI_BAD_I2C_ADDR, "bad I2C address");
errorMessages.put(PI_BAD_SPI_CHANNEL, "bad SPI channel");
errorMessages.put(PI_BAD_FLAGS, "bad i2c/spi/ser open flags");
errorMessages.put(PI_BAD_SPI_SPEED, "bad SPI speed");
errorMessages.put(PI_BAD_SER_DEVICE, "bad serial device name");
errorMessages.put(PI_BAD_SER_SPEED, "bad serial baud rate");
errorMessages.put(PI_BAD_PARAM, "bad i2c/spi/ser parameter");
errorMessages.put(PI_I2C_WRITE_FAILED, "I2C write failed");
errorMessages.put(PI_I2C_READ_FAILED, "I2C read failed");
errorMessages.put(PI_BAD_SPI_COUNT, "bad SPI count");
errorMessages.put(PI_SER_WRITE_FAILED, "ser write failed");
errorMessages.put(PI_SER_READ_FAILED, "ser read failed");
errorMessages.put(PI_SER_READ_NO_DATA, "ser read no data available");
errorMessages.put(PI_UNKNOWN_COMMAND, "unknown command");
errorMessages.put(PI_SPI_XFER_FAILED, "SPI xfer/read/write failed");
errorMessages.put(PI_BAD_POINTER, "bad (NULL) pointer");
errorMessages.put(PI_NO_AUX_SPI, "no auxiliary SPI on Pi A or B");
errorMessages.put(PI_NOT_PWM_GPIO, "GPIO is not in use for PWM");
errorMessages.put(PI_NOT_SERVO_GPIO, "GPIO is not in use for servo pulses");
errorMessages.put(PI_NOT_HCLK_GPIO, "GPIO has no hardware clock");
errorMessages.put(PI_NOT_HPWM_GPIO, "GPIO has no hardware PWM");
errorMessages.put(PI_BAD_HPWM_FREQ, "hardware PWM frequency not 1-125M");
errorMessages.put(PI_BAD_HPWM_DUTY, "hardware PWM dutycycle not 0-1M");
errorMessages.put(PI_BAD_HCLK_FREQ, "hardware clock frequency not 4689-250M");
errorMessages.put(PI_BAD_HCLK_PASS, "need password to use hardware clock 1");
errorMessages.put(PI_HPWM_ILLEGAL, "illegal, PWM in use for main clock");
errorMessages.put(PI_BAD_DATABITS, "serial data bits not 1-32");
errorMessages.put(PI_BAD_STOPBITS, "serial (half) stop bits not 2-8");
errorMessages.put(PI_MSG_TOOBIG, "socket/pipe message too big");
errorMessages.put(PI_BAD_MALLOC_MODE, "bad memory allocation mode");
errorMessages.put(PI_TOO_MANY_SEGS, "too many I2C transaction segments");
errorMessages.put(PI_BAD_I2C_SEG, "an I2C transaction segment failed");
errorMessages.put(PI_BAD_SMBUS_CMD, "SMBus command not supported");
errorMessages.put(PI_NOT_I2C_GPIO, "no bit bang I2C in progress on GPIO");
errorMessages.put(PI_BAD_I2C_WLEN, "bad I2C write length");
errorMessages.put(PI_BAD_I2C_RLEN, "bad I2C read length");
errorMessages.put(PI_BAD_I2C_CMD, "bad I2C command");
errorMessages.put(PI_BAD_I2C_BAUD, "bad I2C baud rate, not 50-500k");
errorMessages.put(PI_CHAIN_LOOP_CNT, "bad chain loop count");
errorMessages.put(PI_BAD_CHAIN_LOOP, "empty chain loop");
errorMessages.put(PI_CHAIN_COUNTER, "too many chain counters");
errorMessages.put(PI_BAD_CHAIN_CMD, "bad chain command");
errorMessages.put(PI_BAD_CHAIN_DELAY, "bad chain delay micros");
errorMessages.put(PI_CHAIN_NESTING, "chain counters nested too deeply");
errorMessages.put(PI_CHAIN_TOO_BIG, "chain is too long");
errorMessages.put(PI_DEPRECATED, "deprecated function removed");
errorMessages.put(PI_BAD_SER_INVERT, "bit bang serial invert not 0 or 1");
errorMessages.put(PI_BAD_EDGE, "bad ISR edge value, not 0-2");
errorMessages.put(PI_BAD_ISR_INIT, "bad ISR initialisation");
errorMessages.put(PI_BAD_FOREVER, "loop forever must be last chain command");
errorMessages.put(PI_BAD_FILTER, "bad filter parameter");
}
public static String getMessageForError(int errorCode) {
return errorMessages.get(errorCode);
}
}

View File

@@ -0,0 +1,95 @@
/*
* 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.GPIO.pi;
import static org.photonvision.common.hardware.GPIO.pi.PigpioException.*;
import org.photonvision.common.hardware.GPIO.GPIOBase;
import org.photonvision.common.logging.LogGroup;
import org.photonvision.common.logging.Logger;
public class PigpioPin extends GPIOBase {
public static final Logger logger = new Logger(PigpioPin.class, LogGroup.General);
private static final PigpioSocket piSocket = new PigpioSocket();
private final boolean isHardwarePWMPin;
private final int pinNo;
private boolean hasFailedHardwarePWM;
public PigpioPin(int pinNo) {
isHardwarePWMPin = pinNo == 12 || pinNo == 13 || pinNo == 17 || pinNo == 18;
this.pinNo = pinNo;
}
@Override
public int getPinNumber() {
return pinNo;
}
@Override
protected void setStateImpl(boolean state) {
try {
piSocket.gpioWrite(pinNo, state);
} catch (PigpioException e) {
logger.error("gpioWrite FAIL - " + e.getMessage());
}
}
@Override
public boolean shutdown() {
setState(false);
return true;
}
@Override
public boolean getStateImpl() {
try {
return piSocket.gpioRead(pinNo);
} catch (PigpioException e) {
logger.error("gpioRead FAIL - " + e.getMessage());
return false;
}
}
@Override
protected void blinkImpl(int pulseTimeMillis, int blinks) {
try {
piSocket.generateAndSendWaveform(pulseTimeMillis, blinks, pinNo);
} catch (PigpioException e) {
logger.error("Could not set blink - " + e.getMessage());
}
}
@Override
protected void setBrightnessImpl(int brightness) {
if (isHardwarePWMPin) {
try {
piSocket.hardwarePWM(pinNo, 22000, (int) (1000000 * (brightness / 100.0)));
} catch (PigpioException e) {
logger.error("Failed to hardPWM - " + e.getMessage());
}
} else if (!hasFailedHardwarePWM) {
logger.warn(
"Specified pin ("
+ pinNo
+ ") is not capable of hardware PWM - no action will be taken.");
hasFailedHardwarePWM = true;
}
}
}

View File

@@ -0,0 +1,39 @@
/*
* 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.GPIO.pi;
public class PigpioPulse {
int gpioOn;
int gpioOff;
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.
*/
public PigpioPulse(int gpioOn, int gpioOff, int delayMicros) {
this.gpioOn = gpioOn != 0 ? 1 << gpioOn : 0;
this.gpioOff = gpioOff != 0 ? 1 << gpioOff : 0;
this.delayMicros = delayMicros;
}
}

View File

@@ -0,0 +1,373 @@
/*
* 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.GPIO.pi;
import static org.photonvision.common.hardware.GPIO.pi.PigpioException.*;
import static org.photonvision.common.hardware.GPIO.pi.PigpioException.PI_NO_WAVEFORM_ID;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.util.ArrayList;
import org.photonvision.common.logging.LogGroup;
import org.photonvision.common.logging.Logger;
@SuppressWarnings({"SpellCheckingInspection", "unused"})
public class PigpioSocket {
private static final Logger logger = new Logger(PigpioSocket.class, LogGroup.General);
private static final int PIGPIOD_MESSAGE_SIZE = 12;
private PigpioSocketLock commandSocket;
private int activeWaveformID = -1;
/** Creates and starts a socket connection to a pigpio daemon on localhost */
public PigpioSocket() {
this("127.0.0.1", 8888);
}
/**
* 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);
} catch (IOException e) {
logger.error("Failed to create or connect to Pigpio Daemon socket", e);
}
}
/**
* Reconnects to the pigpio daemon
*
* @throws PigpioException on failure
*/
public void reconnect() throws PigpioException {
try {
commandSocket.reconnect();
} catch (IOException e) {
logger.error("Failed to reconnect to Pigpio Daemon socket", e);
throw new PigpioException("reconnect", e);
}
}
/**
* Terminates the connection to the pigpio daemon
*
* @throws PigpioException on failure
*/
public void gpioTerminate() throws PigpioException {
try {
commandSocket.terminate();
} catch (IOException e) {
logger.error("Failed to terminate connection to Pigpio Daemon socket", e);
throw new PigpioException("gpioTerminate", e);
}
}
/**
* 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);
if (retCode < 0) throw new PigpioException(retCode);
return retCode != 0;
} catch (IOException e) {
logger.error("Failed to read GPIO pin: " + pin, e);
throw new PigpioException("gpioRead", e);
}
}
/**
* 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);
if (retCode < 0) throw new PigpioException(retCode);
} catch (IOException e) {
logger.error("Failed to write to GPIO pin: " + pin, e);
throw new PigpioException("gpioWrite", e);
}
}
/**
* 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);
if (retCode < 0) throw new PigpioException(retCode);
} catch (IOException e) {
logger.error("Failed to clear waveforms", e);
throw new PigpioException("waveClear", e);
}
}
/**
* 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
// I p1 0
// I p2 0
// I p3 pulses * 12
// ## extension ##
// III on/off/delay * pulses
if (pulses == null || pulses.size() == 0) return 0;
try {
ByteBuffer bb = ByteBuffer.allocate(pulses.size() * 12);
bb.order(ByteOrder.LITTLE_ENDIAN);
for (var pulse : pulses) {
bb.putInt(pulse.gpioOn).putInt(pulse.gpioOff).putInt(pulse.delayMicros);
}
int retCode =
commandSocket.sendCmd(
PigpioCommand.PCMD_WVAG.value,
0,
0,
pulses.size() * PIGPIOD_MESSAGE_SIZE,
bb.array());
if (retCode < 0) throw new PigpioException(retCode);
return retCode;
} catch (IOException e) {
logger.error("Failed to add pulse(s) to waveform", e);
throw new PigpioException("waveAddGeneric", e);
}
}
/**
* 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;
if (blinks == 0) return;
if (repeat) {
blinks = 1;
}
try {
ArrayList<PigpioPulse> pulses = new ArrayList<>();
var startPulse = new PigpioPulse(pinNo, 0, pulseTimeMillis * 1000);
var endPulse = new PigpioPulse(0, pinNo, pulseTimeMillis * 1000);
for (int i = 0; i < blinks; i++) {
pulses.add(startPulse);
pulses.add(endPulse);
}
waveAddGeneric(pulses);
pulses.clear();
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 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;
boolean repeat = blinks == -1;
if (blinks == 0) return;
// stop any active waves
waveTxStop();
waveClear();
if (activeWaveformID != -1) {
waveDelete(activeWaveformID);
activeWaveformID = -1;
}
for (int pin : pins) {
addBlinkPulsesToWaveform(pulseTimeMillis, blinks, pin);
}
int waveformId = waveCreate();
if (waveformId >= 0) {
if (repeat) {
waveSendRepeat(waveformId);
} else {
waveSendOnce(waveformId);
}
} else {
String error = "";
switch (waveformId) {
case PI_EMPTY_WAVEFORM:
error = "Waveform empty";
break;
case PI_TOO_MANY_CBS:
error = "Too many CBS";
break;
case PI_TOO_MANY_OOL:
error = "Too many OOL";
break;
case PI_NO_WAVEFORM_ID:
error = "No waveform ID";
break;
}
logger.error("Failed to send wave: " + error);
}
}
/**
* 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);
if (retCode < 0) throw new PigpioException(retCode);
return retCode == 0;
} catch (IOException e) {
logger.error("Failed to stop waveform", e);
throw new PigpioException("waveTxStop", e);
}
}
/**
* 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);
if (retCode < 0) throw new PigpioException(retCode);
return retCode;
} catch (IOException e) {
logger.error("Failed to create new waveform", e);
throw new PigpioException("waveCreate", e);
}
}
/**
* 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);
if (retCode < 0) throw new PigpioException(retCode);
} catch (IOException e) {
logger.error("Failed to delete wave: " + waveId, e);
throw new PigpioException("waveDelete", e);
}
}
/**
* 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);
if (retCode < 0) throw new PigpioException(retCode);
return retCode;
} catch (IOException e) {
throw new PigpioException("waveSendOnce", e);
}
}
/**
* 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);
if (retCode < 0) throw new PigpioException(retCode);
return retCode;
} catch (IOException e) {
throw new PigpioException("waveSendRepeat", e);
}
}
/**
* 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);
bb.order(ByteOrder.LITTLE_ENDIAN);
bb.putInt(pwmDuty);
int retCode =
commandSocket.sendCmd(PigpioCommand.PCMD_HP.value, pin, pwmFrequency, 4, bb.array());
if (retCode < 0) throw new PigpioException(retCode);
} catch (IOException e) {
throw new PigpioException("hardwarePWM", e);
}
}
}

View File

@@ -0,0 +1,147 @@
/*
* 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.GPIO.pi;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.net.Socket;
import java.nio.ByteBuffer;
/**
* Credit to nkolban
* https://github.com/nkolban/jpigpio/blob/master/JPigpio/src/jpigpio/SocketLock.java
*/
final class PigpioSocketLock {
private static final int replyTimeoutMillis = 1000;
private final String addr;
private final int port;
private Socket socket;
private DataInputStream in;
private DataOutputStream out;
public PigpioSocketLock(String addr, int port) throws IOException {
this.addr = addr;
this.port = port;
reconnect();
}
public void reconnect() throws IOException {
socket = new Socket(addr, port);
out = new DataOutputStream(socket.getOutputStream());
in = new DataInputStream(socket.getInputStream());
}
public void terminate() throws IOException {
in.close();
in = null;
out.flush();
out.close();
out = null;
socket.close();
socket = null;
}
public synchronized int sendCmd(int cmd) throws IOException {
byte[] b = {};
return sendCmd(cmd, 0, 0, 0, b);
}
public synchronized int sendCmd(int cmd, int p1) throws IOException {
byte[] b = {};
return sendCmd(cmd, p1, 0, 0, b);
}
public synchronized int sendCmd(int cmd, int p1, int p2) throws IOException {
byte[] b = {};
return sendCmd(cmd, p1, p2, 0, b);
}
public synchronized int sendCmd(int cmd, int p1, int p2, int p3) throws IOException {
byte[] b = {};
return sendCmd(cmd, p1, p2, p3, b);
}
/**
* 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);
bb.putInt(Integer.reverseBytes(cmd));
bb.putInt(Integer.reverseBytes(p1));
bb.putInt(Integer.reverseBytes(p2));
bb.putInt(Integer.reverseBytes(p3));
if (ext.length > 0) {
bb.put(ext);
}
out.write(bb.array());
out.flush();
int w = replyTimeoutMillis;
int a = in.available();
// if by any chance there is no response from pigpiod, then wait up to
// specified timeout
while (w > 0 && a < 16) {
w -= 10;
try {
Thread.sleep(10);
} catch (InterruptedException ignored) {
}
a = in.available();
}
// throw exception if response from pigpiod has not arrived yet
if (in.available() < 16) {
throw new IOException(
"Timeout: No response from pigpio daemon within " + replyTimeoutMillis + " ms.");
}
int resp = Integer.reverseBytes(in.readInt()); // ignore response
resp = Integer.reverseBytes(in.readInt()); // ignore response
resp = Integer.reverseBytes(in.readInt()); // ignore response
resp = Integer.reverseBytes(in.readInt()); // contains error or response
return resp;
}
/**
* 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

@@ -1,5 +1,5 @@
/*
* Copyright (C) 2020 Photon Vision.
* 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
@@ -19,15 +19,14 @@ package org.photonvision.common.hardware;
import edu.wpi.first.networktables.NetworkTableEntry;
import java.io.IOException;
import java.util.HashMap;
import org.photonvision.common.ProgramStatus;
import org.photonvision.common.configuration.ConfigManager;
import org.photonvision.common.configuration.HardwareConfig;
import org.photonvision.common.configuration.HardwareSettings;
import org.photonvision.common.dataflow.networktables.NTDataChangeListener;
import org.photonvision.common.dataflow.networktables.NetworkTablesManager;
import org.photonvision.common.hardware.GPIO.CustomGPIO;
import org.photonvision.common.hardware.GPIO.GPIOBase;
import org.photonvision.common.hardware.VisionLED.VisionLEDMode;
import org.photonvision.common.hardware.GPIO.pi.PigpioSocket;
import org.photonvision.common.hardware.metrics.MetricsBase;
import org.photonvision.common.logging.LogGroup;
import org.photonvision.common.logging.Logger;
@@ -36,45 +35,101 @@ import org.photonvision.common.util.ShellExec;
public class HardwareManager {
private static HardwareManager instance;
private final HashMap<Integer, GPIOBase> VisionLEDs = new HashMap<>();
private final ShellExec shellExec = new ShellExec(true, false);
private final Logger logger = new Logger(HardwareManager.class, LogGroup.General);
private final HardwareConfig hardwareConfig;
private final HardwareSettings hardwareSettings;
@SuppressWarnings({"FieldCanBeLocal", "unused"})
private final StatusLED statusLED;
@SuppressWarnings("FieldCanBeLocal")
private final NetworkTableEntry ledModeEntry;
@SuppressWarnings({"FieldCanBeLocal", "unused"})
private final NTDataChangeListener ledModeListener;
public final VisionLED visionLED;
public final VisionLED visionLED; // May be null if no LED is specified
private final PigpioSocket pigpioSocket; // will be null unless on Raspi
public static HardwareManager getInstance() {
if (instance == null) {
instance = new HardwareManager(ConfigManager.getInstance().getConfig().getHardwareConfig());
var conf = ConfigManager.getInstance().getConfig();
instance = new HardwareManager(conf.getHardwareConfig(), conf.getHardwareSettings());
}
return instance;
}
private HardwareManager(HardwareConfig hardwareConfig) {
private HardwareManager(HardwareConfig hardwareConfig, HardwareSettings hardwareSettings) {
this.hardwareConfig = hardwareConfig;
this.hardwareSettings = hardwareSettings;
CustomGPIO.setConfig(hardwareConfig);
MetricsBase.setConfig(hardwareConfig);
statusLED = new StatusLED(hardwareConfig.statusRGBPins);
if (Platform.isRaspberryPi()) {
pigpioSocket = new PigpioSocket();
} else {
pigpioSocket = null;
}
statusLED =
hardwareConfig.statusRGBPins.size() == 3
? new StatusLED(hardwareConfig.statusRGBPins)
: null;
var hasBrightnessRange = hardwareConfig.ledBrightnessRange.size() == 2;
visionLED =
new VisionLED(
hardwareConfig.ledPins,
hardwareConfig.ledPWMFrequency,
hardwareConfig.ledPWMRange.get(1));
hardwareConfig.ledPins.isEmpty()
? null
: new VisionLED(
hardwareConfig.ledPins,
hasBrightnessRange ? hardwareConfig.ledBrightnessRange.get(0) : 0,
hasBrightnessRange ? hardwareConfig.ledBrightnessRange.get(1) : 100,
pigpioSocket);
ledModeEntry = NetworkTablesManager.getInstance().kRootTable.getEntry("ledMode");
ledModeEntry.setNumber(VisionLEDMode.VLM_DEFAULT.value);
ledModeListener = new NTDataChangeListener(ledModeEntry, visionLED::onLedModeChange);
ledModeEntry.setNumber(VisionLEDMode.kDefault.value);
ledModeListener =
visionLED == null
? null
: new NTDataChangeListener(ledModeEntry, visionLED::onLedModeChange);
Runtime.getRuntime().addShutdownHook(new Thread(this::onJvmExit));
if (visionLED != null) {
visionLED.setBrightness(hardwareSettings.ledBrightnessPercentage);
visionLED.blink(85, 4); // bootup blink
}
// Start hardware metrics thread (Disabled until implemented)
// if (Platform.isLinux()) MetricsPublisher.getInstance().startTask();
}
public void setBrightnessPercent(int percent) {
if (percent != hardwareSettings.ledBrightnessPercentage) {
hardwareSettings.ledBrightnessPercentage = percent;
if (visionLED != null) visionLED.setBrightness(percent);
ConfigManager.getInstance().requestSave();
logger.info("Setting led brightness to " + percent + "%");
}
}
private void onJvmExit() {
logger.info("Shutting down LEDs...");
if (visionLED != null) visionLED.setState(false);
}
public boolean restartDevice() {
if (Platform.isRaspberryPi()) {
try {
return shellExec.executeBashCommand("reboot now") == 0;
} catch (IOException e) {
logger.error("Could not restart device!", e);
return false;
}
}
try {
return shellExec.executeBashCommand(hardwareConfig.restartHardwareCommand) == 0;
} catch (IOException e) {

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

@@ -1,5 +1,5 @@
/*
* Copyright (C) 2020 Photon Vision.
* 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
@@ -17,7 +17,7 @@
package org.photonvision.common.hardware;
import edu.wpi.first.wpiutil.RuntimeDetector;
import edu.wpi.first.util.RuntimeDetector;
import java.io.IOException;
import org.photonvision.common.util.ShellExec;
@@ -46,7 +46,11 @@ 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();
// These are queried on init and should never change after
public static final Platform currentPlatform = getCurrentPlatform();
protected static final String currentPiVersionStr = getPiVersionString();
public static final PiVersion currentPiVersion = PiVersion.getPiVersion();
private static String UnknownPlatformString =
String.format("Unknown Platform. OS: %s, Architecture: %s", OS_NAME, OS_ARCH);
@@ -62,7 +66,7 @@ public enum Platform {
}
public static boolean isRaspberryPi() {
return CurrentPlatform.equals(LINUX_RASPBIAN);
return currentPlatform.equals(LINUX_RASPBIAN);
}
@SuppressWarnings("StatementWithEmptyBody")
@@ -88,7 +92,7 @@ public enum Platform {
return false;
}
private static Platform getCurrentPlatform() {
public static Platform getCurrentPlatform() {
if (RuntimeDetector.isWindows()) {
if (RuntimeDetector.is32BitIntel()) return WINDOWS_32;
if (RuntimeDetector.is64BitIntel()) return WINDOWS_64;
@@ -115,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

@@ -1,5 +1,5 @@
/*
* Copyright (C) 2020 Photon Vision.
* 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
@@ -20,7 +20,7 @@ package org.photonvision.common.hardware;
import java.util.List;
import org.photonvision.common.hardware.GPIO.CustomGPIO;
import org.photonvision.common.hardware.GPIO.GPIOBase;
import org.photonvision.common.hardware.GPIO.PiGPIO;
import org.photonvision.common.hardware.GPIO.pi.PigpioPin;
public class StatusLED {
public final GPIOBase redLED;
@@ -36,9 +36,9 @@ public class StatusLED {
}
if (Platform.isRaspberryPi()) {
redLED = new PiGPIO(statusLedPins.get(0));
greenLED = new PiGPIO(statusLedPins.get(1));
blueLED = new PiGPIO(statusLedPins.get(2));
redLED = new PigpioPin(statusLedPins.get(0));
greenLED = new PigpioPin(statusLedPins.get(1));
blueLED = new PigpioPin(statusLedPins.get(2));
} else {
redLED = new CustomGPIO(statusLedPins.get(0));
greenLED = new CustomGPIO(statusLedPins.get(1));

View File

@@ -1,5 +1,5 @@
/*
* Copyright (C) 2020 Photon Vision.
* 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
@@ -23,25 +23,40 @@ import java.util.List;
import java.util.function.BooleanSupplier;
import org.photonvision.common.hardware.GPIO.CustomGPIO;
import org.photonvision.common.hardware.GPIO.GPIOBase;
import org.photonvision.common.hardware.GPIO.PiGPIO;
import org.photonvision.common.hardware.GPIO.pi.PigpioException;
import org.photonvision.common.hardware.GPIO.pi.PigpioPin;
import org.photonvision.common.hardware.GPIO.pi.PigpioSocket;
import org.photonvision.common.logging.LogGroup;
import org.photonvision.common.logging.Logger;
import org.photonvision.common.util.TimedTaskManager;
import org.photonvision.common.util.math.MathUtils;
public class VisionLED {
private static final Logger logger = new Logger(VisionLED.class, LogGroup.VisionModule);
public final List<GPIOBase> leds = new ArrayList<>();
private final int[] ledPins;
private final List<GPIOBase> visionLEDs = new ArrayList<>();
private final int brightnessMin;
private final int brightnessMax;
private final PigpioSocket pigpioSocket;
private VisionLEDMode currentLedMode = VisionLEDMode.VLM_DEFAULT;
private VisionLEDMode currentLedMode = VisionLEDMode.kDefault;
private BooleanSupplier pipelineModeSupplier;
public VisionLED(List<Integer> ledPins, int pwmFreq, int pwmRangeMax) {
private int mappedBrightnessPercentage;
public VisionLED(
List<Integer> ledPins, int brightnessMin, int brightnessMax, PigpioSocket pigpioSocket) {
this.brightnessMin = brightnessMin;
this.brightnessMax = brightnessMax;
this.pigpioSocket = pigpioSocket;
this.ledPins = ledPins.stream().mapToInt(i -> i).toArray();
ledPins.forEach(
pin -> {
if (Platform.isRaspberryPi()) {
leds.add(new PiGPIO(pin, pwmFreq, pwmRangeMax));
visionLEDs.add(new PigpioPin(pin));
} else {
leds.add(new CustomGPIO(pin));
visionLEDs.add(new CustomGPIO(pin));
}
});
pipelineModeSupplier = () -> false;
@@ -52,19 +67,51 @@ public class VisionLED {
}
public void setBrightness(int percentage) {
leds.forEach((led) -> led.setBrightness(percentage));
mappedBrightnessPercentage = MathUtils.map(percentage, 0, 100, brightnessMin, brightnessMax);
setInternal(currentLedMode, false);
}
public void blink(int pulseLengthMillis, int blinkCount) {
blinkImpl(pulseLengthMillis, blinkCount);
int blinkDuration = pulseLengthMillis * blinkCount * 2;
TimedTaskManager.getInstance()
.addOneShotTask(() -> setInternal(this.currentLedMode, false), blinkDuration + 150);
}
private void blinkImpl(int pulseLengthMillis, int blinkCount) {
leds.forEach((led) -> led.blink(pulseLengthMillis, blinkCount));
if (Platform.isRaspberryPi()) {
try {
setStateImpl(false); // hack to ensure hardware PWM has stopped before trying to blink
pigpioSocket.generateAndSendWaveform(pulseLengthMillis, blinkCount, ledPins);
} catch (PigpioException e) {
logger.error("Failed to blink!", e);
}
} else {
for (GPIOBase led : visionLEDs) {
led.blink(pulseLengthMillis, blinkCount);
}
}
}
private void setStateImpl(boolean state) {
leds.forEach((led) -> led.setState(state));
if (Platform.isRaspberryPi()) {
try {
// stop any active blink
pigpioSocket.waveTxStop();
} catch (PigpioException e) {
logger.error("Failed to stop blink!", 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));
}
}
public void setState(boolean on) {
setInternal(on ? VisionLEDMode.VLM_ON : VisionLEDMode.VLM_OFF, false);
setInternal(on ? VisionLEDMode.kOn : VisionLEDMode.kOff, false);
}
void onLedModeChange(EntryNotification entryNotification) {
@@ -73,20 +120,20 @@ public class VisionLED {
VisionLEDMode newLedMode;
switch (newLedModeRaw) {
case -1:
newLedMode = VisionLEDMode.VLM_DEFAULT;
newLedMode = VisionLEDMode.kDefault;
break;
case 0:
newLedMode = VisionLEDMode.VLM_OFF;
newLedMode = VisionLEDMode.kOff;
break;
case 1:
newLedMode = VisionLEDMode.VLM_ON;
newLedMode = VisionLEDMode.kOn;
break;
case 2:
newLedMode = VisionLEDMode.VLM_BLINK;
newLedMode = VisionLEDMode.kBlink;
break;
default:
logger.warn("User supplied invalid LED mode, falling back to Default");
newLedMode = VisionLEDMode.VLM_DEFAULT;
newLedMode = VisionLEDMode.kDefault;
break;
}
setInternal(newLedMode, true);
@@ -98,17 +145,17 @@ public class VisionLED {
if (fromNT) {
switch (newLedMode) {
case VLM_DEFAULT:
case kDefault:
setStateImpl(pipelineModeSupplier.getAsBoolean());
break;
case VLM_OFF:
case kOff:
setStateImpl(false);
break;
case VLM_ON:
case kOn:
setStateImpl(true);
break;
case VLM_BLINK:
blinkImpl(175, -1);
case kBlink:
blinkImpl(85, -1);
break;
}
currentLedMode = newLedMode;
@@ -119,12 +166,15 @@ public class VisionLED {
+ newLedMode.toString()
+ "\"");
} else {
if (currentLedMode == VisionLEDMode.VLM_DEFAULT) {
if (currentLedMode == VisionLEDMode.kDefault) {
switch (newLedMode) {
case VLM_OFF:
case kDefault:
setStateImpl(pipelineModeSupplier.getAsBoolean());
break;
case kOff:
setStateImpl(false);
break;
case VLM_ON:
case kOn:
setStateImpl(true);
break;
}
@@ -132,32 +182,4 @@ public class VisionLED {
logger.info("Changing LED internal state to " + newLedMode.toString());
}
}
public enum VisionLEDMode {
VLM_DEFAULT(-1),
VLM_OFF(0),
VLM_ON(1),
VLM_BLINK(2);
public final int value;
VisionLEDMode(int value) {
this.value = value;
}
@Override
public String toString() {
switch (this) {
case VLM_DEFAULT:
return "Default";
case VLM_OFF:
return "Off";
case VLM_ON:
return "On";
case VLM_BLINK:
return "Blink";
}
return "";
}
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright (C) 2020 Photon Vision.
* 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
@@ -18,18 +18,23 @@
package org.photonvision.common.hardware.metrics;
public class CPUMetrics extends MetricsBase {
public CPUMetrics() {}
private String cpuMemSplit = null;
public String getMemory() {
if (cpuMemoryCommand.isEmpty()) return "";
return execute(cpuMemoryCommand);
if (cpuMemSplit == null) {
cpuMemSplit = execute(cpuMemoryCommand);
}
return cpuMemSplit;
}
// TODO: Command should return in Celsius
public String getTemp() {
if (cpuTemperatureCommand.isEmpty()) return "";
return execute(cpuTemperatureCommand);
try {
return execute(cpuTemperatureCommand);
} catch (Exception e) {
return "N/A";
}
}
public String getUtilization() {

View File

@@ -1,5 +1,5 @@
/*
* Copyright (C) 2020 Photon Vision.
* 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
@@ -17,12 +17,9 @@
package org.photonvision.common.hardware.metrics;
public class GPUMetrics extends MetricsBase {
public String getMemory() {
return execute(gpuMemoryCommand);
}
public String getTemp() {
return execute(gpuTemperatureCommand);
public class DiskMetrics extends MetricsBase {
public String getUsedDiskPct() {
if (diskUsageCommand.isEmpty()) return "";
return execute(diskUsageCommand);
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright (C) 2020 Photon Vision.
* 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
@@ -15,12 +15,19 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.photonvision.vision.processes;
package org.photonvision.common.hardware.metrics;
import org.photonvision.vision.frame.FrameProvider;
public class GPUMetrics extends MetricsBase {
private String gpuMemSplit = null;
public interface VisionSource {
FrameProvider getFrameProvider();
public String getGPUMemorySplit() {
if (gpuMemSplit == null) {
gpuMemSplit = execute(gpuMemoryCommand);
}
return gpuMemSplit;
}
VisionSourceSettables getSettables();
public String getMallocedMemory() {
return execute(gpuMemUsageCommand);
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright (C) 2020 Photon Vision.
* 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
@@ -17,6 +17,8 @@
package org.photonvision.common.hardware.metrics;
import java.io.PrintWriter;
import java.io.StringWriter;
import org.photonvision.common.configuration.HardwareConfig;
import org.photonvision.common.hardware.Platform;
import org.photonvision.common.logging.LogGroup;
@@ -26,18 +28,21 @@ import org.photonvision.common.util.ShellExec;
public abstract class MetricsBase {
private static final Logger logger = new Logger(MetricsBase.class, LogGroup.General);
// CPU
public static String cpuMemoryCommand = "sudo vcgencmd get_mem arm | grep -Eo '[0-9]+'";
public static String cpuMemoryCommand = "vcgencmd get_mem arm | grep -Eo '[0-9]+'";
public static String cpuTemperatureCommand =
"sudo cat /sys/class/thermal/thermal_zone0/temp | grep -x -E '[0-9]+'";
"sed 's/.\\{3\\}$/.&/' <<< cat /sys/class/thermal/thermal_zone0/temp";
public static String cpuUtilizationCommand =
"sudo top -bn1 | grep \"Cpu(s)\" | sed \"s/.*, *\\([0-9.]*\\)%* id.*/\\1/\" | awk '{print 100 - $1}'";
"top -bn1 | grep \"Cpu(s)\" | sed \"s/.*, *\\([0-9.]*\\)%* id.*/\\1/\" | awk '{print 100 - $1}'";
// GPU
public static String gpuMemoryCommand = "sudo vcgencmd get_mem gpu | grep -Eo '[0-9]+'";
public static String gpuTemperatureCommand = "sudo vcgencmd measure_temp | sed 's/[^0-9]*//g'\n";
public static String gpuMemoryCommand = "vcgencmd get_mem gpu | grep -Eo '[0-9]+'";
public static String gpuMemUsageCommand = "vcgencmd get_mem malloc | grep -Eo '[0-9]+'";
// RAM
public static String ramUsageCommand = "sudo free | awk -v i=2 -v j=3 'FNR == i {print $j}'";
public static String ramUsageCommand = "free --mega | awk -v i=2 -v j=3 'FNR == i {print $j}'";
// Disk
public static String diskUsageCommand = "df ./ --output=pcent | tail -n +2";
private static ShellExec runCommand = new ShellExec(true, true);
@@ -48,16 +53,22 @@ public abstract class MetricsBase {
cpuUtilizationCommand = config.cpuUtilCommand;
gpuMemoryCommand = config.gpuMemoryCommand;
gpuTemperatureCommand = config.gpuTempCommand;
gpuMemUsageCommand = config.gpuMemUsageCommand;
diskUsageCommand = config.diskUsageCommand;
ramUsageCommand = config.ramUtilCommand;
}
public static String execute(String command) {
public static synchronized String execute(String command) {
try {
runCommand.executeBashCommand(command);
return runCommand.getOutput();
} catch (Exception e) {
StringWriter sw = new StringWriter();
PrintWriter pw = new PrintWriter(sw);
e.printStackTrace(pw);
logger.error(
"Command: \""
+ command
@@ -71,7 +82,10 @@ public abstract class MetricsBase {
+ "\nError completed: "
+ runCommand.isErrorCompleted()
+ "\nExit code: "
+ runCommand.getExitCode());
+ runCommand.getExitCode()
+ "\n Exception: "
+ e.toString()
+ sw.toString());
return "";
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright (C) 2020 Photon Vision.
* 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
@@ -20,16 +20,17 @@ package org.photonvision.common.hardware.metrics;
import java.util.HashMap;
import org.photonvision.common.dataflow.DataChangeService;
import org.photonvision.common.dataflow.events.OutgoingUIEvent;
import org.photonvision.common.hardware.Platform;
import org.photonvision.common.logging.LogGroup;
import org.photonvision.common.logging.Logger;
import org.photonvision.common.util.TimedTaskManager;
public class MetricsPublisher {
private final HashMap<String, String> metrics;
private static final Logger logger = new Logger(MetricsPublisher.class, LogGroup.General);
private static CPUMetrics cpuMetrics;
private static GPUMetrics gpuMetrics;
private static RAMMetrics ramMetrics;
private static DiskMetrics diskMetrics;
public static MetricsPublisher getInstance() {
return Singleton.INSTANCE;
@@ -39,26 +40,7 @@ public class MetricsPublisher {
cpuMetrics = new CPUMetrics();
gpuMetrics = new GPUMetrics();
ramMetrics = new RAMMetrics();
metrics = new HashMap<>();
}
public void startTask() {
TimedTaskManager.getInstance()
.addTask(
"Metrics",
() -> {
metrics.put("cpuTemp", cpuMetrics.getTemp());
metrics.put("cpuUtil", cpuMetrics.getUtilization());
metrics.put("cpuMem", cpuMetrics.getMemory());
metrics.put("gpuTemp", gpuMetrics.getTemp());
metrics.put("gpuMem", gpuMetrics.getMemory());
metrics.put("ramUtil", ramMetrics.getUsedRam());
DataChangeService.getInstance()
.publishEvent(new OutgoingUIEvent<>("metrics", metrics));
},
1000);
diskMetrics = new DiskMetrics();
}
public void stopTask() {
@@ -66,6 +48,26 @@ public class MetricsPublisher {
logger.info("This device does not support running bash commands. Stopped metrics thread.");
}
public void publish() {
if (!Platform.isRaspberryPi()) {
logger.debug("Ignoring metrics on non-Pi devices");
return;
}
logger.debug("Publishing Metrics...");
final var metrics = new HashMap<String, String>();
metrics.put("cpuTemp", cpuMetrics.getTemp());
metrics.put("cpuUtil", cpuMetrics.getUtilization());
metrics.put("cpuMem", cpuMetrics.getMemory());
metrics.put("gpuMem", gpuMetrics.getGPUMemorySplit());
metrics.put("ramUtil", ramMetrics.getUsedRam());
metrics.put("gpuMemUtil", gpuMetrics.getMallocedMemory());
metrics.put("diskUtilPct", diskMetrics.getUsedDiskPct());
DataChangeService.getInstance().publishEvent(OutgoingUIEvent.wrappedOf("metrics", metrics));
}
private static class Singleton {
public static final MetricsPublisher INSTANCE = new MetricsPublisher();
}

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