Compare commits
311 Commits
v2020.9.1-
...
v2023.1.2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e12f360a29 | ||
|
|
d0641d0cb6 | ||
|
|
871aa8b44b | ||
|
|
beaee9f6c0 | ||
|
|
11f5069148 | ||
|
|
6716d41a62 | ||
|
|
63b3cfe7e1 | ||
|
|
967be84b4b | ||
|
|
16ca2671f0 | ||
|
|
5e977445ee | ||
|
|
8117b5814b | ||
|
|
087429dab9 | ||
|
|
dbe7464ea9 | ||
|
|
ebef19af3d | ||
|
|
bde023c025 | ||
|
|
0f427bb52b | ||
|
|
05198ef294 | ||
|
|
b263fe19cc | ||
|
|
e68e6f3181 | ||
|
|
326701b74f | ||
|
|
af6f5eb0c4 | ||
|
|
0b5256df12 | ||
|
|
971b471f92 | ||
|
|
aaa886bd73 | ||
|
|
7c49cfe625 | ||
|
|
ea293f57d2 | ||
|
|
dc663657ff | ||
|
|
eedbfe3d49 | ||
|
|
1ab5b66829 | ||
|
|
d0bf64af6c | ||
|
|
8028d1887c | ||
|
|
74b807343e | ||
|
|
15fbe29d34 | ||
|
|
550194152a | ||
|
|
3a10f49b54 | ||
|
|
7ff630dc44 | ||
|
|
4088a394f3 | ||
|
|
78ab5e7c1d | ||
|
|
14d263a567 | ||
|
|
cf1a45d35b | ||
|
|
2ebc27aa3b | ||
|
|
95c55f08cf | ||
|
|
4382b8ea3f | ||
|
|
b1905954bc | ||
|
|
548f52e117 | ||
|
|
1971744589 | ||
|
|
6c51d8ab51 | ||
|
|
8330bf9d92 | ||
|
|
e1b39a1723 | ||
|
|
915f784d9d | ||
|
|
96006fc501 | ||
|
|
4fd7533456 | ||
|
|
bb63af601d | ||
|
|
da1aabae3a | ||
|
|
643db9c435 | ||
|
|
ec7bef7a4b | ||
|
|
b72f4ca2a9 | ||
|
|
ffd741ec0a | ||
|
|
678961e4f2 | ||
|
|
4c004fc780 | ||
|
|
41a00bc90f | ||
|
|
dcad7f34a2 | ||
|
|
72d8f49145 | ||
|
|
df852410b0 | ||
|
|
3c7165bb0d | ||
|
|
f193a2331a | ||
|
|
c7aa84ca41 | ||
|
|
209cdbf45f | ||
|
|
e03ec862a8 | ||
|
|
8169da5ad4 | ||
|
|
916431b4ff | ||
|
|
7dd1719fbd | ||
|
|
b408a58e9e | ||
|
|
a64697e714 | ||
|
|
e971db2f52 | ||
|
|
7b6afd545b | ||
|
|
0f99044468 | ||
|
|
1412155c50 | ||
|
|
b1280e49d5 | ||
|
|
aaac6a4fbb | ||
|
|
b68b0ca5f6 | ||
|
|
45d99f1f6b | ||
|
|
a42fef67f2 | ||
|
|
bd4d74c192 | ||
|
|
c4500ce12b | ||
|
|
81d19672d2 | ||
|
|
04bde1b230 | ||
|
|
4f355f2749 | ||
|
|
5e604cf98d | ||
|
|
2d7a88e231 | ||
|
|
27198a3e32 | ||
|
|
fbf6fb304e | ||
|
|
d24a8d4188 | ||
|
|
def40484e3 | ||
|
|
aff163fc6a | ||
|
|
c392d5fa4d | ||
|
|
8dbd428359 | ||
|
|
ccd3a512d6 | ||
|
|
bfc5e45cd0 | ||
|
|
a1b09100e0 | ||
|
|
2bf7a77885 | ||
|
|
d1bfb86ab4 | ||
|
|
07904589df | ||
|
|
5540bbf115 | ||
|
|
c827afb25f | ||
|
|
87e7c3ca74 | ||
|
|
4d5904dd6d | ||
|
|
9bf589ebc6 | ||
|
|
1e4a92c71f | ||
|
|
4ad9d97508 | ||
|
|
2c6b0ddac3 | ||
|
|
dafee954e0 | ||
|
|
5ac541642e | ||
|
|
ad0474d42a | ||
|
|
4b4a0a1cd9 | ||
|
|
a764ace7f2 | ||
|
|
a3bcd3ac4f | ||
|
|
661f8b2c04 | ||
|
|
72717cecf0 | ||
|
|
971ff3ac40 | ||
|
|
b80e436f02 | ||
|
|
be1a053cbe | ||
|
|
f4555dc545 | ||
|
|
54fdd1db51 | ||
|
|
1805785cc6 | ||
|
|
e62f6419b5 | ||
|
|
fa7824c616 | ||
|
|
9090aa6bcc | ||
|
|
5655ca6890 | ||
|
|
50fdfd8bce | ||
|
|
3120a6439b | ||
|
|
ab3e8c8db7 | ||
|
|
5144819ce2 | ||
|
|
d779fe23f0 | ||
|
|
b2a3f34433 | ||
|
|
b09a6d6a2d | ||
|
|
9893cf1f7e | ||
|
|
fc91daf397 | ||
|
|
a3e205cb6f | ||
|
|
553bed32b5 | ||
|
|
6c91feaf3f | ||
|
|
4ddb9aa08f | ||
|
|
4aadebdbb2 | ||
|
|
da8d70f887 | ||
|
|
80a0d8de1c | ||
|
|
3ad476bc28 | ||
|
|
e6d8e05b91 | ||
|
|
46fa17dfd8 | ||
|
|
43c35286f3 | ||
|
|
3d317f7035 | ||
|
|
a161bd5be9 | ||
|
|
0f730fc28d | ||
|
|
12e06b09c3 | ||
|
|
641101f574 | ||
|
|
6a1201432c | ||
|
|
e77a06bfa6 | ||
|
|
8b0b18bd07 | ||
|
|
1766f3bf0f | ||
|
|
4578fa756c | ||
|
|
d6e1e28fc2 | ||
|
|
49048c3998 | ||
|
|
8b079d9b20 | ||
|
|
1522adaa0e | ||
|
|
3cd57b8b43 | ||
|
|
c944967476 | ||
|
|
dbd631da61 | ||
|
|
f731ae37d2 | ||
|
|
0a8da1a0bd | ||
|
|
be5f0880c8 | ||
|
|
a02cd4a50e | ||
|
|
efd31543f6 | ||
|
|
a151f23319 | ||
|
|
822811c853 | ||
|
|
23834c87f4 | ||
|
|
f103a6b712 | ||
|
|
3257736ffa | ||
|
|
9df25eda88 | ||
|
|
5a13739818 | ||
|
|
5ca39e7f84 | ||
|
|
ffe34f00fe | ||
|
|
a5cc0808c4 | ||
|
|
08fafe2607 | ||
|
|
a2af7d9273 | ||
|
|
a731c7a8db | ||
|
|
7e74da5cff | ||
|
|
0977fd0dff | ||
|
|
3241ef7b1b | ||
|
|
f922466d41 | ||
|
|
243f06da2d | ||
|
|
44e91a184d | ||
|
|
e6b0f398b6 | ||
|
|
5a475e1071 | ||
|
|
f8def88e4d | ||
|
|
db09e5209f | ||
|
|
9fdd945a52 | ||
|
|
00b8e7d1c5 | ||
|
|
798b8e398a | ||
|
|
affb27038b | ||
|
|
6767781a41 | ||
|
|
6007cc752d | ||
|
|
9cf5c77d69 | ||
|
|
9dc5481d1c | ||
|
|
3948650e6c | ||
|
|
49fcdb64ed | ||
|
|
1e715ce4bb | ||
|
|
f9fd7a0b45 | ||
|
|
e9a3c2d1b8 | ||
|
|
129575dd23 | ||
|
|
ba8d2691fc | ||
|
|
f3d3a59ca0 | ||
|
|
b653fc7db1 | ||
|
|
71ee03a531 | ||
|
|
4a2493ff2e | ||
|
|
0b20111824 | ||
|
|
449977e63b | ||
|
|
b0504cbef5 | ||
|
|
fccb395564 | ||
|
|
b510417541 | ||
|
|
0330467874 | ||
|
|
4c15a46cda | ||
|
|
d59f8d1227 | ||
|
|
bbc8a3137b | ||
|
|
951c038f19 | ||
|
|
1b0c5af86e | ||
|
|
a113bd4192 | ||
|
|
d3c23345da | ||
|
|
2e1b3d0f83 | ||
|
|
58b39f47aa | ||
|
|
dc0f8cf296 | ||
|
|
3d34d1ca40 | ||
|
|
5298de0f64 | ||
|
|
2330b72451 | ||
|
|
69d2499e1a | ||
|
|
0a4dcd17e0 | ||
|
|
15c687655a | ||
|
|
839f959681 | ||
|
|
0b2de7d9f1 | ||
|
|
5d139e0a4e | ||
|
|
b2d939b3b5 | ||
|
|
f7e29a1992 | ||
|
|
08a51fd237 | ||
|
|
b8bc65ec32 | ||
|
|
8e190ce5f7 | ||
|
|
f676023a5d | ||
|
|
d92595f622 | ||
|
|
69142928b3 | ||
|
|
28e3b510c7 | ||
|
|
fc05bcab2c | ||
|
|
abf5226405 | ||
|
|
d327428e1b | ||
|
|
79fc194575 | ||
|
|
7a9d999c15 | ||
|
|
2cf725876f | ||
|
|
2ca879c82d | ||
|
|
be5d8f6518 | ||
|
|
7a032cce6e | ||
|
|
f2f32da2f9 | ||
|
|
5768648cde | ||
|
|
6856427f86 | ||
|
|
2a687a1db8 | ||
|
|
771f7442c9 | ||
|
|
c7d092a775 | ||
|
|
11a66b15ed | ||
|
|
0c89db421c | ||
|
|
36de88f903 | ||
|
|
a49f3ac7f0 | ||
|
|
229570d522 | ||
|
|
d346513ad7 | ||
|
|
a5437f7215 | ||
|
|
0c3aeb409b | ||
|
|
e4b6559b81 | ||
|
|
e608d073bd | ||
|
|
663684fb10 | ||
|
|
c3dbd45716 | ||
|
|
15d21b7841 | ||
|
|
14ed7ce7a4 | ||
|
|
d2b0e465ff | ||
|
|
5e385b5925 | ||
|
|
c11f1f3f28 | ||
|
|
308e108953 | ||
|
|
2ae750f00f | ||
|
|
da2aaba033 | ||
|
|
e74f750fc0 | ||
|
|
3f48346557 | ||
|
|
bf7c9fea44 | ||
|
|
8109a2a437 | ||
|
|
0ce49bd8f2 | ||
|
|
33bbb4c69f | ||
|
|
35a6c0bfa4 | ||
|
|
869e4628ce | ||
|
|
866ce2197e | ||
|
|
3a78e23a55 | ||
|
|
31013346c0 | ||
|
|
e37fcdea98 | ||
|
|
23f3c0e6e1 | ||
|
|
6e0a6b804e | ||
|
|
2d7a4dd1b9 | ||
|
|
28459704c6 | ||
|
|
125cd35557 | ||
|
|
3305c6619d | ||
|
|
24132555b8 | ||
|
|
44bfc3ea6c | ||
|
|
71fc8a7017 | ||
|
|
b73c698e4d | ||
|
|
45686b7c9d | ||
|
|
90f8397688 | ||
|
|
2495d348ea | ||
|
|
a35f775b05 | ||
|
|
ddd15d362b | ||
|
|
7bf92a9db3 | ||
|
|
73fc8e04ca |
167
.clang-format
Normal 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
@@ -1,3 +1,2 @@
|
||||
# These owners will be the default owners for everything in the repo.
|
||||
* @PhotonVision/program-devs
|
||||
|
||||
|
||||
456
.github/workflows/main.yml
vendored
@@ -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:
|
||||
@@ -20,26 +22,22 @@ jobs:
|
||||
working-directory: photon-client
|
||||
|
||||
# The type of runner that the job will run on.
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
# Grab the docker container.
|
||||
container:
|
||||
image: docker://node:10
|
||||
runs-on: ubuntu-22.04
|
||||
|
||||
steps:
|
||||
# Checkout code.
|
||||
- uses: actions/checkout@v1
|
||||
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
# Setup Node.js
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v1
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 10
|
||||
node-version: 16
|
||||
|
||||
# Run npm
|
||||
- run: |
|
||||
npm ci
|
||||
npm run build --if-present
|
||||
- run: npm update -g npm
|
||||
- run: npm ci
|
||||
- run: npm run build --if-present
|
||||
|
||||
# Upload client artifact.
|
||||
- uses: actions/upload-artifact@master
|
||||
@@ -47,71 +45,123 @@ 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
|
||||
|
||||
# The type of runner that the job will run on.
|
||||
runs-on: ubuntu-latest
|
||||
photon-build-examples:
|
||||
runs-on: ubuntu-22.04
|
||||
name: "Build Examples"
|
||||
|
||||
steps:
|
||||
# Checkout code.
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v1
|
||||
|
||||
# Install Java 11.
|
||||
- name: Install Java 11
|
||||
uses: actions/setup-java@v1
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
java-version: 11
|
||||
fetch-depth: 0
|
||||
|
||||
# Run Gradle build.
|
||||
# Fetch tags.
|
||||
- name: Fetch tags
|
||||
run: git fetch --tags --force
|
||||
|
||||
# Install Java 17.
|
||||
- name: Install Java 17
|
||||
uses: actions/setup-java@v3
|
||||
with:
|
||||
java-version: 17
|
||||
distribution: temurin
|
||||
|
||||
# Need to publish to maven local first, so that C++ sim can pick it up
|
||||
# Still haven't figure out how to make the vendordep file be copied before trying to build examples
|
||||
- name: Publish photonlib to maven local
|
||||
run: |
|
||||
chmod +x gradlew
|
||||
./gradlew publishtomavenlocal -x check
|
||||
|
||||
- name: Build Java examples
|
||||
working-directory: photonlib-java-examples
|
||||
run: |
|
||||
chmod +x gradlew
|
||||
./gradlew copyPhotonlib -x check
|
||||
./gradlew buildAllExamples -x check --max-workers 2
|
||||
|
||||
- name: Build C++ examples
|
||||
working-directory: photonlib-cpp-examples
|
||||
run: |
|
||||
chmod +x gradlew
|
||||
./gradlew copyPhotonlib -x check
|
||||
./gradlew buildAllExamples -x check --max-workers 2
|
||||
|
||||
photon-build-all:
|
||||
# The type of runner that the job will run on.
|
||||
runs-on: ubuntu-22.04
|
||||
|
||||
steps:
|
||||
# Checkout code.
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
# Fetch tags.
|
||||
- name: Fetch tags
|
||||
run: git fetch --tags --force
|
||||
|
||||
# Install Java 17.
|
||||
- name: Install Java 17
|
||||
uses: actions/setup-java@v3
|
||||
with:
|
||||
java-version: 17
|
||||
distribution: temurin
|
||||
|
||||
# Run only build tasks, no checks??
|
||||
- name: Gradle Build
|
||||
run: |
|
||||
chmod +x gradlew
|
||||
./gradlew build -x check
|
||||
./gradlew photon-server:build photon-lib:build -x check --max-workers 2
|
||||
|
||||
# Run Gradle Tests.
|
||||
- name: Gradle Tests
|
||||
run: ./gradlew testHeadless -i --max-workers 1 --stacktrace
|
||||
|
||||
# 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
|
||||
uses: codecov/codecov-action@v1
|
||||
- name: Publish Server Coverage Report
|
||||
uses: codecov/codecov-action@v3
|
||||
with:
|
||||
file: ./photon-server/build/reports/jacoco/test/jacocoTestReport.xml
|
||||
|
||||
build-offline-docs:
|
||||
runs-on: ubuntu-latest
|
||||
- name: Publish Core Coverage Report
|
||||
uses: codecov/codecov-action@v3
|
||||
with:
|
||||
file: ./photon-core/build/reports/jacoco/test/jacocoTestReport.xml
|
||||
|
||||
photonserver-build-offline-docs:
|
||||
runs-on: ubuntu-22.04
|
||||
|
||||
steps:
|
||||
# Checkout docs.
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
repository: 'PhotonVision/photonvision-docs.git'
|
||||
ref: master
|
||||
|
||||
# Install Python.
|
||||
- uses: actions/setup-python@v2
|
||||
- uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: '3.6'
|
||||
|
||||
python-version: '3.9'
|
||||
|
||||
- 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
|
||||
|
||||
|
||||
# Don't check the docs. If a PR was merged to the docs repo, it ought to pass CI. No need to re-check here.
|
||||
# - name: Check the docs
|
||||
# run: |
|
||||
# make linkcheck
|
||||
# make lint
|
||||
|
||||
- name: Build the docs
|
||||
run: |
|
||||
make html
|
||||
@@ -121,91 +171,287 @@ jobs:
|
||||
with:
|
||||
name: built-docs
|
||||
path: build/html
|
||||
|
||||
|
||||
build-package:
|
||||
needs: [build-client, build-server, build-offline-docs]
|
||||
|
||||
# Let all steps run within the photon-server dir.
|
||||
defaults:
|
||||
run:
|
||||
working-directory: photon-server
|
||||
|
||||
photonserver-check-lint:
|
||||
# The type of runner that the job will run on.
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-22.04
|
||||
|
||||
steps:
|
||||
# Checkout code.
|
||||
- uses: actions/checkout@v1
|
||||
|
||||
# Install Java 11.
|
||||
- uses: actions/setup-java@v1
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
java-version: 11
|
||||
fetch-depth: 0
|
||||
|
||||
# Install Java 17.
|
||||
- uses: actions/setup-java@v3
|
||||
with:
|
||||
java-version: 17
|
||||
distribution: temurin
|
||||
|
||||
# Check server code with Spotless.
|
||||
- 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-2022
|
||||
artifact-name: Win64
|
||||
- os: macos-11
|
||||
artifact-name: macOS
|
||||
- os: ubuntu-22.04
|
||||
artifact-name: Linux
|
||||
|
||||
runs-on: ${{ matrix.os }}
|
||||
name: "Photonlib - Build Host - ${{ matrix.artifact-name }}"
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- uses: actions/setup-java@v3
|
||||
with:
|
||||
java-version: 17
|
||||
distribution: temurin
|
||||
- run: git fetch --tags --force
|
||||
- run: |
|
||||
chmod +x gradlew
|
||||
./gradlew photon-lib:build --max-workers 1
|
||||
- run: ./gradlew photon-lib:publish photon-targeting:publish
|
||||
name: Publish
|
||||
env:
|
||||
ARTIFACTORY_API_KEY: ${{ secrets.ARTIFACTORY_API_KEY }}
|
||||
if: github.event_name == 'push'
|
||||
|
||||
photonlib-build-docker:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- container: wpilib/roborio-cross-ubuntu:2023-22.04
|
||||
artifact-name: Athena
|
||||
- container: wpilib/raspbian-cross-ubuntu:bullseye-22.04
|
||||
artifact-name: Raspbian
|
||||
- container: wpilib/aarch64-cross-ubuntu:bullseye-22.04
|
||||
artifact-name: Aarch64
|
||||
|
||||
runs-on: ubuntu-22.04
|
||||
container: ${{ matrix.container }}
|
||||
name: "Photonlib - Build Docker - ${{ matrix.artifact-name }}"
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Config Git
|
||||
run: |
|
||||
git config --global --add safe.directory /__w/photonvision/photonvision
|
||||
- name: Build PhotonLib
|
||||
run: |
|
||||
chmod +x gradlew
|
||||
./gradlew photon-lib:build --max-workers 1
|
||||
- name: Publish
|
||||
run: |
|
||||
chmod +x gradlew
|
||||
./gradlew photon-lib:publish
|
||||
env:
|
||||
ARTIFACTORY_API_KEY: ${{ secrets.ARTIFACTORY_API_KEY }}
|
||||
if: github.event_name == 'push'
|
||||
|
||||
photonlib-wpiformat:
|
||||
name: "wpiformat"
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Fetch all history and metadata
|
||||
run: |
|
||||
git fetch --prune --unshallow
|
||||
git checkout -b pr
|
||||
git branch -f master origin/master
|
||||
- name: Set up Python 3.8
|
||||
uses: actions/setup-python@v4
|
||||
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@v3
|
||||
with:
|
||||
name: wpiformat fixes
|
||||
path: wpiformat-fixes.patch
|
||||
if: ${{ failure() }}
|
||||
|
||||
photon-build-package:
|
||||
needs: [photonclient-build, photon-build-all, photonserver-build-offline-docs]
|
||||
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- os: windows-latest
|
||||
artifact-name: Win64
|
||||
architecture: x64
|
||||
arch-override: none
|
||||
- os: macos-latest
|
||||
artifact-name: macOS
|
||||
architecture: x64
|
||||
arch-override: none
|
||||
- os: ubuntu-latest
|
||||
artifact-name: Linux
|
||||
architecture: x64
|
||||
arch-override: none
|
||||
- os: macos-latest
|
||||
artifact-name: macOSArm
|
||||
architecture: x64
|
||||
arch-override: macarm64
|
||||
- os: ubuntu-latest
|
||||
artifact-name: LinuxArm32
|
||||
architecture: x64
|
||||
arch-override: linuxarm32
|
||||
- os: ubuntu-latest
|
||||
artifact-name: LinuxArm64
|
||||
architecture: x64
|
||||
arch-override: linuxarm64
|
||||
|
||||
# The type of runner that the job will run on.
|
||||
runs-on: ${{ matrix.os }}
|
||||
name: "Build fat JAR - ${{ matrix.artifact-name }}"
|
||||
|
||||
steps:
|
||||
# Checkout code.
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
# Install Java 17.
|
||||
- uses: actions/setup-java@v3
|
||||
with:
|
||||
java-version: 17
|
||||
distribution: temurin
|
||||
|
||||
# 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
|
||||
if: ${{ (matrix.os) != 'windows-latest' }}
|
||||
- run: |
|
||||
del photon-server\src\main\resources\web\*.*
|
||||
mkdir photon-server\src\main\resources\web\docs
|
||||
if: ${{ (matrix.os) == 'windows-latest' }}
|
||||
|
||||
# Download client artifact to resources folder.
|
||||
- uses: actions/download-artifact@v2
|
||||
- uses: actions/download-artifact@v3
|
||||
with:
|
||||
name: built-client
|
||||
path: photon-server/src/main/resources/web/
|
||||
|
||||
# Download docs artifact to resources folder.
|
||||
- uses: actions/download-artifact@v2
|
||||
- uses: actions/download-artifact@v3
|
||||
with:
|
||||
name: built-docs
|
||||
path: photon-server/src/main/resources/web/docs
|
||||
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 2 -PArchOverride=${{ matrix.arch-override }}
|
||||
if: ${{ (matrix.arch-override != 'none') }}
|
||||
- run: |
|
||||
chmod +x gradlew
|
||||
./gradlew photon-server:shadowJar --max-workers 2
|
||||
if: ${{ (matrix.arch-override == 'none') }}
|
||||
|
||||
# Upload final fat jar as artifact.
|
||||
- uses: actions/upload-artifact@master
|
||||
- uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: jar
|
||||
name: jar-${{ matrix.artifact-name }}
|
||||
path: photon-server/build/libs
|
||||
|
||||
- uses: eine/tip@master
|
||||
|
||||
|
||||
photon-image-generator:
|
||||
needs: [photon-build-package]
|
||||
if: ${{ github.event_name != 'pull_request' }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- os: ubuntu-latest
|
||||
artifact-name: LinuxArm64
|
||||
image_suffix: RaspberryPi
|
||||
image_url: https://api.github.com/repos/photonvision/photon-pi-gen/releases/tags/v2023.1.0-beta-4_arm64
|
||||
- os: ubuntu-latest
|
||||
artifact-name: LinuxArm64
|
||||
image_suffix: limelight
|
||||
image_url: https://api.github.com/repos/photonvision/photon-pi-gen/releases/tags/v2023.1.0-beta-5_limelight-arm64
|
||||
|
||||
runs-on: ${{ matrix.os }}
|
||||
name: "Build image - ${{ matrix.image_url }}"
|
||||
|
||||
steps:
|
||||
# Checkout code.
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- uses: actions/download-artifact@v2
|
||||
with:
|
||||
name: jar-${{ matrix.artifact-name }}
|
||||
|
||||
- name: Generate image
|
||||
run: |
|
||||
chmod +x scripts/generatePiImage.sh
|
||||
./scripts/generatePiImage.sh ${{ matrix.image_url }} ${{ matrix.image_suffix }}
|
||||
|
||||
- uses: actions/upload-artifact@v3
|
||||
name: Upload image
|
||||
with:
|
||||
name: image-${{ matrix.image_suffix }}
|
||||
path: photonvision*.xz
|
||||
|
||||
|
||||
photon-release:
|
||||
needs: [photon-build-package, photon-image-generator]
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
# Download literally every single artifact. This also downloads client and docs,
|
||||
# but the filtering below won't pick these up (I hope)
|
||||
- uses: actions/download-artifact@v2
|
||||
|
||||
- run: find
|
||||
|
||||
# Push to dev release
|
||||
- uses: pyTooling/Actions/releaser@r0
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
tag: 'Dev'
|
||||
rm: true
|
||||
files: |
|
||||
photon-server/build/libs/*.jar
|
||||
**/*.xz
|
||||
**/*.jar
|
||||
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.
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
# Checkout code.
|
||||
- uses: actions/checkout@v1
|
||||
|
||||
# Install Java 11.
|
||||
- uses: actions/setup-java@v1
|
||||
# Upload all jars and xz archives
|
||||
- uses: softprops/action-gh-release@v1
|
||||
with:
|
||||
java-version: 11
|
||||
|
||||
# Check server code with Spotless.
|
||||
- run: |
|
||||
chmod +x gradlew
|
||||
./gradlew spotlessCheck
|
||||
|
||||
files: |
|
||||
**/*.xz
|
||||
**/*.jar
|
||||
if: startsWith(github.ref, 'refs/tags/v')
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
46
.gitignore
vendored
@@ -30,6 +30,7 @@ backend/settings/
|
||||
*.nar
|
||||
*.ear
|
||||
*.zip
|
||||
*.xz
|
||||
*.tar.gz
|
||||
*.rar
|
||||
|
||||
@@ -104,17 +105,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 +134,22 @@ 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
|
||||
.gitattributes
|
||||
lib/*
|
||||
photon-server/lib/libapriltag.so
|
||||
photon-server/bin/main/nativelibraries/apriltag/*
|
||||
photon-server/src/main/resources/nativelibraries/apriltag/*
|
||||
|
||||
photonlib-java-examples/*/vendordeps/*
|
||||
photonlib-cpp-examples/*/vendordeps/*
|
||||
|
||||
30
.styleguide
Normal file
@@ -0,0 +1,30 @@
|
||||
cppHeaderFileInclude {
|
||||
\.h$
|
||||
\.hpp$
|
||||
\.inc$
|
||||
\.inl$
|
||||
}
|
||||
|
||||
cppSrcFileInclude {
|
||||
\.cpp$
|
||||
}
|
||||
|
||||
modifiableFileExclude {
|
||||
\.jpg$
|
||||
\.jpeg$
|
||||
\.png$
|
||||
\.gif$
|
||||
\.so$
|
||||
\.dll$
|
||||
}
|
||||
|
||||
includeProject {
|
||||
^photonLib/
|
||||
}
|
||||
|
||||
includeOtherLibs {
|
||||
^frc/
|
||||
^networktables/
|
||||
^units/
|
||||
^wpi/
|
||||
}
|
||||
@@ -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/>.
|
||||
*/
|
||||
|
||||
23
LICENSE_MathUtils_orthogonalizeRotationMatrix.txt
Normal file
@@ -0,0 +1,23 @@
|
||||
Copyright (c) 2022 Photon Vision. All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are met:
|
||||
* Redistributions of source code must retain the above copyright
|
||||
notice, this list of conditions and the following disclaimer.
|
||||
* Redistributions in binary form must reproduce the above copyright
|
||||
notice, this list of conditions and the following disclaimer in the
|
||||
documentation and/or other materials provided with the distribution.
|
||||
* Neither the name of FIRST, WPILib, nor the names of other WPILib
|
||||
contributors may be used to endorse or promote products derived from
|
||||
this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY FIRST AND OTHER WPILIB CONTRIBUTORS "AS IS" AND
|
||||
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||
WARRANTIES OF MERCHANTABILITY NONINFRINGEMENT AND FITNESS FOR A PARTICULAR
|
||||
PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL FIRST OR CONTRIBUTORS BE LIABLE FOR
|
||||
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
||||
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
|
||||
ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
69
README.md
@@ -1,25 +1,64 @@
|
||||
# Photon Vision
|
||||
|
||||
[](https://github.com/PhotonVision/photonvision/actions?query=workflow%3ACI) [](https://codecov.io/gh/PhotonVision/photonvision)
|
||||
[](https://github.com/PhotonVision/photonvision/actions?query=workflow%3ACI) [](https://codecov.io/gh/PhotonVision/photonvision) [](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/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!
|
||||
|
||||
* `-PArchOverride=foobar`: builds for a target system other than your current architecture. Valid overrides are winx32, winx64,
|
||||
macx64, macarm64, linuxx64, linuxarm64, linuxarm32, and linuxathena.
|
||||
- `-PtgtIp`: deploys (builds and copies the JAR) to the coprocessor at the specified IP
|
||||
- `-Pprofile`: enables JVM profiling
|
||||
|
||||
## Building
|
||||
|
||||
Gradle is used for all C++ and Java code, and NPM is used for the web UI. Instructions to compile PhotonVision yourself can be found [in our docs](https://docs.photonvision.org/en/latest/docs/contributing/photonvision/build-instructions.html?highlight=npm%20install#compiling-instructions).
|
||||
|
||||
You can run one of the many built in examples straight from the command line, too! They contain a fully featured robot project, and some include simulation support. The projects can be found inside the `photonlib-java-examples` and `photonlib-cpp-examples` subdirectories, respectively. The projects currently available include:
|
||||
|
||||
- photonlib-java-examples:
|
||||
- aimandrange:simulateJava
|
||||
- aimattarget:simulateJava
|
||||
- getinrange:simulateJava
|
||||
- simaimandrange:simulateJava
|
||||
- simposeest:simulateJava
|
||||
- photonlib-cpp-examples:
|
||||
- aimandrange:simulateNative
|
||||
- getinrange:simulateNative
|
||||
|
||||
To run them, use the commands listed below. Photonlib must first be published to your local maven repository, then the `copyPhotonlib` task will copy the generated vendordep json file into each example. After that, the simulateJava/simulateNative task can be used like a normal robot project. Robot simulation with attached debugger is technically possible by using simulateExternalJava and modifying the launch script it exports, though unsupported.
|
||||
|
||||
```
|
||||
~/photonvision$ ./gradlew publishToMavenLocal
|
||||
|
||||
~/photonvision$ cd photonlib-java-examples
|
||||
~/photonvision/photonlib-java-examples$ ./gradlew copyPhotonlib
|
||||
~/photonvision/photonlib-java-examples$ ./gradlew <example-name>:simulateJava
|
||||
|
||||
~/photonvision$ cd photonlib-cpp-examples
|
||||
~/photonvision/photonlib-cpp-examples$ ./gradlew copyPhotonlib
|
||||
~/photonvision/photonlib-cpp-examples$ ./gradlew <example-name>:simulateNative
|
||||
```
|
||||
|
||||
|
||||
## 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 +68,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)
|
||||
|
||||
62
build.gradle
Normal file
@@ -0,0 +1,62 @@
|
||||
plugins {
|
||||
id "com.diffplug.spotless" version "6.1.2"
|
||||
id "com.github.johnrengelman.shadow" version "7.1.2"
|
||||
id "com.github.node-gradle.node" version "3.1.1" apply false
|
||||
id "edu.wpi.first.GradleJni" version "1.0.0"
|
||||
id "edu.wpi.first.GradleVsCode" version "1.1.0"
|
||||
id "edu.wpi.first.NativeUtils" version "2023.11.1" apply false
|
||||
id "edu.wpi.first.wpilib.repositories.WPILibRepositoriesPlugin" version "2020.2"
|
||||
id "org.hidetake.ssh" version "2.10.1"
|
||||
id 'edu.wpi.first.WpilibTools' version '1.0.0'
|
||||
}
|
||||
|
||||
import org.gradle.api.internal.artifacts.dependencies.DefaultExternalModuleDependency;
|
||||
|
||||
allprojects {
|
||||
repositories {
|
||||
mavenCentral()
|
||||
mavenLocal()
|
||||
maven { url = "https://maven.photonvision.org/repository/internal/" }
|
||||
}
|
||||
wpilibRepositories.addAllReleaseRepositories(it)
|
||||
wpilibRepositories.addAllDevelopmentRepositories(it)
|
||||
}
|
||||
|
||||
// Configure the version number.
|
||||
apply from: "versioningHelper.gradle"
|
||||
|
||||
ext {
|
||||
wpilibVersion = "2023.1.1"
|
||||
opencvVersion = "4.6.0-4"
|
||||
joglVersion = "2.4.0-rc-20200307"
|
||||
pubVersion = versionString
|
||||
isDev = pubVersion.startsWith("dev")
|
||||
|
||||
// A list, for legacy reasons, with only the current platform contained
|
||||
String nativeName = wpilibTools.platformMapper.currentPlatform.platformName;
|
||||
if (nativeName == "linuxx64") nativeName = "linuxx86-64";
|
||||
if (nativeName == "winx64") nativeName = "windowsx86-64";
|
||||
if (nativeName == "macx64") nativeName = "osxx86-64";
|
||||
if (nativeName == "macarm64") nativeName = "osxarm64";
|
||||
jniPlatform = nativeName
|
||||
println("Building for platform " + jniPlatform)
|
||||
}
|
||||
|
||||
wpilibTools.deps.wpilibVersion = wpilibVersion
|
||||
|
||||
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")
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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
|
||||
@@ -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.5.1-bin.zip
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
190
gradlew
vendored
Executable file
@@ -0,0 +1,190 @@
|
||||
#!/bin/sh
|
||||
|
||||
#
|
||||
# Copyright <20> 2015-2021 the original authors.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# https://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
|
||||
##############################################################################
|
||||
|
||||
# Attempt to set APP_HOME
|
||||
|
||||
# Resolve links: $0 may be a link
|
||||
app_path=$0
|
||||
|
||||
# Need this for daisy-chained symlinks.
|
||||
while
|
||||
APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
|
||||
[ -h "$app_path" ]
|
||||
do
|
||||
ls=$( ls -ld "$app_path" )
|
||||
link=${ls#*' -> '}
|
||||
case $link in #(
|
||||
/*) app_path=$link ;; #(
|
||||
*) app_path=$APP_HOME$link ;;
|
||||
esac
|
||||
done
|
||||
|
||||
APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
|
||||
|
||||
APP_NAME="Gradle"
|
||||
APP_BASE_NAME=${0##*/}
|
||||
|
||||
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
|
||||
|
||||
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
||||
MAX_FD=maximum
|
||||
|
||||
warn () {
|
||||
echo "$*"
|
||||
} >&2
|
||||
|
||||
die () {
|
||||
echo
|
||||
echo "$*"
|
||||
echo
|
||||
exit 1
|
||||
} >&2
|
||||
|
||||
# OS specific support (must be 'true' or 'false').
|
||||
cygwin=false
|
||||
msys=false
|
||||
darwin=false
|
||||
nonstop=false
|
||||
case "$( uname )" in #(
|
||||
CYGWIN* ) cygwin=true ;; #(
|
||||
Darwin* ) darwin=true ;; #(
|
||||
MSYS* | MINGW* ) msys=true ;; #(
|
||||
NONSTOP* ) nonstop=true ;;
|
||||
esac
|
||||
|
||||
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
|
||||
|
||||
|
||||
# Determine the Java command to use to start the JVM.
|
||||
if [ -n "$JAVA_HOME" ] ; then
|
||||
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
|
||||
# IBM's JDK on AIX uses strange locations for the executables
|
||||
JAVACMD=$JAVA_HOME/jre/sh/java
|
||||
else
|
||||
JAVACMD=$JAVA_HOME/bin/java
|
||||
fi
|
||||
if [ ! -x "$JAVACMD" ] ; then
|
||||
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
|
||||
|
||||
Please set the JAVA_HOME variable in your environment to match the
|
||||
location of your Java installation."
|
||||
fi
|
||||
else
|
||||
JAVACMD=java
|
||||
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||
|
||||
Please set the JAVA_HOME variable in your environment to match the
|
||||
location of your Java installation."
|
||||
fi
|
||||
|
||||
# Increase the maximum file descriptors if we can.
|
||||
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
|
||||
case $MAX_FD in #(
|
||||
max*)
|
||||
MAX_FD=$( ulimit -H -n ) ||
|
||||
warn "Could not query maximum file descriptor limit"
|
||||
esac
|
||||
case $MAX_FD in #(
|
||||
'' | soft) :;; #(
|
||||
*)
|
||||
ulimit -n "$MAX_FD" ||
|
||||
warn "Could not set maximum file descriptor limit to $MAX_FD"
|
||||
esac
|
||||
fi
|
||||
|
||||
# Collect all arguments for the java command, stacking in reverse order:
|
||||
# * args from the command line
|
||||
# * the main class name
|
||||
# * -classpath
|
||||
# * -D...appname settings
|
||||
# * --module-path (only if needed)
|
||||
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
|
||||
|
||||
# For Cygwin or MSYS, switch paths to Windows format before running java
|
||||
if "$cygwin" || "$msys" ; then
|
||||
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
|
||||
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
|
||||
|
||||
JAVACMD=$( cygpath --unix "$JAVACMD" )
|
||||
|
||||
# Now convert the arguments - kludge to limit ourselves to /bin/sh
|
||||
for arg do
|
||||
if
|
||||
case $arg in #(
|
||||
-*) false ;; # don't mess with options #(
|
||||
/?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
|
||||
[ -e "$t" ] ;; #(
|
||||
*) false ;;
|
||||
esac
|
||||
then
|
||||
arg=$( cygpath --path --ignore --mixed "$arg" )
|
||||
fi
|
||||
# Roll the args list around exactly as many times as the number of
|
||||
# args, so each arg winds up back in the position where it started, but
|
||||
# possibly modified.
|
||||
#
|
||||
# NB: a `for` loop captures its iteration list before it begins, so
|
||||
# changing the positional parameters here affects neither the number of
|
||||
# iterations, nor the values presented in `arg`.
|
||||
shift # remove old arg
|
||||
set -- "$@" "$arg" # push replacement arg
|
||||
done
|
||||
fi
|
||||
|
||||
# Collect all arguments for the java command;
|
||||
# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of
|
||||
# shell script including quotes and variable substitutions, so put them in
|
||||
# double quotes to make sure that they get re-expanded; and
|
||||
# * put everything else in single quotes, so that it's not re-expanded.
|
||||
|
||||
set -- \
|
||||
"-Dorg.gradle.appname=$APP_BASE_NAME" \
|
||||
-classpath "$CLASSPATH" \
|
||||
org.gradle.wrapper.GradleWrapperMain \
|
||||
"$@"
|
||||
|
||||
# Use "xargs" to parse quoted args.
|
||||
#
|
||||
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
|
||||
#
|
||||
# In Bash we could simply go:
|
||||
#
|
||||
# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
|
||||
# set -- "${ARGS[@]}" "$@"
|
||||
#
|
||||
# but POSIX shell has neither arrays nor command substitution, so instead we
|
||||
# post-process each arg (as a line of input to sed) to backslash-escape any
|
||||
# character that might be a shell metacharacter, then use eval to reverse
|
||||
# that process (while maintaining the separation between arguments), and wrap
|
||||
# the whole thing up as a single "set" statement.
|
||||
#
|
||||
# This will of course break if any of these variables contains a newline or
|
||||
# an unmatched quote.
|
||||
#
|
||||
|
||||
eval "set -- $(
|
||||
printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
|
||||
xargs -n1 |
|
||||
sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
|
||||
tr '\n' ' '
|
||||
)" '"$@"'
|
||||
|
||||
exec "$JAVACMD" "$@"
|
||||
25
photon-server/gradlew.bat → gradlew.bat
vendored
@@ -29,6 +29,9 @@ 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"
|
||||
|
||||
@@ -37,7 +40,7 @@ if defined JAVA_HOME goto findJavaFromJavaHome
|
||||
|
||||
set JAVA_EXE=java.exe
|
||||
%JAVA_EXE% -version >NUL 2>&1
|
||||
if "%ERRORLEVEL%" == "0" goto init
|
||||
if "%ERRORLEVEL%" == "0" goto execute
|
||||
|
||||
echo.
|
||||
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||
@@ -51,7 +54,7 @@ goto fail
|
||||
set JAVA_HOME=%JAVA_HOME:"=%
|
||||
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
|
||||
|
||||
if exist "%JAVA_EXE%" goto init
|
||||
if exist "%JAVA_EXE%" goto execute
|
||||
|
||||
echo.
|
||||
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
|
||||
@@ -61,28 +64,14 @@ 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%
|
||||
"%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
|
||||
25674
photon-client/package-lock.json
generated
@@ -13,11 +13,13 @@
|
||||
"axios": "^0.19.2",
|
||||
"core-js": "^2.6.11",
|
||||
"downloadjs": "^1.4.7",
|
||||
"jspdf": "^2.4.0",
|
||||
"material-design-icons-iconfont": "^5.0.1",
|
||||
"msgpack5": "^4.2.1",
|
||||
"three-full": "^28.0.2",
|
||||
"vue": "^2.6.12",
|
||||
"vue-axios": "^2.1.5",
|
||||
"vue-native-websocket": "git+https://github.com/PhotonVision/vue-native-websocket.git#7a32791",
|
||||
"vue-native-websocket": "git+https://git@github.com/PhotonVision/vue-native-websocket.git#5189f29",
|
||||
"vue-router": "^3.4.3",
|
||||
"vuetify": "^2.3.10",
|
||||
"vuex": "^3.5.1"
|
||||
|
||||
@@ -17,4 +17,4 @@
|
||||
<!-- built files will be auto injected -->
|
||||
</body>
|
||||
|
||||
</html>
|
||||
</html>
|
||||
|
||||
BIN
photon-client/public/loading.gif
Normal file
|
After Width: | Height: | Size: 12 KiB |
317
photon-client/public/thinclient.html
Normal file
@@ -0,0 +1,317 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<title>ThinClient</title>
|
||||
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
.imgbox {
|
||||
display: grid;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
.center-fit {
|
||||
|
||||
width: 90vw;
|
||||
margin: auto;
|
||||
}
|
||||
</style>
|
||||
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<hr>
|
||||
<div class="imgbox">
|
||||
<img id="streamImg" class="center-fit" src=''>
|
||||
</div>
|
||||
<hr>
|
||||
|
||||
<form id="frm1">
|
||||
Host <input type="text" id="host" value="photonvision.local"><br>
|
||||
Port <input type="text" id="port" value="1181"><br>
|
||||
</form>
|
||||
|
||||
<button>Start Stream</button>
|
||||
|
||||
<script type="module">
|
||||
class WebsocketVideoStream{
|
||||
|
||||
constructor(drawDiv, streamPort, host) {
|
||||
|
||||
this.drawDiv = drawDiv;
|
||||
this.image = document.getElementById(this.drawDiv);
|
||||
this.streamPort = streamPort;
|
||||
this.newStreamPortReq = null;
|
||||
this.serverAddr = "ws://" + host + "/websocket_cameras";
|
||||
this.dispNoStream();
|
||||
this.ws_connect();
|
||||
this.imgData = null;
|
||||
this.imgDataTime = -1;
|
||||
this.imgObjURL = null;
|
||||
this.frameRxCount = 0;
|
||||
|
||||
//Display state machine
|
||||
this.DSM_DISCONNECTED = "DISCONNECTED";
|
||||
this.DSM_WAIT_FOR_VALID_PORT = "WAIT_FOR_VALID_PORT";
|
||||
this.DSM_SUBSCRIBE = "SUBSCRIBE";
|
||||
this.DSM_WAIT_FOR_FIRST_FRAME = "WAIT_FOR_FIRST_FRAME";
|
||||
this.DSM_SHOWING = "SHOWING";
|
||||
this.DSM_RESTART_UNSUBSCRIBE = "UNSUBSCRIBE";
|
||||
this.DSM_RESTART_WAIT = "WAIT_BEFORE_SUBSCRIBE";
|
||||
|
||||
this.dsm_cur_state = this.DSM_DISCONNECTED;
|
||||
this.dsm_prev_state = this.DSM_DISCONNECTED;
|
||||
this.dsm_restart_start_time = window.performance.now();
|
||||
|
||||
requestAnimationFrame(()=>this.animationLoop());
|
||||
}
|
||||
|
||||
dispImageData(){
|
||||
//From https://stackoverflow.com/questions/67507616/set-image-src-from-image-blob/67507685#67507685
|
||||
if(this.imgObjURL != null){
|
||||
URL.revokeObjectURL(this.imgObjURL)
|
||||
}
|
||||
this.imgObjURL = URL.createObjectURL(this.imgData);
|
||||
|
||||
//Update the image with the new mimetype and image
|
||||
this.image.src = this.imgObjURL;
|
||||
}
|
||||
|
||||
dispNoStream() {
|
||||
this.image.src = "loading.gif";
|
||||
}
|
||||
|
||||
animationLoop(){
|
||||
// Update time metrics
|
||||
var now = window.performance.now();
|
||||
var timeInState = now - this.dsm_restart_start_time;
|
||||
|
||||
// Save previous state
|
||||
this.dsm_prev_state = this.dsm_cur_state;
|
||||
|
||||
// Evaluate state transitions
|
||||
if(this.serverConnectionActive == false){
|
||||
//Any state - if the server connection goes false, always transition to disconnected
|
||||
this.dsm_cur_state = this.DSM_DISCONNECTED;
|
||||
} else {
|
||||
//Conditional transitions
|
||||
switch(this.dsm_cur_state) {
|
||||
case this.DSM_DISCONNECTED:
|
||||
//Immediately transition to waiting for the first frame
|
||||
this.dsm_cur_state = this.DSM_WAIT_FOR_VALID_PORT;
|
||||
break;
|
||||
case this.DSM_WAIT_FOR_VALID_PORT:
|
||||
// Wait until the user has configured a valid port
|
||||
if(this.streamPort > 0){
|
||||
this.dsm_cur_state = this.DSM_SUBSCRIBE;
|
||||
} else {
|
||||
this.dsm_cur_state = this.DSM_WAIT_FOR_VALID_PORT;
|
||||
}
|
||||
break;
|
||||
case this.DSM_SUBSCRIBE:
|
||||
// Immediately transition after subscriptions is sent
|
||||
this.dsm_cur_state = this.DSM_WAIT_FOR_FIRST_FRAME;
|
||||
break;
|
||||
case this.DSM_WAIT_FOR_FIRST_FRAME:
|
||||
if(this.imgData != null){
|
||||
//we got some image data, start showing it
|
||||
this.dsm_cur_state = this.DSM_SHOWING;
|
||||
} else if (this.newStreamPortReq != null){
|
||||
//Stream port requested changed, unsubscribe and restart
|
||||
this.dsm_cur_state = this.DSM_RESTART_UNSUBSCRIBE;
|
||||
} else {
|
||||
this.dsm_cur_state = this.DSM_WAIT_FOR_FIRST_FRAME;
|
||||
}
|
||||
break;
|
||||
case this.DSM_SHOWING:
|
||||
if((now - this.imgDataTime) > 2500){
|
||||
//timeout, begin the restart sequence
|
||||
this.dsm_cur_state = this.DSM_RESTART_UNSUBSCRIBE;
|
||||
} else if (this.newStreamPortReq != null){
|
||||
//Stream port requested changed, unsubscribe and restart
|
||||
this.dsm_cur_state = this.DSM_RESTART_UNSUBSCRIBE;
|
||||
} else {
|
||||
//stay in this state.
|
||||
this.dsm_cur_state = this.DSM_SHOWING;
|
||||
}
|
||||
break;
|
||||
case this.DSM_RESTART_UNSUBSCRIBE:
|
||||
//Only should spend one loop in Unsubscribe, immediately transition
|
||||
this.dsm_cur_state = this.DSM_RESTART_WAIT;
|
||||
break;
|
||||
case this.DSM_RESTART_WAIT:
|
||||
if (timeInState > 250) {
|
||||
//we've waited long enough, go to try to re-subscribe
|
||||
this.dsm_cur_state = this.DSM_WAIT_FOR_VALID_PORT;
|
||||
} else {
|
||||
//stay in this state.
|
||||
this.dsm_cur_state = this.DSM_RESTART_WAIT;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
// Shouldn't get here, default back to init
|
||||
this.dsm_cur_state = this.DSM_DISCONNECTED;
|
||||
}
|
||||
}
|
||||
|
||||
//take current-state or state-transition actions
|
||||
|
||||
if(this.dsm_cur_state != this.dsm_prev_state){
|
||||
//Any state transition
|
||||
console.log("State Change: " + this.dsm_prev_state + " -> " + this.dsm_cur_state);
|
||||
}
|
||||
|
||||
if(this.dsm_cur_state == this.DSM_SHOWING){
|
||||
// Currently in SHOWING
|
||||
this.dispImageData();
|
||||
}
|
||||
|
||||
if(this.dsm_cur_state != this.DSM_SHOWING && this.dsm_prev_state == this.DSM_SHOWING ){
|
||||
//Any transition out of showing - no stream
|
||||
this.dispNoStream();
|
||||
}
|
||||
|
||||
if(this.dsm_cur_state == this.DSM_RESTART_UNSUBSCRIBE){
|
||||
// Currently in UNSUBSCRIBE, do the unsubscribe actions
|
||||
this.stopStream();
|
||||
this.dsm_restart_start_time = now;
|
||||
}
|
||||
|
||||
if(this.dsm_cur_state == this.DSM_SUBSCRIBE){
|
||||
// Currently in SUBSCRIBE, do the subscribe actions
|
||||
this.startStream();
|
||||
this.dsm_restart_start_time = now;
|
||||
}
|
||||
|
||||
if(this.dsm_cur_state == this.DSM_WAIT_FOR_VALID_PORT){
|
||||
// Currently waiting for a vaild port to be requested
|
||||
if(this.newStreamPortReq != null){
|
||||
this.streamPort = this.newStreamPortReq;
|
||||
this.newStreamPortReq = null;
|
||||
}
|
||||
}
|
||||
|
||||
requestAnimationFrame(()=>this.animationLoop());
|
||||
}
|
||||
|
||||
startStream() {
|
||||
console.log("Subscribing to port " + this.streamPort);
|
||||
this.imgData = null;
|
||||
this.ws.send(JSON.stringify({"cmd": "subscribe", "port":this.streamPort}));
|
||||
}
|
||||
|
||||
stopStream() {
|
||||
console.log("Unsubscribing");
|
||||
this.ws.send(JSON.stringify({"cmd": "unsubscribe"}));
|
||||
this.imgData = null;
|
||||
}
|
||||
|
||||
setPort(streamPort){
|
||||
console.log("Port set to " + streamPort);
|
||||
this.newStreamPortReq = streamPort;
|
||||
}
|
||||
|
||||
ws_onOpen() {
|
||||
// Set the flag allowing general server communication
|
||||
this.serverConnectionActive = true;
|
||||
console.log("Connected!");
|
||||
}
|
||||
|
||||
ws_onClose(e) {
|
||||
//Clear flags to stop server communication
|
||||
this.ws = null;
|
||||
this.serverConnectionActive = false;
|
||||
|
||||
console.log('Camera Socket is closed. Reconnect will be attempted in 0.5 second.', e.reason);
|
||||
setTimeout(this.ws_connect.bind(this), 500);
|
||||
|
||||
if(!e.wasClean){
|
||||
console.error('Socket encountered error!');
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
ws_onError(e){
|
||||
e; //prevent unused failure
|
||||
this.ws.close();
|
||||
}
|
||||
|
||||
ws_onMessage(e){
|
||||
if(typeof e.data === 'string'){
|
||||
//string data from host
|
||||
//TODO - anything to recieve info here? Maybe "avaialble streams?"
|
||||
} else {
|
||||
if(e.data.size > 0){
|
||||
//binary data - a frame
|
||||
this.imgData = e.data;
|
||||
this.imgDataTime = window.performance.now();
|
||||
this.frameRxCount++;
|
||||
} else {
|
||||
//TODO - server is sending empty frames?
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
ws_connect() {
|
||||
this.serverConnectionActive = false;
|
||||
this.ws = new WebSocket(this.serverAddr);
|
||||
this.ws.binaryType = "blob";
|
||||
this.ws.onopen = this.ws_onOpen.bind(this);
|
||||
this.ws.onmessage = this.ws_onMessage.bind(this);
|
||||
this.ws.onclose = this.ws_onClose.bind(this);
|
||||
this.ws.onerror = this.ws_onError.bind(this);
|
||||
console.log("Connecting to server " + this.serverAddr);
|
||||
}
|
||||
|
||||
ws_close(){
|
||||
this.ws.close();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
var stream = null;
|
||||
|
||||
function streamStartRequest() {
|
||||
var host = document.getElementById("host").value + ":5800";
|
||||
var port = document.getElementById("port").value;
|
||||
if(stream == null){
|
||||
stream = new WebsocketVideoStream("streamImg",port,host);
|
||||
} else {
|
||||
stream.setPort(port);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Attach listener
|
||||
document.querySelector('button').addEventListener('click', streamStartRequest);
|
||||
|
||||
// Deal with URLParams, validating inputs
|
||||
const queryString = window.location.search;
|
||||
const urlParams = new URLSearchParams(queryString);
|
||||
const port_in = urlParams.get('port')
|
||||
const host_in = urlParams.get('host')
|
||||
if(port_in != ""){
|
||||
document.getElementById("port").value = port_in;
|
||||
}
|
||||
|
||||
if(host_in != ""){
|
||||
document.getElementById("host").value = host_in;
|
||||
}
|
||||
|
||||
if(port_in != "" && host_in != ""){
|
||||
streamStartRequest(); //we got valid inputs, auto-start the stream
|
||||
}
|
||||
|
||||
|
||||
</script>
|
||||
|
||||
</body>
|
||||
|
||||
|
||||
</html>
|
||||
@@ -1,35 +1,17 @@
|
||||
<template>
|
||||
<v-app>
|
||||
<!-- Although most of the app runs with the "light" theme, the navigation drawer needs to have white text and icons so it uses the dark theme-->
|
||||
<v-navigation-drawer
|
||||
dark
|
||||
app
|
||||
permanent
|
||||
:mini-variant="compact"
|
||||
color="primary"
|
||||
>
|
||||
<v-navigation-drawer dark app permanent :mini-variant="compact" color="primary">
|
||||
<v-list>
|
||||
<!-- List item for the heading; note that there are some tricks in setting padding and image width make things look right -->
|
||||
<v-list-item :class="compact ? 'pr-0 pl-0' : ''">
|
||||
<v-list-item-icon class="mr-0">
|
||||
<img
|
||||
v-if="!compact"
|
||||
class="logo"
|
||||
src="./assets/logoLarge.png"
|
||||
>
|
||||
<img
|
||||
v-else
|
||||
class="logo"
|
||||
src="./assets/logoSmall.png"
|
||||
>
|
||||
<img v-if="!compact" class="logo" src="./assets/logoLarge.png">
|
||||
<img v-else class="logo" src="./assets/logoSmall.png">
|
||||
</v-list-item-icon>
|
||||
</v-list-item>
|
||||
|
||||
<v-list-item
|
||||
link
|
||||
to="dashboard"
|
||||
@click="rollbackPipelineIndex()"
|
||||
>
|
||||
<v-list-item link to="dashboard" @click="rollbackPipelineIndex()">
|
||||
<v-list-item-icon>
|
||||
<v-icon>mdi-view-dashboard</v-icon>
|
||||
</v-list-item-icon>
|
||||
@@ -37,12 +19,7 @@
|
||||
<v-list-item-title>Dashboard</v-list-item-title>
|
||||
</v-list-item-content>
|
||||
</v-list-item>
|
||||
<v-list-item
|
||||
link
|
||||
to="cameras"
|
||||
ref="camerasTabOpener"
|
||||
@click="switchToDriverMode()"
|
||||
>
|
||||
<v-list-item ref="camerasTabOpener" link to="cameras" @click="switchToDriverMode()">
|
||||
<v-list-item-icon>
|
||||
<v-icon>mdi-camera</v-icon>
|
||||
</v-list-item-icon>
|
||||
@@ -50,10 +27,7 @@
|
||||
<v-list-item-title>Cameras</v-list-item-title>
|
||||
</v-list-item-content>
|
||||
</v-list-item>
|
||||
<v-list-item
|
||||
link
|
||||
to="settings"
|
||||
>
|
||||
<v-list-item link to="settings" @click="switchToSettingsTab()">
|
||||
<v-list-item-icon>
|
||||
<v-icon>mdi-settings</v-icon>
|
||||
</v-list-item-icon>
|
||||
@@ -61,10 +35,7 @@
|
||||
<v-list-item-title>Settings</v-list-item-title>
|
||||
</v-list-item-content>
|
||||
</v-list-item>
|
||||
<v-list-item
|
||||
link
|
||||
to="docs"
|
||||
>
|
||||
<v-list-item link to="docs">
|
||||
<v-list-item-icon>
|
||||
<v-icon>mdi-bookshelf</v-icon>
|
||||
</v-list-item-icon>
|
||||
@@ -72,11 +43,7 @@
|
||||
<v-list-item-title>Documentation</v-list-item-title>
|
||||
</v-list-item-content>
|
||||
</v-list-item>
|
||||
<v-list-item
|
||||
v-if="this.$vuetify.breakpoint.mdAndUp"
|
||||
link
|
||||
@click.stop="toggleCompactMode"
|
||||
>
|
||||
<v-list-item v-if="this.$vuetify.breakpoint.mdAndUp" link @click.stop="toggleCompactMode">
|
||||
<v-list-item-icon>
|
||||
<v-icon v-if="compact">
|
||||
mdi-chevron-right
|
||||
@@ -86,169 +53,224 @@
|
||||
</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>
|
||||
<v-container
|
||||
fluid
|
||||
fill-height
|
||||
>
|
||||
<v-container fluid fill-height>
|
||||
<v-layout>
|
||||
<v-flex>
|
||||
<router-view v-on:switch-to-cameras="switchToDriverMode" />
|
||||
<router-view @switch-to-cameras="switchToDriverMode" />
|
||||
</v-flex>
|
||||
</v-layout>
|
||||
</v-container>
|
||||
</v-main>
|
||||
|
||||
<v-dialog
|
||||
v-model="$store.state.logsOverlay"
|
||||
width="1500"
|
||||
dark
|
||||
>
|
||||
<v-dialog v-model="$store.state.logsOverlay" width="1500" dark>
|
||||
<logs />
|
||||
</v-dialog>
|
||||
<v-dialog v-model="needsTeamNumberSet" width="500" dark persistent>
|
||||
<v-card dark color="primary" flat>
|
||||
<v-card-title>No team number set!</v-card-title>
|
||||
<v-card-text>
|
||||
PhotonVision cannot connect to your robot! Please
|
||||
<router-link to="settings" class="accent--text" @click="switchToSettingsTab">
|
||||
visit the settings tab
|
||||
</router-link>
|
||||
and set your team number.
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
</v-app>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Logs from "./views/LogsView"
|
||||
import { ReconnectingWebsocket } from "./plugins/ReconnectingWebsocket.js"
|
||||
|
||||
export default {
|
||||
name: 'App',
|
||||
components: {
|
||||
Logs
|
||||
},
|
||||
data: () => ({
|
||||
// Used so that we can switch back to the previously selected pipeline after camera calibration
|
||||
previouslySelectedIndex: undefined,
|
||||
timer: undefined,
|
||||
}),
|
||||
computed: {
|
||||
compact: {
|
||||
get() {
|
||||
if (this.$store.state.compactMode === undefined) {
|
||||
return this.$vuetify.breakpoint.smAndDown;
|
||||
} else {
|
||||
return this.$store.state.compactMode || this.$vuetify.breakpoint.smAndDown;
|
||||
}
|
||||
},
|
||||
set(value) {
|
||||
// compactMode is the user's preference for compact mode; it overrides screen size
|
||||
this.$store.commit("compactMode", value);
|
||||
localStorage.setItem("compactMode", value);
|
||||
},
|
||||
},
|
||||
},
|
||||
created() {
|
||||
document.addEventListener("keydown", e => {
|
||||
switch (e.key) {
|
||||
case "`":
|
||||
this.$store.state.logsOverlay = !this.$store.state.logsOverlay;
|
||||
break;
|
||||
case "z":
|
||||
if (e.ctrlKey && this.$store.getters.canUndo) {
|
||||
this.$store.dispatch('undo', {vm: this});
|
||||
}
|
||||
break;
|
||||
case "y":
|
||||
if (e.ctrlKey && this.$store.getters.canRedo) {
|
||||
this.$store.dispatch('redo', {vm: this});
|
||||
}
|
||||
break;
|
||||
|
||||
}
|
||||
});
|
||||
|
||||
this.$options.sockets.onmessage = (data) => {
|
||||
try {
|
||||
let message = this.$msgPack.decode(data.data);
|
||||
for (let prop in message) {
|
||||
if (message.hasOwnProperty(prop)) {
|
||||
this.handleMessage(prop, message[prop]);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('error: ' + JSON.stringify(data.data) + " , " + error);
|
||||
}
|
||||
};
|
||||
this.$options.sockets.onopen = () => {
|
||||
this.$store.state.backendConnected = true;
|
||||
};
|
||||
|
||||
let closed = () => {
|
||||
this.$store.state.backendConnected = false;
|
||||
};
|
||||
this.$options.sockets.onclose = closed;
|
||||
this.$options.sockets.onerror = closed;
|
||||
|
||||
this.$connect();
|
||||
},
|
||||
methods: {
|
||||
handleMessage(key, value) {
|
||||
if (key === "logMessage") {
|
||||
this.logMessage(value["logMessage"], value["logLevel"]);
|
||||
} else if (key === "updatePipelineResult") {
|
||||
this.$store.commit('mutatePipelineResults', value)
|
||||
} else if (this.$store.state.hasOwnProperty(key)) {
|
||||
this.$store.commit(key, value);
|
||||
} else if (this.$store.getters.currentPipelineSettings.hasOwnProperty(key)) {
|
||||
this.$store.commit('mutatePipeline', {[key]: value});
|
||||
} else if (this.$store.state.settings.hasOwnProperty(key)) {
|
||||
this.$store.commit('mutateSettings', {[key]: value});
|
||||
} else {
|
||||
switch (key) {
|
||||
default: {
|
||||
console.error("Unknown message from backend: " + value);
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
toggleCompactMode() {
|
||||
this.compact = !this.compact;
|
||||
},
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
logMessage(message, levelInt) {
|
||||
this.$store.commit('logString', {
|
||||
['level']: levelInt,
|
||||
['message']: message
|
||||
})
|
||||
},
|
||||
switchToDriverMode() {
|
||||
this.previouslySelectedIndex = this.$store.getters.currentPipelineIndex;
|
||||
this.handleInputWithIndex('currentPipeline', -1)
|
||||
},
|
||||
rollbackPipelineIndex() {
|
||||
if (this.previouslySelectedIndex !== null) {
|
||||
this.handleInputWithIndex('currentPipeline', this.previouslySelectedIndex || 0);
|
||||
}
|
||||
this.previouslySelectedIndex = null;
|
||||
}
|
||||
export default {
|
||||
name: 'App',
|
||||
components: {
|
||||
Logs
|
||||
},
|
||||
data: () => ({
|
||||
// Used so that we can switch back to the previously selected pipeline after camera calibration
|
||||
previouslySelectedIndices: [],
|
||||
timer: undefined,
|
||||
teamNumberDialog: true,
|
||||
websocket: null,
|
||||
}),
|
||||
computed: {
|
||||
needsTeamNumberSet: {
|
||||
get() {
|
||||
return this.$store.state.settings.networkSettings.teamNumber < 1
|
||||
&& this.teamNumberDialog && this.$store.state.backendConnected
|
||||
&& !this.$route.name.toLowerCase().includes("settings");
|
||||
}
|
||||
},
|
||||
compact: {
|
||||
get() {
|
||||
if (this.$store.state.compactMode === undefined) {
|
||||
return this.$vuetify.breakpoint.smAndDown;
|
||||
} else {
|
||||
return this.$store.state.compactMode || this.$vuetify.breakpoint.smAndDown;
|
||||
}
|
||||
};
|
||||
},
|
||||
set(value) {
|
||||
// compactMode is the user's preference for compact mode; it overrides screen size
|
||||
this.$store.commit("compactMode", value);
|
||||
localStorage.setItem("compactMode", value);
|
||||
},
|
||||
},
|
||||
},
|
||||
created() {
|
||||
document.addEventListener("keydown", e => {
|
||||
switch (e.key) {
|
||||
case "`":
|
||||
this.$store.state.logsOverlay = !this.$store.state.logsOverlay;
|
||||
break;
|
||||
case "z":
|
||||
if (e.ctrlKey && this.$store.getters.canUndo) {
|
||||
this.$store.dispatch('undo', { vm: this });
|
||||
}
|
||||
break;
|
||||
case "y":
|
||||
if (e.ctrlKey && this.$store.getters.canRedo) {
|
||||
this.$store.dispatch('redo', { vm: this });
|
||||
}
|
||||
break;
|
||||
|
||||
}
|
||||
});
|
||||
|
||||
const wsDataURL = 'ws://' + this.$address + '/websocket_data';
|
||||
this.websocket = new ReconnectingWebsocket(
|
||||
wsDataURL,
|
||||
|
||||
// On data in
|
||||
(event) => {
|
||||
try {
|
||||
let message = this.$msgPack.decode(event.data);
|
||||
for (let prop in message) {
|
||||
if (message.hasOwnProperty(prop)) {
|
||||
this.handleMessage(prop, message[prop]);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(event)
|
||||
console.error('error: ' + JSON.stringify(event.data) + " , " + error);
|
||||
}
|
||||
},
|
||||
|
||||
// on connect
|
||||
(event) => {
|
||||
event; this.$store.commit("backendConnected", true);
|
||||
this.$store.state.connectedCallbacks.forEach(it => it());
|
||||
},
|
||||
|
||||
// on disconnect
|
||||
(event) => { event; this.$store.commit("backendConnected", false) }
|
||||
);
|
||||
|
||||
this.$store.commit("websocket", this.websocket);
|
||||
},
|
||||
methods: {
|
||||
handleMessage(key, value) {
|
||||
if (key === "logMessage") {
|
||||
this.logMessage(value["logMessage"], value["logLevel"]);
|
||||
} else if (key === "log") {
|
||||
this.logMessage(value["logMessage"]["logMessage"], value["logMessage"]["logLevel"]);
|
||||
} else if (key === "updatePipelineResult") {
|
||||
this.$store.commit('mutatePipelineResults', value)
|
||||
} else if (this.$store.state.hasOwnProperty(key)) {
|
||||
this.$store.commit(key, value);
|
||||
} else if (this.$store.getters.currentPipelineSettings.hasOwnProperty(key)) {
|
||||
this.$store.commit('mutatePipeline', { [key]: value });
|
||||
} else if (this.$store.state.settings.hasOwnProperty(key)) {
|
||||
this.$store.commit('mutateSettings', { [key]: value });
|
||||
} else {
|
||||
console.error("Unknown message from backend: " + value);
|
||||
}
|
||||
},
|
||||
toggleCompactMode() {
|
||||
this.compact = !this.compact;
|
||||
},
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
logMessage(message, levelInt) {
|
||||
this.$store.commit('logString', {
|
||||
['level']: levelInt,
|
||||
['message']: message
|
||||
})
|
||||
},
|
||||
switchToDriverMode() {
|
||||
if (!this.previouslySelectedIndices) this.previouslySelectedIndices = [];
|
||||
|
||||
for (const [i, cameraSettings] of this.$store.state.cameraSettings.entries()) {
|
||||
this.previouslySelectedIndices[i] = cameraSettings.currentPipelineIndex;
|
||||
this.handleInputWithIndex('currentPipeline', -1, i);
|
||||
}
|
||||
},
|
||||
rollbackPipelineIndex() {
|
||||
if (this.previouslySelectedIndices !== null) {
|
||||
for (const [i] of this.$store.state.cameraSettings.entries()) {
|
||||
this.handleInputWithIndex('currentPipeline', this.previouslySelectedIndices[i] || 0, i);
|
||||
}
|
||||
}
|
||||
this.previouslySelectedIndices = null;
|
||||
},
|
||||
switchToSettingsTab() {
|
||||
this.axios.post('http://' + this.$address + '/api/sendMetrics', {})
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="sass">
|
||||
@@ -256,76 +278,77 @@ import Logs from "./views/LogsView"
|
||||
</style>
|
||||
|
||||
<style>
|
||||
.pulse {
|
||||
animation: pulse-animation 2s infinite;
|
||||
}
|
||||
.pulse {
|
||||
animation: pulse-animation 2s infinite;
|
||||
}
|
||||
|
||||
@keyframes pulse-animation {
|
||||
0% {
|
||||
box-shadow: 0 0 0 0px rgba(0, 0, 0, 0.2);
|
||||
background-color: rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
100% {
|
||||
box-shadow: 0 0 0 20px rgba(0, 0, 0, 0);
|
||||
background-color: rgba(0, 0, 0, 0);
|
||||
}
|
||||
}
|
||||
@keyframes pulse-animation {
|
||||
0% {
|
||||
box-shadow: 0 0 0 0px rgba(0, 0, 0, 0.2);
|
||||
background-color: rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.logo {
|
||||
width: 100%;
|
||||
height: 70px;
|
||||
object-fit: contain;
|
||||
}
|
||||
100% {
|
||||
box-shadow: 0 0 0 20px rgba(0, 0, 0, 0);
|
||||
background-color: rgba(0, 0, 0, 0);
|
||||
}
|
||||
}
|
||||
|
||||
::-webkit-scrollbar {
|
||||
width: 0.5em;
|
||||
border-radius: 5px;
|
||||
}
|
||||
.logo {
|
||||
width: 100%;
|
||||
height: 70px;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
-webkit-box-shadow: inset 0 0 6px rgba(0, 0, 0, 0.3);
|
||||
border-radius: 10px;
|
||||
}
|
||||
::-webkit-scrollbar {
|
||||
width: 0.5em;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
-webkit-box-shadow: inset 0 0 6px rgba(0, 0, 0, 0.3);
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background-color: #ffd843;
|
||||
border-radius: 10px;
|
||||
}
|
||||
::-webkit-scrollbar-thumb {
|
||||
background-color: #ffd843;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.container {
|
||||
background-color: #232c37;
|
||||
padding: 0 !important;
|
||||
}
|
||||
.container {
|
||||
background-color: #232c37;
|
||||
padding: 0 !important;
|
||||
}
|
||||
|
||||
#title {
|
||||
color: #ffd843;
|
||||
}
|
||||
#title {
|
||||
color: #ffd843;
|
||||
}
|
||||
</style>
|
||||
|
||||
<style>
|
||||
/* Hacks */
|
||||
/* Hacks */
|
||||
|
||||
.v-divider {
|
||||
border-color: white !important;
|
||||
}
|
||||
.v-divider {
|
||||
border-color: white !important;
|
||||
}
|
||||
|
||||
.v-input {
|
||||
font-size: 1rem !important;
|
||||
}
|
||||
.v-input {
|
||||
font-size: 1rem !important;
|
||||
}
|
||||
|
||||
/* This is unfortunately the only way to override table background color */
|
||||
.theme--dark.v-data-table > .v-data-table__wrapper > table > tbody > tr:hover:not(.v-data-table__expanded__content):not(.v-data-table__empty-wrapper) {
|
||||
background: #005281 !important;
|
||||
}
|
||||
/* This is unfortunately the only way to override table background color */
|
||||
.theme--dark.v-data-table>.v-data-table__wrapper>table>tbody>tr:hover:not(.v-data-table__expanded__content):not(.v-data-table__empty-wrapper) {
|
||||
background: #005281 !important;
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="scss">
|
||||
@import '~vuetify/src/styles/settings/_variables';
|
||||
@import '~vuetify/src/styles/settings/_variables';
|
||||
|
||||
@media #{map-get($display-breakpoints, 'md-and-down')} {
|
||||
html {
|
||||
font-size: 14px !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@media #{map-get($display-breakpoints, 'md-and-down')} {
|
||||
html {
|
||||
font-size: 14px !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
BIN
photon-client/src/assets/loading.gif
Normal file
|
After Width: | Height: | Size: 12 KiB |
BIN
photon-client/src/assets/logoMono.png
Normal file
|
After Width: | Height: | Size: 49 KiB |
|
Before Width: | Height: | Size: 48 KiB After Width: | Height: | Size: 76 KiB |
1
photon-client/src/assets/robot-off.svg
Normal 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 |
1
photon-client/src/assets/robot.svg
Normal 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 |
@@ -51,4 +51,4 @@
|
||||
.hover:hover {
|
||||
color: white !important;
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
||||
@@ -4,16 +4,22 @@
|
||||
crossOrigin="anonymous"
|
||||
:style="styleObject"
|
||||
:src="src"
|
||||
alt=""
|
||||
@click="e => $emit('click', e)"
|
||||
>
|
||||
:alt="alt"
|
||||
@click="clickHandler"
|
||||
@error="loadErrHandler"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
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', 'alt'],
|
||||
data() {
|
||||
return {
|
||||
seed: 1.0,
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
styleObject: {
|
||||
get() {
|
||||
@@ -21,18 +27,21 @@
|
||||
"border-radius": "3px",
|
||||
"display": "block",
|
||||
"object-fit": "contain",
|
||||
"background-size:": "contain",
|
||||
"object-position": "50% 50%",
|
||||
"max-width": "100%",
|
||||
"margin-left": "auto",
|
||||
"margin-right": "auto",
|
||||
"max-height": this.maxHeight,
|
||||
height: `${this.scale}%`,
|
||||
cursor: (this.colorPicking ? `url(${require("../../assets/eyedropper.svg")}),` : "") + "default",
|
||||
cursor: (this.colorPicking ? `url(${require("../../assets/eyedropper.svg")}),` : "pointer") + "default",
|
||||
};
|
||||
|
||||
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 +50,61 @@
|
||||
},
|
||||
src: {
|
||||
get() {
|
||||
return this.disconnected ? require("../../assets/noStream.jpg") : this.address;
|
||||
var port = this.getCurPort();
|
||||
if(port <= 0){
|
||||
//Invalid port, keep it spinny
|
||||
return require("../../assets/loading.gif");
|
||||
} else {
|
||||
//Valid port, connect
|
||||
return this.getSrcURLFromPort(port);
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.reload(); // Force reload image on creation
|
||||
},
|
||||
methods: {
|
||||
getCurPort(){
|
||||
var port = -1;
|
||||
if(this.disconnected){
|
||||
//Disconnected, port is unknown.
|
||||
port = -1;
|
||||
} else {
|
||||
//Connected - get the port
|
||||
if(this.id == 'raw-stream'){
|
||||
port = this.$store.state.cameraSettings[this.$store.state.currentCameraIndex].inputStreamPort
|
||||
} else {
|
||||
port = this.$store.state.cameraSettings[this.$store.state.currentCameraIndex].outputStreamPort
|
||||
}
|
||||
}
|
||||
return port;
|
||||
},
|
||||
getSrcURLFromPort(port){
|
||||
return "http://" + location.hostname + ":" + port + "/stream.mjpg" + "?" + this.seed;
|
||||
},
|
||||
loadErrHandler(event) {
|
||||
console.log(event);
|
||||
console.log("Error loading image, attempting to do it again...");
|
||||
this.reload();
|
||||
},
|
||||
clickHandler(event) {
|
||||
if(this.colorPicking){
|
||||
this.$emit('click', event);
|
||||
} else {
|
||||
var port = this.getCurPort();
|
||||
if(port <= 0){
|
||||
console.log("No valid port, ignoring click.");
|
||||
} else {
|
||||
//Valid port, connect
|
||||
window.open(this.getSrcURLFromPort(port), '_blank');
|
||||
}
|
||||
}
|
||||
|
||||
},
|
||||
reload() {
|
||||
this.seed = new Date().getTime();
|
||||
}
|
||||
},
|
||||
}
|
||||
</script>
|
||||
</script>
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
</v-row>
|
||||
</div>
|
||||
</template>
|
||||
s
|
||||
|
||||
<script>
|
||||
import TooltippedLabel from "./cv-tooltipped-label";
|
||||
|
||||
@@ -61,4 +61,4 @@ s
|
||||
</script>
|
||||
|
||||
<style lang="css" scoped>
|
||||
</style>
|
||||
</style>
|
||||
|
||||
@@ -54,4 +54,4 @@
|
||||
|
||||
<style lang="" scoped>
|
||||
|
||||
</style>
|
||||
</style>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -37,7 +37,7 @@ import TooltippedLabel from "./cv-tooltipped-label";
|
||||
TooltippedLabel,
|
||||
},
|
||||
// eslint-disable-next-line vue/require-prop-types
|
||||
props: ['list', 'name', 'value', 'disabled', 'selectCols', 'rules', 'tooltip'],
|
||||
props: ['list', 'name', 'value', 'disabled', 'filteredIndices', 'selectCols', 'rules', 'tooltip'],
|
||||
computed: {
|
||||
localValue: {
|
||||
get() {
|
||||
@@ -50,6 +50,7 @@ import TooltippedLabel from "./cv-tooltipped-label";
|
||||
indexList() {
|
||||
let list = [];
|
||||
for (let i = 0; i < this.list.length; i++) {
|
||||
if (this.filteredIndices instanceof Set && this.filteredIndices.has(i)) continue;
|
||||
list.push({
|
||||
name: this.list[i],
|
||||
index: i
|
||||
@@ -62,4 +63,4 @@ import TooltippedLabel from "./cv-tooltipped-label";
|
||||
</script>
|
||||
|
||||
<style>
|
||||
</style>
|
||||
</style>
|
||||
|
||||
@@ -105,4 +105,4 @@ export default {
|
||||
</script>
|
||||
|
||||
<style lang="" scoped>
|
||||
</style>
|
||||
</style>
|
||||
|
||||
@@ -48,4 +48,4 @@ export default {
|
||||
|
||||
<style lang="" scoped>
|
||||
|
||||
</style>
|
||||
</style>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -1,154 +1,268 @@
|
||||
<template>
|
||||
<div>
|
||||
<div
|
||||
id="MapContainer"
|
||||
style="flex-grow:1"
|
||||
>
|
||||
<v-row>
|
||||
<v-col
|
||||
align="center"
|
||||
cols="12"
|
||||
>
|
||||
<span class="white--text">Target Location</span>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row>
|
||||
<v-col
|
||||
align="center"
|
||||
cols="12"
|
||||
align-self="stretch"
|
||||
>
|
||||
<canvas
|
||||
id="canvasId"
|
||||
class="mt-2"
|
||||
width="800"
|
||||
height="800"
|
||||
style="width:100%;height:100%"
|
||||
/>
|
||||
</v-col>
|
||||
<v-row>
|
||||
<v-col>
|
||||
<v-btn
|
||||
class="ml-10"
|
||||
color="secondary"
|
||||
@click="resetCamFirstPerson"
|
||||
>
|
||||
First Person
|
||||
</v-btn>
|
||||
</v-col>
|
||||
<v-col>
|
||||
<v-btn
|
||||
class="ml-10"
|
||||
color="secondary"
|
||||
@click="resetCamThirdPerson"
|
||||
>
|
||||
Third Person
|
||||
</v-btn>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-row>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import theme from "../../../theme";
|
||||
|
||||
export default {
|
||||
name: "MiniMap",
|
||||
props: {
|
||||
// eslint-disable-next-line vue/require-default-prop
|
||||
targets: Array,
|
||||
// eslint-disable-next-line vue/require-default-prop
|
||||
horizontalFOV: Number
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
ctx: undefined,
|
||||
canvas: undefined,
|
||||
x: 0,
|
||||
y: 0,
|
||||
targetWidth: 40,
|
||||
targetHeight: 6
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
hLen: {
|
||||
get() {
|
||||
return Math.tan(this.horizontalFOV / 2 * Math.PI / 180) * 150;
|
||||
}
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
targets: {
|
||||
deep: true,
|
||||
handler() {
|
||||
this.draw();
|
||||
}
|
||||
},
|
||||
horizontalFOV() {
|
||||
this.draw();
|
||||
}
|
||||
},
|
||||
mounted: function () {
|
||||
const canvas = document.getElementById("canvasId"); // getting the canvas element
|
||||
const ctx = canvas.getContext("2d"); // getting the canvas context
|
||||
this.canvas = canvas; // setting the canvas as a vue variable
|
||||
this.ctx = ctx; // setting the canvas context as a vue variable
|
||||
this.grad = this.ctx.createLinearGradient(400, 800, 400, 600);
|
||||
this.grad.addColorStop(0, "rgb(119,119,119)");
|
||||
this.grad.addColorStop(0.05, "rgba(14,92,22,0.96)");
|
||||
this.grad.addColorStop(0.8, 'rgba(43,43,43,0.48)');
|
||||
import {
|
||||
ArrowHelper,
|
||||
BoxGeometry,
|
||||
ConeGeometry,
|
||||
Mesh,
|
||||
MeshNormalMaterial,
|
||||
PerspectiveCamera,
|
||||
Quaternion,
|
||||
Scene,
|
||||
TrackballControls,
|
||||
Vector3,
|
||||
Color,
|
||||
WebGLRenderer
|
||||
} from "three-full";
|
||||
|
||||
// setting canvas context values for drawing
|
||||
export default {
|
||||
name: "MiniMap",
|
||||
props: {
|
||||
// eslint-disable-next-line vue/require-default-prop
|
||||
targets: Array,
|
||||
// eslint-disable-next-line vue/require-default-prop
|
||||
horizontalFOV: Number
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
scene: undefined,
|
||||
cubes: [],
|
||||
|
||||
|
||||
this.ctx.font = "26px Arial";
|
||||
this.ctx.strokeStyle = "whitesmoke";
|
||||
this.ctx.lineWidth = 2;
|
||||
|
||||
this.$nextTick(function () {
|
||||
this.drawPlayer();
|
||||
});
|
||||
},
|
||||
methods: {
|
||||
draw() {
|
||||
this.clearBoard();
|
||||
this.drawPlayer();
|
||||
for (let index in this.targets) {
|
||||
this.drawTarget(index, this.targets[index].pose);
|
||||
}
|
||||
},
|
||||
drawTarget(index, target) {
|
||||
// first save the untranslated/unrotated context
|
||||
let x = 800 - (160 * target.x); // getting meters as pixels
|
||||
let y = 400 - (160 * target.y);
|
||||
this.ctx.save();
|
||||
this.ctx.beginPath();
|
||||
// move the rotation point to the center of the rect
|
||||
this.ctx.translate(y + this.targetWidth / 2, x + this.targetHeight / 2); // wpi lib makes x forward and back and y left to right
|
||||
// rotate the rect
|
||||
this.ctx.rotate(target.rot * -1 * Math.PI / 180.0);
|
||||
|
||||
// draw the rect on the transformed context
|
||||
// Note: after transforming [0,0] is visually [x,y]
|
||||
// so the rect needs to be offset accordingly when drawn
|
||||
this.ctx.rect(-this.targetWidth / 2, -this.targetHeight / 2, this.targetWidth, this.targetHeight);
|
||||
|
||||
this.ctx.fillStyle = theme.accent;
|
||||
this.ctx.fill();
|
||||
|
||||
// restore the context to its untranslated/unrotated state
|
||||
this.ctx.restore();
|
||||
this.ctx.fillStyle = "whitesmoke";
|
||||
this.ctx.beginPath();
|
||||
this.ctx.arc(y + this.targetWidth / 2, x + this.targetHeight / 2, 3, 0, 2 * Math.PI, true);
|
||||
this.ctx.fill();
|
||||
this.ctx.fillText(index, y - 30, x - 5);
|
||||
|
||||
},
|
||||
drawPlayer() {
|
||||
this.ctx.beginPath();
|
||||
this.ctx.moveTo(400, 820);
|
||||
this.ctx.lineTo(400 + this.hLen, 650);
|
||||
this.ctx.lineTo(400 - this.hLen, 650);
|
||||
this.ctx.closePath();
|
||||
this.ctx.fillStyle = this.grad;
|
||||
this.ctx.fill();
|
||||
this.ctx.beginPath();
|
||||
this.ctx.moveTo(400, 820);
|
||||
this.ctx.lineTo(400 + this.hLen, 650);
|
||||
this.ctx.stroke();
|
||||
this.ctx.moveTo(400, 820);
|
||||
this.ctx.lineTo(400 - this.hLen, 650);
|
||||
this.ctx.stroke();
|
||||
|
||||
},
|
||||
clearBoard() {
|
||||
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height); // clearing the canvas
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
targets: {
|
||||
deep: true,
|
||||
handler() {
|
||||
this.drawTargets();
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
mounted() {
|
||||
const scene = new Scene();
|
||||
this.scene = scene;
|
||||
const camera = new PerspectiveCamera(75, 800 / 800, 0.1, 1000);
|
||||
this.camera = camera;
|
||||
|
||||
const canvas = document.getElementById("canvasId"); // getting the canvas element
|
||||
this.canvas = canvas;
|
||||
const renderer = new WebGLRenderer({"canvas": canvas});
|
||||
this.renderer = renderer;
|
||||
scene.background = new Color(0xa9a9a9)
|
||||
|
||||
//Set up resize handlers
|
||||
this.onWindowResize();
|
||||
window.addEventListener( 'resize', this.onWindowResize, false );
|
||||
|
||||
//Add the reference frame cues
|
||||
this.refFrameCues = []
|
||||
// coordinate system
|
||||
this.refFrameCues.push(new ArrowHelper(new Vector3(1, 0, 0).normalize(), new Vector3(0, 0, 0),
|
||||
1, // length
|
||||
0xff0000,
|
||||
0.1,
|
||||
0.1,
|
||||
))
|
||||
this.refFrameCues.push(new ArrowHelper(new Vector3(0, 1, 0).normalize(), new Vector3(0, 0, 0),
|
||||
1, // length
|
||||
0x00ff00,
|
||||
0.1,
|
||||
0.1,
|
||||
))
|
||||
this.refFrameCues.push(new ArrowHelper(new Vector3(0, 0, 1).normalize(), new Vector3(0, 0, 0),
|
||||
1, // length
|
||||
0x0000ff,
|
||||
0.1,
|
||||
0.1,
|
||||
))
|
||||
|
||||
//something that looks vaguely like a camera
|
||||
const camSize = 0.2;
|
||||
const camBodyGeometry = new BoxGeometry(camSize, camSize, camSize);
|
||||
const camLensGeometry = new ConeGeometry(camSize*0.4, camSize*0.8, 30);
|
||||
const camMaterial = new MeshNormalMaterial();
|
||||
const camBody = new Mesh(camBodyGeometry, camMaterial);
|
||||
const camLens = new Mesh(camLensGeometry, camMaterial);
|
||||
camBody.position.set(0,0,0);
|
||||
camLens.rotateZ(Math.PI / 2);
|
||||
camLens.position.set(camSize*0.8,0,0);
|
||||
this.refFrameCues.push(camBody)
|
||||
this.refFrameCues.push(camLens)
|
||||
|
||||
var controls = new TrackballControls(
|
||||
camera,
|
||||
renderer.domElement
|
||||
);
|
||||
controls.rotateSpeed = 1.0;
|
||||
controls.zoomSpeed = 1.2;
|
||||
controls.panSpeed = 0.8;
|
||||
controls.noZoom = false;
|
||||
controls.noPan = false;
|
||||
controls.staticMoving = true;
|
||||
controls.dynamicDampingFactor = 0.3;
|
||||
controls.keys = [65, 83, 68];
|
||||
this.controls = controls;
|
||||
|
||||
this.scene.add(...this.refFrameCues)
|
||||
this.resetCamFirstPerson();
|
||||
|
||||
controls.update();
|
||||
|
||||
function animate() {
|
||||
requestAnimationFrame(animate);
|
||||
|
||||
controls.update();
|
||||
renderer.render(scene, camera);
|
||||
|
||||
//camera.updateMatrixWorld();
|
||||
//console.log("================")
|
||||
//console.log(camera.position);
|
||||
//console.log(camera.rotation);
|
||||
//console.log(camera.up);
|
||||
|
||||
}
|
||||
|
||||
this.drawTargets()
|
||||
|
||||
animate();
|
||||
},
|
||||
methods: {
|
||||
drawTargets() {
|
||||
this.scene.remove(...this.cubes)
|
||||
this.cubes = []
|
||||
|
||||
for (const target of this.targets) {
|
||||
const geometry = new BoxGeometry(0.3 / 5, 0.2, 0.2);
|
||||
const material = new MeshNormalMaterial();
|
||||
let quat = (new Quaternion(
|
||||
target.pose.qx,
|
||||
target.pose.qy,
|
||||
target.pose.qz,
|
||||
target.pose.qw,
|
||||
))
|
||||
const cube = new Mesh(geometry, material);
|
||||
cube.position.set(target.pose.x, target.pose.y, target.pose.z)
|
||||
cube.rotation.setFromQuaternion(quat);
|
||||
this.cubes.push(cube)
|
||||
|
||||
let arrow = (new ArrowHelper(new Vector3(1, 0, 0).normalize(), new Vector3(0, 0, 0),
|
||||
1, // length
|
||||
0xff0000,
|
||||
0.1,
|
||||
0.1,
|
||||
));
|
||||
arrow.rotation.setFromQuaternion(quat)
|
||||
arrow.rotateZ(-Math.PI / 2)
|
||||
arrow.position.set(target.pose.x, target.pose.y, target.pose.z)
|
||||
this.cubes.push(arrow);
|
||||
|
||||
arrow = (new ArrowHelper(new Vector3(1, 0, 0).normalize(), new Vector3(0, 0, 0),
|
||||
1, // length
|
||||
0x00ff00,
|
||||
0.1,
|
||||
0.1,
|
||||
));
|
||||
arrow.rotation.setFromQuaternion(quat)
|
||||
// arrow.rotateX(Math.PI / 2)
|
||||
arrow.position.set(target.pose.x, target.pose.y, target.pose.z)
|
||||
this.cubes.push(arrow);
|
||||
arrow = (new ArrowHelper(new Vector3(1, 0, 0).normalize(), new Vector3(0, 0, 0),
|
||||
1, // length
|
||||
0x0000ff,
|
||||
0.1,
|
||||
0.1,
|
||||
));
|
||||
arrow.setRotationFromQuaternion(quat)
|
||||
arrow.rotateX(Math.PI / 2)
|
||||
arrow.position.set(target.pose.x, target.pose.y, target.pose.z)
|
||||
this.cubes.push(arrow);
|
||||
}
|
||||
if(this.cubes.length > 0)
|
||||
this.scene.add(...this.cubes);
|
||||
},
|
||||
|
||||
onWindowResize() {
|
||||
var container = document.getElementById("MapContainer")
|
||||
if(container){
|
||||
this.canvas.width = container.clientWidth * 0.95;
|
||||
this.canvas.height = container.clientWidth * 0.85;
|
||||
this.camera.aspect = this.canvas.width / this.canvas.height;
|
||||
this.camera.updateProjectionMatrix();
|
||||
this.renderer.setSize( this.canvas.width, this.canvas.height );
|
||||
}
|
||||
},
|
||||
resetCamThirdPerson(){
|
||||
//Sets camera to third person position
|
||||
this.controls.reset();
|
||||
this.camera.position.set(-1.39,-1.09,1.17);
|
||||
this.camera.up.set(0,0,1);
|
||||
this.controls.target.set(4.0,0.0,0.0);
|
||||
this.controls.update();
|
||||
this.scene.add(...this.refFrameCues)
|
||||
},
|
||||
resetCamFirstPerson(){
|
||||
//Sets camera to first person position
|
||||
this.controls.reset();
|
||||
this.camera.position.set(-0.1,0,0);
|
||||
this.camera.up.set(0,0,1);
|
||||
this.controls.target.set(0.0,0.0,0.0);
|
||||
this.controls.update();
|
||||
this.scene.remove(...this.refFrameCues)
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
#canvasId {
|
||||
width: 400px;
|
||||
height: 400px;
|
||||
background-color: #232C37;
|
||||
border-radius: 5px;
|
||||
border: 2px solid grey;
|
||||
box-shadow: 0 0 5px 1px;
|
||||
}
|
||||
|
||||
th {
|
||||
width: 80px;
|
||||
text-align: center;
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
||||
@@ -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', 'AprilTag']"
|
||||
@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>
|
||||
|
||||
@@ -60,4 +60,4 @@
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
</style>
|
||||
|
||||
@@ -45,4 +45,4 @@
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
</style>
|
||||
|
||||
7
photon-client/src/jsPDFFonts/Prompt-Regular-normal.js
Normal file
7
photon-client/src/jsPDFFonts/readme.md
Normal 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.
|
||||
@@ -15,22 +15,24 @@ if (process.env.NODE_ENV === "production") {
|
||||
Vue.prototype.$address = location.hostname + ":5800";
|
||||
}
|
||||
|
||||
const wsURL = '//' + Vue.prototype.$address + '/websocket';
|
||||
// const wsDataURL = '//' + Vue.prototype.$address + '/websocket_data';
|
||||
// import VueNativeSock from 'vue-native-websocket';
|
||||
// Vue.use(VueNativeSock, wsDataURL, {
|
||||
// reconnection: true,
|
||||
// reconnectionDelay: 100,
|
||||
// connectManually: true,
|
||||
// format: "arraybuffer",
|
||||
// });
|
||||
|
||||
import VueNativeSock from 'vue-native-websocket';
|
||||
|
||||
Vue.use(VueNativeSock, wsURL, {
|
||||
reconnection: true,
|
||||
connectManually: true,
|
||||
format: "arraybuffer",
|
||||
});
|
||||
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,
|
||||
|
||||
@@ -2,14 +2,14 @@ export const dataHandleMixin = {
|
||||
methods: {
|
||||
handleInput(key, value) {
|
||||
let msg = this.$msgPack.encode({[key]: value});
|
||||
this.$socket.send(msg);
|
||||
this.$store.state.websocket.ws.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);
|
||||
this.$store.state.websocket.ws.send(msg);
|
||||
},
|
||||
handleData(val) {
|
||||
this.handleInput(val, this[val]);
|
||||
@@ -22,7 +22,7 @@ export const dataHandleMixin = {
|
||||
["cameraIndex"]: this.$store.getters.currentCameraIndex
|
||||
}
|
||||
});
|
||||
this.$socket.send(msg);
|
||||
this.$store.state.websocket.ws.send(msg);
|
||||
this.$emit('update')
|
||||
},
|
||||
handlePipelineUpdate(key, val) {
|
||||
@@ -32,7 +32,7 @@ export const dataHandleMixin = {
|
||||
["cameraIndex"]: this.$store.getters.currentCameraIndex
|
||||
}
|
||||
});
|
||||
this.$socket.send(msg);
|
||||
this.$store.state.websocket.ws.send(msg);
|
||||
this.$emit('update')
|
||||
},
|
||||
handleTruthyPipelineData(val) {
|
||||
@@ -42,7 +42,7 @@ export const dataHandleMixin = {
|
||||
["cameraIndex"]: this.$store.getters.currentCameraIndex
|
||||
}
|
||||
});
|
||||
this.$socket.send(msg);
|
||||
this.$store.state.websocket.ws.send(msg);
|
||||
this.$emit('update')
|
||||
},
|
||||
rollback(val, e) {
|
||||
|
||||
10
photon-client/src/mixins/global/stateMixin.js
Normal file
@@ -0,0 +1,10 @@
|
||||
export const stateMixin = {
|
||||
methods: {
|
||||
currentPipelineType() {
|
||||
return this.$store.getters.pipelineType
|
||||
},
|
||||
currentPipelineSettings() {
|
||||
return this.$store.getters.currentPipelineSettings
|
||||
},
|
||||
}
|
||||
};
|
||||
@@ -5,9 +5,11 @@ function initColorPicker() {
|
||||
if (!canvas)
|
||||
canvas = document.createElement('canvas');
|
||||
|
||||
image = document.querySelector('#normal-stream');
|
||||
canvas.width = image.width;
|
||||
canvas.height = image.height;
|
||||
image = document.querySelector('#raw-stream');
|
||||
if (image !== null) {
|
||||
canvas.width = image.width;
|
||||
canvas.height = image.height;
|
||||
}
|
||||
}
|
||||
|
||||
//Called on click of the image,
|
||||
@@ -122,4 +124,4 @@ function shrinkRange(range, color) {
|
||||
}
|
||||
|
||||
|
||||
export default {initColorPicker, colorPickerClick, eyeDrop, expand, shrink}
|
||||
export default {initColorPicker, colorPickerClick, eyeDrop, expand, shrink}
|
||||
|
||||
74
photon-client/src/plugins/ReconnectingWebsocket.js
Normal file
@@ -0,0 +1,74 @@
|
||||
/**
|
||||
* Auto-reconnecting Websocket, a stripped down version of the NT4 client from
|
||||
* https://raw.githubusercontent.com/wpilibsuite/NetworkTablesClients/2f8d378ac08d5ca703d590cfb019fc4af062db89/nt4/js/src/nt4.js
|
||||
*/
|
||||
export class ReconnectingWebsocket {
|
||||
constructor(serverAddr,
|
||||
onDataIn_in,
|
||||
onConnect_in,
|
||||
onDisconnect_in) {
|
||||
|
||||
this.onDataIn = onDataIn_in;
|
||||
this.onConnect = onConnect_in;
|
||||
this.onDisconnect = onDisconnect_in;
|
||||
|
||||
// WS Connection State (with defaults)
|
||||
this.serverAddr = serverAddr;
|
||||
this.serverConnectionActive = false;
|
||||
|
||||
//Trigger the websocket to connect automatically
|
||||
this.ws_connect();
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////////////
|
||||
// Websocket connection Maintenance
|
||||
|
||||
ws_onOpen() {
|
||||
// Set the flag allowing general server communication
|
||||
this.serverConnectionActive = true;
|
||||
|
||||
console.log("[WebSocket] Connected!");
|
||||
|
||||
// User connection-opened hook
|
||||
this.onConnect();
|
||||
}
|
||||
|
||||
ws_onClose(e) {
|
||||
//Clear flags to stop server communication
|
||||
this.ws = null;
|
||||
this.serverConnectionActive = false;
|
||||
|
||||
// User connection-closed hook
|
||||
this.onDisconnect();
|
||||
|
||||
console.log('[WebSocket] Socket is closed. Reconnect will be attempted in 0.5 second.', e.reason);
|
||||
setTimeout(this.ws_connect.bind(this), 500);
|
||||
|
||||
if (!e.wasClean) {
|
||||
console.error('Socket encountered error!');
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
ws_onError(e) {
|
||||
console.log("[WebSocket] Websocket error - " + e.toString());
|
||||
this.ws.close();
|
||||
}
|
||||
|
||||
ws_onMessage(e) {
|
||||
this.onDataIn(e);
|
||||
}
|
||||
|
||||
ws_connect() {
|
||||
this.ws = new WebSocket(this.serverAddr);
|
||||
this.ws.binaryType = "arraybuffer";
|
||||
this.ws.onopen = this.ws_onOpen.bind(this);
|
||||
this.ws.onmessage = this.ws_onMessage.bind(this);
|
||||
this.ws.onclose = this.ws_onClose.bind(this);
|
||||
this.ws.onerror = this.ws_onError.bind(this);
|
||||
|
||||
console.log("[WebSocket] Starting...");
|
||||
}
|
||||
}
|
||||
|
||||
export default { ReconnectingWebsocket }
|
||||
359
photon-client/src/plugins/WebsocketVideoStream.js
Normal file
@@ -0,0 +1,359 @@
|
||||
// Circular buffer storage. Externally-apparent 'length' increases indefinitely
|
||||
// while any items with indexes below length-n will be forgotten (undefined
|
||||
// will be returned if you try to get them, trying to set is an exception).
|
||||
// n represents the initial length of the array, not a maximum
|
||||
class StatsHistoryBuffer{
|
||||
constructor (){
|
||||
this.windowLen = 10;
|
||||
this._array= new Array(this.windowLen);
|
||||
this.headPtr = 0;
|
||||
this.frameCount = 0;
|
||||
this.bitAvgAccum = 0;
|
||||
|
||||
//calculated vals
|
||||
this.bitRate_Mbps = 0;
|
||||
this.framerate_fps = 0;
|
||||
}
|
||||
|
||||
putAndPop(v){
|
||||
this.headPtr++;
|
||||
var idx = (this.headPtr)%this._array.length;
|
||||
var poppedVal = this._array[idx];
|
||||
this._array[idx] = v;
|
||||
return poppedVal;
|
||||
}
|
||||
|
||||
addSample(time, frameSize_bits, dispFrame_count) {
|
||||
var oldVal = this.putAndPop([time, frameSize_bits, dispFrame_count]);
|
||||
|
||||
this.bitAvgAccum += frameSize_bits;
|
||||
|
||||
if(oldVal !=null){
|
||||
var oldTime = oldVal[0];
|
||||
var oldFrameSize = oldVal[1];
|
||||
var oldFrameCount = oldVal[2];
|
||||
|
||||
var deltaTime_s = (time - oldTime);
|
||||
|
||||
this.bitAvgAccum -= oldFrameSize;
|
||||
|
||||
//bitrate - total bits transferred over the time period, divided by the period length
|
||||
// converted to mbps
|
||||
this.bitRate_Mbps = ( this.bitAvgAccum / deltaTime_s ) * (1.0/1048576.0);
|
||||
|
||||
//framerate - total frames displayed over the time period, divided by the period length
|
||||
this.framerate_fps = (dispFrame_count - oldFrameCount) / deltaTime_s;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
getText(){
|
||||
return "Streaming @ " + this.framerate_fps.toFixed(1) + "FPS " + this.bitRate_Mbps.toFixed(1) + "Mbps";
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
export class WebsocketVideoStream{
|
||||
|
||||
constructor(drawDiv, streamPort, host) {
|
||||
console.log("host " + host + " port " + streamPort)
|
||||
|
||||
this.drawDiv = drawDiv;
|
||||
this.image = document.getElementById(this.drawDiv);
|
||||
this.streamPort = streamPort;
|
||||
this.newStreamPortReq = null;
|
||||
this.serverAddr = "ws://" + host + "/websocket_cameras";
|
||||
this.imgData = null;
|
||||
this.imgDataTime = -1;
|
||||
this.prevImgDataTime = -1;
|
||||
this.imgObjURL = null;
|
||||
this.frameRxCount = 0;
|
||||
this.dispFrameCount = 0;
|
||||
this.stats = null;
|
||||
|
||||
//Set up div for stream stats info provided for users
|
||||
this.statsTextDiv = this.image.parentNode.appendChild(document.createElement("div"));
|
||||
|
||||
//Centered over the image
|
||||
this.statsTextDiv.style.position = "absolute";
|
||||
this.statsTextDiv.style.left = "50%";
|
||||
this.statsTextDiv.style.top = "50%";
|
||||
this.statsTextDiv.style.transform = "translate(-50%, -50%)";
|
||||
|
||||
// Big enough for a line or two of text, with centered text
|
||||
this.statsTextDiv.style.padding = "0.5em"
|
||||
this.statsTextDiv.style.overflow = "hidden";
|
||||
this.statsTextDiv.style.textAlign = "center";
|
||||
this.statsTextDiv.style.verticalAlign = "middle";
|
||||
|
||||
// Styled to be black with grey text
|
||||
this.statsTextDiv.style.backgroundColor = "black";
|
||||
this.statsTextDiv.style.color = "#9E9E9E";
|
||||
this.statsTextDiv.style.borderRadius = "3px";
|
||||
|
||||
//Default no text
|
||||
this.statsTextDiv.innerHTML = "";
|
||||
|
||||
// Only show on mouseover, with opacity fade-in/fade-out
|
||||
this.statsTextDiv.style.opacity = "0.0";
|
||||
this.statsTextDiv.style.transition = "opacity 0.25s ease 0.25s";
|
||||
this.statsTextDiv.style.transitionDelay = "opacity 0.5s";
|
||||
this.image.addEventListener('mouseover', () => {this.statsTextDiv.style.opacity = "0.6";});
|
||||
this.statsTextDiv.addEventListener('mouseover', () => {this.statsTextDiv.style.opacity = "0.6";});
|
||||
this.image.addEventListener('mouseout', () => {this.statsTextDiv.style.opacity = "0.0";});
|
||||
|
||||
//Display state machine descriptions
|
||||
this.DSM_DISCONNECTED = "Disconnected";
|
||||
this.DSM_WAIT_FOR_VALID_PORT = "Waiting for valid port ID";
|
||||
this.DSM_SUBSCRIBE = "Subscribing";
|
||||
this.DSM_WAIT_FOR_FIRST_FRAME = "Waiting for frame data";
|
||||
this.DSM_SHOWING = "Showing Frames";
|
||||
this.DSM_RESTART_UNSUBSCRIBE = "Unsubscribing";
|
||||
this.DSM_RESTART_WAIT = "Waiting before resubscribe";
|
||||
|
||||
this.dsm_cur_state = this.DSM_DISCONNECTED;
|
||||
this.dsm_prev_state = this.DSM_DISCONNECTED;
|
||||
this.dsm_restart_start_time = window.performance.now();
|
||||
|
||||
this.dispNoStream();
|
||||
this.ws_connect();
|
||||
|
||||
requestAnimationFrame(()=>this.animationLoop());
|
||||
|
||||
}
|
||||
|
||||
dispImageData(){
|
||||
if(this.prevImgDataTime != this.imgDataTime){
|
||||
//From https://stackoverflow.com/questions/67507616/set-image-src-from-image-blob/67507685#67507685
|
||||
//Ensure uniqueness by making the new one before revoking the old one.
|
||||
var oldURL = this.imgObjURL
|
||||
this.imgObjURL = URL.createObjectURL(this.imgData);
|
||||
if(oldURL != null){
|
||||
URL.revokeObjectURL(oldURL)
|
||||
}
|
||||
|
||||
//Update the image with the new mimetype and image
|
||||
this.image.src = this.imgObjURL;
|
||||
|
||||
this.dispFrameCount++;
|
||||
this.prevImgDataTime = this.imgDataTime;
|
||||
} // else no new image, don't update anything
|
||||
}
|
||||
|
||||
dispNoStream() {
|
||||
this.image.src = require("../assets/loading.gif");
|
||||
}
|
||||
|
||||
animationLoop(){
|
||||
// Update time metrics
|
||||
var curTime_s = window.performance.now() / 1000.0;
|
||||
var timeInState = curTime_s - this.dsm_restart_start_time;
|
||||
|
||||
// Save previous state
|
||||
this.dsm_prev_state = this.dsm_cur_state;
|
||||
|
||||
// Evaluate state transitions
|
||||
if(this.serverConnectionActive == false){
|
||||
//Any state - if the server connection goes false, always transition to disconnected
|
||||
this.dsm_cur_state = this.DSM_DISCONNECTED;
|
||||
} else {
|
||||
//Conditional transitions
|
||||
switch(this.dsm_cur_state) {
|
||||
case this.DSM_DISCONNECTED:
|
||||
//Immediately transition to waiting for the first frame
|
||||
this.dsm_cur_state = this.DSM_WAIT_FOR_VALID_PORT;
|
||||
break;
|
||||
case this.DSM_WAIT_FOR_VALID_PORT:
|
||||
// Wait until the user has configured a valid port
|
||||
if(this.streamPort > 0){
|
||||
this.dsm_cur_state = this.DSM_SUBSCRIBE;
|
||||
} else {
|
||||
this.dsm_cur_state = this.DSM_WAIT_FOR_VALID_PORT;
|
||||
}
|
||||
break;
|
||||
case this.DSM_SUBSCRIBE:
|
||||
// Immediately transition after subscriptions is sent
|
||||
this.dsm_cur_state = this.DSM_WAIT_FOR_FIRST_FRAME;
|
||||
break;
|
||||
case this.DSM_WAIT_FOR_FIRST_FRAME:
|
||||
if(this.imgData != null){
|
||||
//we got some image data, start showing it
|
||||
this.dsm_cur_state = this.DSM_SHOWING;
|
||||
} else if (this.newStreamPortReq != null){
|
||||
//Stream port requested changed, unsubscribe and restart
|
||||
this.dsm_cur_state = this.DSM_RESTART_UNSUBSCRIBE;
|
||||
} else {
|
||||
this.dsm_cur_state = this.DSM_WAIT_FOR_FIRST_FRAME;
|
||||
}
|
||||
break;
|
||||
case this.DSM_SHOWING:
|
||||
if((curTime_s - this.imgDataTime) > 2.5){
|
||||
//timeout, begin the restart sequence
|
||||
this.dsm_cur_state = this.DSM_RESTART_UNSUBSCRIBE;
|
||||
} else if (this.newStreamPortReq != null){
|
||||
//Stream port requested changed, unsubscribe and restart
|
||||
this.dsm_cur_state = this.DSM_RESTART_UNSUBSCRIBE;
|
||||
} else {
|
||||
//stay in this state.
|
||||
this.dsm_cur_state = this.DSM_SHOWING;
|
||||
}
|
||||
break;
|
||||
case this.DSM_RESTART_UNSUBSCRIBE:
|
||||
//Only should spend one loop in Unsubscribe, immediately transition
|
||||
this.dsm_cur_state = this.DSM_RESTART_WAIT;
|
||||
break;
|
||||
case this.DSM_RESTART_WAIT:
|
||||
if (timeInState > 0.25) {
|
||||
//we've waited long enough, go to try to re-subscribe
|
||||
this.dsm_cur_state = this.DSM_WAIT_FOR_VALID_PORT;
|
||||
} else {
|
||||
//stay in this state.
|
||||
this.dsm_cur_state = this.DSM_RESTART_WAIT;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
// Shouldn't get here, default back to init
|
||||
this.dsm_cur_state = this.DSM_DISCONNECTED;
|
||||
}
|
||||
}
|
||||
|
||||
//take current-state or state-transition actions
|
||||
|
||||
if(this.dsm_cur_state != this.dsm_prev_state){
|
||||
//Any state transition
|
||||
console.log("State Change: " + this.dsm_prev_state + " -> " + this.dsm_cur_state);
|
||||
}
|
||||
|
||||
if(this.dsm_cur_state == this.DSM_SHOWING){
|
||||
// Currently in SHOWING
|
||||
// Show image and update status text
|
||||
this.dispImageData();
|
||||
this.statsTextDiv.innerHTML = this.stats.getText();
|
||||
} else {
|
||||
//Just show the state for debug
|
||||
this.statsTextDiv.innerHTML = this.dsm_cur_state;
|
||||
}
|
||||
|
||||
if(this.dsm_cur_state != this.DSM_SHOWING && this.dsm_prev_state == this.DSM_SHOWING ){
|
||||
//Any transition out of showing - no stream
|
||||
this.dispNoStream();
|
||||
}
|
||||
|
||||
if(this.dsm_cur_state == this.DSM_RESTART_UNSUBSCRIBE){
|
||||
// Currently in UNSUBSCRIBE, do the unsubscribe actions
|
||||
this.stopStream();
|
||||
this.dsm_restart_start_time = curTime_s;
|
||||
}
|
||||
|
||||
if(this.dsm_cur_state == this.DSM_SUBSCRIBE){
|
||||
// Currently in SUBSCRIBE, do the subscribe actions
|
||||
this.startStream();
|
||||
this.dsm_restart_start_time = curTime_s;
|
||||
}
|
||||
|
||||
if(this.dsm_cur_state == this.DSM_WAIT_FOR_VALID_PORT){
|
||||
// Currently waiting for a vaild port to be requested
|
||||
if(this.newStreamPortReq != null){
|
||||
this.streamPort = this.newStreamPortReq;
|
||||
this.newStreamPortReq = null;
|
||||
}
|
||||
}
|
||||
|
||||
//Update status text
|
||||
|
||||
requestAnimationFrame(()=>this.animationLoop());
|
||||
}
|
||||
|
||||
startStream() {
|
||||
console.log("Subscribing to port " + this.streamPort);
|
||||
this.imgData = null;
|
||||
this.ws.send(JSON.stringify({"cmd": "subscribe", "port":this.streamPort}));
|
||||
}
|
||||
|
||||
stopStream() {
|
||||
console.log("Unsubscribing");
|
||||
this.ws.send(JSON.stringify({"cmd": "unsubscribe"}));
|
||||
this.imgData = null;
|
||||
}
|
||||
|
||||
setPort(streamPort){
|
||||
console.log("Port set to " + streamPort);
|
||||
this.newStreamPortReq = streamPort;
|
||||
}
|
||||
|
||||
ws_onOpen() {
|
||||
// Set the flag allowing general server communication
|
||||
this.serverConnectionActive = true;
|
||||
console.log("Camera Websockets Connected!");
|
||||
|
||||
// New websocket connection, reset stats
|
||||
this.frameRxCount = 0;
|
||||
this.dispFrameCount = 0;
|
||||
this.stats = new StatsHistoryBuffer();
|
||||
}
|
||||
|
||||
ws_onClose(e) {
|
||||
//Clear flags to stop server communication
|
||||
this.ws = null;
|
||||
this.serverConnectionActive = false;
|
||||
|
||||
console.log('Camera Socket is closed. Reconnect will be attempted in 0.5 second.', e.reason);
|
||||
setTimeout(this.ws_connect.bind(this), 500);
|
||||
|
||||
if(!e.wasClean){
|
||||
console.error('Socket encountered error!');
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
ws_onError(e){
|
||||
e; //prevent unused failure
|
||||
this.ws.close();
|
||||
}
|
||||
|
||||
ws_onMessage(e){
|
||||
//console.log("Got message from " + this.serverAddr)
|
||||
var msgTime_s = window.performance.now() / 1000.0;
|
||||
if(typeof e.data === 'string'){
|
||||
//string data from host
|
||||
//TODO - anything to receive info here? Maybe "available streams?"
|
||||
} else {
|
||||
if(e.data.size > 0){
|
||||
//binary data - a frame!
|
||||
//Save frame data for display in the next animation thread
|
||||
this.imgData = e.data;
|
||||
this.imgDataTime = msgTime_s;
|
||||
|
||||
//Count the incoming frame
|
||||
this.frameRxCount++;
|
||||
|
||||
//keep the stats up to date
|
||||
this.stats.addSample(msgTime_s,this.imgData.size * 8,this.dispFrameCount);
|
||||
} else {
|
||||
console.log("WS Stream Error: Server sent empty frame!");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
ws_connect() {
|
||||
this.serverConnectionActive = false;
|
||||
this.ws = new WebSocket(this.serverAddr);
|
||||
this.ws.binaryType = "blob";
|
||||
this.ws.onopen = this.ws_onOpen.bind(this);
|
||||
this.ws.onmessage = this.ws_onMessage.bind(this);
|
||||
this.ws.onclose = this.ws_onClose.bind(this);
|
||||
this.ws.onerror = this.ws_onError.bind(this);
|
||||
console.log("Connecting to server " + this.serverAddr);
|
||||
}
|
||||
|
||||
ws_close(){
|
||||
this.ws.close();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
export default {WebsocketVideoStream}
|
||||
@@ -1,121 +0,0 @@
|
||||
//https://gomakethings.com/getting-the-differences-between-two-objects-with-vanilla-js/
|
||||
export const diff = function (obj1, obj2) {
|
||||
|
||||
// Make sure an object to compare is provided
|
||||
if (!obj2 || Object.prototype.toString.call(obj2) !== '[object Object]') {
|
||||
return obj1;
|
||||
}
|
||||
|
||||
//
|
||||
// Variables
|
||||
//
|
||||
|
||||
let diffs = {};
|
||||
let key;
|
||||
|
||||
|
||||
//
|
||||
// Methods
|
||||
//
|
||||
|
||||
/**
|
||||
* Check if two arrays are equal
|
||||
* @param {Array} arr1 The first array
|
||||
* @param {Array} arr2 The second array
|
||||
* @return {Boolean} If true, both arrays are equal
|
||||
*/
|
||||
const arraysMatch = function (arr1, arr2) {
|
||||
|
||||
// Check if the arrays are the same length
|
||||
if (arr1.length !== arr2.length) return false;
|
||||
|
||||
// Check if all items exist and are in the same order
|
||||
for (let i = 0; i < arr1.length; i++) {
|
||||
if (arr1[i] !== arr2[i]) return false;
|
||||
}
|
||||
|
||||
// Otherwise, return true
|
||||
return true;
|
||||
|
||||
};
|
||||
|
||||
/**
|
||||
* Compare two items and push non-matches to object
|
||||
* @param {*} item1 The first item
|
||||
* @param {*} item2 The second item
|
||||
* @param {String} key The key in our object
|
||||
*/
|
||||
const compare = function (item1, item2, key) {
|
||||
|
||||
// Get the object type
|
||||
let type1 = Object.prototype.toString.call(item1);
|
||||
let type2 = Object.prototype.toString.call(item2);
|
||||
|
||||
// If type2 is undefined it has been removed
|
||||
if (type2 === '[object Undefined]') {
|
||||
diffs[key] = null;
|
||||
return;
|
||||
}
|
||||
|
||||
// If items are different types
|
||||
if (type1 !== type2) {
|
||||
diffs[key] = item2;
|
||||
return;
|
||||
}
|
||||
|
||||
// If an object, compare recursively
|
||||
if (type1 === '[object Object]') {
|
||||
let objDiff = diff(item1, item2);
|
||||
if (Object.keys(objDiff).length > 1) {
|
||||
diffs[key] = objDiff;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// If an array, compare
|
||||
if (type1 === '[object Array]') {
|
||||
if (!arraysMatch(item1, item2)) {
|
||||
diffs[key] = item2;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Else if it's a function, convert to a string and compare
|
||||
// Otherwise, just compare
|
||||
if (type1 === '[object Function]') {
|
||||
if (item1.toString() !== item2.toString()) {
|
||||
diffs[key] = item2;
|
||||
}
|
||||
} else {
|
||||
if (item1 !== item2) {
|
||||
diffs[key] = item2;
|
||||
}
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
|
||||
//
|
||||
// Compare our objects
|
||||
//
|
||||
|
||||
// Loop through the first object
|
||||
for (key in obj1) {
|
||||
if (obj1.hasOwnProperty(key)) {
|
||||
compare(obj1[key], obj2[key], key);
|
||||
}
|
||||
}
|
||||
|
||||
// Loop through the second object and find missing items
|
||||
for (key in obj2) {
|
||||
if (obj2.hasOwnProperty(key)) {
|
||||
if (!obj1[key] && obj1[key] !== obj2[key] ) {
|
||||
diffs[key] = obj2[key];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Return the object of differences
|
||||
return diffs;
|
||||
|
||||
};
|
||||
@@ -5,4 +5,4 @@ $body-font-family: $default-font;
|
||||
$heading-font-family: $default-font;
|
||||
.v-application {
|
||||
font-family: $default-font !important;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,19 +15,29 @@ export default new Vuex.Store({
|
||||
},
|
||||
state: {
|
||||
backendConnected: false,
|
||||
websocket: null,
|
||||
ntConnectionInfo: {
|
||||
connected: false,
|
||||
address: "",
|
||||
clients: 0,
|
||||
},
|
||||
networkInfo: {
|
||||
possibleRios: ["Loading..."],
|
||||
deviceips: ["Loading..."],
|
||||
},
|
||||
connectedCallbacks: [],
|
||||
colorPicking: false,
|
||||
logsOverlay: false,
|
||||
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,
|
||||
currentPipelineIndex: 0,
|
||||
pipelineNicknames: ["Unknown"],
|
||||
outputStreamPort: 1181,
|
||||
inputStreamPort: 1182,
|
||||
outputStreamPort: 0,
|
||||
inputStreamPort: 0,
|
||||
nickname: "Unknown",
|
||||
videoFormatList: [
|
||||
{
|
||||
@@ -42,13 +52,15 @@ export default new Vuex.Store({
|
||||
isFovConfigurable: true,
|
||||
calibrated: false,
|
||||
currentPipelineSettings: {
|
||||
pipelineType: 2, // One of "driver", "reflective", "shape"
|
||||
pipelineType: 5, // One of "calib", "driver", "reflective", "shape", "AprilTag"
|
||||
// 2 is reflective
|
||||
|
||||
// Settings that apply to all pipeline types
|
||||
cameraExposure: 1,
|
||||
cameraBrightness: 2,
|
||||
cameraGain: 3,
|
||||
cameraAutoExposure: false,
|
||||
cameraRedGain: 3,
|
||||
cameraBlueGain: 4,
|
||||
inputImageRotationMode: 0,
|
||||
cameraVideoModeIndex: 0,
|
||||
streamingFrameDivisor: 0,
|
||||
@@ -57,15 +69,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,
|
||||
@@ -75,7 +90,16 @@ export default new Vuex.Store({
|
||||
|
||||
cornerDetectionAccuracyPercentage: 10,
|
||||
|
||||
// Settings that apply to shape
|
||||
// Settings that apply to AprilTag
|
||||
tagFamily: 1,
|
||||
decimate: 1.0,
|
||||
blur: 0.0,
|
||||
threads: 1,
|
||||
debug: false,
|
||||
refineEdges: true,
|
||||
numIterations: 1,
|
||||
decisionMargin: 0,
|
||||
hammingDist: 0,
|
||||
}
|
||||
}
|
||||
],
|
||||
@@ -89,9 +113,18 @@ export default new Vuex.Store({
|
||||
skew: 0,
|
||||
area: 0,
|
||||
// 3D only
|
||||
pose: {x: 0, y: 0, rot: 0},
|
||||
}]
|
||||
},
|
||||
pose: {x: 1, y: 1, z: 0, qw: 1, qx: 0, qy: 0, qz: 0},
|
||||
},
|
||||
{
|
||||
// Available in both 2D and 3D
|
||||
pitch: 0,
|
||||
yaw: 0,
|
||||
skew: 0,
|
||||
area: 0,
|
||||
// 3D only
|
||||
pose: {x: 2, y: 3, z: 0, qw: 1, qx: 0, qy: 0, qz: 0},
|
||||
}]
|
||||
},
|
||||
settings: {
|
||||
general: {
|
||||
version: "Unknown",
|
||||
@@ -108,8 +141,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,24 +152,37 @@ 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'),
|
||||
websocket: set('websocket'),
|
||||
cameraSettings: set('cameraSettings'),
|
||||
currentCameraIndex: set('currentCameraIndex'),
|
||||
selectedOutputs: set('selectedOutputs'),
|
||||
settings: set('settings'),
|
||||
calibrationData: set('calibrationData'),
|
||||
metrics: set('metrics'),
|
||||
ntConnectionInfo: set('ntConnectionInfo'),
|
||||
networkInfo: set('networkInfo'),
|
||||
backendConnected: set('backendConnected'),
|
||||
logString: (state, newStr) => {
|
||||
const str = state.logMessages;
|
||||
str.push(newStr)
|
||||
str.push(newStr);
|
||||
Vue.set(state, 'logString', str)
|
||||
},
|
||||
|
||||
@@ -172,6 +218,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 +238,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 +276,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
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
@@ -69,4 +69,4 @@ export default {
|
||||
}
|
||||
}
|
||||
|
||||
};
|
||||
};
|
||||
|
||||
@@ -5,4 +5,4 @@ const theme = Object.freeze({
|
||||
background: "#232C37",
|
||||
});
|
||||
|
||||
export default theme;
|
||||
export default theme;
|
||||
|
||||
@@ -19,22 +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"
|
||||
/>
|
||||
<br>
|
||||
<CVnumberinput
|
||||
v-model="cameraSettings.tiltDegrees"
|
||||
name="Camera pitch"
|
||||
tooltip="How many degrees above the horizontal the physical camera is tilted"
|
||||
:step="0.01"
|
||||
:label-cols="$vuetify.breakpoint.mdAndUp ? undefined : 7"
|
||||
/>
|
||||
<br>
|
||||
<v-btn
|
||||
@@ -66,43 +60,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 -->
|
||||
@@ -136,6 +138,24 @@
|
||||
text="Standard Deviation"
|
||||
/>
|
||||
</th>
|
||||
<th class="text-center">
|
||||
<tooltipped-label
|
||||
tooltip="Estimated Horizontal FOV, in degrees"
|
||||
text="Horizontal FOV"
|
||||
/>
|
||||
</th>
|
||||
<th class="text-center">
|
||||
<tooltipped-label
|
||||
tooltip="Estimated Vertical FOV, in degrees"
|
||||
text="Vertical FOV"
|
||||
/>
|
||||
</th>
|
||||
<th class="text-center">
|
||||
<tooltipped-label
|
||||
tooltip="Estimated Diagonal FOV, in degrees"
|
||||
text="Diagonal FOV"
|
||||
/>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@@ -143,11 +163,14 @@
|
||||
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>
|
||||
<td> {{ isCalibrated(value) ? value.standardDeviation.toFixed(2) + "px" : "—" }} </td>
|
||||
<td> {{ isCalibrated(value) ? value.horizontalFOV.toFixed(2) + "°" : "—" }} </td>
|
||||
<td> {{ isCalibrated(value) ? value.verticalFOV.toFixed(2) + "°" : "—" }} </td>
|
||||
<td> {{ isCalibrated(value) ? value.diagonalFOV.toFixed(2) + "°" : "—" }} </td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</v-simple-table>
|
||||
@@ -171,28 +194,60 @@
|
||||
>
|
||||
<CVslider
|
||||
v-model="$store.getters.currentPipelineSettings.cameraExposure"
|
||||
:disabled="$store.getters.currentPipelineSettings.cameraAutoExposure"
|
||||
name="Exposure"
|
||||
:min="0"
|
||||
:max="100"
|
||||
slider-cols="8"
|
||||
step="0.1"
|
||||
tooltip="Directly controls how much light is allowed to fall onto the sensor, which affects apparent brightness"
|
||||
@input="e => handlePipelineUpdate('cameraExposure', e)"
|
||||
/>
|
||||
<CVslider
|
||||
v-model="this.$store.getters.currentPipelineSettings.cameraBrightness"
|
||||
v-model="$store.getters.currentPipelineSettings.cameraBrightness"
|
||||
name="Brightness"
|
||||
:min="0"
|
||||
:max="100"
|
||||
slider-cols="8"
|
||||
@input="e => handlePipelineUpdate('cameraBrightness', e)"
|
||||
/>
|
||||
<CVswitch
|
||||
v-model="$store.getters.currentPipelineSettings.cameraAutoExposure"
|
||||
class="pt-2"
|
||||
name="Auto Exposure"
|
||||
tooltip="Enables or Disables camera automatic adjustment for current lighting conditions"
|
||||
@input="e => handlePipelineUpdate('cameraAutoExposure', e)"
|
||||
/>
|
||||
<CVslider
|
||||
v-if="$store.getters.currentPipelineSettings.cameraGain !== -1"
|
||||
v-model="$store.getters.currentPipelineSettings.cameraGain"
|
||||
name="Gain"
|
||||
:min="0"
|
||||
:max="100"
|
||||
slider-cols="8"
|
||||
@input="e => handlePipelineUpdate('cameraGain', e)"
|
||||
v-if="cameraGain >= 0"
|
||||
v-model="cameraGain"
|
||||
name="Camera Gain"
|
||||
min="0"
|
||||
max="100"
|
||||
tooltip="Controls camera gain, similar to brightness"
|
||||
:slider-cols="largeBox"
|
||||
@input="handlePipelineData('cameraGain')"
|
||||
@rollback="e => rollback('cameraGain', e)"
|
||||
/>
|
||||
<CVslider
|
||||
v-if="$store.getters.currentPipelineSettings.cameraRedGain !== -1"
|
||||
v-model="$store.getters.currentPipelineSettings.cameraRedGain"
|
||||
name="Red AWB Gain"
|
||||
min="0"
|
||||
max="100"
|
||||
tooltip="Controls red automatic white balance gain, which affects how the camera captures colors in different conditions"
|
||||
:slider-cols="8"
|
||||
@input="e => handlePipelineData('cameraRedGain', e)"
|
||||
/>
|
||||
<CVslider
|
||||
v-if="$store.getters.currentPipelineSettings.cameraBlueGain !== -1"
|
||||
v-model="$store.getters.currentPipelineSettings.cameraBlueGain"
|
||||
name="Blue AWB Gain"
|
||||
min="0"
|
||||
max="100"
|
||||
tooltip="Controls blue automatic white balance gain, which affects how the camera captures colors in different conditions"
|
||||
:slider-cols="8"
|
||||
@input="e => handlePipelineData('cameraBlueGain', e)"
|
||||
/>
|
||||
</v-col>
|
||||
</v-row>
|
||||
@@ -218,7 +273,7 @@
|
||||
:disabled="checkCancellation"
|
||||
@click="sendCalibrationFinish"
|
||||
>
|
||||
{{ hasEnough ? "End Calibration" : "Cancel Calibration" }}
|
||||
{{ hasEnough ? "Finish Calibration" : "Cancel Calibration" }}
|
||||
</v-btn>
|
||||
</v-col>
|
||||
<v-col>
|
||||
@@ -227,19 +282,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 +300,73 @@
|
||||
cols="12"
|
||||
md="5"
|
||||
>
|
||||
<CVimage
|
||||
:address="$store.getters.streamAddress[1]"
|
||||
:disconnected="!$store.state.backendConnected"
|
||||
scale="100"
|
||||
style="border-radius: 5px;"
|
||||
/>
|
||||
<template>
|
||||
<CVimage
|
||||
:id="cameras-cal"
|
||||
:idx=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>
|
||||
|
||||
@@ -272,31 +374,34 @@
|
||||
import CVselect from '../components/common/cv-select';
|
||||
import CVnumberinput from '../components/common/cv-number-input';
|
||||
import CVslider from '../components/common/cv-slider';
|
||||
import CVswitch from '../components/common/cv-switch';
|
||||
import CVimage from "../components/common/cv-image";
|
||||
import TooltippedLabel from "../components/common/cv-tooltipped-label";
|
||||
import jsPDF from "jspdf";
|
||||
import "../jsPDFFonts/Prompt-Regular-normal.js";
|
||||
|
||||
export default {
|
||||
name: 'Cameras',
|
||||
components: {
|
||||
TooltippedLabel,
|
||||
TooltippedLabel,
|
||||
CVselect,
|
||||
CVnumberinput,
|
||||
CVslider,
|
||||
CVswitch,
|
||||
CVimage
|
||||
},
|
||||
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) {
|
||||
@@ -316,6 +421,15 @@ export default {
|
||||
}
|
||||
},
|
||||
|
||||
cameraGain: {
|
||||
get() {
|
||||
return parseInt(this.$store.getters.currentPipelineSettings.cameraGain)
|
||||
},
|
||||
set(val) {
|
||||
this.$store.commit("mutatePipeline", {"cameraGain": parseInt(val)});
|
||||
}
|
||||
},
|
||||
|
||||
// Makes sure there's only one entry per resolution
|
||||
filteredResolutionList: {
|
||||
get() {
|
||||
@@ -325,9 +439,12 @@ 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;
|
||||
it['horizontalFOV'] = 2 * Math.atan2(it.width/2,calib.intrinsics[0]) * (180/Math.PI);
|
||||
it['verticalFOV'] = 2 * Math.atan2(it.height/2,calib.intrinsics[4]) * (180/Math.PI);
|
||||
it['diagonalFOV'] = 2 * Math.atan2(Math.sqrt(it.width**2 + (it.height/(calib.intrinsics[4]/calib.intrinsics[0]))**2)/2,calib.intrinsics[0]) * (180/Math.PI);
|
||||
}
|
||||
filtered.push(it);
|
||||
}
|
||||
@@ -336,13 +453,11 @@ export default {
|
||||
return filtered
|
||||
}
|
||||
},
|
||||
|
||||
stringResolutionList: {
|
||||
get() {
|
||||
return this.filteredResolutionList.map(res => `${res['width']} X ${res['height']}`);
|
||||
}
|
||||
},
|
||||
|
||||
cameraSettings: {
|
||||
get() {
|
||||
return this.$store.getters.currentCameraSettings;
|
||||
@@ -351,7 +466,6 @@ export default {
|
||||
this.$store.commit('cameraSettings', value);
|
||||
}
|
||||
},
|
||||
|
||||
boardType: {
|
||||
get() {
|
||||
return this.calibrationData.boardType
|
||||
@@ -409,7 +523,6 @@ export default {
|
||||
return this.$store.getters.currentPipelineIndex === -2;
|
||||
}
|
||||
},
|
||||
|
||||
selectedFilteredResIndex: {
|
||||
get() {
|
||||
return this.filteredVideomodeIndex
|
||||
@@ -422,28 +535,119 @@ 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", {
|
||||
"settings": this.cameraSettings,
|
||||
"index": this.$store.state.currentCameraIndex
|
||||
}).then(
|
||||
function (response) {
|
||||
}).then(response => {
|
||||
if (response.status === 200) {
|
||||
this.$store.state.saveBar = true;
|
||||
}
|
||||
@@ -464,46 +668,32 @@ export default {
|
||||
if (this.isCalibrating === true) {
|
||||
data['takeCalibrationSnapshot'] = true
|
||||
} else {
|
||||
// This store prevents an edge case of a user not selecting a different resolution, which causes the set logic to not be called
|
||||
this.$store.commit('mutateCalibrationState', {['videoModeIndex']: this.filteredResolutionList[this.selectedFilteredResIndex].index});
|
||||
const calData = this.calibrationData;
|
||||
calData.isCalibrating = true;
|
||||
data['startPnpCalibration'] = calData;
|
||||
|
||||
console.log("starting calibration with index " + calData.videoModeIndex);
|
||||
}
|
||||
|
||||
this.$socket.send(this.$msgPack.encode(data));
|
||||
this.$store.commit('currentPipelineIndex', -2);
|
||||
this.$store.state.websocket.ws.send(this.$msgPack.encode(data));
|
||||
},
|
||||
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 +701,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>
|
||||
|
||||
@@ -1,67 +1,78 @@
|
||||
<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="$refs.exportLogFile.click()"
|
||||
>
|
||||
<v-icon left>
|
||||
mdi-download
|
||||
</v-icon>
|
||||
Download Log
|
||||
|
||||
<!-- Special hidden link that gets 'clicked' when the user exports journalctl logs -->
|
||||
<a
|
||||
ref="exportLogFile"
|
||||
style="color: black; text-decoration: none; display: none"
|
||||
:href="'http://' + this.$address + '/api/settings/photonvision-journalctl.txt'"
|
||||
download="photonvision-journalctl.txt"
|
||||
/>
|
||||
|
||||
</v-btn>
|
||||
</v-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>
|
||||
|
||||
@@ -1,61 +1,75 @@
|
||||
<template>
|
||||
<div>
|
||||
<v-container
|
||||
class="pa-3"
|
||||
fluid
|
||||
class="pa-3"
|
||||
fluid
|
||||
>
|
||||
<v-row
|
||||
no-gutters
|
||||
align="center"
|
||||
justify="center"
|
||||
no-gutters
|
||||
align="center"
|
||||
justify="center"
|
||||
>
|
||||
<v-col
|
||||
cols="12"
|
||||
:class="['pb-3 ', 'pr-lg-3']"
|
||||
lg="8"
|
||||
align-self="stretch"
|
||||
cols="12"
|
||||
:class="['pb-3 ', 'pr-lg-3']"
|
||||
lg="8"
|
||||
align-self="stretch"
|
||||
>
|
||||
<v-card
|
||||
color="primary"
|
||||
height="100%"
|
||||
style="display: flex; flex-direction: column"
|
||||
dark
|
||||
color="primary"
|
||||
height="100%"
|
||||
style="display: flex; flex-direction: column"
|
||||
dark
|
||||
>
|
||||
<v-card-title
|
||||
class="pb-0 mb-0 pl-4 pt-1"
|
||||
style="height: 15%; min-height: 50px;"
|
||||
class="pb-0 mb-0 pl-4 pt-1"
|
||||
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">Processing @ {{ Math.round($store.state.pipelineResults.fps) }} FPS –</span>
|
||||
<span v-if="fpsTooLow && !$store.getters.currentPipelineSettings.inputShouldShow && $store.getters.pipelineType == 2">HSV thresholds are too broad; narrow them for better performance</span>
|
||||
<span v-else-if="fpsTooLow && getters.currentCameraSettings.inputShouldShow">stop viewing the raw stream for better performance</span>
|
||||
<span v-else>{{ Math.min(Math.round($store.state.pipelineResults.latency), 9999) }} ms latency</span>
|
||||
</v-chip>
|
||||
<v-switch
|
||||
v-model="driverMode"
|
||||
label="Driver Mode"
|
||||
style="margin-left: auto;"
|
||||
color="accent"
|
||||
v-model="driverMode"
|
||||
label="Driver Mode"
|
||||
style="margin-left: auto;"
|
||||
color="accent"
|
||||
/>
|
||||
</v-card-title>
|
||||
<v-row
|
||||
align="center"
|
||||
align="center"
|
||||
>
|
||||
<v-col
|
||||
v-for="idx in (selectedOutputs instanceof Array ? selectedOutputs : [selectedOutputs])"
|
||||
:key="idx"
|
||||
cols="12"
|
||||
:md="selectedOutputs.length === 1 ? 12 : Math.floor(12 / selectedOutputs.length)"
|
||||
class="pb-0 pt-0"
|
||||
style="height: 100%;"
|
||||
v-for="idx in (selectedOutputs instanceof Array ? selectedOutputs : [selectedOutputs])"
|
||||
:key="idx"
|
||||
cols="12"
|
||||
:md="selectedOutputs.length === 1 ? 12 : Math.floor(12 / selectedOutputs.length)"
|
||||
class="pb-0 pt-0"
|
||||
style="height: 100%;"
|
||||
>
|
||||
<div style="position: relative; width: 100%; height: 100%;">
|
||||
<cvImage
|
||||
:id="idx === 0 ? 'normal-stream' : ''"
|
||||
: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-xl="$store.getters.isDriverMode ? '60vh' : '450px'"
|
||||
:alt="'Stream' + idx"
|
||||
:color-picking="$store.state.colorPicking && idx == 0"
|
||||
@click="onImageClick"
|
||||
<cv-image
|
||||
:id="idx === 0 ? 'raw-stream' : 'processed-stream'"
|
||||
ref="streams"
|
||||
:idx=idx
|
||||
:disconnected="!$store.state.backendConnected"
|
||||
scale="100"
|
||||
:max-height="$store.getters.isDriverMode ? '40vh' : '300px'"
|
||||
:max-height-md="$store.getters.isDriverMode ? '50vh' : '380px'"
|
||||
:max-height-lg="$store.getters.isDriverMode ? '55vh' : '390px'"
|
||||
:max-height-xl="$store.getters.isDriverMode ? '60vh' : '450px'"
|
||||
:alt="idx === 0 ? 'Raw stream' : 'Processed stream'"
|
||||
:color-picking="$store.state.colorPicking && idx === 0"
|
||||
@click="onImageClick"
|
||||
/>
|
||||
</div>
|
||||
</v-col>
|
||||
@@ -63,45 +77,44 @@
|
||||
</v-card>
|
||||
</v-col>
|
||||
<v-col
|
||||
cols="12"
|
||||
class="pb-3"
|
||||
lg="4"
|
||||
align-self="stretch"
|
||||
cols="12"
|
||||
class="pb-3"
|
||||
lg="4"
|
||||
align-self="stretch"
|
||||
>
|
||||
<v-card
|
||||
color="primary"
|
||||
color="primary"
|
||||
>
|
||||
<camera-and-pipeline-select />
|
||||
</v-card>
|
||||
<v-card
|
||||
:disabled="$store.getters.isDriverMode || $store.state.colorPicking"
|
||||
class="mt-3"
|
||||
color="primary"
|
||||
:disabled="$store.getters.isDriverMode || $store.state.colorPicking"
|
||||
class="mt-3"
|
||||
color="primary"
|
||||
>
|
||||
<v-row
|
||||
align="center"
|
||||
class="pl-3 pr-3"
|
||||
align="center"
|
||||
class="pl-3 pr-3"
|
||||
>
|
||||
<!-- -->
|
||||
<v-col lg="12">
|
||||
<p style="color: white;">
|
||||
Processing mode:
|
||||
</p>
|
||||
<v-btn-toggle
|
||||
v-model="processingMode"
|
||||
mandatory
|
||||
dark
|
||||
class="fill"
|
||||
v-model="processingMode"
|
||||
mandatory
|
||||
dark
|
||||
class="fill"
|
||||
>
|
||||
<v-btn
|
||||
color="secondary"
|
||||
color="secondary"
|
||||
>
|
||||
<v-icon>mdi-crop-square</v-icon>
|
||||
<span>2D</span>
|
||||
</v-btn>
|
||||
<v-btn
|
||||
color="secondary"
|
||||
@click="on3DClick"
|
||||
color="secondary"
|
||||
@click="on3DClick"
|
||||
>
|
||||
<v-icon>mdi-cube-outline</v-icon>
|
||||
<span>3D</span>
|
||||
@@ -113,25 +126,25 @@
|
||||
Stream display:
|
||||
</p>
|
||||
<v-btn-toggle
|
||||
v-model="selectedOutputs"
|
||||
:multiple="$vuetify.breakpoint.mdAndUp"
|
||||
mandatory
|
||||
dark
|
||||
class="fill"
|
||||
v-model="selectedOutputs"
|
||||
:multiple="$vuetify.breakpoint.mdAndUp"
|
||||
mandatory
|
||||
dark
|
||||
class="fill"
|
||||
>
|
||||
<v-btn
|
||||
color="secondary"
|
||||
class="fill"
|
||||
color="secondary"
|
||||
class="fill"
|
||||
>
|
||||
<v-icon>mdi-palette</v-icon>
|
||||
<span>Normal</span>
|
||||
<v-icon>mdi-import</v-icon>
|
||||
<span>Raw</span>
|
||||
</v-btn>
|
||||
<v-btn
|
||||
color="secondary"
|
||||
class="fill"
|
||||
color="secondary"
|
||||
class="fill"
|
||||
>
|
||||
<v-icon>mdi-compare</v-icon>
|
||||
<span>Threshold</span>
|
||||
<v-icon>mdi-export</v-icon>
|
||||
<span>Processed</span>
|
||||
</v-btn>
|
||||
</v-btn-toggle>
|
||||
</v-col>
|
||||
@@ -141,29 +154,29 @@
|
||||
</v-row>
|
||||
<v-row no-gutters>
|
||||
<v-col
|
||||
v-for="(tabs, idx) in tabGroups"
|
||||
:key="idx"
|
||||
:cols="Math.floor(12 / tabGroups.length)"
|
||||
:class="idx != tabGroups.length - 1 ? 'pr-3' : ''"
|
||||
align-self="stretch"
|
||||
v-for="(tabs, idx) in tabGroups"
|
||||
:key="idx"
|
||||
:cols="Math.floor(12 / tabGroups.length)"
|
||||
:class="idx !== tabGroups.length - 1 ? 'pr-3' : ''"
|
||||
align-self="stretch"
|
||||
>
|
||||
<v-card
|
||||
color="primary"
|
||||
height="100%"
|
||||
class="pr-4 pl-4"
|
||||
color="primary"
|
||||
height="100%"
|
||||
class="pr-4 pl-4"
|
||||
>
|
||||
<v-tabs
|
||||
v-if="!$store.getters.isDriverMode"
|
||||
v-model="selectedTabs[idx]"
|
||||
grow
|
||||
background-color="primary"
|
||||
dark
|
||||
height="48"
|
||||
slider-color="accent"
|
||||
v-if="!$store.getters.isDriverMode"
|
||||
v-model="selectedTabs[idx]"
|
||||
grow
|
||||
background-color="primary"
|
||||
dark
|
||||
height="48"
|
||||
slider-color="accent"
|
||||
>
|
||||
<v-tab
|
||||
v-for="(tab, i) in tabs.filter(it => it.name !== '3D' || $store.getters.currentPipelineSettings.solvePNPEnabled)"
|
||||
:key="i"
|
||||
v-for="(tab, i) in tabs"
|
||||
:key="i"
|
||||
>
|
||||
{{ tab.name }}
|
||||
</v-tab>
|
||||
@@ -171,10 +184,10 @@
|
||||
<div class="pl-4 pr-4 pt-2">
|
||||
<keep-alive>
|
||||
<component
|
||||
:is="(tabs[selectedTabs[idx]] || tabs[0]).component"
|
||||
:ref="(tabs[selectedTabs[idx]] || tabs[0]).name"
|
||||
v-model="$store.getters.pipeline"
|
||||
@update="$emit('save')"
|
||||
:is="(tabs[selectedTabs[idx]] || tabs[0]).component"
|
||||
:ref="(tabs[selectedTabs[idx]] || tabs[0]).name"
|
||||
v-model="$store.getters.pipeline"
|
||||
@update="$emit('save')"
|
||||
/>
|
||||
</keep-alive>
|
||||
</div>
|
||||
@@ -182,43 +195,44 @@
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-container>
|
||||
<!-- snack bar and modal -->
|
||||
|
||||
<v-snackbar
|
||||
v-model="snackbar"
|
||||
:timeout="3000"
|
||||
top
|
||||
color="error"
|
||||
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
|
||||
v-model="dialog"
|
||||
width="500"
|
||||
v-model="dialog"
|
||||
width="500"
|
||||
>
|
||||
<v-card
|
||||
color="primary"
|
||||
dark
|
||||
color="primary"
|
||||
dark
|
||||
>
|
||||
<v-card-title>
|
||||
Current resolution not calibrated
|
||||
</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"
|
||||
@click="$emit('switch-to-cameras')"
|
||||
href="/#/cameras"
|
||||
class="white--text"
|
||||
@click="$emit('switch-to-cameras')"
|
||||
> visit the Cameras tab</a> to calibrate this resolution. For now, SolvePNP will do nothing.
|
||||
</v-card-text>
|
||||
|
||||
@@ -227,9 +241,9 @@
|
||||
<v-card-actions>
|
||||
<v-spacer />
|
||||
<v-btn
|
||||
color="white"
|
||||
text
|
||||
@click="closeUncalibratedDialog"
|
||||
color="white"
|
||||
text
|
||||
@click="closeUncalibratedDialog"
|
||||
>
|
||||
OK
|
||||
</v-btn>
|
||||
@@ -240,202 +254,274 @@
|
||||
</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 Map3DTab from './PipelineViews/Map3DTab';
|
||||
import PnPTab from './PipelineViews/PnPTab';
|
||||
import AprilTagTab from './PipelineViews/AprilTagTab';
|
||||
import ArucoTab from './PipelineViews/ArucoTab';
|
||||
|
||||
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
|
||||
}
|
||||
},
|
||||
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];
|
||||
}
|
||||
|
||||
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 = 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;
|
||||
}
|
||||
}
|
||||
},
|
||||
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);
|
||||
}
|
||||
}
|
||||
export default {
|
||||
name: 'Pipeline',
|
||||
components: {
|
||||
CameraAndPipelineSelect,
|
||||
cvImage,
|
||||
InputTab,
|
||||
ThresholdTab,
|
||||
ContoursTab,
|
||||
OutputTab,
|
||||
TargetsTab,
|
||||
Map3DTab,
|
||||
PnPTab,
|
||||
AprilTagTab,
|
||||
ArucoTab,
|
||||
},
|
||||
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;
|
||||
}
|
||||
},
|
||||
tabGroups: {
|
||||
get() {
|
||||
let tabs = {
|
||||
input: {
|
||||
name: "Input",
|
||||
component: "InputTab",
|
||||
},
|
||||
threshold: {
|
||||
name: "Threshold",
|
||||
component: "ThresholdTab",
|
||||
},
|
||||
contours: {
|
||||
name: "Contours",
|
||||
component: "ContoursTab",
|
||||
},
|
||||
apriltag: {
|
||||
name: "AprilTag",
|
||||
component: "AprilTagTab",
|
||||
},
|
||||
aruco: {
|
||||
name: "Aruco",
|
||||
component: "ArucoTab",
|
||||
},
|
||||
output: {
|
||||
name: "Output",
|
||||
component: "OutputTab",
|
||||
},
|
||||
targets: {
|
||||
name: "Targets",
|
||||
component: "TargetsTab",
|
||||
},
|
||||
pnp: {
|
||||
name: "PnP",
|
||||
component: "PnPTab",
|
||||
},
|
||||
map3d: {
|
||||
name: "3D",
|
||||
component: "Map3DTab",
|
||||
}
|
||||
};
|
||||
|
||||
// If not in 3d, name "3D" is illegal
|
||||
const allow3d = this.$store.getters.currentPipelineSettings.solvePNPEnabled;
|
||||
// If in apriltag, "Threshold" and "Contours" are illegal -- otherwise "AprilTag" is
|
||||
const isAprilTag = (this.$store.getters.currentPipelineSettings.pipelineType - 2) === 2;
|
||||
const isAruco = (this.$store.getters.currentPipelineSettings.pipelineType - 2) === 3;
|
||||
|
||||
// 2D array of tab names and component names; each sub-array is a separate tab group
|
||||
let ret = [];
|
||||
if (this.$vuetify.breakpoint.smAndDown || this.$store.getters.isDriverMode || (this.$vuetify.breakpoint.mdAndDown && !this.$store.state.compactMode)) {
|
||||
// One big tab group with all the tabs
|
||||
ret[0] = Object.values(tabs);
|
||||
} else if (this.$vuetify.breakpoint.mdAndDown || !this.$store.state.compactMode) {
|
||||
// Two tab groups, one with "input, threshold, contours, output" and the other with "target info, 3D"
|
||||
ret[0] = [tabs.input, tabs.threshold, tabs.contours, tabs.apriltag, tabs.aruco, tabs.output];
|
||||
ret[1] = [tabs.targets, tabs.pnp, tabs.map3d];
|
||||
} else if (this.$vuetify.breakpoint.lgAndDown) {
|
||||
// Three tab groups, one with "input", one with "threshold, contours, output", and the other with "target info, 3D"
|
||||
ret[0] = [tabs.input];
|
||||
ret[1] = [tabs.threshold, tabs.contours, tabs.apriltag,tabs.aruco, tabs.output];
|
||||
ret[2] = [tabs.targets, tabs.pnp, tabs.map3d];
|
||||
} else if (this.$vuetify.breakpoint.xl) {
|
||||
// Three tab groups, one with "input", one with "threshold, contours", and the other with "output, target info, 3D"
|
||||
ret[0] = [tabs.input];
|
||||
ret[1] = [tabs.threshold];
|
||||
ret[2] = [tabs.contours, tabs.apriltag, tabs.aruco,tabs.output];
|
||||
ret[3] = [tabs.targets, tabs.pnp, tabs.map3d];
|
||||
}
|
||||
|
||||
for(let i = 0; i < ret.length; i++) {
|
||||
const group = ret[i];
|
||||
|
||||
// All the tabs we allow
|
||||
const filteredGroup = group.filter(it =>
|
||||
!(!allow3d && it.name === "3D") //Filter out 3D tab any time 3D isn't calibrated
|
||||
&& !((!allow3d || isAprilTag || isAruco) && it.name === "PnP") //Filter out the PnP config tab if 3D isn't available, or we're doing Apriltags
|
||||
&& !((isAprilTag || isAruco) && (it.name === "Threshold")) //Filter out threshold tab if we're doing apriltags
|
||||
&& !((isAprilTag || isAruco)&& (it.name === "Contours")) //Filter out contours if we're doing Apriltag
|
||||
&& !(!isAprilTag && it.name === "AprilTag") //Filter out apriltag unless we actually are doing Apriltags
|
||||
&& !(!isAruco && it.name === "Aruco")
|
||||
);
|
||||
ret[i] = filteredGroup;
|
||||
}
|
||||
|
||||
// One last filter to remove empty lists
|
||||
return ret.filter(it => it !== undefined && it.length > 0);
|
||||
}
|
||||
},
|
||||
processingMode: {
|
||||
get() {
|
||||
return (this.$store.getters.currentPipelineSettings.solvePNPEnabled || this.processingModeOverride) ? 1 : 0;
|
||||
},
|
||||
set(value) {
|
||||
if (this.$store.getters.isCalibrated) {
|
||||
this.$store.getters.currentPipelineSettings.solvePNPEnabled = value === 1;
|
||||
this.handlePipelineUpdate("solvePNPEnabled", value === 1);
|
||||
}
|
||||
}
|
||||
},
|
||||
driverMode: {
|
||||
get() {
|
||||
return this.$store.getters.isDriverMode;
|
||||
},
|
||||
set(value) {
|
||||
this.$store.getters.currentCameraSettings.currentPipelineIndex = value ? -1 : 0;
|
||||
this.handleInputWithIndex('currentPipeline', value ? -1 : 0);
|
||||
}
|
||||
},
|
||||
selectedOutputs: {
|
||||
// All this logic exists to deal with the reality that the output select buttons sometimes need an array and sometimes need a number (depending on whether or not they're exclusive)
|
||||
get() {
|
||||
// We switch the selector to single-select only on sm-and-down size devices, so we have to return a Number instead of an Array in that state
|
||||
let ret = [];
|
||||
if (this.$store.state.colorPicking) {
|
||||
ret = [0]; // We want the input stream only while color picking
|
||||
} else if (this.$store.getters.isDriverMode) {
|
||||
ret = [1]; // We want only the output stream in driver mode
|
||||
} else {
|
||||
if (this.$store.getters.currentPipelineSettings.inputShouldShow) ret = ret.concat([0]);
|
||||
if (this.$store.getters.currentPipelineSettings.outputShouldShow) ret = ret.concat([1]);
|
||||
if (!ret.length) ret = [0];
|
||||
}
|
||||
|
||||
if (this.$vuetify.breakpoint.mdAndUp) {
|
||||
return ret;
|
||||
} else {
|
||||
return ret[0] || 0;
|
||||
}
|
||||
},
|
||||
set(value) {
|
||||
let valToCommit = [0];
|
||||
if (value instanceof Array) {
|
||||
// Value is already an array, we don't need to do anything
|
||||
valToCommit = value;
|
||||
} else if (value) {
|
||||
// Value is assumed to be a number, so we wrap it into an array
|
||||
valToCommit = [value];
|
||||
}
|
||||
|
||||
this.$store.commit("mutatePipeline", {"inputShouldShow": valToCommit.includes(0)});
|
||||
this.$store.commit("mutatePipeline", {"outputShouldShow": valToCommit.includes(1)});
|
||||
this.handlePipelineUpdate("inputShouldShow", valToCommit.includes(0));
|
||||
}
|
||||
},
|
||||
fpsTooLow: {
|
||||
get() {
|
||||
// For now we only show the FPS is too low warning when GPU acceleration is enabled, because we don't really trust the presented video modes otherwise
|
||||
const currFPS = this.$store.state.pipelineResults.fps;
|
||||
const targetFPS = this.$store.getters.currentVideoFormat.fps;
|
||||
const driverMode = this.$store.getters.isDriverMode;
|
||||
const gpuAccel = this.$store.state.settings.general.gpuAcceleration === true;
|
||||
const isReflective = this.$store.getters.pipelineType === 2;
|
||||
|
||||
return (currFPS - targetFPS) < -5 && this.$store.state.pipelineResults.fps !== 0 && !driverMode && gpuAccel && isReflective;
|
||||
}
|
||||
},
|
||||
latency: {
|
||||
get() {
|
||||
return this.$store.getters.currentPipelineResults.latency;
|
||||
}
|
||||
},
|
||||
isCalibrated: {
|
||||
get() {
|
||||
const resolution = this.$store.getters.videoFormatList[this.$store.getters.currentPipelineSettings.cameraVideoModeIndex];
|
||||
return this.$store.getters.currentCameraSettings.calibrations
|
||||
.some(e => e.width === resolution.width && e.height === resolution.height)
|
||||
}
|
||||
},
|
||||
isRobotConnected: {
|
||||
get() {
|
||||
// return this.$store.state.ntConnectionInfo.connected && this.$store.state.backendConnected;
|
||||
return true;
|
||||
}
|
||||
},
|
||||
showNTWarning: {
|
||||
get() {
|
||||
return (!this.$store.state.ntConnectionInfo.connected || this.$store.state.settings.networkSettings.runNTServer) && this.$store.state.settings.networkSettings.teamNumber > 0 && this.$store.state.backendConnected && !this.hideNTWarning;
|
||||
}
|
||||
},
|
||||
},
|
||||
created() {
|
||||
this.$store.state.connectedCallbacks.push(this.reloadStreams)
|
||||
},
|
||||
methods: {
|
||||
reloadStreams() {
|
||||
// Reload the streams as we technically close and reopen them
|
||||
this.$refs.streams.forEach(it => it.reload())
|
||||
},
|
||||
onImageClick(event) {
|
||||
// Get a reference to the threshold tab (if it is shown) and call its "onClick" method
|
||||
let ref = this.$refs["Threshold"];
|
||||
if (ref && ref[0])
|
||||
ref[0].onClick(event)
|
||||
},
|
||||
on3DClick() {
|
||||
if (!this.$store.getters.isCalibrated) {
|
||||
this.dialog = true;
|
||||
this.processingModeOverride = true;
|
||||
}
|
||||
},
|
||||
closeUncalibratedDialog() {
|
||||
this.dialog = false;
|
||||
this.processingModeOverride = false;
|
||||
// this.$store.getters.currentPipelineSettings.solvePNPEnabled = false;
|
||||
this.handlePipelineUpdate("solvePNPEnabled", false);
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<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>
|
||||
|
||||
154
photon-client/src/views/PipelineViews/AprilTagTab.vue
Normal file
@@ -0,0 +1,154 @@
|
||||
<template>
|
||||
<div>
|
||||
<CVselect
|
||||
v-model="selectedFamily"
|
||||
name="Target family"
|
||||
:list="['AprilTag family 36h11', 'AprilTag family 25h9', 'AprilTag family 16h5']"
|
||||
select-cols="8"
|
||||
@input="handlePipelineUpdate('tagFamily', selectedFamily)"
|
||||
/>
|
||||
<CVslider
|
||||
v-model="decimate"
|
||||
class="pt-2"
|
||||
slider-cols="8"
|
||||
name="Decimate"
|
||||
min="1"
|
||||
max="8"
|
||||
step="1.0"
|
||||
tooltip="Increases FPS at the expense of range by reducing image resolution initially"
|
||||
@input="handlePipelineData('decimate')"
|
||||
/>
|
||||
<CVslider
|
||||
v-model="blur"
|
||||
class="pt-2"
|
||||
slider-cols="8"
|
||||
name="Blur"
|
||||
min="0"
|
||||
max="5"
|
||||
step=".01"
|
||||
tooltip="Gaussian blur added to the image, high FPS cost for slightly decreased noise"
|
||||
@input="handlePipelineData('blur')"
|
||||
/>
|
||||
<CVslider
|
||||
v-model="threads"
|
||||
class="pt-2"
|
||||
slider-cols="8"
|
||||
name="Threads"
|
||||
min="1"
|
||||
max="8"
|
||||
step="1"
|
||||
tooltip="Number of threads spawned by the AprilTag detector"
|
||||
@input="handlePipelineData('threads')"
|
||||
/>
|
||||
<CVswitch
|
||||
v-model="refineEdges"
|
||||
class="pt-2"
|
||||
slider-cols="8"
|
||||
name="Refine Edges"
|
||||
tooltip="Further refines the apriltag corner position initial estimate, suggested left on"
|
||||
@input="handlePipelineData('refineEdges')"
|
||||
/>
|
||||
<CVslider
|
||||
v-model="decisionMargin"
|
||||
class="pt-2 pb-4"
|
||||
slider-cols="8"
|
||||
name="Decision Margin Cutoff"
|
||||
min="0"
|
||||
max="250"
|
||||
step="1"
|
||||
tooltip="Tags with a 'margin' (decoding quality score) less than this wil be rejected. Increase this to reduce the number of false positive detections"
|
||||
@input="handlePipelineData('decisionMargin')"
|
||||
/>
|
||||
<CVslider
|
||||
v-model="numIterations"
|
||||
class="pt-2 pb-4"
|
||||
slider-cols="8"
|
||||
name="Pose Estimation Iterations"
|
||||
min="0"
|
||||
max="500"
|
||||
step="1"
|
||||
tooltip="Number of iterations the pose estimation algorithm will run, 50-100 is a good starting point"
|
||||
@input="handlePipelineData('numIterations')"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import CVslider from '../../components/common/cv-slider'
|
||||
import CVswitch from '../../components/common/cv-switch'
|
||||
import CVselect from '../../components/common/cv-select'
|
||||
|
||||
export default {
|
||||
name: "AprilTag",
|
||||
components: {
|
||||
CVslider,
|
||||
CVswitch,
|
||||
CVselect,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
familyList: ["AprilTag family 36h11", "AprilTag family 25h9", "AprilTag family 16h5"],
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
selectedFamily: {
|
||||
get() {
|
||||
return this.$store.getters.currentPipelineSettings.tagFamily
|
||||
},
|
||||
set(val) {
|
||||
this.$store.commit("mutatePipeline", {"tagFamily": val})
|
||||
}
|
||||
},
|
||||
decimate: {
|
||||
get() {
|
||||
return this.$store.getters.currentPipelineSettings.decimate
|
||||
},
|
||||
set(val) {
|
||||
this.$store.commit("mutatePipeline", {"decimate": val});
|
||||
}
|
||||
},
|
||||
decisionMargin: {
|
||||
get() {
|
||||
return this.$store.getters.currentPipelineSettings.decisionMargin
|
||||
},
|
||||
set(val) {
|
||||
this.$store.commit("mutatePipeline", {"decisionMargin": val});
|
||||
}
|
||||
},
|
||||
numIterations: {
|
||||
get() {
|
||||
return this.$store.getters.currentPipelineSettings.numIterations
|
||||
},
|
||||
set(val) {
|
||||
this.$store.commit("mutatePipeline", {"numIterations": val});
|
||||
}
|
||||
},
|
||||
blur: {
|
||||
get() {
|
||||
return this.$store.getters.currentPipelineSettings.blur
|
||||
},
|
||||
set(val) {
|
||||
this.$store.commit("mutatePipeline", {"blur": val});
|
||||
}
|
||||
},
|
||||
threads: {
|
||||
get() {
|
||||
return this.$store.getters.currentPipelineSettings.threads
|
||||
},
|
||||
set(val) {
|
||||
this.$store.commit("mutatePipeline", {"threads": val});
|
||||
}
|
||||
},
|
||||
refineEdges: {
|
||||
get() {
|
||||
return this.$store.getters.currentPipelineSettings.refineEdges
|
||||
},
|
||||
set(val) {
|
||||
this.$store.commit("mutatePipeline", {"refineEdges": val});
|
||||
}
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
}
|
||||
}
|
||||
</script>
|
||||
76
photon-client/src/views/PipelineViews/ArucoTab.vue
Normal file
@@ -0,0 +1,76 @@
|
||||
<template>
|
||||
<div>
|
||||
<CVslider
|
||||
v-model="decimate"
|
||||
class="pt-2"
|
||||
slider-cols="8"
|
||||
name="Decimate"
|
||||
min="1"
|
||||
max="8"
|
||||
step=".5"
|
||||
tooltip="Increases FPS at the expense of range by reducing image resolution initially"
|
||||
@input="handlePipelineData('decimate')"
|
||||
/>
|
||||
<CVslider
|
||||
v-model="numIterations"
|
||||
class="pt-2"
|
||||
slider-cols="8"
|
||||
name="Corner Iterations"
|
||||
min="30"
|
||||
max="1000"
|
||||
step="5"
|
||||
tooltip="How many iterations are going to be used in order to refine corners. Higher values are lead to more accuracy at the cost of performance"
|
||||
@input="handlePipelineData('numIterations')"
|
||||
/>
|
||||
<CVslider
|
||||
v-model="cornerAccuracy"
|
||||
class="pt-2"
|
||||
slider-cols="8"
|
||||
name="Corner Accuracy"
|
||||
min=".01"
|
||||
max="100"
|
||||
step=".01"
|
||||
tooltip="Minimum accuracy for the corners, lower is better but more performance intensive "
|
||||
@input="handlePipelineData('cornerAccuracy')"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import CVslider from '../../components/common/cv-slider'
|
||||
|
||||
export default {
|
||||
name: "Aruco",
|
||||
components: {
|
||||
CVslider
|
||||
},
|
||||
computed: {
|
||||
decimate: {
|
||||
get() {
|
||||
return this.$store.getters.currentPipelineSettings.decimate
|
||||
},
|
||||
set(val) {
|
||||
this.$store.commit("mutatePipeline", {"decimate": val});
|
||||
},
|
||||
},
|
||||
numIterations: {
|
||||
get() {
|
||||
return this.$store.getters.currentPipelineSettings.numIterations
|
||||
},
|
||||
set(val) {
|
||||
this.$store.commit("mutatePipeline", {"numIterations": val});
|
||||
},
|
||||
},
|
||||
cornerAccuracy: {
|
||||
get() {
|
||||
return this.$store.getters.currentPipelineSettings.cornerAccuracy
|
||||
},
|
||||
set(val) {
|
||||
this.$store.commit("mutatePipeline", {"cornerAccuracy": val});
|
||||
},
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -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>
|
||||
|
||||
@@ -2,10 +2,12 @@
|
||||
<div>
|
||||
<CVslider
|
||||
v-model="cameraExposure"
|
||||
:disabled="cameraAutoExposure"
|
||||
name="Exposure"
|
||||
min="0"
|
||||
max="100"
|
||||
tooltip="Directly controls how much light is allowed to fall onto the sensor, which affects brightness"
|
||||
step="0.1"
|
||||
tooltip="Directly controls how much light is allowed to fall onto the sensor, which affects apparent brightness"
|
||||
:slider-cols="largeBox"
|
||||
@input="handlePipelineData('cameraExposure')"
|
||||
@rollback="e => rollback('cameraExposure', e)"
|
||||
@@ -20,22 +22,53 @@
|
||||
@input="handlePipelineData('cameraBrightness')"
|
||||
@rollback="e => rollback('cameraBrightness', e)"
|
||||
/>
|
||||
<CVswitch
|
||||
v-model="cameraAutoExposure"
|
||||
class="pt-2"
|
||||
name="Auto Exposure"
|
||||
tooltip="Enables or Disables camera automatic adjustment for current lighting conditions"
|
||||
@input="handlePipelineData('cameraAutoExposure')"
|
||||
/>
|
||||
<CVslider
|
||||
v-if="cameraGain !== -1"
|
||||
v-if="cameraGain >= 0"
|
||||
v-model="cameraGain"
|
||||
name="Gain"
|
||||
name="Camera Gain"
|
||||
min="0"
|
||||
max="100"
|
||||
tooltip="Controls automatic white balance gain, which affects how the camera captures colors in different conditions"
|
||||
tooltip="Controls camera gain, similar to brightness"
|
||||
:slider-cols="largeBox"
|
||||
@input="handlePipelineData('cameraGain')"
|
||||
@rollback="e => rollback('cameraGain', e)"
|
||||
/>
|
||||
<CVslider
|
||||
v-if="cameraRedGain !== -1"
|
||||
v-model="cameraRedGain"
|
||||
name="Red Balance"
|
||||
min="0"
|
||||
max="100"
|
||||
tooltip="Controls red automatic white balance gain, which affects how the camera captures colors in different conditions"
|
||||
:slider-cols="largeBox"
|
||||
@input="handlePipelineData('cameraRedGain')"
|
||||
@rollback="e => rollback('cameraRedGain', e)"
|
||||
/>
|
||||
<CVslider
|
||||
v-if="cameraBlueGain !== -1"
|
||||
v-model="cameraBlueGain"
|
||||
name="Blue Balance"
|
||||
min="0"
|
||||
max="100"
|
||||
tooltip="Controls blue automatic white balance gain, which affects how the camera captures colors in different conditions"
|
||||
:slider-cols="largeBox"
|
||||
@input="handlePipelineData('cameraBlueGain')"
|
||||
@rollback="e => rollback('cameraBlueGain', e)"
|
||||
/>
|
||||
<!-- TODO: stop filtering out the 90 degree rotation modes when we fix those in libcamera -->
|
||||
<CVselect
|
||||
v-model="inputImageRotationMode"
|
||||
name="Orientation"
|
||||
tooltip="Rotates the camera stream"
|
||||
:list="['Normal','90° CW','180°','90° CCW']"
|
||||
:filtered-indices="this.$store.state.settings.general.gpuAcceleration ? new Set([1, 3]) : undefined"
|
||||
:select-cols="largeBox"
|
||||
@input="handlePipelineData('inputImageRotationMode')"
|
||||
@rollback="e => rollback('inputImageRotationMode',e)"
|
||||
@@ -55,7 +88,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>
|
||||
@@ -64,18 +96,19 @@
|
||||
<script>
|
||||
import CVslider from '../../components/common/cv-slider'
|
||||
import CVselect from '../../components/common/cv-select'
|
||||
import CVswitch from '../../components/common/cv-switch'
|
||||
|
||||
const unfilteredStreamDivisors = [1, 2, 4, 6];
|
||||
|
||||
export default {
|
||||
name: 'Input',
|
||||
components: {
|
||||
CVslider,
|
||||
CVselect,
|
||||
CVswitch,
|
||||
},
|
||||
// eslint-disable-next-line vue/require-prop-types
|
||||
props: ['value'],
|
||||
data() {
|
||||
return {}
|
||||
},
|
||||
computed: {
|
||||
largeBox: {
|
||||
get() {
|
||||
@@ -87,10 +120,18 @@
|
||||
},
|
||||
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)});
|
||||
}
|
||||
},
|
||||
cameraAutoExposure: {
|
||||
get() {
|
||||
return this.$store.getters.currentPipelineSettings.cameraAutoExposure;
|
||||
},
|
||||
set(val) {
|
||||
this.$store.commit("mutatePipeline", {"cameraAutoExposure": val});
|
||||
}
|
||||
},
|
||||
cameraBrightness: {
|
||||
@@ -109,6 +150,22 @@
|
||||
this.$store.commit("mutatePipeline", {"cameraGain": parseInt(val)});
|
||||
}
|
||||
},
|
||||
cameraRedGain: {
|
||||
get() {
|
||||
return parseInt(this.$store.getters.currentPipelineSettings.cameraRedGain)
|
||||
},
|
||||
set(val) {
|
||||
this.$store.commit("mutatePipeline", {"cameraRedGain": parseInt(val)});
|
||||
}
|
||||
},
|
||||
cameraBlueGain: {
|
||||
get() {
|
||||
return parseInt(this.$store.getters.currentPipelineSettings.cameraBlueGain)
|
||||
},
|
||||
set(val) {
|
||||
this.$store.commit("mutatePipeline", {"cameraBlueGain": parseInt(val)});
|
||||
}
|
||||
},
|
||||
inputImageRotationMode: {
|
||||
get() {
|
||||
return this.$store.getters.currentPipelineSettings.inputImageRotationMode
|
||||
@@ -119,18 +176,29 @@
|
||||
},
|
||||
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.$store.commit("mutatePipeline", {"streamingFrameDivisor": 0});
|
||||
|
||||
// If we don't have 3d mode calibrated at the new resolution either, we should disable it here
|
||||
// (TODO) This probably belongs in the backend (Matt)
|
||||
if (!this.$store.getters.isCalibrated) {
|
||||
this.handlePipelineUpdate("solvePNPEnabled", false);
|
||||
this.$store.commit("mutatePipeline", {"solvePNPEnabled": false});
|
||||
}
|
||||
}
|
||||
},
|
||||
streamingFrameDivisor: {
|
||||
get() {
|
||||
return this.$store.getters.currentPipelineSettings.streamingFrameDivisor
|
||||
return this.$store.getters.currentPipelineSettings.streamingFrameDivisor;
|
||||
},
|
||||
set(val) {
|
||||
this.$store.commit("mutatePipeline", {"streamingFrameDivisor": val});
|
||||
this.handlePipelineUpdate("streamingFrameDivisor", this.getNumSkippedStreamDivisors() + val);
|
||||
}
|
||||
},
|
||||
|
||||
@@ -146,21 +214,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>
|
||||
|
||||
53
photon-client/src/views/PipelineViews/Map3DTab.vue
Normal file
@@ -0,0 +1,53 @@
|
||||
<template>
|
||||
<div>
|
||||
<mini-map
|
||||
class="miniMapClass"
|
||||
:targets="targets"
|
||||
:horizontal-f-o-v="horizontalFOV"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import miniMap from '../../components/pipeline/3D/MiniMap';
|
||||
|
||||
export default {
|
||||
name: "Map3D",
|
||||
components: {
|
||||
miniMap
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
targets: {
|
||||
get() {
|
||||
return this.$store.getters.currentPipelineResults.targets;
|
||||
}
|
||||
},
|
||||
horizontalFOV: {
|
||||
get() {
|
||||
let index = this.$store.getters.currentPipelineSettings.cameraVideoModeIndex;
|
||||
let FOV = this.$store.getters.currentCameraSettings.fov;
|
||||
let resolution = this.$store.getters.videoFormatList[index];
|
||||
let diagonalView = FOV * (Math.PI / 180);
|
||||
let diagonalAspect = Math.hypot(resolution.width, resolution.height);
|
||||
return Math.atan(Math.tan(diagonalView / 2) * (resolution.width / diagonalAspect)) * 2 * (180 / Math.PI)
|
||||
}
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.miniMapClass {
|
||||
width: 400px !important;
|
||||
height: 100% !important;
|
||||
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
</style>
|
||||
@@ -1,8 +1,5 @@
|
||||
<template>
|
||||
<div>
|
||||
<span class="white--text">Target Manipulation</span>
|
||||
<v-divider class="mt-2" />
|
||||
|
||||
<CVselect
|
||||
v-model="contourTargetOffsetPointEdge"
|
||||
name="Target Offset Point"
|
||||
@@ -13,6 +10,7 @@
|
||||
/>
|
||||
|
||||
<CVselect
|
||||
v-if="!isTagPipeline"
|
||||
v-model="contourTargetOrientation"
|
||||
name="Target Orientation"
|
||||
tooltip="Used to determine how to calculate target landmarks (e.g. the top, left, or bottom of the target)"
|
||||
@@ -24,15 +22,15 @@
|
||||
<CVswitch
|
||||
v-model="outputShowMultipleTargets"
|
||||
name="Show Multiple Targets"
|
||||
tooltip="If enabled, up to five targets will be displayed and sent to user code"
|
||||
tooltip="If enabled, up to five targets will be displayed and sent to user code, instead of just one"
|
||||
:disabled="isTagPipeline"
|
||||
class="mb-4"
|
||||
text-cols="3"
|
||||
@input="handlePipelineData('outputShowMultipleTargets')"
|
||||
|
||||
@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"
|
||||
@@ -141,6 +139,11 @@
|
||||
get() {
|
||||
return undefined; // TODO fix
|
||||
}
|
||||
},
|
||||
isTagPipeline: {
|
||||
get() {
|
||||
return this.$store.getters.currentPipelineSettings.pipelineType > 3;
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
@@ -156,4 +159,4 @@
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
</style>
|
||||
</style>
|
||||
|
||||
@@ -6,7 +6,6 @@
|
||||
type="file"
|
||||
accept=".csv"
|
||||
style="display: none;"
|
||||
|
||||
@change="readFile"
|
||||
>
|
||||
|
||||
@@ -16,10 +15,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"
|
||||
@@ -32,11 +31,7 @@
|
||||
@input="handlePipelineData('cornerDetectionAccuracyPercentage')"
|
||||
@rollback="e => rollback('cornerDetectionAccuracyPercentage', e)"
|
||||
/>
|
||||
<mini-map
|
||||
class="miniMapClass"
|
||||
:targets="targets"
|
||||
:horizontal-f-o-v="horizontalFOV"
|
||||
/>
|
||||
|
||||
<v-snackbar
|
||||
v-model="snack"
|
||||
top
|
||||
@@ -49,30 +44,33 @@
|
||||
|
||||
<script>
|
||||
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
|
||||
CVslider
|
||||
},
|
||||
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', '6.5in (36h11) AprilTag', '6in (16h5) AprilTag'], //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
|
||||
return this.targetList[ret];
|
||||
},
|
||||
set(val) {
|
||||
this.$store.commit("mutatePipeline", {"targetModel": this.targetList.indexOf(val)})
|
||||
}
|
||||
},
|
||||
cornerDetectionAccuracyPercentage: {
|
||||
get() {
|
||||
return this.$store.getters.currentPipelineSettings.cornerDetectionAccuracyPercentage
|
||||
@@ -81,35 +79,6 @@
|
||||
this.$store.commit("mutatePipeline", {"cornerDetectionAccuracyPercentage": val});
|
||||
}
|
||||
},
|
||||
targets: {
|
||||
get() {
|
||||
return this.$store.getters.currentPipelineResults.targets;
|
||||
}
|
||||
},
|
||||
horizontalFOV: {
|
||||
get() {
|
||||
let index = this.$store.getters.currentPipelineSettings.cameraVideoModeIndex;
|
||||
let FOV = this.$store.getters.currentCameraSettings.fov;
|
||||
let resolution = this.$store.getters.videoFormatList[index];
|
||||
let diagonalView = FOV * (Math.PI / 180);
|
||||
let diagonalAspect = Math.hypot(resolution.width, resolution.height);
|
||||
return Math.atan(Math.tan(diagonalView / 2) * (resolution.width / diagonalAspect)) * 2 * (180 / Math.PI)
|
||||
}
|
||||
},
|
||||
},
|
||||
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) {
|
||||
@@ -119,13 +88,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 +120,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 +132,4 @@
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
||||
@@ -18,29 +18,40 @@
|
||||
<th class="text-center">
|
||||
Target
|
||||
</th>
|
||||
<th
|
||||
v-if="$store.getters.pipelineType === 4 || (($store.getters.pipelineType - 2) === 3)"
|
||||
class="text-center"
|
||||
>
|
||||
Fiducial ID
|
||||
</th>
|
||||
<template v-if="!$store.getters.currentPipelineSettings.solvePNPEnabled">
|
||||
<th class="text-center">
|
||||
Pitch
|
||||
Pitch, °
|
||||
</th>
|
||||
<th class="text-center">
|
||||
Yaw
|
||||
Yaw, °
|
||||
</th>
|
||||
<th class="text-center">
|
||||
Skew
|
||||
Skew, °
|
||||
</th>
|
||||
<th class="text-center">
|
||||
Area, %
|
||||
</th>
|
||||
</template>
|
||||
<th class="text-center">
|
||||
Area
|
||||
</th>
|
||||
<template v-if="$store.getters.currentPipelineSettings.solvePNPEnabled">
|
||||
<template v-else>
|
||||
<th class="text-center">
|
||||
X
|
||||
X, m
|
||||
</th>
|
||||
<th class="text-center">
|
||||
Y
|
||||
Y, m
|
||||
</th>
|
||||
<th class="text-center">
|
||||
Angle
|
||||
Z Angle, °
|
||||
</th>
|
||||
</template>
|
||||
<template v-if="$store.getters.pipelineType === 4 && $store.getters.currentPipelineSettings.solvePNPEnabled">
|
||||
<th class="text-center">
|
||||
Ambiguity
|
||||
</th>
|
||||
</template>
|
||||
</tr>
|
||||
@@ -51,17 +62,29 @@
|
||||
:key="index"
|
||||
>
|
||||
<td>{{ index }}</td>
|
||||
<td v-if="$store.getters.pipelineType === 4 || (($store.getters.pipelineType - 2) === 3)">
|
||||
{{ parseInt(value.fiducialId) }}
|
||||
</td>
|
||||
<template v-if="!$store.getters.currentPipelineSettings.solvePNPEnabled">
|
||||
<td>{{ parseFloat(value.pitch).toFixed(2) }}</td>
|
||||
<td>{{ parseFloat(value.yaw).toFixed(2) }}</td>
|
||||
<td>{{ parseFloat(value.skew).toFixed(2) }}</td>
|
||||
<td>{{ parseFloat(value.area).toFixed(2) }}</td>
|
||||
</template>
|
||||
<td>{{ parseFloat(value.area).toFixed(2) }}</td>
|
||||
<template v-if="$store.getters.currentPipelineSettings.solvePNPEnabled">
|
||||
<!-- TODO: Make sure that units are correct -->
|
||||
<template v-else-if="$store.getters.currentPipelineSettings.solvePNPEnabled && $store.getters.pipelineType === 4">
|
||||
<td>{{ parseFloat(value.pose.x).toFixed(2) }} m</td>
|
||||
<td>{{ parseFloat(value.pose.y).toFixed(2) }} m</td>
|
||||
<td>{{ parseFloat(value.pose.rot).toFixed(2) }}°</td>
|
||||
<td>{{ (parseFloat(value.pose.angle_z) * 180 / Math.PI).toFixed(2) }}°</td>
|
||||
</template>
|
||||
<template v-else-if="$store.getters.currentPipelineSettings.solvePNPEnabled">
|
||||
<td>{{ parseFloat(value.pose.x).toFixed(2) }} m</td>
|
||||
<td>{{ parseFloat(value.pose.y).toFixed(2) }} m</td>
|
||||
<td>{{ (parseFloat(value.pose.angle_z) * 180 / Math.PI).toFixed(2) }}°</td>
|
||||
</template>
|
||||
<template v-if="$store.getters.pipelineType === 4 && $store.getters.currentPipelineSettings.solvePNPEnabled">
|
||||
<td>
|
||||
{{ parseFloat(value.ambiguity).toFixed(2) }}
|
||||
</td>
|
||||
</template>
|
||||
</tr>
|
||||
</tbody>
|
||||
@@ -97,4 +120,4 @@
|
||||
.v-data-table td {
|
||||
font-family: monospace !important;
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
||||
@@ -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.$store.state.websocket.ws.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>
|
||||
|
||||
@@ -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
|
||||
@@ -50,7 +37,8 @@
|
||||
import Networking from './SettingsViews/Networking'
|
||||
import Lighting from "./SettingsViews/Lighting";
|
||||
import cvImage from '../components/common/cv-image'
|
||||
import General from "./SettingsViews/General";
|
||||
import Stats from "./SettingsViews/Stats";
|
||||
import DeviceControl from "./SettingsViews/DeviceControl";
|
||||
|
||||
export default {
|
||||
name: 'SettingsTab',
|
||||
@@ -61,8 +49,8 @@
|
||||
data() {
|
||||
return {
|
||||
selectedTab: 0,
|
||||
valid: true, // Are all settings valid
|
||||
snack: false,
|
||||
calibrationInProgress: false,
|
||||
snackbar: {
|
||||
color: "accent",
|
||||
text: ""
|
||||
@@ -82,32 +70,10 @@
|
||||
},
|
||||
tabList: {
|
||||
get() {
|
||||
return [General, Networking].concat(this.$store.state.settings.lighting.supported ? Lighting : []);
|
||||
return [Stats, DeviceControl, Networking].concat(this.$store.state.settings.lighting.supported ? Lighting : []);
|
||||
}
|
||||
}
|
||||
},
|
||||
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 +87,4 @@
|
||||
height: auto !important;
|
||||
vertical-align: middle;
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
||||
289
photon-client/src/views/SettingsViews/DeviceControl.vue
Normal file
@@ -0,0 +1,289 @@
|
||||
<template>
|
||||
<div>
|
||||
<v-row>
|
||||
<v-col cols="12" lg="4" md="6">
|
||||
<v-btn color="red" @click="restartProgram()">
|
||||
<v-icon left>
|
||||
mdi-restart
|
||||
</v-icon>
|
||||
Restart PhotonVision
|
||||
</v-btn>
|
||||
</v-col>
|
||||
<v-col cols="12" lg="4" md="6">
|
||||
<v-btn color="red" @click="restartDevice()">
|
||||
<v-icon left>
|
||||
mdi-restart-alert
|
||||
</v-icon>
|
||||
Restart Device
|
||||
</v-btn>
|
||||
</v-col>
|
||||
<v-col cols="12" lg="4">
|
||||
<v-btn color="secondary" @click="$refs.offlineUpdate.click()">
|
||||
<v-icon left>
|
||||
mdi-update
|
||||
</v-icon>
|
||||
Offline Update
|
||||
</v-btn>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-divider />
|
||||
<v-row>
|
||||
<v-col cols="12" sm="6">
|
||||
<v-btn color="secondary" @click="$refs.exportSettings.click()">
|
||||
<v-icon left>
|
||||
mdi-download
|
||||
</v-icon>
|
||||
Export Settings
|
||||
</v-btn>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6">
|
||||
<v-btn color="secondary" @click="$refs.importSettings.click()">
|
||||
<v-icon left>
|
||||
mdi-upload
|
||||
</v-icon>
|
||||
Import Settings
|
||||
</v-btn>
|
||||
</v-col>
|
||||
|
||||
<v-col cols="12" sm="6">
|
||||
<v-btn color="secondary" @click="$refs.exportLogFile.click()">
|
||||
<v-icon left>
|
||||
mdi-file
|
||||
</v-icon>
|
||||
Export current log
|
||||
|
||||
<!-- Special hidden link that gets 'clicked' when the user exports journalctl logs -->
|
||||
<a
|
||||
ref="exportLogFile"
|
||||
style="color: black; text-decoration: none; display: none"
|
||||
:href="
|
||||
'http://' +
|
||||
this.$address +
|
||||
'/api/settings/photonvision-journalctl.txt'
|
||||
"
|
||||
download="photonvision-journalctl.txt"
|
||||
/>
|
||||
</v-btn>
|
||||
</v-col>
|
||||
|
||||
<v-col cols="12" sm="6">
|
||||
<v-btn color="secondary" @click="showLogs()">
|
||||
<v-icon left>
|
||||
mdi-bug
|
||||
</v-icon>
|
||||
Show log viewer
|
||||
</v-btn>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-snackbar v-model="snack" top :color="snackbar.color" timeout="-1">
|
||||
<span>{{ snackbar.text }}</span>
|
||||
</v-snackbar>
|
||||
|
||||
<!-- Special hidden upload input that gets 'clicked' when the user imports settings -->
|
||||
<input
|
||||
ref="importSettings"
|
||||
type="file"
|
||||
accept=".zip, .json"
|
||||
style="display: none;"
|
||||
@change="readImportedSettings"
|
||||
/>
|
||||
<!-- Special hidden link that gets 'clicked' when the user exports settings -->
|
||||
<a
|
||||
ref="exportSettings"
|
||||
style="color: black; text-decoration: none; display: none"
|
||||
:href="
|
||||
'http://' + this.$address + '/api/settings/photonvision_config.zip'
|
||||
"
|
||||
download="photonvision-settings.zip"
|
||||
/>
|
||||
|
||||
<!-- Special hidden new jar upload input that gets 'clicked' when the user posts a new .jar -->
|
||||
<input
|
||||
ref="offlineUpdate"
|
||||
type="file"
|
||||
accept=".jar"
|
||||
style="display: none;"
|
||||
@change="doOfflineUpdate"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "Device Control",
|
||||
data() {
|
||||
return {
|
||||
snack: false,
|
||||
uploadPercentage: 0.0,
|
||||
snackbar: {
|
||||
color: "success",
|
||||
text: "",
|
||||
},
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
settings() {
|
||||
return this.$store.state.settings.general;
|
||||
},
|
||||
version() {
|
||||
return `${this.settings.version}`;
|
||||
},
|
||||
hwModel() {
|
||||
if (this.settings.hardwareModel !== "") {
|
||||
return `${this.settings.hardwareModel}`;
|
||||
} else {
|
||||
return `Unknown`;
|
||||
}
|
||||
},
|
||||
platform() {
|
||||
return `${this.settings.hardwarePlatform}`;
|
||||
},
|
||||
gpuAccel() {
|
||||
return `${this.settings.gpuAcceleration ? "Enabled" : "Unsupported"} ${
|
||||
this.settings.gpuAcceleration
|
||||
? "(" + this.settings.gpuAcceleration + ")"
|
||||
: ""
|
||||
}`;
|
||||
},
|
||||
metrics() {
|
||||
// console.log(this.$store.state.metrics);
|
||||
return this.$store.state.metrics;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
restartProgram() {
|
||||
this.axios.post("http://" + this.$address + "/api/restartProgram", {});
|
||||
},
|
||||
restartDevice() {
|
||||
this.axios.post("http://" + this.$address + "/api/restartDevice", {});
|
||||
},
|
||||
readImportedSettings(event) {
|
||||
let formData = new FormData();
|
||||
formData.append("zipData", event.target.files[0]);
|
||||
this.axios
|
||||
.post("http://" + this.$address + "/api/settings/import", formData, {
|
||||
headers: { "Content-Type": "multipart/form-data" },
|
||||
})
|
||||
.then(() => {
|
||||
this.snackbar = {
|
||||
color: "success",
|
||||
text:
|
||||
"Settings imported successfully! PhotonVision will restart in the background...",
|
||||
};
|
||||
this.snack = true;
|
||||
})
|
||||
.catch((err) => {
|
||||
if (err.response) {
|
||||
this.snackbar = {
|
||||
color: "error",
|
||||
text:
|
||||
"Error while uploading settings file! Could not process provided file.",
|
||||
};
|
||||
} else if (err.request) {
|
||||
this.snackbar = {
|
||||
color: "error",
|
||||
text:
|
||||
"Error while uploading settings file! No respond to upload attempt.",
|
||||
};
|
||||
} else {
|
||||
this.snackbar = {
|
||||
color: "error",
|
||||
text: "Error while uploading settings file!",
|
||||
};
|
||||
}
|
||||
this.snack = true;
|
||||
});
|
||||
},
|
||||
doOfflineUpdate(event) {
|
||||
this.snackbar = {
|
||||
color: "secondary",
|
||||
text: "New Software Upload in Process...",
|
||||
};
|
||||
this.snack = true;
|
||||
|
||||
let formData = new FormData();
|
||||
formData.append("jarData", event.target.files[0]);
|
||||
this.axios
|
||||
.post(
|
||||
"http://" + this.$address + "/api/settings/offlineUpdate",
|
||||
formData,
|
||||
{
|
||||
headers: { "Content-Type": "multipart/form-data" },
|
||||
onUploadProgress: function(progressEvent) {
|
||||
this.uploadPercentage = parseInt(
|
||||
Math.round((progressEvent.loaded / progressEvent.total) * 100)
|
||||
);
|
||||
if (this.uploadPercentage < 99.5) {
|
||||
this.snackbar.text =
|
||||
"New Software Upload in Process, " +
|
||||
this.uploadPercentage +
|
||||
"% complete";
|
||||
} else {
|
||||
this.snackbar.text = "Installing uploaded software...";
|
||||
}
|
||||
}.bind(this),
|
||||
}
|
||||
)
|
||||
.then(() => {
|
||||
this.snackbar = {
|
||||
color: "success",
|
||||
text:
|
||||
"New .jar copied successfully! PhotonVision will restart in the background...",
|
||||
};
|
||||
this.snack = true;
|
||||
})
|
||||
.catch((err) => {
|
||||
if (err.response) {
|
||||
this.snackbar = {
|
||||
color: "error",
|
||||
text:
|
||||
"Error while uploading new .jar file! Could not process provided file.",
|
||||
};
|
||||
} else if (err.request) {
|
||||
this.snackbar = {
|
||||
color: "error",
|
||||
text:
|
||||
"Error while uploading new .jar file! No respond to upload attempt.",
|
||||
};
|
||||
} else {
|
||||
this.snackbar = {
|
||||
color: "error",
|
||||
text: "Error while uploading new .jar file!",
|
||||
};
|
||||
}
|
||||
this.snack = true;
|
||||
});
|
||||
},
|
||||
showLogs(event) {
|
||||
event;
|
||||
this.$store.state.logsOverlay = true;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="css" scoped>
|
||||
.v-btn {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.infoTable {
|
||||
border: 1px solid;
|
||||
border-collapse: separate;
|
||||
border-spacing: 0px;
|
||||
border-radius: 5px;
|
||||
text-align: left;
|
||||
margin-bottom: 10px;
|
||||
width: 100%;
|
||||
display: block;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.infoElem {
|
||||
padding-right: 15px;
|
||||
padding-bottom: 1px;
|
||||
padding-top: 1px;
|
||||
padding-left: 10px;
|
||||
border-right: 1px solid;
|
||||
}
|
||||
</style>
|
||||
@@ -1,138 +0,0 @@
|
||||
<template>
|
||||
<div>
|
||||
<span>Version: {{ settings.version }}</span>
|
||||
—
|
||||
<span>Hardware model: {{ settings.hardwareModel }}</span>
|
||||
—
|
||||
<span>Platform: {{ settings.hardwarePlatform }}</span>
|
||||
—
|
||||
<span>GPU Acceleration: {{ settings.gpuAcceleration ? "Enabled" : "Unsupported" }}{{ settings.gpuAcceleration ? " (" + settings.gpuAcceleration + " mode)" : "" }}</span>
|
||||
<v-row>
|
||||
<v-col
|
||||
cols="12"
|
||||
sm="6"
|
||||
lg="3"
|
||||
>
|
||||
<v-btn
|
||||
color="secondary"
|
||||
@click="$refs.exportSettings.click()"
|
||||
>
|
||||
<v-icon left>
|
||||
mdi-download
|
||||
</v-icon> Export Settings
|
||||
</v-btn>
|
||||
</v-col>
|
||||
<v-col
|
||||
cols="12"
|
||||
sm="6"
|
||||
lg="3"
|
||||
>
|
||||
<v-btn
|
||||
color="secondary"
|
||||
@click="$refs.importSettings.click()"
|
||||
>
|
||||
<v-icon left>
|
||||
mdi-upload
|
||||
</v-icon> Import Settings
|
||||
</v-btn>
|
||||
</v-col>
|
||||
<v-col
|
||||
cols="12"
|
||||
lg="3"
|
||||
>
|
||||
<v-btn
|
||||
color="red"
|
||||
@click="axios.post('http://' + this.$address + '/api/restartProgram')"
|
||||
>
|
||||
<v-icon left>
|
||||
mdi-restart
|
||||
</v-icon> Restart Photon
|
||||
</v-btn>
|
||||
</v-col>
|
||||
<v-col
|
||||
cols="12"
|
||||
lg="3"
|
||||
>
|
||||
<v-btn
|
||||
color="red"
|
||||
@click="axios.post('http://' + this.$address + '/api/restartDevice')"
|
||||
>
|
||||
<v-icon left>
|
||||
mdi-restart
|
||||
</v-icon> Restart Device
|
||||
</v-btn>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-snackbar
|
||||
v-model="snack"
|
||||
top
|
||||
:color="snackbar.color"
|
||||
timeout="0"
|
||||
>
|
||||
<span>{{ snackbar.text }}</span>
|
||||
</v-snackbar>
|
||||
|
||||
<!-- Special hidden upload input that gets 'clicked' when the user imports settings -->
|
||||
<input
|
||||
ref="importSettings"
|
||||
type="file"
|
||||
accept=".zip"
|
||||
style="display: none;"
|
||||
|
||||
@change="readImportedSettings"
|
||||
>
|
||||
<!-- Special hidden link that gets 'clicked' when the user exports settings -->
|
||||
<a
|
||||
ref="exportSettings"
|
||||
style="color: black; text-decoration: none; display: none"
|
||||
:href="'http://' + this.$address + '/api/settings/photonvision_config.zip'"
|
||||
download="photonvision-settings.zip"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'General',
|
||||
data() {
|
||||
return {
|
||||
snack: false,
|
||||
snackbar: {
|
||||
color: "success",
|
||||
text: ""
|
||||
},
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
settings() {
|
||||
return this.$store.state.settings.general;
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
readImportedSettings(event) {
|
||||
let formData = new FormData();
|
||||
formData.append("zipData", event.target.files[0]);
|
||||
this.axios.post("http://" + this.$address + "/api/settings/import", formData,
|
||||
{headers: {"Content-Type": "multipart/form-data"}}).then(() => {
|
||||
this.snackbar = {
|
||||
color: "success",
|
||||
text: "Settings imported successfully! Program will now exit...",
|
||||
};
|
||||
this.snack = true;
|
||||
}).catch(() => {
|
||||
this.snackbar = {
|
||||
color: "success",
|
||||
text: "Settings imported successfully! Program will now exit...",
|
||||
};
|
||||
this.snack = true;
|
||||
});
|
||||
},
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="css" scoped>
|
||||
.v-btn {
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
@@ -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>
|
||||
|
||||
@@ -1,101 +1,293 @@
|
||||
<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-snackbar
|
||||
v-model="snack"
|
||||
top
|
||||
:color="snackbar.color"
|
||||
timeout="5000"
|
||||
>
|
||||
<span>{{ snackbar.text }}</span>
|
||||
</v-snackbar>
|
||||
<v-divider class="mt-4 mb-4" />
|
||||
<!-- TEMP - RIO finder is not currently enabled
|
||||
<v-row>
|
||||
<v-col
|
||||
cols="12"
|
||||
sm="6"
|
||||
>
|
||||
<v-simple-table
|
||||
fixed-header
|
||||
height="100%"
|
||||
dense
|
||||
>
|
||||
<template v-slot:default>
|
||||
<thead style="font-size: 1.25rem;">
|
||||
<tr>
|
||||
<th>
|
||||
Device IPs
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr
|
||||
v-for="(value, index) in $store.state.networkInfo.deviceips"
|
||||
:key="index"
|
||||
>
|
||||
<td>{{ value }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</template>
|
||||
</v-simple-table>
|
||||
</v-col>
|
||||
<v-col
|
||||
cols="12"
|
||||
sm="6"
|
||||
>
|
||||
<v-simple-table
|
||||
fixed-header
|
||||
height="100%"
|
||||
dense
|
||||
>
|
||||
<template v-slot:default>
|
||||
<thead style="font-size: 1.25rem;">
|
||||
<tr>
|
||||
<th>
|
||||
Possible RoboRIOs
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr
|
||||
v-for="(value, index) in $store.state.networkInfo.possibleRios"
|
||||
:key="index"
|
||||
>
|
||||
<td>{{ value }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</template>
|
||||
</v-simple-table>
|
||||
</v-col>
|
||||
</v-row>
|
||||
-->
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<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(
|
||||
response => {
|
||||
if (response.status === 200) {
|
||||
this.snackbar = {
|
||||
color: "success",
|
||||
text: "Settings updated successfully"
|
||||
};
|
||||
this.snack = true;
|
||||
}
|
||||
},
|
||||
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>
|
||||
|
||||
311
photon-client/src/views/SettingsViews/Stats.vue
Normal file
@@ -0,0 +1,311 @@
|
||||
<template>
|
||||
<div>
|
||||
<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>
|
||||
<th class="infoElem">
|
||||
<v-tooltip top>
|
||||
<template v-slot:activator="{ on, attrs }">
|
||||
<span
|
||||
v-bind="attrs"
|
||||
v-on="on"
|
||||
>
|
||||
ⓘ CPU Throttling
|
||||
</span>
|
||||
</template>
|
||||
<span>
|
||||
Current or Previous Reason for the cpu being held back from maximum performance.
|
||||
</span>
|
||||
</v-tooltip>
|
||||
</th>
|
||||
<th class="infoElem">
|
||||
CPU Uptime
|
||||
</th>
|
||||
</tr>
|
||||
<tr v-if="metrics.cpuUtil !== 'N/A'">
|
||||
<td class="infoElem">
|
||||
{{ metrics.cpuUtil }}%
|
||||
</td>
|
||||
<td class="infoElem">
|
||||
{{ parseInt(metrics.cpuTemp) }}° C
|
||||
</td>
|
||||
<td class="infoElem">
|
||||
{{ metrics.ramUtil }}MB of {{ metrics.cpuMem }}MB
|
||||
</td>
|
||||
<td class="infoElem">
|
||||
{{ metrics.gpuMemUtil }}MB of {{ metrics.gpuMem }}MB
|
||||
</td>
|
||||
<td class="infoElem">
|
||||
{{ metrics.diskUtilPct }}
|
||||
</td>
|
||||
<td class="infoElem">
|
||||
{{ metrics.cpuThr }}
|
||||
</td>
|
||||
<td class="infoElem">
|
||||
{{ metrics.cpuUptime }}
|
||||
</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>
|
||||
<td class="infoElem">
|
||||
---
|
||||
</td>
|
||||
<td class="infoElem">
|
||||
---
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</v-row>
|
||||
|
||||
<v-snackbar
|
||||
v-model="snack"
|
||||
top
|
||||
:color="snackbar.color"
|
||||
timeout="-1"
|
||||
>
|
||||
<span>{{ snackbar.text }}</span>
|
||||
</v-snackbar>
|
||||
|
||||
<!-- Special hidden upload input that gets 'clicked' when the user imports settings -->
|
||||
<input
|
||||
ref="importSettings"
|
||||
type="file"
|
||||
accept=".zip, .json"
|
||||
style="display: none;"
|
||||
|
||||
@change="readImportedSettings"
|
||||
>
|
||||
<!-- Special hidden link that gets 'clicked' when the user exports settings -->
|
||||
<a
|
||||
ref="exportSettings"
|
||||
style="color: black; text-decoration: none; display: none"
|
||||
:href="'http://' + this.$address + '/api/settings/photonvision_config.zip'"
|
||||
download="photonvision-settings.zip"
|
||||
/>
|
||||
|
||||
<!-- Special hidden new jar upload input that gets 'clicked' when the user posts a new .jar -->
|
||||
<input
|
||||
ref="offlineUpdate"
|
||||
type="file"
|
||||
accept=".jar"
|
||||
style="display: none;"
|
||||
@change="doOfflineUpdate"
|
||||
>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'Stats',
|
||||
data() {
|
||||
return {
|
||||
snack: false,
|
||||
uploadPercentage: 0.0,
|
||||
snackbar: {
|
||||
color: "success",
|
||||
text: ""
|
||||
},
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
settings() {
|
||||
return this.$store.state.settings.general;
|
||||
},
|
||||
version() {
|
||||
return `${this.settings.version}`;
|
||||
},
|
||||
hwModel() {
|
||||
if (this.settings.hardwareModel !== '') {
|
||||
return `${this.settings.hardwareModel}`;
|
||||
} else {
|
||||
return `Unknown`;
|
||||
}
|
||||
},
|
||||
platform() {
|
||||
return `${this.settings.hardwarePlatform}`;
|
||||
},
|
||||
gpuAccel() {
|
||||
return `${this.settings.gpuAcceleration ? "Enabled" : "Unsupported"} ${this.settings.gpuAcceleration ? "(" + this.settings.gpuAcceleration + ")" : ""}`
|
||||
},
|
||||
metrics() {
|
||||
// console.log(this.$store.state.metrics);
|
||||
return this.$store.state.metrics;
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
restartProgram() {
|
||||
this.axios.post('http://' + this.$address + '/api/restartProgram', {});
|
||||
},
|
||||
restartDevice() {
|
||||
this.axios.post('http://' + this.$address + '/api/restartDevice', {});
|
||||
},
|
||||
readImportedSettings(event) {
|
||||
let formData = new FormData();
|
||||
formData.append("zipData", event.target.files[0]);
|
||||
this.axios.post("http://" + this.$address + "/api/settings/import", formData,
|
||||
{headers: {"Content-Type": "multipart/form-data"}}).then(() => {
|
||||
this.snackbar = {
|
||||
color: "success",
|
||||
text: "Settings imported successfully! PhotonVision will restart in the background...",
|
||||
};
|
||||
this.snack = true;
|
||||
}).catch(err => {
|
||||
if (err.response) {
|
||||
this.snackbar = {
|
||||
color: "error",
|
||||
text: "Error while uploading settings file! Could not process provided file.",
|
||||
};
|
||||
} else if (err.request) {
|
||||
this.snackbar = {
|
||||
color: "error",
|
||||
text: "Error while uploading settings file! No respond to upload attempt.",
|
||||
};
|
||||
} else {
|
||||
this.snackbar = {
|
||||
color: "error",
|
||||
text: "Error while uploading settings file!",
|
||||
};
|
||||
}
|
||||
this.snack = true;
|
||||
});
|
||||
},
|
||||
doOfflineUpdate(event) {
|
||||
this.snackbar = {
|
||||
color: "secondary",
|
||||
text: "New Software Upload in Process..."
|
||||
};
|
||||
this.snack = true;
|
||||
|
||||
let formData = new FormData();
|
||||
formData.append("jarData", event.target.files[0]);
|
||||
this.axios.post("http://" + this.$address + "/api/settings/offlineUpdate", formData,
|
||||
{headers: {"Content-Type": "multipart/form-data"},
|
||||
onUploadProgress: function( progressEvent ) {
|
||||
this.uploadPercentage = parseInt( Math.round( ( progressEvent.loaded / progressEvent.total ) * 100 ) );
|
||||
if(this.uploadPercentage < 99.5){
|
||||
this.snackbar.text = "New Software Upload in Process, " + this.uploadPercentage + "% complete";
|
||||
} else {
|
||||
this.snackbar.text = "Installing uploaded software...";
|
||||
}
|
||||
|
||||
}.bind(this)
|
||||
}).then(() => {
|
||||
this.snackbar = {
|
||||
color: "success",
|
||||
text: "New .jar copied successfully! PhotonVision will restart in the background...",
|
||||
};
|
||||
this.snack = true;
|
||||
}).catch(err => {
|
||||
if (err.response) {
|
||||
this.snackbar = {
|
||||
color: "error",
|
||||
text: "Error while uploading new .jar file! Could not process provided file.",
|
||||
};
|
||||
} else if (err.request) {
|
||||
this.snackbar = {
|
||||
color: "error",
|
||||
text: "Error while uploading new .jar file! No respond to upload attempt.",
|
||||
};
|
||||
} else {
|
||||
this.snackbar = {
|
||||
color: "error",
|
||||
text: "Error while uploading new .jar file!",
|
||||
};
|
||||
}
|
||||
this.snack = true;
|
||||
});
|
||||
},
|
||||
showLogs(event) {
|
||||
event;
|
||||
this.$store.state.logsOverlay = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="css" scoped>
|
||||
.v-btn {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.infoTable{
|
||||
border: 1px solid;
|
||||
border-collapse: separate;
|
||||
border-spacing: 0px;
|
||||
border-radius: 5px;
|
||||
text-align: left;
|
||||
margin-bottom: 10px;
|
||||
width: 100%;
|
||||
display: block;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.infoElem {
|
||||
padding-right: 15px;
|
||||
padding-bottom: 1px;
|
||||
padding-top: 1px;
|
||||
padding-left: 10px;
|
||||
border-right: 1px solid;
|
||||
}
|
||||
|
||||
</style>
|
||||
15
photon-core/.gitignore
vendored
Normal file
@@ -0,0 +1,15 @@
|
||||
bin/*
|
||||
.settings/*
|
||||
.project
|
||||
.classpath
|
||||
*.prefs
|
||||
.gradle
|
||||
.gradle/*
|
||||
build
|
||||
build/*
|
||||
photonvision/*
|
||||
photonvision_config/*
|
||||
photon-server/lib/*
|
||||
photon-server/package-lock.json
|
||||
|
||||
src/main/java/org/photonvision/PhotonVersion.java
|
||||
57
photon-core/build.gradle
Normal file
@@ -0,0 +1,57 @@
|
||||
plugins {
|
||||
id 'edu.wpi.first.WpilibTools' version '1.0.0'
|
||||
}
|
||||
|
||||
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'
|
||||
|
||||
// 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'
|
||||
|
||||
implementation wpilibTools.deps.wpilibJava("apriltag")
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
def testNativeConfigName = 'wpilibTestNative'
|
||||
def testNativeConfig = configurations.create(testNativeConfigName)
|
||||
|
||||
def folder = project.layout.buildDirectory.dir('NativeTest')
|
||||
|
||||
def testNativeTasks = wpilibTools.createExtractionTasks {
|
||||
taskPostfix = "Test"
|
||||
configurationName = testNativeConfigName
|
||||
rootTaskFolder.set(folder)
|
||||
}
|
||||
|
||||
testNativeTasks.addToSourceSetResources(sourceSets.test)
|
||||
|
||||
testNativeConfig.dependencies.add wpilibTools.deps.cscore()
|
||||
testNativeConfig.dependencies.add wpilibTools.deps.wpilib("ntcore")
|
||||
testNativeConfig.dependencies.add wpilibTools.deps.wpilib("wpinet")
|
||||
testNativeConfig.dependencies.add wpilibTools.deps.wpilib("hal")
|
||||
testNativeConfig.dependencies.add wpilibTools.deps.wpilib("wpiutil")
|
||||
testNativeConfig.dependencies.add wpilibTools.deps.wpilib("apriltag")
|
||||
testNativeConfig.dependencies.add wpilibTools.deps.wpilib("wpimath")
|
||||
1
photon-core/settings.gradle
Normal file
@@ -0,0 +1 @@
|
||||
rootProject.name = 'photon-core'
|
||||
@@ -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
|
||||
@@ -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,8 +20,8 @@ package org.photonvision.common.configuration;
|
||||
import com.fasterxml.jackson.annotation.JsonCreator;
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import edu.wpi.first.wpilibj.geometry.Rotation2d;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import org.photonvision.common.logging.LogGroup;
|
||||
import org.photonvision.common.logging.Logger;
|
||||
@@ -46,12 +46,14 @@ public class CameraConfiguration {
|
||||
/** Can be either path (ex /dev/videoX) or index (ex 1). */
|
||||
public String path = "";
|
||||
|
||||
@JsonIgnore public String[] otherPaths = {};
|
||||
|
||||
public CameraType cameraType = CameraType.UsbCamera;
|
||||
public double FOV = 70;
|
||||
public final List<CameraCalibrationCoefficients> calibrations;
|
||||
public 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<>();
|
||||
@@ -60,19 +62,22 @@ public class CameraConfiguration {
|
||||
public DriverModePipelineSettings driveModeSettings = new DriverModePipelineSettings();
|
||||
|
||||
public CameraConfiguration(String baseName, String path) {
|
||||
this(baseName, baseName, baseName, path);
|
||||
this(baseName, baseName, baseName, path, new String[0]);
|
||||
}
|
||||
|
||||
public CameraConfiguration(String baseName, String uniqueName, String nickname, String path) {
|
||||
public CameraConfiguration(
|
||||
String baseName, String uniqueName, String nickname, String path, String[] alternates) {
|
||||
this.baseName = baseName;
|
||||
this.uniqueName = uniqueName;
|
||||
this.nickname = nickname;
|
||||
this.path = path;
|
||||
this.calibrations = new ArrayList<>();
|
||||
this.otherPaths = alternates;
|
||||
|
||||
logger.debug(
|
||||
"Creating USB camera configuration for "
|
||||
+ cameraType
|
||||
+ " "
|
||||
+ baseName
|
||||
+ " (AKA "
|
||||
+ nickname
|
||||
@@ -89,9 +94,7 @@ 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) {
|
||||
@JsonProperty("currentPipelineIndex") int currentPipelineIndex) {
|
||||
this.baseName = baseName;
|
||||
this.uniqueName = uniqueName;
|
||||
this.nickname = nickname;
|
||||
@@ -99,13 +102,12 @@ 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;
|
||||
|
||||
logger.debug(
|
||||
"Creating camera configuration for "
|
||||
+ cameraType
|
||||
+ " "
|
||||
+ baseName
|
||||
+ " (AKA "
|
||||
+ nickname
|
||||
@@ -148,4 +150,33 @@ public class CameraConfiguration {
|
||||
.ifPresent(calibrations::remove);
|
||||
calibrations.add(calibration);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "CameraConfiguration [baseName="
|
||||
+ baseName
|
||||
+ ", uniqueName="
|
||||
+ uniqueName
|
||||
+ ", nickname="
|
||||
+ nickname
|
||||
+ ", path="
|
||||
+ path
|
||||
+ ", otherPaths="
|
||||
+ Arrays.toString(otherPaths)
|
||||
+ ", cameraType="
|
||||
+ cameraType
|
||||
+ ", FOV="
|
||||
+ FOV
|
||||
+ ", calibrations="
|
||||
+ calibrations
|
||||
+ ", currentPipelineIndex="
|
||||
+ currentPipelineIndex
|
||||
+ ", streamIndex="
|
||||
+ streamIndex
|
||||
+ ", pipelineSettings="
|
||||
+ pipelineSettings
|
||||
+ ", driveModeSettings="
|
||||
+ driveModeSettings
|
||||
+ "]";
|
||||
}
|
||||
}
|
||||
@@ -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 semaphore", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void unloadCameraConfigs() {
|
||||
this.config.getCameraConfigurations().clear();
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
@@ -40,13 +41,17 @@ public class HardwareConfig {
|
||||
public final String cpuTempCommand;
|
||||
public final String cpuMemoryCommand;
|
||||
public final String cpuUtilCommand;
|
||||
public final String cpuThrottleReasonCmd;
|
||||
public final String cpuUptimeCommand;
|
||||
public final String gpuMemoryCommand;
|
||||
public final String 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 +60,24 @@ 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 = "";
|
||||
cpuThrottleReasonCmd = "";
|
||||
cpuUptimeCommand = "";
|
||||
gpuMemoryCommand = "";
|
||||
gpuTempCommand = "";
|
||||
ramUtilCommand = "";
|
||||
ledBlinkCommand = "";
|
||||
gpuMemUsageCommand = "";
|
||||
diskUsageCommand = "";
|
||||
|
||||
restartHardwareCommand = "";
|
||||
vendorFOV = -1;
|
||||
blacklistedResIndices = Collections.emptyList();
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
@@ -81,43 +88,62 @@ 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 cpuThrottleReasonCmd,
|
||||
String cpuUptimeCommand,
|
||||
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;
|
||||
this.cpuTempCommand = cpuTempCommand;
|
||||
this.cpuMemoryCommand = cpuMemoryCommand;
|
||||
this.cpuUtilCommand = cpuUtilCommand;
|
||||
this.cpuThrottleReasonCmd = cpuThrottleReasonCmd;
|
||||
this.cpuUptimeCommand = cpuUptimeCommand;
|
||||
this.gpuMemoryCommand = gpuMemoryCommand;
|
||||
this.gpuTempCommand = gpuTempCommand;
|
||||
this.ramUtilCommand = ramUtilCommand;
|
||||
this.gpuMemUsageCommand = gpuMemUsageCommand;
|
||||
this.diskUsageCommand = diskUsageCommand;
|
||||
this.restartHardwareCommand = restartHardwareCommand;
|
||||
this.vendorFOV = vendorFOV;
|
||||
this.blacklistedResIndices = blacklistedResIndices;
|
||||
}
|
||||
|
||||
/** @return True if the FOV has been preset to a sane value, false otherwise */
|
||||
public final boolean hasPresetFOV() {
|
||||
return vendorFOV > 0;
|
||||
}
|
||||
|
||||
/** @return True if any command has been configured to a non-default empty, false otherwise */
|
||||
public final boolean hasCommandsConfigured() {
|
||||
return cpuTempCommand != ""
|
||||
|| cpuMemoryCommand != ""
|
||||
|| cpuUtilCommand != ""
|
||||
|| cpuThrottleReasonCmd != ""
|
||||
|| cpuUptimeCommand != ""
|
||||
|| gpuMemoryCommand != ""
|
||||
|| ramUtilCommand != ""
|
||||
|| ledBlinkCommand != ""
|
||||
|| gpuMemUsageCommand != ""
|
||||
|| diskUsageCommand != "";
|
||||
}
|
||||
}
|
||||
@@ -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,10 +15,8 @@
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.photonvision.vision.frame;
|
||||
package org.photonvision.common.configuration;
|
||||
|
||||
import java.util.function.Supplier;
|
||||
|
||||
public interface FrameProvider extends Supplier<Frame> {
|
||||
String getName();
|
||||
public class HardwareSettings {
|
||||
public int ledBrightnessPercentage = 100;
|
||||
}
|
||||
@@ -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.isLinux();
|
||||
}
|
||||
|
||||
@JsonSetter("shouldManage")
|
||||
public void setShouldManage(boolean shouldManage) {
|
||||
this.shouldManage = shouldManage || Platform.isLinux();
|
||||
}
|
||||
}
|
||||
@@ -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.LibCameraJNI;
|
||||
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,23 +101,35 @@ 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",
|
||||
LibCameraJNI.isSupported()
|
||||
? "Zerocopy Libcamera on " + LibCameraJNI.getSensorModel().getFriendlyName()
|
||||
: ""); // TODO add support for other types of GPU accel
|
||||
generalSubmap.put("hardwareModel", hardwareConfig.deviceName);
|
||||
generalSubmap.put("hardwarePlatform", Platform.getPlatformName());
|
||||
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;
|
||||
public double fov;
|
||||
|
||||
public String nickname;
|
||||
public HashMap<String, Object> currentPipelineSettings;
|
||||
public int currentPipelineIndex;
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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 {
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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;
|
||||
|
||||
@@ -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<>();
|
||||
@@ -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,23 +17,29 @@
|
||||
|
||||
package org.photonvision.common.dataflow.networktables;
|
||||
|
||||
import edu.wpi.first.networktables.EntryListenerFlags;
|
||||
import edu.wpi.first.networktables.EntryNotification;
|
||||
import edu.wpi.first.networktables.NetworkTableEntry;
|
||||
import edu.wpi.first.networktables.NetworkTableEvent;
|
||||
import edu.wpi.first.networktables.NetworkTableInstance;
|
||||
import edu.wpi.first.networktables.Subscriber;
|
||||
import java.util.EnumSet;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
public class NTDataChangeListener {
|
||||
|
||||
private final NetworkTableEntry watchedEntry;
|
||||
private final NetworkTableInstance instance;
|
||||
private final Subscriber watchedEntry;
|
||||
private final int listenerID;
|
||||
|
||||
public NTDataChangeListener(
|
||||
NetworkTableEntry watchedEntry, Consumer<EntryNotification> dataChangeConsumer) {
|
||||
this.watchedEntry = watchedEntry;
|
||||
listenerID = watchedEntry.addListener(dataChangeConsumer, EntryListenerFlags.kUpdate);
|
||||
NetworkTableInstance instance,
|
||||
Subscriber watchedSubscriber,
|
||||
Consumer<NetworkTableEvent> dataChangeConsumer) {
|
||||
this.watchedEntry = watchedSubscriber;
|
||||
this.instance = instance;
|
||||
listenerID =
|
||||
this.instance.addListener(
|
||||
watchedEntry, EnumSet.of(NetworkTableEvent.Kind.kValueAll), dataChangeConsumer);
|
||||
}
|
||||
|
||||
public void remove() {
|
||||
watchedEntry.removeListener(listenerID);
|
||||
this.instance.removeListener(listenerID);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,223 @@
|
||||
/*
|
||||
* 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.dataflow.networktables;
|
||||
|
||||
import edu.wpi.first.networktables.NetworkTable;
|
||||
import edu.wpi.first.networktables.NetworkTableEvent;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.function.BooleanSupplier;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Supplier;
|
||||
import org.opencv.core.Point;
|
||||
import org.photonvision.common.dataflow.CVPipelineResultConsumer;
|
||||
import org.photonvision.common.dataflow.structures.Packet;
|
||||
import org.photonvision.common.logging.LogGroup;
|
||||
import org.photonvision.common.logging.Logger;
|
||||
import org.photonvision.common.networktables.NTTopicSet;
|
||||
import org.photonvision.targeting.PhotonPipelineResult;
|
||||
import org.photonvision.targeting.PhotonTrackedTarget;
|
||||
import org.photonvision.targeting.TargetCorner;
|
||||
import org.photonvision.vision.pipeline.result.CVPipelineResult;
|
||||
import org.photonvision.vision.target.TrackedTarget;
|
||||
|
||||
public class NTDataPublisher implements CVPipelineResultConsumer {
|
||||
private final Logger logger = new Logger(NTDataPublisher.class, LogGroup.General);
|
||||
|
||||
private final NetworkTable rootTable = NetworkTablesManager.getInstance().kRootTable;
|
||||
|
||||
private NTTopicSet ts = new NTTopicSet();
|
||||
|
||||
NTDataChangeListener pipelineIndexListener;
|
||||
private final Supplier<Integer> pipelineIndexSupplier;
|
||||
private final Consumer<Integer> pipelineIndexConsumer;
|
||||
|
||||
NTDataChangeListener driverModeListener;
|
||||
private final BooleanSupplier driverModeSupplier;
|
||||
private final Consumer<Boolean> driverModeConsumer;
|
||||
|
||||
private long heartbeatCounter = 0;
|
||||
|
||||
public NTDataPublisher(
|
||||
String cameraNickname,
|
||||
Supplier<Integer> pipelineIndexSupplier,
|
||||
Consumer<Integer> pipelineIndexConsumer,
|
||||
BooleanSupplier driverModeSupplier,
|
||||
Consumer<Boolean> driverModeConsumer) {
|
||||
this.pipelineIndexSupplier = pipelineIndexSupplier;
|
||||
this.pipelineIndexConsumer = pipelineIndexConsumer;
|
||||
this.driverModeSupplier = driverModeSupplier;
|
||||
this.driverModeConsumer = driverModeConsumer;
|
||||
|
||||
updateCameraNickname(cameraNickname);
|
||||
updateEntries();
|
||||
}
|
||||
|
||||
private void onPipelineIndexChange(NetworkTableEvent entryNotification) {
|
||||
var newIndex = (int) entryNotification.valueData.value.getInteger();
|
||||
var originalIndex = pipelineIndexSupplier.get();
|
||||
|
||||
// ignore indexes below 0
|
||||
if (newIndex < 0) {
|
||||
ts.pipelineIndexPublisher.set(originalIndex);
|
||||
return;
|
||||
}
|
||||
|
||||
if (newIndex == originalIndex) {
|
||||
logger.debug("Pipeline index is already " + newIndex);
|
||||
return;
|
||||
}
|
||||
|
||||
pipelineIndexConsumer.accept(newIndex);
|
||||
var setIndex = pipelineIndexSupplier.get();
|
||||
if (newIndex != setIndex) { // set failed
|
||||
ts.pipelineIndexPublisher.set(setIndex);
|
||||
// TODO: Log
|
||||
}
|
||||
logger.debug("Successfully set pipeline index to " + newIndex);
|
||||
}
|
||||
|
||||
private void onDriverModeChange(NetworkTableEvent entryNotification) {
|
||||
var newDriverMode = entryNotification.valueData.value.getBoolean();
|
||||
var originalDriverMode = driverModeSupplier.getAsBoolean();
|
||||
|
||||
if (newDriverMode == originalDriverMode) {
|
||||
logger.debug("Driver mode is already " + newDriverMode);
|
||||
return;
|
||||
}
|
||||
|
||||
driverModeConsumer.accept(newDriverMode);
|
||||
logger.debug("Successfully set driver mode to " + newDriverMode);
|
||||
}
|
||||
|
||||
private void removeEntries() {
|
||||
if (pipelineIndexListener != null) pipelineIndexListener.remove();
|
||||
if (driverModeListener != null) driverModeListener.remove();
|
||||
ts.removeEntries();
|
||||
}
|
||||
|
||||
private void updateEntries() {
|
||||
if (pipelineIndexListener != null) pipelineIndexListener.remove();
|
||||
if (driverModeListener != null) driverModeListener.remove();
|
||||
|
||||
ts.updateEntries();
|
||||
|
||||
pipelineIndexListener =
|
||||
new NTDataChangeListener(
|
||||
ts.subTable.getInstance(), ts.pipelineIndexSubscriber, this::onPipelineIndexChange);
|
||||
|
||||
driverModeListener =
|
||||
new NTDataChangeListener(
|
||||
ts.subTable.getInstance(), ts.driverModeSubscriber, this::onDriverModeChange);
|
||||
}
|
||||
|
||||
public void updateCameraNickname(String newCameraNickname) {
|
||||
removeEntries();
|
||||
ts.subTable = rootTable.getSubTable(newCameraNickname);
|
||||
updateEntries();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void accept(CVPipelineResult result) {
|
||||
var simplified =
|
||||
new PhotonPipelineResult(
|
||||
result.getLatencyMillis(), simpleFromTrackedTargets(result.targets));
|
||||
Packet packet = new Packet(simplified.getPacketSize());
|
||||
simplified.populatePacket(packet);
|
||||
|
||||
ts.rawBytesEntry.set(packet.getData());
|
||||
|
||||
ts.pipelineIndexPublisher.setDefault(pipelineIndexSupplier.get());
|
||||
ts.driverModePublisher.setDefault(driverModeSupplier.getAsBoolean());
|
||||
ts.latencyMillisEntry.set(result.getLatencyMillis());
|
||||
ts.hasTargetEntry.set(result.hasTargets());
|
||||
|
||||
if (result.hasTargets()) {
|
||||
var bestTarget = result.targets.get(0);
|
||||
|
||||
ts.targetPitchEntry.set(bestTarget.getPitch());
|
||||
ts.targetYawEntry.set(bestTarget.getYaw());
|
||||
ts.targetAreaEntry.set(bestTarget.getArea());
|
||||
ts.targetSkewEntry.set(bestTarget.getSkew());
|
||||
|
||||
var pose = bestTarget.getBestCameraToTarget3d();
|
||||
ts.targetPoseEntry.set(
|
||||
new double[] {
|
||||
pose.getTranslation().getX(),
|
||||
pose.getTranslation().getY(),
|
||||
pose.getTranslation().getZ(),
|
||||
pose.getRotation().getQuaternion().getW(),
|
||||
pose.getRotation().getQuaternion().getX(),
|
||||
pose.getRotation().getQuaternion().getY(),
|
||||
pose.getRotation().getQuaternion().getZ()
|
||||
});
|
||||
|
||||
var targetOffsetPoint = bestTarget.getTargetOffsetPoint();
|
||||
ts.bestTargetPosX.set(targetOffsetPoint.x);
|
||||
ts.bestTargetPosY.set(targetOffsetPoint.y);
|
||||
} else {
|
||||
ts.targetPitchEntry.set(0);
|
||||
ts.targetYawEntry.set(0);
|
||||
ts.targetAreaEntry.set(0);
|
||||
ts.targetSkewEntry.set(0);
|
||||
ts.targetPoseEntry.set(new double[] {0, 0, 0});
|
||||
ts.bestTargetPosX.set(0);
|
||||
ts.bestTargetPosY.set(0);
|
||||
}
|
||||
|
||||
ts.heartbeatPublisher.set(heartbeatCounter++);
|
||||
|
||||
// TODO...nt4... is this needed?
|
||||
rootTable.getInstance().flush();
|
||||
}
|
||||
|
||||
public static List<PhotonTrackedTarget> simpleFromTrackedTargets(List<TrackedTarget> targets) {
|
||||
var ret = new ArrayList<PhotonTrackedTarget>();
|
||||
for (var t : targets) {
|
||||
var minAreaRectCorners = new ArrayList<TargetCorner>();
|
||||
var detectedCorners = new ArrayList<TargetCorner>();
|
||||
{
|
||||
var points = new Point[4];
|
||||
t.getMinAreaRect().points(points);
|
||||
for (int i = 0; i < 4; i++) {
|
||||
minAreaRectCorners.add(new TargetCorner(points[i].x, points[i].y));
|
||||
}
|
||||
}
|
||||
{
|
||||
var points = t.getTargetCorners();
|
||||
for (int i = 0; i < points.size(); i++) {
|
||||
detectedCorners.add(new TargetCorner(points.get(i).x, points.get(i).y));
|
||||
}
|
||||
}
|
||||
|
||||
ret.add(
|
||||
new PhotonTrackedTarget(
|
||||
t.getYaw(),
|
||||
t.getPitch(),
|
||||
t.getArea(),
|
||||
t.getSkew(),
|
||||
t.getFiducialId(),
|
||||
t.getBestCameraToTarget3d(),
|
||||
t.getAltCameraToTarget3d(),
|
||||
t.getPoseAmbiguity(),
|
||||
minAreaRectCorners,
|
||||
detectedCorners));
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,148 @@
|
||||
/*
|
||||
* 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.dataflow.networktables;
|
||||
|
||||
import edu.wpi.first.networktables.NetworkTable;
|
||||
import edu.wpi.first.networktables.NetworkTableEvent;
|
||||
import edu.wpi.first.networktables.NetworkTableInstance;
|
||||
import java.util.HashMap;
|
||||
import java.util.function.Consumer;
|
||||
import org.photonvision.PhotonVersion;
|
||||
import org.photonvision.common.configuration.ConfigManager;
|
||||
import org.photonvision.common.configuration.NetworkConfig;
|
||||
import org.photonvision.common.dataflow.DataChangeService;
|
||||
import org.photonvision.common.dataflow.events.OutgoingUIEvent;
|
||||
import org.photonvision.common.logging.LogGroup;
|
||||
import org.photonvision.common.logging.Logger;
|
||||
import org.photonvision.common.scripting.ScriptEventType;
|
||||
import org.photonvision.common.scripting.ScriptManager;
|
||||
import org.photonvision.common.util.TimedTaskManager;
|
||||
|
||||
public class NetworkTablesManager {
|
||||
private final NetworkTableInstance ntInstance = NetworkTableInstance.getDefault();
|
||||
private final String kRootTableName = "/photonvision";
|
||||
public final NetworkTable kRootTable = ntInstance.getTable(kRootTableName);
|
||||
|
||||
private boolean isRetryingConnection = false;
|
||||
|
||||
private NetworkTablesManager() {
|
||||
ntInstance.addLogger(0, 255, new NTLogger()); // to hide error messages
|
||||
TimedTaskManager.getInstance().addTask("NTManager", this::ntTick, 5000);
|
||||
}
|
||||
|
||||
private static NetworkTablesManager INSTANCE;
|
||||
|
||||
public static NetworkTablesManager getInstance() {
|
||||
if (INSTANCE == null) INSTANCE = new NetworkTablesManager();
|
||||
return INSTANCE;
|
||||
}
|
||||
|
||||
private static final Logger logger = new Logger(NetworkTablesManager.class, LogGroup.General);
|
||||
|
||||
private static class NTLogger implements Consumer<NetworkTableEvent> {
|
||||
private boolean hasReportedConnectionFailure = false;
|
||||
private long lastConnectMessageMillis = 0;
|
||||
|
||||
@Override
|
||||
public void accept(NetworkTableEvent event) {
|
||||
if (!hasReportedConnectionFailure && event.logMessage.message.contains("timed out")) {
|
||||
logger.error("NT Connection has failed! Will retry in background.");
|
||||
hasReportedConnectionFailure = true;
|
||||
getInstance().broadcastConnectedStatus();
|
||||
} else if (event.logMessage.message.contains("connected")
|
||||
&& System.currentTimeMillis() - lastConnectMessageMillis > 125) {
|
||||
logger.info("NT Connected!");
|
||||
hasReportedConnectionFailure = false;
|
||||
lastConnectMessageMillis = System.currentTimeMillis();
|
||||
ScriptManager.queueEvent(ScriptEventType.kNTConnected);
|
||||
getInstance().broadcastVersion();
|
||||
getInstance().broadcastConnectedStatus();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void broadcastConnectedStatus() {
|
||||
TimedTaskManager.getInstance().addOneShotTask(this::broadcastConnectedStatusImpl, 1000L);
|
||||
}
|
||||
|
||||
private void broadcastConnectedStatusImpl() {
|
||||
HashMap<String, Object> map = new HashMap<>();
|
||||
var subMap = new HashMap<String, Object>();
|
||||
|
||||
subMap.put("connected", ntInstance.isConnected());
|
||||
if (ntInstance.isConnected()) {
|
||||
var connections = getInstance().ntInstance.getConnections();
|
||||
if (connections.length > 0) {
|
||||
subMap.put("address", connections[0].remote_ip + ":" + connections[0].remote_port);
|
||||
}
|
||||
subMap.put("clients", connections.length);
|
||||
}
|
||||
|
||||
map.put("ntConnectionInfo", subMap);
|
||||
DataChangeService.getInstance()
|
||||
.publishEvent(new OutgoingUIEvent<>("networkTablesConnected", map));
|
||||
}
|
||||
|
||||
private void broadcastVersion() {
|
||||
kRootTable.getEntry("version").setString(PhotonVersion.versionString);
|
||||
kRootTable.getEntry("buildDate").setString(PhotonVersion.buildDate);
|
||||
}
|
||||
|
||||
public void setConfig(NetworkConfig config) {
|
||||
if (config.runNTServer) {
|
||||
setServerMode();
|
||||
} else {
|
||||
setClientMode(config.teamNumber);
|
||||
}
|
||||
broadcastVersion();
|
||||
}
|
||||
|
||||
private void setClientMode(int teamNumber) {
|
||||
if (!isRetryingConnection) logger.info("Starting NT Client");
|
||||
ntInstance.stopServer();
|
||||
ntInstance.startClient4("photonvision");
|
||||
ntInstance.setServerTeam(teamNumber);
|
||||
ntInstance.startDSClient();
|
||||
broadcastVersion();
|
||||
}
|
||||
|
||||
private void setServerMode() {
|
||||
logger.info("Starting NT Server");
|
||||
ntInstance.stopClient();
|
||||
ntInstance.startServer();
|
||||
broadcastVersion();
|
||||
}
|
||||
|
||||
// So it seems like if Photon starts before the robot NT server does, and both aren't static IP,
|
||||
// it'll never connect. This hack works around it by restarting the client/server while the nt
|
||||
// instance
|
||||
// isn't connected, same as clicking the save button in the settings menu (or restarting the
|
||||
// service)
|
||||
private void ntTick() {
|
||||
if (!ntInstance.isConnected()
|
||||
&& !ConfigManager.getInstance().getConfig().getNetworkConfig().runNTServer) {
|
||||
setConfig(ConfigManager.getInstance().getConfig().getNetworkConfig());
|
||||
}
|
||||
|
||||
if (!ntInstance.isConnected() && !isRetryingConnection) {
|
||||
isRetryingConnection = true;
|
||||
logger.error(
|
||||
"[NetworkTablesManager] Could not connect to the robot! Will retry in the background...");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||