UI bug fixes and feature refinements (#59)

* Rework settings page; touch up contour, output, and 3D tabs; font sizing

No stream placeholder; driver mode refined; cameras page

Make settings snackbar work

Lint fix

Fix settings page padding

Actually hide settings fields if unsupported

* Make toggle buttons less confusing; fix driver toggle; form validation

* Make eyedropper work and make input/select styling more consistent

* Fix color picker and tabbing bugs

* Set up camera and settings pages to talk to the backend

* Add auto reconnect

* Add lots of tooltips and improve related thematic consistency

* Only show output stream while color picking

* Unbreak robot offset

* Increase tooltip delay and refactor tooltip label into a component

* Remove toggle button switching behavior

* Fix PnP tab and add a flag to disable FOV configuration

* Move FPS indicator

* Make GPU acceleration status use one value in the store

* Only allow IPv4 static IPs and remove accidentally committed index
This commit is contained in:
Declan Freeman-Gleason
2020-07-31 13:50:50 -07:00
committed by GitHub
parent 0b98dc3c9f
commit 19b57235fe
34 changed files with 1099 additions and 566 deletions

View File

@@ -12282,9 +12282,8 @@
}
},
"vue-native-websocket": {
"version": "2.0.14",
"resolved": "https://registry.npmjs.org/vue-native-websocket/-/vue-native-websocket-2.0.14.tgz",
"integrity": "sha512-oK8+xG1gmqRs4JngHGwEc4zWoRjsdMB20Sz8pemkh4lW2Vr2676/cDRtd30aGnO2xF7Oxf003wS5JO0kypUsCw==",
"version": "git+https://github.com/PhotonVision/vue-native-websocket.git#7a327918e03b215b6899b0d648c5130ece1fa912",
"from": "git+https://github.com/PhotonVision/vue-native-websocket.git#7a32791",
"dependencies": {
"abbrev": {
"version": "1.0.9",

View File

@@ -17,7 +17,7 @@
"msgpack5": "^4.2.1",
"vue": "^2.6.11",
"vue-axios": "^2.1.5",
"vue-native-websocket": "^2.0.14",
"vue-native-websocket": "git+https://github.com/PhotonVision/vue-native-websocket.git#7a32791",
"vue-router": "^3.3.2",
"vuetify": "^2.2.34",
"vuex": "^3.4.0"

View File

@@ -25,11 +25,10 @@
</v-list-item-icon>
</v-list-item>
<v-divider />
<v-list-item
link
to="dashboard"
@click="rollbackPipelineIndex()"
>
<v-list-item-icon>
<v-icon>mdi-view-dashboard</v-icon>
@@ -38,11 +37,22 @@
<v-list-item-title>Dashboard</v-list-item-title>
</v-list-item-content>
</v-list-item>
<v-list-item
link
to="cameras"
@click="switchToDriverMode()"
>
<v-list-item-icon>
<v-icon>mdi-camera</v-icon>
</v-list-item-icon>
<v-list-item-content>
<v-list-item-title>Cameras</v-list-item-title>
</v-list-item-content>
</v-list-item>
<v-list-item
link
to="settings"
>
<!-- TODO: Expandable sub-elements? -->
<v-list-item-icon>
<v-icon>mdi-settings</v-icon>
</v-list-item-icon>
@@ -79,6 +89,24 @@
<v-list-item-title>Advanced 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>
</v-list>
</v-navigation-drawer>
<v-content>
@@ -123,6 +151,8 @@
logView
},
data: () => ({
// Used so that we can switch back to the previously selected pipeline after camera calibration
previouslySelectedIndex: null,
timer: undefined,
isLogger: false,
log: "",
@@ -138,11 +168,16 @@
},
compact: {
get() {
return this.$store.state.compactMode === undefined ? this.$vuetify.breakpoint.smAndDown : this.$store.state.compactMode || this.$vuetify.breakpoint.smAndDown;
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);
},
}
},
@@ -165,6 +200,7 @@
}
});
this.$options.sockets.onmessage = (data) => {
try {
let message = this.$msgPack.decode(data.data);
@@ -176,13 +212,24 @@
} catch (error) {
console.error('error: ' + 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") {
console.log(value)
this.logMessage(value, 0)
console.log("[FROM BACKEND]" + value);
this.logMessage(value, 0);
} else if (key === "updatePipelineResult") {
this.$store.commit('mutatePipelineResults', value)
} else if (this.$store.state.hasOwnProperty(key)) {
@@ -192,7 +239,7 @@
} else {
switch (key) {
default: {
console.log(value);
console.error("Unknown message from backend: " + value);
}
}
}
@@ -215,6 +262,16 @@
const colors = ["\u001b[31m", "\u001b[32m", "\u001b[33m", "\u001b[34m"]
const reset = "\u001b[0m"
this.log += `${colors[level]}${message}${reset}\n`
},
switchToDriverMode() {
this.previouslySelectedIndex = this.$store.getters.currentPipelineIndex;
this.handleInputWithIndex('currentPipeline', -1)
},
rollbackPipelineIndex() {
if (this.previouslySelectedIndex !== null) {
this.handleInputWithIndex('currentPipeline', this.previouslySelectedIndex)
}
this.previouslySelectedIndex = null;
}
}
};
@@ -225,6 +282,21 @@
</style>
<style>
.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);
}
}
.logo {
width: 100%;
height: 70px;
@@ -274,6 +346,20 @@
<style>
/* Hack */
.v-divider {
border-color: #23add9 !important;
border-color: white !important;
}
.v-input {
font-size: 1rem !important;
}
</style>
<style lang="scss">
@import '~vuetify/src/styles/settings/_variables';
@media #{map-get($display-breakpoints, 'md-and-down')} {
html {
font-size: 14px !important;
}
}
</style>

View File

@@ -0,0 +1,68 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
version="1.1"
width="24"
height="24"
viewBox="0 0 24 24"
id="svg865"
sodipodi:docname="eyedropper.svg"
inkscape:version="0.92.4 (unknown)">
<metadata
id="metadata871">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
</cc:Work>
</rdf:RDF>
</metadata>
<defs
id="defs869" />
<sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="1916"
inkscape:window-height="1040"
id="namedview867"
showgrid="false"
inkscape:zoom="35.541667"
inkscape:cx="12.112544"
inkscape:cy="10.171169"
inkscape:window-x="0"
inkscape:window-y="1458"
inkscape:window-maximized="0"
inkscape:current-layer="svg865" />
<g
id="g905"
inkscape:export-xdpi="77.2733"
inkscape:export-ydpi="77.2733">
<path
inkscape:connector-curvature="0"
id="path863"
d="m 12.28,19.4725 -2.13,-2.13 1.42,-1.41 -7.71,-7.71 -1.86,-4.6 1.5,-1.5 4.6,1.86 7.71,7.71 1.41,-1.42 2.13,2.13 -7.07,7.07 m 8.72,-2.59 c 1.17,1.17 1.17,3.07 0,4.24 -1.17,1.17 -3.07,1.17 -4.24,0 l -1.92,-1.92 4.24,-4.24 1.92,1.92 m -14.03,-11.2 -2.47,-1.06 1.06,2.47 7.44,7.43 1.4,-1.4 z" />
<path
inkscape:export-ydpi="161.91951"
inkscape:export-xdpi="161.91951"
d="m 3.5996094,2.6132812 -1.109375,1.109375 1.7246094,4.2636719 7.6503902,7.6503909 a 0.41783994,0.41783994 0 0 1 0,0.591797 l -1.123046,1.115234 1.537109,1.539062 6.480469,-6.480468 -1.53711,-1.53711 -1.115234,1.123047 a 0.41783994,0.41783994 0 0 1 -0.591797,0 L 7.8652344,4.3378906 Z m 0.9101562,1.5917969 a 0.41783994,0.41783994 0 0 1 0.1542969,0.033203 L 7.1347656,5.296875 a 0.41783994,0.41783994 0 0 1 0.1308594,0.089844 l 7.429687,7.4414062 a 0.41783994,0.41783994 0 0 1 0,0.589844 l -1.40039,1.40039 a 0.41783994,0.41783994 0 0 1 -0.589844,0 L 5.265625,7.3867188 A 0.41783994,0.41783994 0 0 1 5.1757812,7.2558594 L 4.1152344,4.7871094 A 0.41783994,0.41783994 0 0 1 4.5097656,4.2050781 Z m 14.5703124,11.3476559 -3.65039,3.650391 1.625,1.625 c 1.010062,1.010062 2.640328,1.010062 3.65039,0 1.010062,-1.010062 1.010062,-2.640327 0,-3.650391 z"
id="path889"
style="fill:#ffffff;stroke-width:21.16535378;stroke-miterlimit:4;stroke-dasharray:none"
inkscape:original="M 3.5 2.1230469 L 2 3.6230469 L 3.859375 8.2226562 L 11.570312 15.931641 L 10.150391 17.341797 L 12.279297 19.472656 L 19.349609 12.402344 L 17.220703 10.273438 L 15.810547 11.693359 L 8.0996094 3.9824219 L 3.5 2.1230469 z M 4.5 4.6230469 L 6.9707031 5.6816406 L 14.400391 13.123047 L 13 14.523438 L 5.5605469 7.0917969 L 4.5 4.6230469 z M 19.080078 14.962891 L 14.839844 19.203125 L 16.759766 21.123047 C 17.929766 22.293047 19.83 22.293047 21 21.123047 C 22.17 19.953047 22.17 18.052813 21 16.882812 L 19.080078 14.962891 z "
inkscape:radius="-0.41779816"
sodipodi:type="inkscape:offset" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

View File

@@ -1,8 +1,9 @@
<template>
<img
id="CameraStream"
:id="id"
crossOrigin="anonymous"
:style="styleObject"
:src="address"
:src="src"
alt=""
@click="e => $emit('click', e)"
>
@@ -11,20 +12,24 @@
<script>
export default {
name: "CvImage",
// eslint-disable-next-line vue/require-prop-types
props: ['address', 'scale', 'maxHeight', 'maxHeightMd', 'maxHeightXl'],
data: () => {
return {}
},
// eslint-disable-next-line vue/require-prop-types
props: ['address', 'scale', 'maxHeight', 'maxHeightMd', 'maxHeightXl', 'colorPicking', 'id', 'disconnected'],
computed: {
styleObject: {
get() {
let ret = {
"border-radius": "3px",
"display": "block",
"object-fit": "contain",
"object-position": "50% 50%",
"max-width": "100%",
"margin-left": "auto",
"margin-right": "auto",
"max-height": this.maxHeight,
width: `${this.scale}%`,
height: `${this.scale}%`,
cursor: (this.colorPicking ? `url(${require("../../assets/eyedropper.svg")}),` : "") + "default",
};
console.log(ret);
if (this.$vuetify.breakpoint.xl) {
ret["max-height"] = this.maxHeightXl;
@@ -34,8 +39,12 @@
return ret;
}
}
}
},
src: {
get() {
return this.disconnected ? require("../../assets/noStream.jpg") : this.address;
},
},
},
}
</script>

View File

@@ -4,16 +4,22 @@
dense
align="center"
>
<v-col cols="4">
<span class="ml-2">{{ name }}</span>
<v-col :cols="12 - (inputCols || 8)">
<tooltipped-label
:tooltip="tooltip"
:text="name"
/>
</v-col>
<v-col cols="8">
<v-col :cols="inputCols || 8">
<v-text-field
v-model="localValue"
dark
dense
color="accent"
:disabled="disabled"
:error-messages="errorMessage"
:rules="rules"
class="mt-1 pt-2"
@keydown="handleKeyboard"
/>
</v-col>
@@ -22,10 +28,15 @@
</template>
s
<script>
import TooltippedLabel from "./cv-tooltipped-label";
export default {
name: 'Input',
// eslint-disable-next-line vue/require-prop-types
props: ['name', 'value', 'disabled', 'errorMessage'],
components: {
TooltippedLabel
},
// eslint-disable-next-line vue/require-prop-types
props: ['name', 'value', 'disabled', 'errorMessage', 'inputCols', 'rules', 'tooltip'],
data() {
return {}
},
@@ -49,6 +60,5 @@ s
}
</script>
<style lang="" scoped>
<style lang="css" scoped>
</style>

View File

@@ -4,8 +4,11 @@
dense
align="center"
>
<v-col :cols="2">
<span>{{ name }}</span>
<v-col :cols="labelCols || 2">
<tooltipped-label
:tooltip="tooltip"
:text="name"
/>
</v-col>
<v-col>
<v-text-field
@@ -14,9 +17,11 @@
class="mt-0 pt-0"
hide-details
single-line
color="accent"
type="number"
style="width: 70px"
:step="step"
:rules="rules"
/>
</v-col>
</v-row>
@@ -24,13 +29,15 @@
</template>
<script>
import TooltippedLabel from "./cv-tooltipped-label";
export default {
name: 'NumberInput',
// eslint-disable-next-line vue/require-prop-types
props: ['name', 'value', 'step'],
data() {
return {}
components: {
TooltippedLabel,
},
// eslint-disable-next-line vue/require-prop-types
props: ['name', 'value', 'step', 'labelCols', 'rules', 'tooltip'],
computed: {
localValue: {
get() {

View File

@@ -1,10 +1,16 @@
<template>
<div>
<v-row dense align="center">
<v-col :cols="2">
<span>{{ name }}</span>
<v-row
dense
align="center"
>
<v-col cols="2">
<tooltipped-label
:tooltip="tooltip"
:text="name"
/>
</v-col>
<v-col :cols="10">
<v-col cols="10">
<v-range-slider
:value="localValue"
:max="max"
@@ -12,7 +18,7 @@
hide-details
class="align-center"
dark
color="#ffd843"
color="accent"
:step="step"
@input="handleInput"
@mousedown="$emit('rollback', localValue)"
@@ -20,6 +26,7 @@
<template v-slot:prepend>
<v-text-field
dark
color="accent"
:value="localValue[0]"
:max="max"
:min="min"
@@ -38,6 +45,7 @@
<template v-slot:append>
<v-text-field
dark
color="accent"
:value="localValue[1]"
:max="max"
:min="min"
@@ -59,10 +67,15 @@
</template>
<script>
import TooltippedLabel from "./cv-tooltipped-label";
export default {
name: "RangeSlider",
components: {
TooltippedLabel,
},
// eslint-disable-next-line vue/require-prop-types
props: ["name", "min", "max", "value", "step"],
props: ["name", "min", "max", "value", "step", "tooltip"],
data() {
return {
prependFocused: false,

View File

@@ -5,7 +5,10 @@
align="center"
>
<v-col :cols="12 - (selectCols || 9)">
<span>{{ name }}</span>
<tooltipped-label
:tooltip="tooltip"
:text="name"
/>
</v-col>
<v-col :cols="selectCols || 9">
<v-select
@@ -17,6 +20,7 @@
color="accent"
item-color="secondary"
:disabled="disabled"
:rules="rules"
@change="$emit('rollback', localValue)"
/>
</v-col>
@@ -25,13 +29,15 @@
</template>
<script>
import TooltippedLabel from "./cv-tooltipped-label";
export default {
name: 'Select',
// eslint-disable-next-line vue/require-prop-types
props: ['list', 'name', 'value', 'disabled', 'selectCols'],
data() {
return {}
components: {
TooltippedLabel,
},
// eslint-disable-next-line vue/require-prop-types
props: ['list', 'name', 'value', 'disabled', 'selectCols', 'rules', 'tooltip'],
computed: {
localValue: {
get() {

View File

@@ -5,7 +5,10 @@
align="center"
>
<v-col :cols="12 - (sliderCols || 8)">
<span>{{ name }}</span>
<tooltipped-label
:tooltip="tooltip"
:text="name"
/>
</v-col>
<v-col :cols="sliderCols || 8">
<v-slider
@@ -27,6 +30,7 @@
<template v-slot:append>
<v-text-field
dark
color="accent"
:max="max"
:min="min"
:disabled="disabled"
@@ -49,10 +53,15 @@
</template>
<script>
import TooltippedLabel from "./cv-tooltipped-label";
export default {
name: "Slider",
components: {
TooltippedLabel,
},
// eslint-disable-next-line vue/require-prop-types
props: ["min", "max", "name", "value", "step", "sliderCols", "disabled"],
props: ["min", "max", "name", "value", "step", "sliderCols", "disabled", "tooltip"],
data() {
return {
isFocused: false,

View File

@@ -4,10 +4,13 @@
dense
align="center"
>
<v-col :cols="2">
<span>{{ name }}</span>
<v-col :cols="textCols || 2">
<tooltipped-label
:tooltip="tooltip"
:text="name"
/>
</v-col>
<v-col>
<v-col :cols="12 - (textCols || 2)">
<v-switch
v-model="localValue"
dark
@@ -21,24 +24,26 @@
</template>
<script>
export default {
name: 'CVSwitch',
// eslint-disable-next-line vue/require-prop-types
props: ['name', 'value', 'disabled'],
data() {
return {}
},
computed: {
localValue: {
get() {
return this.value;
},
set(value) {
this.$emit('input', value)
}
import TooltippedLabel from "./cv-tooltipped-label";
export default {
name: 'CVSwitch',
components: {
TooltippedLabel,
},
// eslint-disable-next-line vue/require-prop-types
props: ['name', 'value', 'disabled', 'textCols', 'tooltip'],
computed: {
localValue: {
get() {
return this.value;
},
set(value) {
this.$emit('input', value)
}
}
}
}
</script>
<style lang="" scoped>

View File

@@ -0,0 +1,26 @@
<template>
<div>
<v-tooltip
:disabled="tooltip === undefined"
right
open-delay="600"
>
<template v-slot:activator="{ on, attrs }">
<span
style="cursor: text !important;"
v-bind="attrs"
v-on="on"
>{{ text }}</span>
</template>
<span>{{ tooltip }}</span>
</v-tooltip>
</div>
</template>
<script>
export default {
name: 'TooltippedLabel',
// eslint-disable-next-line vue/require-prop-types
props: ['text', 'tooltip'],
}
</script>

View File

@@ -24,9 +24,9 @@
name: "MiniMap",
props: {
// eslint-disable-next-line vue/require-default-prop
targets: Array,
targets: Array,
// eslint-disable-next-line vue/require-default-prop
horizontalFOV: Number
horizontalFOV: Number
},
data() {
return {
@@ -75,7 +75,6 @@
this.$nextTick(function () {
this.drawPlayer();
});
},
methods: {
@@ -88,8 +87,8 @@
},
drawTarget(index, target) {
// first save the untranslated/unrotated context
let x = 800 - (160 * target.translation.x); // getting meters as pixels
let y = 400 - (160 * target.translation.y);
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

View File

@@ -18,6 +18,7 @@
v-else
v-model="newCameraName"
name="Camera"
input-cols="9"
:error-message="checkCameraName"
@Enter="saveCameraNameChange"
/>
@@ -63,8 +64,10 @@
<CVselect
v-model="currentPipelineIndex"
name="Pipeline"
:list="['Driver Mode'].concat($store.getters.pipelineList)"
@input="handleInputWithIndex('currentPipeline',currentPipelineIndex - 1)"
tooltip="Each pipeline runs on a camera output and stores a unique set of processing settings"
:disabled="$store.getters.isDriverMode"
:list="($store.getters.isDriverMode ? ['Driver Mode'] : []).concat($store.getters.pipelineList)"
@input="handleInputWithIndex('currentPipeline', currentPipelineIndex)"
/>
</v-col>
<v-col
@@ -128,15 +131,6 @@
</v-list>
</v-menu>
</v-col>
<!-- <v-btn-->
<!-- outlined-->
<!-- color="accent"-->
<!-- @click="handleInput('command','save')"-->
<!-- >-->
<!-- <v-icon>save</v-icon>-->
<!-- Save-->
<!-- </v-btn>-->
</v-row>
<!--pipeline duplicate dialog-->
<v-dialog
@@ -305,10 +299,10 @@
},
currentPipelineIndex: {
get() {
return this.$store.getters.currentPipelineIndex + 1;
return this.$store.getters.currentPipelineIndex + this.$store.getters.isDriverMode ? 1 : 0;
},
set(value) {
this.$store.commit('currentPipelineIndex', value - 1);
this.$store.commit('currentPipelineIndex', value - this.$store.getters.isDriverMode ? 1 : 0);
}
}
},

View File

@@ -4,34 +4,33 @@
align="center"
justify="start"
>
<v-col
style="padding-right:0"
:cols="3"
>
<v-col cols="4">
<v-btn
small
color="#ffd843"
color="accent"
style="width: 100%;"
class="black--text"
@click="takePointA"
>
Take Point A
</v-btn>
</v-col>
<v-col
style="margin-left:0"
:cols="3"
>
<v-col cols="4">
<v-btn
small
color="#ffd843"
color="accent"
style="width: 100%;"
class="black--text"
@click="takePointB"
>
Take Point B
</v-btn>
</v-col>
<v-col>
<v-col cols="4">
<v-btn
small
color="yellow darken-3"
style="width: 100%;"
@click="clearSlope"
>
Clear All Points

View File

@@ -4,22 +4,22 @@
align="center"
justify="start"
>
<v-col
style="padding-right:0"
:cols="3"
>
<v-col cols="6">
<v-btn
small
color="#ffd843"
color="accent"
class="black--text"
style="width: 100%;"
@click="takePoint"
>
Take Point
</v-btn>
</v-col>
<v-col>
<v-col cols="6">
<v-btn
small
color="yellow darken-3"
style="width: 100%;"
@click="clearPoint"
>
Clear Point

View File

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

View File

@@ -2,8 +2,10 @@ var canvas = undefined;
var image = undefined;
function initColorPicker() {
canvas = document.createElement('canvas');
image = document.getElementById('CameraStream');
if (!canvas)
canvas = document.createElement('canvas');
image = document.querySelector('#normal-stream');
canvas.width = image.width;
canvas.height = image.height;
}
@@ -15,11 +17,12 @@ function initColorPicker() {
//calls the function to handle the button (either eyedrop,expand or shrink)
function colorPickerClick(event, currentFunction, currentRange) {
let rect = image.getBoundingClientRect();
let x = Math.round(event.clientX - rect.left);
let y = Math.round(event.clientY - rect.top);
let x = Math.round((event.clientX - rect.left) / rect.width * image.width);
let y = Math.round((event.clientY - rect.top) / rect.height * image.height);
let context = canvas.getContext('2d');
context.drawImage(image, 0, 0, image.width, image.height);
let pixelData = context.getImageData(x, y, 1, 1).data;
if (currentFunction !== undefined) {
return currentFunction(pixelData, currentRange);
}
@@ -114,6 +117,7 @@ function shrinkRange(range, color) {
range[1][j] = Math.max(range[1][j] - 10, range[0][j]);//shrink from max side
}
}
return inside;//returns if color is inside or not
}

View File

@@ -1,6 +1,7 @@
import Vue from 'vue'
import Router from 'vue-router'
import Dashboard from "./views/PipelineView";
import Cameras from "./views/CamerasView";
import Settings from "./views/SettingsView";
import Docs from "./views/DocsView";
Vue.use(Router);
@@ -15,6 +16,10 @@ export default new Router({
path: '/dashboard',
name: 'Dashboard',
component: Dashboard
}, {
path: '/cameras',
name: 'Cameras',
component: Cameras
}, {
path: '/settings',
name: 'Settings',

View File

@@ -21,8 +21,10 @@ export default new Vuex.Store({
undoRedo: undoRedo
},
state: {
backendConnected: false,
colorPicking: false,
saveBar: false,
compactMode: undefined, // Compact mode is initially unset on purpose
compactMode: localStorage.getItem("compactMode") === undefined ? undefined : localStorage.getItem("compactMode") === "true", // Compact mode is initially unset on purpose
currentCameraIndex: 0,
selectedOutputs: [0, 1], // 0 indicates normal, 1 indicates threshold
cameraSettings: [ // This is a list of objects representing the settings of all cameras
@@ -42,6 +44,7 @@ export default new Vuex.Store({
}
],
fov: 70.0,
isFovConfigurable: true,
calibrated: false,
currentPipelineSettings: {
pipelineType: 2, // One of "driver", "reflective", "shape"
@@ -72,7 +75,8 @@ export default new Vuex.Store({
offsetRobotOffsetMode: 0,
solvePNPEnabled: false,
targetRegion: 0,
contourTargetOrientation: 1
contourTargetOrientation: 1,
is3D: false,
// Settings that apply to shape
}
@@ -89,10 +93,34 @@ export default new Vuex.Store({
skew: 0,
area: 0,
// 3D only
pose: {x: 0, y: 0, rot: 0},
pose: {x: 0, y: 0, rotation: 0},
}]
}
]
],
settings: {
general: {
version: "Unknown",
// Empty string means unsupported, otherwise the value in the string is the transfer mode
gpuAcceleration: "",
hardwareModel: "Unknown",
hardwarePlatform: "Unknown",
},
networking: {
teamNumber: 0,
supported: true,
// Below options are only configurable if supported is true
connectionType: 0, // 0 = DHCP, 1 = Static
staticIp: "",
netmask: "",
hostname: "photonvision",
},
lighting: {
supported: true,
brightness: 0.0,
},
}
},
mutations: {
saveBar: set('saveBar'),
@@ -103,6 +131,10 @@ export default new Vuex.Store({
networkSettings: set('networkSettings'),
selectedOutputs: set('selectedOutputs'),
is3D: (state, val) => {
state.cameraSettings[state.currentCameraIndex].currentPipelineSettings.is3D = val;
},
currentPipelineIndex: (state, val) => {
const settings = state.cameraSettings[state.currentCameraIndex];
Vue.set(settings, 'currentPipelineIndex', val);
@@ -114,9 +146,7 @@ export default new Vuex.Store({
if (!payload.hasOwnProperty(key)) continue;
const value = payload[key];
const settings = state.cameraSettings[state.currentCameraIndex].currentPipelineSettings;
if (key === "selectedOutputs") console.log(settings);
if (settings.hasOwnProperty(key)) {
if (key === "selectedOutputs") console.log('here');
Vue.set(settings, key, value);
}
}

View File

@@ -1,133 +1,180 @@
<template>
<div>
<div>
<CVselect
v-model="currentCameraIndex"
name="Camera"
:list="$store.getters.cameraList"
@input="handleInput('currentCamera',currentCameraIndex)"
/>
<CVnumberinput
v-model="cameraSettings.fov"
name="Diagonal FOV"
/>
<br>
<CVnumberinput
v-model="cameraSettings.tilt"
name="Camera pitch"
:step="0.01"
/>
<br>
<v-btn
style="margin-top:10px"
small
color="#ffd843"
@click="sendCameraSettings"
<v-row
no-gutters
class="pa-3"
>
<v-col
cols="12"
md="7"
>
Save Camera Settings
</v-btn>
</div>
<div style="margin-top: 15px">
<span>3D Calibration</span>
<v-divider
color="white"
style="margin-bottom: 10px"
/>
<v-row>
<v-col>
<CVselect
v-model="resolutionIndex"
name="Resolution"
:list="stringResolutionList"
/>
</v-col>
<v-col>
<CVnumberinput
v-model="squareSize"
name="Square Size (in)"
/>
</v-col>
</v-row>
<v-row>
<v-col>
<v-btn
small
:color="calibrationModeButton.color"
:disabled="checkResolution"
@click="sendCalibrationMode"
>
{{ calibrationModeButton.text }}
</v-btn>
</v-col>
<v-col>
<v-btn
small
:color="cancellationModeButton.color"
:disabled="checkCancellation"
@click="sendCalibrationFinish"
>
{{ cancellationModeButton.text }}
</v-btn>
</v-col>
<v-col>
<v-btn
color="whitesmoke"
small
@click="downloadBoard"
>
Download Checkerboard
</v-btn>
<a
ref="calibrationFile"
style="color: black; text-decoration: none; display: none"
:href="require('../../assets/chessboard.png')"
download="Calibration Board.png"
/>
</v-col>
</v-row>
<v-row v-if="isCalibrating">
<v-col>
<span>Snapshot Amount: {{ snapshotAmount }}</span>
</v-col>
</v-row>
<div v-if="isCalibrating">
<v-checkbox
v-model="isAdvanced"
label="Advanced Menu"
<v-card
class="mb-3 pr-6 pb-3"
color="primary"
dark
>
<v-card-title>Camera Settings</v-card-title>
<div class="ml-5">
<CVselect
v-model="currentCameraIndex"
name="Camera"
select-cols="10"
:list="$store.getters.cameraList"
@input="handleInput('currentCamera',currentCameraIndex)"
/>
<CVnumberinput
v-if="cameraSettings.isFovConfigurable"
v-model="cameraSettings.fov"
tooltip="Field of view (in degrees) of the camera measured across the diagonal of the frame"
name="Diagonal FOV"
/>
<br>
<CVnumberinput
v-model="cameraSettings.tilt"
name="Camera pitch"
tooltip="How many degrees above the horizontal the physical camera is tilted"
:step="0.01"
/>
<br>
<v-btn
style="margin-top:10px"
small
color="secondary"
@click="sendCameraSettings"
>
<v-icon left>
mdi-content-save
</v-icon>
Save Camera Settings
</v-btn>
</div>
</v-card>
<v-card
class="pr-6 pb-3"
color="primary"
dark
>
<v-card-title>Camera Calibration</v-card-title>
<div class="ml-5">
<v-row>
<v-col cols="8">
<CVselect
v-model="resolutionIndex"
name="Resolution"
:list="stringResolutionList"
/>
</v-col>
<v-col
cols="4"
align-self="center"
>
<CVnumberinput
v-model="squareSize"
name="Square Size (in)"
tooltip="Length of one side of the checkerboard's square in inches"
label-cols="unset"
/>
</v-col>
</v-row>
<v-row>
<v-col>
<v-btn
small
color="secondary"
:disabled="checkResolution"
@click="sendCalibrationMode"
>
{{ calibrationModeButton.text }}
</v-btn>
</v-col>
<v-col>
<v-btn
small
color="red"
:disabled="checkCancellation"
@click="sendCalibrationFinish"
>
{{ cancellationModeButton.text }}
</v-btn>
</v-col>
<v-col>
<v-btn
color="accent"
small
outlined
@click="downloadBoard"
>
<v-icon left>
mdi-download
</v-icon>
Download Checkerboard
</v-btn>
<a
ref="calibrationFile"
style="color: black; text-decoration: none; display: none"
:href="require('../assets/chessboard.png')"
download="Calibration Board.png"
/>
</v-col>
</v-row>
<v-row v-if="isCalibrating">
<v-col>
<span>Snapshot Amount: {{ snapshotAmount }}</span>
</v-col>
</v-row>
<div v-if="isCalibrating">
<v-checkbox
v-model="isAdvanced"
label="Advanced Menu"
dark
/>
<div v-if="isAdvanced">
<CVslider
v-model="$store.getters.pipeline.exposure"
name="Exposure"
:min="0"
:max="100"
@input="e=> handleInput('exposure', e)"
/>
<CVslider
v-model="$store.getters.pipeline.brightness"
name="Brightness"
:min="0"
:max="100"
@input="e=> handleInput('brightness', e)"
/>
<CVslider
v-if="$store.getters.pipeline.gain !== -1"
v-model="$store.getters.pipeline.gain"
name="Gain"
:min="0"
:max="100"
@input="e=> handleInput('gain', e)"
/>
<CVselect
v-model="$store.getters.pipeline.videoModeIndex"
name="FPS"
:list="stringFpsList"
@input="changeFps"
/>
</div>
</div>
</div>
</v-card>
</v-col>
<v-col
class="pl-md-3 pt-3 pt-md-0"
cols="12"
md="5"
>
<CVimage
:address="$store.getters.streamAddress[1]"
:disconnected="!$store.state.backendConnected"
scale="100"
style="border-radius: 5px;"
/>
<div v-if="isAdvanced">
<CVslider
v-model="$store.getters.pipeline.exposure"
name="Exposure"
:min="0"
:max="100"
@input="e=> handleInput('exposure', e)"
/>
<CVslider
v-model="$store.getters.pipeline.brightness"
name="Brightness"
:min="0"
:max="100"
@input="e=> handleInput('brightness', e)"
/>
<CVslider
v-if="$store.getters.pipeline.gain !== -1"
v-model="$store.getters.pipeline.gain"
name="Gain"
:min="0"
:max="100"
@input="e=> handleInput('gain', e)"
/>
<CVselect
v-model="$store.getters.pipeline.videoModeIndex"
name="FPS"
:list="stringFpsList"
@input="changeFps"
/>
</div>
</div>
</div>
</v-col>
</v-row>
<v-snackbar
v-model="snack"
top
@@ -139,16 +186,18 @@
</template>
<script>
import CVselect from '../../components/common/cv-select'
import CVnumberinput from '../../components/common/cv-number-input'
import CVslider from '../../components/common/cv-slider'
import CVselect from '../components/common/cv-select';
import CVnumberinput from '../components/common/cv-number-input';
import CVslider from '../components/common/cv-slider';
import CVimage from "../components/common/cv-image";
export default {
name: 'CameraSettings',
name: 'Cameras',
components: {
CVselect,
CVnumberinput,
CVslider
CVslider,
CVimage
},
data() {
return {
@@ -243,7 +292,7 @@
},
cameraSettings: {
get() {
return this.$store.getters.cameraSettings;
return this.$store.getters.currentCameraSettings;
},
set(value) {
this.$store.commit('cameraSettings', value);
@@ -252,7 +301,7 @@
},
methods: {
downloadBoard() {
this.axios.get("http://" + this.$address + require('../../assets/chessboard.png'), {responseType: 'blob'}).then((response) => {
this.axios.get("http://" + this.$address + require('../assets/chessboard.png'), {responseType: 'blob'}).then((response) => {
require('downloadjs')(response.data, "Calibration Board", "image/png")
})
},

View File

@@ -11,42 +11,58 @@
>
<v-col
cols="12"
:class="['pb-3 ', $store.getters.isDriverMode ? '' : 'pr-lg-3']"
:lg="$store.getters.isDriverMode ? 12 : 8"
:class="['pb-3 ', 'pr-lg-3']"
lg="8"
align-self="stretch"
>
<v-card
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: 10%;"
style="height: 15%; min-height: 50px;"
>
Cameras
<div>
Cameras <span
class="pl-2 caption grey--text text--lighten-2"
style="line-height: 220%; display: inline-block; vertical-align: bottom;"
>{{ parseFloat(fps).toFixed(2) }} FPS</span>
</div>
<v-switch
v-model="driverMode"
label="Driver Mode"
style="margin-left: auto;"
color="accent"
/>
</v-card-title>
<v-row
align="center"
style="height: 90%;"
>
<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%;"
>
<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="300px"
max-height-md="320px"
max-height-xl="450px"
: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"
/>
<span style="position: absolute; top: 2%; left: 2%; font-size: 28px; -webkit-text-stroke: 1px black;">{{ parseFloat(fps).toFixed(2) }}</span>
<!-- <span class="fps-indicator">{{ parseFloat(fps).toFixed(2) }}</span>-->
</div>
</v-col>
</v-row>
@@ -55,7 +71,7 @@
<v-col
cols="12"
class="pb-3"
:lg="$store.getters.isDriverMode ? 12 : 4"
lg="4"
align-self="stretch"
>
<v-card
@@ -64,7 +80,7 @@
<camera-and-pipeline-select />
</v-card>
<v-card
v-if="!$store.getters.isDriverMode"
:disabled="$store.getters.isDriverMode || $store.state.colorPicking"
class="mt-3"
color="primary"
>
@@ -82,14 +98,18 @@
dark
class="fill"
>
<v-btn color="secondary">
<v-icon>mdi-cube-outline</v-icon>
<span>3D</span>
</v-btn>
<v-btn color="secondary">
<v-btn
color="secondary"
>
<v-icon>mdi-crop-square</v-icon>
<span>2D</span>
</v-btn>
<v-btn
color="secondary"
>
<v-icon>mdi-cube-outline</v-icon>
<span>3D</span>
</v-btn>
</v-btn-toggle>
</v-col>
<v-col lg="12">
@@ -146,7 +166,7 @@
slider-color="accent"
>
<v-tab
v-for="(tab, i) in tabs.filter(it => it.name !== '3D' || is3D)"
v-for="(tab, i) in tabs.filter(it => it.name !== '3D' || $store.getters.currentPipelineSettings.is3D)"
:key="i"
>
{{ tab.name }}
@@ -154,12 +174,10 @@
</v-tabs>
<div class="pl-4 pr-4 pt-2">
<keep-alive>
<!-- vision component -->
<component
:is="(tabs[selectedTabs[idx]] || tabs[0]).component"
ref="component"
:ref="(tabs[selectedTabs[idx]] || tabs[0]).name"
v-model="$store.getters.pipeline"
:is3d="is3D"
@update="$emit('save')"
/>
</keep-alive>
@@ -211,12 +229,20 @@
},
data() {
return {
selectedTabs: [0, 0, 0, 0],
selectedTabsData: [0, 0, 0, 0],
snackbar: false,
is3D: false,
counterData: 0,
}
},
computed: {
selectedTabs: {
get() {
return this.$store.getters.isDriverMode ? [0] : this.selectedTabsData;
},
set(value) {
this.selectedTabsData = value;
}
},
tabGroups: {
get() {
let tabs = {
@@ -248,10 +274,10 @@
// 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.state.compactMode || this.$store.getters.isDriverMode) {
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) {
} 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];
@@ -273,10 +299,20 @@
},
processingMode: {
get() {
return this.is3D ? 0 : 1;
return this.$store.getters.currentPipelineSettings.is3D ? 1 : 0;
},
set(value) {
this.is3D = value === 0;
this.$store.getters.currentPipelineSettings.is3D = value === 1;
this.handlePipelineUpdate("is3D", 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: {
@@ -284,7 +320,9 @@
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.getters.isDriverMode) {
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
@@ -324,9 +362,12 @@
},
methods: {
onImageClick(event) {
if (this.selectedTab === 1) {
this.$refs.component.onClick(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)
},
}
}
@@ -343,12 +384,12 @@
height: 100%;
}
.colsClass {
padding: 0 !important;
}
.videoClass {
text-align: center;
.fps-indicator {
position: absolute;
top: 2%;
left: 2%;
font-size: 1.75rem;
text-shadow: 1px 1px 5px rgba(1, 1, 1, 0.65);
}
th {

View File

@@ -12,6 +12,7 @@
<CVrangeSlider
v-model="contourRatio"
name="Ratio (W/H)"
tooltip="Min and max ratio between the width and height of a contour's bounding rectangle"
min="0"
max="100"
step="0.1"
@@ -21,6 +22,7 @@
<CVrangeSlider
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')"
@@ -29,6 +31,7 @@
<CVslider
v-model="contourSpecklePercentage"
name="Speckle Rejection"
tooltip="Rejects contours whose average area is less than the given percentage of the average area of all the other contours"
min="0"
max="100"
:slider-cols="largeBox"
@@ -37,7 +40,8 @@
/>
<CVselect
v-model="contourGroupingMode"
name="Target Group"
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('targetGroup')"
@@ -46,12 +50,22 @@
<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)"
/>
<CVselect
v-model="contourSortMode"
name="Target Sort"
tooltip="Chooses the sorting mode used to determine the 'best' targets to provide to user code"
:select-cols="largeBox"
:list="['Largest','Smallest','Highest','Lowest','Rightmost','Leftmost','Centermost']"
@input="handlePipelineData('contourSortMode')"
@rollback="e => rollback('contourSortMode', e)"
/>
</div>
</template>
@@ -122,6 +136,14 @@
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

View File

@@ -5,6 +5,7 @@
name="Exposure"
min="0"
max="100"
tooltip="Directly controls how much light is allowed to fall onto the sensor, which affects brightness"
:slider-cols="largeBox"
@input="handlePipelineData('cameraExposure')"
@rollback="e => rollback('cameraExposure', e)"
@@ -14,6 +15,7 @@
name="Brightness"
min="0"
max="100"
tooltip="Controls camera postprocessing that brightens or darkens the image uniformly"
:slider-cols="largeBox"
@input="handlePipelineData('cameraBrightness')"
@rollback="e => rollback('cameraBrightness', e)"
@@ -24,6 +26,7 @@
name="Gain"
min="0"
max="100"
tooltip="Controls automatic white balance gain, which affects how the camera captures colors in different conditions"
:slider-cols="largeBox"
@input="handlePipelineData('cameraGain')"
@rollback="e => rollback('cameraGain', e)"
@@ -31,6 +34,7 @@
<CVselect
v-model="inputImageRotationMode"
name="Orientation"
tooltip="Rotates the camera stream"
:list="['Normal','90° CW','180°','90° CCW']"
:select-cols="largeBox"
@input="handlePipelineData('inputImageRotationMode')"
@@ -39,6 +43,7 @@
<CVselect
v-model="cameraVideoModeIndex"
name="Resolution"
tooltip="Resolution and FPS the camera should directly capture at"
:list="resolutionList"
:select-cols="largeBox"
@input="handlePipelineData('cameraVideoModeIndex')"
@@ -47,6 +52,7 @@
<CVselect
v-model="streamingFrameDivisor"
name="Stream Resolution"
tooltip="Resolution to which camera frames are downscaled for streaming to the dashboard"
:list="streamResolutionList"
:select-cols="largeBox"
@input="handlePipelineData('streamingFrameDivisor')"

View File

@@ -1,18 +1,12 @@
<template>
<div>
<span>Contour Sorting</span>
<span>Target Manipulation</span>
<v-divider class="mt-2" />
<CVselect
v-model="contourSortMode"
name="Sort Mode"
:list="['Largest','Smallest','Highest','Lowest','Rightmost','Leftmost','Centermost']"
@input="handlePipelineData('contourSortMode')"
@rollback="e => rollback('contourSortMode', e)"
/>
<CVselect
v-model="contourTargetOffsetPointEdge"
name="Target Offset Point"
tooltip="Changes where the 'center' of the target is (used for calculating e.g. pitch and yaw)"
:list="['Center','Top','Bottom','Left','Right']"
@input="handlePipelineData('contourTargetOffsetPointEdge')"
@rollback="e=> rollback('contourTargetOffsetPointEdge', e)"
@@ -21,6 +15,7 @@
<CVselect
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)"
:list="['Portrait', 'Landscape']"
@input="handlePipelineData('contourTargetOrientation')"
@rollback="e=> rollback('contourTargetOrientation', e)"
@@ -29,7 +24,9 @@
<CVswitch
v-model="outputShowMultipleTargets"
name="Show Multiple Targets"
tooltip="If enabled, up to five targets will be displayed and sent to user code"
class="mb-4"
text-cols="3"
@input="handlePipelineData('outputShowMultipleTargets')"
@rollback="e=> rollback('outputShowMultipleTargets', e)"
@@ -39,6 +36,7 @@
<CVselect
v-model="offsetRobotOffsetMode"
name="Robot Offset Mode"
tooltip="Used to add an arbitrary offset to the location of the targeting crosshair"
:list="['None','Single Point','Dual Point']"
@input="handlePipelineData('offsetRobotOffsetMode')"
@rollback="e=> rollback('offsetRobotOffsetMode',e)"
@@ -93,16 +91,6 @@
}
},
computed: {
contourSortMode: {
get() {
return this.$store.getters.currentPipelineSettings.contourSortMode
},
set(val) {
this.$store.commit("mutatePipeline", {"contourSortMode": val});
}
},
contourTargetOffsetPointEdge: {
get() {
return this.$store.getters.currentPipelineSettings.contourTargetOffsetPointEdge
@@ -138,13 +126,13 @@
selectedComponent: {
get() {
switch (this.value.calibrationMode) {
switch (this.offsetRobotOffsetMode) {
case 0:
return "";
return null;
case 1:
return "Single Point";
return SingleCalibration;
case 2:
return "Dual Point"
return DualCalibration;
}
return ""
}

View File

@@ -1,6 +1,6 @@
<template>
<div>
<!-- Special hidden upload input that gets 'clicked' when the user selects the right dropdown item' -->
<!-- Special hidden upload input that gets 'clicked' when the user selects the right dropdown item -->
<input
ref="file"
type="file"
@@ -21,7 +21,6 @@
item-value="data"
@change="onModelSelect"
/>
<v-divider />
<CVslider
v-model="value.accuracy"
class="pt-2"
@@ -33,7 +32,6 @@
@input="handleData('accuracy')"
@rollback="e => rollback('accuracy', e)"
/>
<v-divider class="pb-2" />
<mini-map
class="miniMapClass"
:targets="targets"
@@ -77,7 +75,7 @@
computed: {
targets: {
get() {
return "FIXME"; // TODO fix
return this.$store.getters.currentPipelineResults.targets;
}
},
horizontalFOV: {
@@ -100,7 +98,7 @@
let tmp = [];
for (let t in FRCtargetsConfig) {
if (FRCtargetsConfig.hasOwnProperty(t)) {
tmp.push({name: t, data: FRCtargetsConfig[t]})
tmp.push({name: t, data: FRCtargetsConfig[t]});
}
}

View File

@@ -13,12 +13,12 @@
dark
>
<template v-slot:default>
<thead style="font-size: 20px;">
<thead style="font-size: 1.25rem;">
<tr>
<th class="text-center">
Target
</th>
<template v-if="!is3D">
<template v-if="!$store.getters.currentPipelineSettings.is3D">
<th class="text-center">
Pitch
</th>
@@ -32,7 +32,7 @@
<th class="text-center">
Area
</th>
<template v-if="is3D">
<template v-if="$store.getters.currentPipelineSettings.is3D">
<th class="text-center">
X
</th>
@@ -51,17 +51,17 @@
:key="index"
>
<td>{{ index }}</td>
<template v-if="!is3D">
<template v-if="!$store.getters.currentPipelineSettings.is3D">
<td>{{ parseFloat(value.pitch).toFixed(2) }}</td>
<td>{{ parseFloat(value.yaw).toFixed(2) }}</td>
<td>{{ parseFloat(value.skew).toFixed(2) }}</td>
</template>
<td>{{ parseFloat(value.area).toFixed(2) }}</td>
<template v-if="is3D">
<template v-if="$store.getters.currentPipelineSettings.is3D">
<!-- TODO: Make sure that units are correct -->
<td>{{ parseFloat(value.pose.x).toFixed(2) }}&nbsp;m</td>
<td>{{ parseFloat(value.pose.y).toFixed(2) }}&nbsp;m</td>
<td>{{ parseFloat(value.pose.rot).toFixed(2) }}&deg;</td>
<td>{{ parseFloat(value.pose.rotation).toFixed(2) }}&deg;</td>
</template>
</tr>
</tbody>
@@ -74,9 +74,6 @@
<script>
export default {
name: "TargetsTab",
props: {
is3D: Boolean,
}
}
</script>
@@ -97,6 +94,10 @@
font-size: 1rem !important;
}
.v-data-table td {
font-family: monospace !important;
}
/** This is unfortunately the only way to override table background color **/
.theme--dark.v-data-table tbody tr:hover:not(.v-data-table__expanded__content):not(.v-data-table__empty-wrapper) {
background: #005281;

View File

@@ -3,6 +3,7 @@
<CVrangeSlider
v-model="hsvHue"
name="Hue"
tooltip="Describes color"
:min="0"
:max="180"
@input="handlePipelineData('hsvHue')"
@@ -11,6 +12,7 @@
<CVrangeSlider
v-model="hsvSaturation"
name="Saturation"
tooltip="Describes colorfulness; the smaller this value the 'whiter' the color becomes"
:min="0"
:max="255"
@input="handlePipelineData('hsvSaturation')"
@@ -19,53 +21,81 @@
<CVrangeSlider
v-model="hsvValue"
name="Value"
tooltip="Describes lightness; the smaller this value the 'blacker' the color becomes"
:min="0"
:max="255"
@input="handlePipelineData('hsvValue')"
@rollback="e => rollback('value',e)"
/>
<div class="pt-3 white--text">
Color Picker
</div>
<v-divider
class="mt-3"
/>
<v-row justify="center">
<v-btn
color="accent"
class="ma-5 black--text"
small
@click="setFunction(1)"
>
<v-icon>colorize</v-icon>
Eye drop
</v-btn>
<v-btn
color="accent"
class="ma-5 black--text"
small
@click="setFunction(2)"
>
<v-icon>add</v-icon>
Expand Selection
</v-btn>
<v-btn
color="accent"
class="ma-5 black--text"
small
@click="setFunction(3)"
>
<v-icon>remove</v-icon>
Shrink Selection
</v-btn>
<v-row
justify="center"
class="mt-3 mb-3"
>
<template v-if="!$store.state.colorPicking">
<v-btn
color="accent"
class="ma-2 black--text"
small
@click="setFunction(3)"
>
<v-icon left>
mdi-minus
</v-icon>
Shrink Range
</v-btn>
<v-btn
color="accent"
class="ma-2 black--text"
small
@click="setFunction(1)"
>
<v-icon left>
mdi-plus-minus
</v-icon>
Set To Average
</v-btn>
<v-btn
color="accent"
class="ma-2 black--text"
small
@click="setFunction(2)"
>
<v-icon left>
mdi-plus
</v-icon>
Expand Range
</v-btn>
</template>
<template v-else>
<v-btn
color="accent"
class="ma-2 black--text"
style="width: 30%;"
small
@click="setFunction(0)"
>
Cancel
</v-btn>
</template>
</v-row>
<v-divider />
<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)"
/>
@@ -143,14 +173,27 @@
methods: {
onClick(event) {
if (this.currentFunction !== undefined) {
this.colorPicker.initColorPicker();
let s = this.$store.getters.currentPipelineSettings;
let hsvArray = this.colorPicker.colorPickerClick(event, this.currentFunction,
[[this.value.hue[0], this.value.saturation[0], this.value.value[0]], [this.value.hue[1], this.value.saturation[1], this.value.value[1]]]);
[
[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;
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': [hsvArray[0][0], hsvArray[1][0]],
'hsvSaturation': [hsvArray[0][1], hsvArray[1][1]],
'hsvValue': [hsvArray[0][2], hsvArray[1][2]],
'hsvHue': s.hsvHue,
'hsvSaturation': s.hsvSaturation,
'hsvValue': s.hsvValue,
'outputShowThresholded': this.showThresholdState,
'cameraIndex': this.$store.state.currentCameraIndex
}
@@ -160,15 +203,11 @@
}
},
setFunction(index) {
this.showThresholdState = this.value.outputShowThresholded;
if (this.showThresholdState === true) {
this.value.outputShowThresholded = false;
this.handlePipelineData('outputShowThresholded')
}
switch (index) {
case 0:
this.currentFunction = undefined;
break;
this.$store.state.colorPicking = false;
return;
case 1:
this.currentFunction = this.colorPicker.eyeDrop;
break;
@@ -179,6 +218,7 @@
this.currentFunction = this.colorPicker.shrink;
break;
}
this.$store.state.colorPicking = true;
}
}
}

View File

@@ -1,64 +1,73 @@
<template>
<div>
<v-row>
<v-row
class="pa-3"
no-gutters
>
<v-col
class="colsClass"
cols="6"
cols="12"
style="max-width: 1400px"
>
<v-tabs
v-model="selectedTab"
background-color="#232c37"
dark
fixed-tabs
height="50"
slider-color="#ffd843"
<v-form
ref="form"
v-model="valid"
>
<v-tab to="">
General
</v-tab>
<v-tab to="">
Cameras
</v-tab>
</v-tabs>
<div style="padding-left:30px">
<component
:is="selectedComponent"
@update="$emit('save')"
/>
</div>
</v-col>
<v-col
v-show="selectedTab === 1"
class="colsClass"
>
<div class="videoClass">
<cvImage
:address="$store.getters.streamAddress"
:scale="75"
/>
</div>
<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-col>
</v-row>
<v-snackbar
v-model="snack"
top
:color="snackbar.color"
>
<span>{{ snackbar.text }}</span>
</v-snackbar>
</div>
</template>
<script>
import General from './SettingsViews/General'
import Cameras from './SettingsViews/Cameras'
import Networking from './SettingsViews/Networking'
import Lighting from "./SettingsViews/Lighting";
import cvImage from '../components/common/cv-image'
import General from "./SettingsViews/General";
export default {
name: 'SettingsTab',
components: {
cvImage,
General,
Cameras,
// General,
},
data() {
return {
selectedTab: 0,
tabList: [General, Cameras]
valid: true, // Are all settings valid
snack: false,
snackbar: {
color: "accent",
text: ""
},
}
},
computed: {
@@ -67,6 +76,39 @@
return this.tabList[this.selectedTab];
}
},
settings: {
get() {
return this.$store.state.settings;
}
},
tabList: {
get() {
return [General, Networking].concat(this.$store.state.settings.lighting.supported ? Lighting : []);
}
}
},
methods: {
sendGeneralSettings() {
const self = this;
this.axios.post("http://" + this.$address + "/api/settings/general", this.settings).then(
function (response) {
if (response.status === 200) {
self.snackbar = {
color: "success",
text: "Settings updated successfully"
};
self.snack = true;
}
},
function (error) {
self.snackbar = {
color: "error",
text: (error.response || {data: "Couldn't save settings"}).data
};
self.snack = true;
}
)
},
}
}
</script>
@@ -81,8 +123,4 @@
height: auto !important;
vertical-align: middle;
}
.colsClass {
padding: 0 !important;
}
</style>

View File

@@ -1,86 +1,55 @@
<template>
<div>
<div style="margin-top: 15px">
<span>General Settings:</span>
<v-divider color="white" />
</div>
<CVnumberinput
v-model="settings.teamNumber"
name="Team Number"
/>
<CVradio
v-model="settings.connectionType"
:list="['DHCP','Static']"
/>
<v-divider color="white" />
<CVinput
v-model="settings.ip"
name="IP"
:disabled="isDisabled"
/>
<CVinput
v-model="settings.netmask"
name="NetMask"
:disabled="isDisabled"
/>
<CVinput
v-model="settings.gateway"
name="Gateway"
:disabled="isDisabled"
/>
<v-divider color="white" />
<CVinput
v-model="settings.hostname"
name="Hostname"
/>
<v-btn
style="margin-top:10px"
small
color="#ffd843"
@click="sendGeneralSettings"
>
Save General Settings
</v-btn>
<div style="margin-top: 20px">
<span>Install or Update:</span>
<v-divider color="white" />
</div>
<div v-if="!isLoading">
<v-row
dense
align="center"
<span>Version: {{ settings.version }}</span>
&mdash;
<span>Hardware model: {{ settings.hardwareModel }}</span>
&mdash;
<span>Platform: {{ settings.hardwarePlatform }}</span>
&mdash;
<span>GPU Acceleration: {{ settings.gpuAcceleration ? "Enabled" : "Unsupported" }}{{ settings.gpuAcceleration ? " (" + settings.gpuAcceleration + " mode)" : "" }}</span>
<v-row>
<v-col
cols="12"
sm="6"
lg="4"
>
<v-col :cols="3">
<span>Choose a newer version: </span>
</v-col>
<v-col :cols="6">
<v-file-input
v-model="file"
accept=".jar"
dark
/>
</v-col>
</v-row>
<v-btn
small
@click="installOrUpdate"
<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="4"
>
{{ fileUploadText }}
</v-btn>
</div>
<div
v-else
style="text-align: center; margin-top: 20px"
>
<v-progress-circular
color="white"
:indeterminate="true"
size="32"
width="4"
/>
<br>
<span>Please wait this may take a while</span>
</div>
<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="4"
>
<v-btn
color="red"
@click="restartDevice"
>
<v-icon left>
mdi-restart
</v-icon> Restart Device
</v-btn>
</v-col>
</v-row>
<v-snackbar
v-model="snack"
top
@@ -88,101 +57,70 @@
>
<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="/api/settings/export"
download="photonvision-settings.zip"
/>
</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'
export default {
name: 'General',
components: {
CVnumberinput,
CVradio,
CVinput
},
data() {
return {
file: undefined,
snackbar: {
color: "success",
text: ""
},
snack: false,
isLoading: false
}
return {
snack: false,
snackbar: {
color: "success",
text: ""
},
}
},
computed: {
fileUploadText() {
if (this.file !== undefined) {
return "Update and run at startup"
} else {
return "Run current version at startup"
}
},
isDisabled() {
return this.settings.connectionType === 0;
},
settings: {
get() {
return this.$store.state.settings;
}
}
settings() {
return this.$store.state.settings.general;
}
},
methods: {
sendGeneralSettings() {
const self = this;
this.axios.post("http://" + this.$address + "/api/settings/general", this.settings).then(
function (response) {
if (response.status === 200) {
self.snackbar = {
color: "success",
text: "Save successful, Please restart for changes to take action"
};
self.snack = true;
}
},
function (error) {
self.snackbar = {
color: "error",
text: error.response.data
};
self.snack = true;
}
)
},
installOrUpdate() {
let formData = new FormData();
formData.append('file', this.file);
if (this.file !== undefined) {
this.isLoading = true;
}
this.axios.post("http://" + this.$address + "/api/install", formData, {
headers: {
'Content-Type': 'multipart/form-data'
}
}).then(() => {
this.snackbar = {
color: "success",
text: "Installation successful"
};
this.isLoading = false;
this.snack = true;
}).catch(error => {
this.snackbar = {
color: "error",
text: error.response.data
};
this.isLoading = false;
this.snack = true;
})
}
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",
};
}).catch(() => {
this.snackbar = {
color: "error",
text: "Couldn't import settings",
}
});
this.snack = true;
},
restartDevice() {
this.axios.post("http://" + this.$address + "/api/restart");
}
}
}
</script>
<style lang="" scoped>
<style lang="css" scoped>
.v-btn {
width: 100%;
}
</style>

View File

@@ -0,0 +1,37 @@
<template>
<div>
<CVslider
v-model="settings.brightness"
class="pt-2"
slider-cols="12"
name="Brightness"
min="0"
max="100"
@input="handleData('accuracy')"
@rollback="e => rollback('accuracy', e)"
/>
</div>
</template>
<script>
import CVslider from "../../components/common/cv-slider";
export default {
name: 'LEDs',
components: {
CVslider,
},
computed: {
isDHCP() {
return this.settings.connectionType === 0;
},
settings() {
return this.$store.state.settings.lighting;
}
},
}
</script>
<style lang="" scoped>
</style>

View File

@@ -0,0 +1,107 @@
<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.networking.supported">
<CVradio
v-model="settings.connectionType"
:list="['DHCP','Static']"
/>
<template v-if="!isDHCP">
<CVinput
v-model="settings.ip"
:input-cols="inputCols"
:rules="[v => isIPv4(v) || 'Invalid IPv4 address']"
name="IP"
/>
<CVinput
v-model="settings.netmask"
:input-cols="inputCols"
:rules="[v => isSubnetMask(v) || 'Invalid subnet mask']"
name="Subnet Mask"
/>
</template>
</template>
<CVinput
v-model="settings.hostname"
:input-cols="inputCols"
:rules="[v => isHostname(v) || 'Invalid hostname']"
name="Hostname"
/>
</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'
// 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
},
data() {
return {
file: undefined,
snackbar: {
color: "success",
text: ""
},
snack: false,
isLoading: false
}
},
computed: {
inputCols() {
return this.$vuetify.breakpoint.smAndUp ? 10 : 7;
},
isDHCP() {
return this.settings.connectionType === 0;
},
settings() {
return this.$store.state.settings.networking;
}
},
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--) {
for (let j = 0; j < 8; j++) {
let bitValue = (octets[i] >>> j & 1) == 1;
if (restAreOnes && !bitValue)
return false;
restAreOnes = bitValue;
}
}
return true;
}
},
}
</script>
<style lang="" scoped>
</style>

View File

@@ -1 +1 @@
<p>UI has not been copied!</p>
<p>UI has not been copied!</p>