mirror of
https://github.com/PhotonVision/photonvision
synced 2026-07-02 02:51:40 +00:00
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:
committed by
GitHub
parent
0b98dc3c9f
commit
19b57235fe
5
photon-client/package-lock.json
generated
5
photon-client/package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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>
|
||||
68
photon-client/src/assets/eyedropper.svg
Normal file
68
photon-client/src/assets/eyedropper.svg
Normal 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 |
BIN
photon-client/src/assets/noStream.jpg
Normal file
BIN
photon-client/src/assets/noStream.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 48 KiB |
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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() {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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>
|
||||
|
||||
26
photon-client/src/components/common/cv-tooltipped-label.vue
Normal file
26
photon-client/src/components/common/cv-tooltipped-label.vue
Normal 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>
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
})
|
||||
},
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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')"
|
||||
|
||||
@@ -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 ""
|
||||
}
|
||||
|
||||
@@ -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]});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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) }} m</td>
|
||||
<td>{{ parseFloat(value.pose.y).toFixed(2) }} m</td>
|
||||
<td>{{ parseFloat(value.pose.rot).toFixed(2) }}°</td>
|
||||
<td>{{ parseFloat(value.pose.rotation).toFixed(2) }}°</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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
—
|
||||
<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="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>
|
||||
37
photon-client/src/views/SettingsViews/Lighting.vue
Normal file
37
photon-client/src/views/SettingsViews/Lighting.vue
Normal 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>
|
||||
107
photon-client/src/views/SettingsViews/Networking.vue
Normal file
107
photon-client/src/views/SettingsViews/Networking.vue
Normal 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>
|
||||
@@ -1 +1 @@
|
||||
<p>UI has not been copied!</p>
|
||||
<p>UI has not been copied!</p>
|
||||
|
||||
Reference in New Issue
Block a user