[PhotonClient] Vite and Typescript complete refactor (#884)

This commit is contained in:
Sriman Achanta
2023-08-21 01:51:35 -04:00
committed by GitHub
parent 8397b43bef
commit f623e4a1cc
119 changed files with 11821 additions and 19318 deletions

View File

@@ -0,0 +1,59 @@
<script setup lang="ts">
import CamerasCard from "@/components/cameras/CameraSettingsCard.vue";
import CalibrationCard from "@/components/cameras/CameraCalibrationCard.vue";
import { useCameraSettingsStore } from "@/stores/settings/CameraSettingsStore";
import { computed } from "vue";
import CamerasView from "@/components/cameras/CamerasView.vue";
import { useStateStore } from "@/stores/StateStore";
const cameraViewType = computed<number[]>({
get: (): number[] => {
// Only show the input stream in Color Picking Mode
if(useStateStore().colorPickingMode) return [0];
// Only show the output stream in Driver Mode or Calibration Mode
if(useCameraSettingsStore().isDriverMode || useCameraSettingsStore().isCalibrationMode) return [1];
const ret: number[] = [];
if(useCameraSettingsStore().currentPipelineSettings.inputShouldShow) {
ret.push(0);
}
if(useCameraSettingsStore().currentPipelineSettings.outputShouldShow) {
ret.push(1);
}
if(ret.length === 0) return [0];
return ret;
},
set: v => {
useCameraSettingsStore().currentPipelineSettings.inputShouldShow = v.includes(0);
useCameraSettingsStore().currentPipelineSettings.outputShouldShow = v.includes(1);
useCameraSettingsStore().changeCurrentPipelineSetting({ inputShouldShow: v.includes(0) }, false);
}
});
</script>
<template>
<div>
<v-row
no-gutters
class="pa-3"
>
<v-col
cols="12"
md="7"
>
<CamerasCard />
<CalibrationCard />
</v-col>
<v-col
class="pl-md-3 pt-3 pt-md-0"
cols="12"
md="5"
>
<CamerasView v-model="cameraViewType" />
</v-col>
</v-row>
</div>
</template>

View File

@@ -1,833 +0,0 @@
<template>
<div>
<v-row
no-gutters
class="pa-3"
>
<v-col
cols="12"
md="7"
>
<!-- Camera card -->
<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"
:list="$store.getters.cameraList"
:select-cols="$vuetify.breakpoint.mdAndUp ? 10 : 7"
@input="handleInput('currentCamera',currentCameraIndex)"
/>
<CVnumberinput
v-model="cameraSettings.fov"
:tooltip="cameraSettings.isFovConfigurable ? 'Field of view (in degrees) of the camera measured across the diagonal of the frame, in a video mode which covers the whole sensor area.' : 'This setting is managed by a vendor'"
name="Maximum Diagonal FOV"
:disabled="!cameraSettings.isFovConfigurable"
:label-cols="$vuetify.breakpoint.mdAndUp ? undefined : 7"
/>
<br>
<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>
<!-- Calibration 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>
<!-- Calibration input -->
<v-col
cols="12"
md="6"
>
<v-form
ref="form"
v-model="settingsValid"
>
<CVselect
v-model="selectedFilteredResIndex"
name="Resolution"
select-cols="7"
:list="stringResolutionList"
:disabled="isCalibrating"
tooltip="Resolution to calibrate at (you will have to calibrate every resolution you use 3D mode on)"
/>
<CVselect
v-model="streamingFrameDivisor"
name="Decimation"
tooltip="Resolution to which camera frames are downscaled for detection. Calibration still uses full-res"
:list="calibrationDivisors"
select-cols="7"
@rollback="e => rollback('streamingFrameDivisor', e)"
/>
<CVselect
v-model="boardType"
name="Board Type"
select-cols="7"
:list="['Chessboard', 'Dot Grid']"
:disabled="isCalibrating"
tooltip="Calibration board pattern to use"
/>
<CVnumberinput
v-model="squareSizeIn"
name="Pattern Spacing (in)"
tooltip="Spacing between pattern features in inches"
:disabled="isCalibrating"
:rules="[v => (v > 0) || 'Size must be positive']"
:label-cols="$vuetify.breakpoint.mdAndUp ? 5 : 7"
/>
<CVnumberinput
v-model="boardWidth"
name="Board Width"
tooltip="Width of the board in dots or chessboard squares"
:disabled="isCalibrating"
:rules="[v => (v >= 4) || 'Width must be at least 4']"
:label-cols="$vuetify.breakpoint.mdAndUp ? 5 : 7"
/>
<CVnumberinput
v-model="boardHeight"
name="Board Height"
tooltip="Height of the board in dots or chessboard squares"
:disabled="isCalibrating"
:rules="[v => (v >= 4) || 'Height must be at least 4']"
:label-cols="$vuetify.breakpoint.mdAndUp ? 5 : 7"
/>
</v-form>
</v-col>
<!-- Calibrated table -->
<v-col
cols="12"
md="6"
>
<v-row
align="start"
class="pb-4"
>
<v-simple-table
fixed-header
height="100%"
dense
>
<thead style="font-size: 1.25rem;">
<tr>
<th class="text-center">
<tooltipped-label text="Resolution" />
</th>
<th class="text-center">
<tooltipped-label
tooltip="Average reprojection error of the calibration, in pixels"
text="Mean Error"
/>
</th>
<th class="text-center">
<tooltipped-label
tooltip="Standard deviation of the mean error, in pixels"
text="Standard Deviation"
/>
</th>
<th class="text-center">
<tooltipped-label
tooltip="Estimated Horizontal FOV, in degrees"
text="Horizontal FOV"
/>
</th>
<th class="text-center">
<tooltipped-label
tooltip="Estimated Vertical FOV, in degrees"
text="Vertical FOV"
/>
</th>
<th class="text-center">
<tooltipped-label
tooltip="Estimated Diagonal FOV, in degrees"
text="Diagonal FOV"
/>
</th>
</tr>
</thead>
<tbody>
<tr
v-for="(value, index) in filteredResolutionList"
:key="index"
>
<td> {{ value.width }} X {{ value.height }}</td>
<td>
{{ isCalibrated(value) ? value.mean.toFixed(2) + "px" : "—" }}
</td>
<td> {{ isCalibrated(value) ? value.standardDeviation.toFixed(2) + "px" : "—" }} </td>
<td> {{ isCalibrated(value) ? value.horizontalFOV.toFixed(2) + "°" : "—" }} </td>
<td> {{ isCalibrated(value) ? value.verticalFOV.toFixed(2) + "°" : "—" }} </td>
<td> {{ isCalibrated(value) ? value.diagonalFOV.toFixed(2) + "°" : "—" }} </td>
</tr>
</tbody>
</v-simple-table>
</v-row>
<v-row justify="center">
<v-chip
v-show="isCalibrating"
label
:color="snapshotAmount < 25 ? 'grey' : 'secondary'"
>
Snapshots: {{ snapshotAmount }} of at least {{ minSnapshots }}
</v-chip>
</v-row>
</v-col>
</v-row>
<v-row v-if="isCalibrating">
<v-col
cols="12"
class="pt-0"
>
<CVslider
v-model="$store.getters.currentPipelineSettings.cameraExposure"
:disabled="$store.getters.currentPipelineSettings.cameraAutoExposure"
name="Exposure"
:min="0"
:max="100"
slider-cols="8"
step="0.1"
tooltip="Directly controls how much light is allowed to fall onto the sensor, which affects apparent brightness"
@input="e => handlePipelineUpdate('cameraExposure', e)"
/>
<CVslider
v-model="$store.getters.currentPipelineSettings.cameraBrightness"
name="Brightness"
:min="0"
:max="100"
slider-cols="8"
@input="e => handlePipelineUpdate('cameraBrightness', e)"
/>
<CVswitch
v-model="$store.getters.currentPipelineSettings.cameraAutoExposure"
class="pt-2"
name="Auto Exposure"
tooltip="Enables or Disables camera automatic adjustment for current lighting conditions"
@input="e => handlePipelineUpdate('cameraAutoExposure', e)"
/>
<CVslider
v-if="cameraGain >= 0"
v-model="cameraGain"
name="Camera Gain"
min="0"
max="100"
tooltip="Controls camera gain, similar to brightness"
:slider-cols="largeBox"
@input="handlePipelineData('cameraGain')"
@rollback="e => rollback('cameraGain', e)"
/>
<CVslider
v-if="$store.getters.currentPipelineSettings.cameraRedGain !== -1"
v-model="$store.getters.currentPipelineSettings.cameraRedGain"
name="Red AWB Gain"
min="0"
max="100"
tooltip="Controls red automatic white balance gain, which affects how the camera captures colors in different conditions"
:slider-cols="8"
@input="e => handlePipelineData('cameraRedGain', e)"
/>
<CVslider
v-if="$store.getters.currentPipelineSettings.cameraBlueGain !== -1"
v-model="$store.getters.currentPipelineSettings.cameraBlueGain"
name="Blue AWB Gain"
min="0"
max="100"
tooltip="Controls blue automatic white balance gain, which affects how the camera captures colors in different conditions"
:slider-cols="8"
@input="e => handlePipelineData('cameraBlueGain', e)"
/>
</v-col>
</v-row>
<v-row>
<v-col align-self="center">
<v-btn
small
color="secondary"
style="width: 100%;"
:disabled="disallowCalibration"
@click="sendCalibrationMode"
>
{{ isCalibrating ? "Take Snapshot" : "Start Calibration" }}
</v-btn>
</v-col>
<v-col align-self="center">
<v-btn
small
:color="hasEnough ? 'accent' : 'red'"
:class="hasEnough ? 'black--text' : 'white---text'"
style="width: 100%;"
:disabled="checkCancellation"
@click="sendCalibrationFinish"
>
{{ hasEnough ? "Finish Calibration" : "Cancel Calibration" }}
</v-btn>
</v-col>
<v-col>
<v-btn
color="accent"
small
outlined
style="width: 100%;"
:disabled="!settingsValid"
@click="downloadBoard"
>
<v-icon left>
mdi-download
</v-icon>
Download Calibration Target
</v-btn>
</v-col>
<v-col>
<v-btn
color="secondary"
:disabled="isCalibrating"
small
style="width: 100%;"
@click="$refs.importCalibrationFromCalibdb.click()"
>
<v-icon left>
mdi-upload
</v-icon>
Import From CalibDB
</v-btn>
</v-col>
</v-row>
</div>
</v-card>
</v-col>
<v-col
class="pl-md-3 pt-3 pt-md-0"
cols="12"
md="5"
>
<template>
<CVimage
:id="'cameras-cal'"
:idx="1"
:disconnected="!$store.state.backendConnected"
scale="100"
style="border-radius: 5px;"
/>
<v-dialog
v-model="calibrationDialog"
width="500px"
:persistent="true"
>
<v-card
color="primary"
dark
>
<v-card-title> Camera Calibration </v-card-title>
<div
class="ml-3"
>
<v-col align="center">
<template v-if="calibrationInProgress && !calibrationFailed">
<v-progress-circular
indeterminate
:size="70"
:width="8"
color="accent"
/>
<v-card-text>Camera is being calibrated. This process may take several minutes...</v-card-text>
</template>
<template v-else-if="!calibrationFailed">
<v-icon
color="green"
size="70"
>
mdi-check-bold
</v-icon>
<v-card-text>Camera has been successfully calibrated at {{ stringResolutionList[selectedFilteredResIndex] }}!</v-card-text>
</template>
<template v-else>
<v-icon
color="red"
size="70"
>
mdi-close
</v-icon>
<v-card-text>Camera calibration failed! Make sure that the photos are taken such that the rainbow grid circles align with the corners of the chessboard, and try again. More information is available in the program logs.</v-card-text>
</template>
</v-col>
</div>
<v-card-actions>
<v-spacer />
<v-btn
v-if="!calibrationInProgress || calibrationFailed"
color="white"
text
@click="closeDialog"
>
OK
</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
</template>
</v-col>
</v-row>
<!-- Special hidden upload input that gets 'clicked' when the user imports calibdb data -->
<input
ref="importCalibrationFromCalibdb"
type="file"
accept=".json"
style="display: none;"
@change="readImportedCalibration"
>
<v-snackbar
v-model="snack"
top
:color="snackbar.color"
timeout="2000"
>
<span>{{ snackbar.text }}</span>
</v-snackbar>
</div>
</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 CVswitch from '../components/common/cv-switch';
import CVimage from "../components/common/cv-image";
import TooltippedLabel from "../components/common/cv-tooltipped-label";
import jsPDF from "jspdf";
import "../jsPDFFonts/Prompt-Regular-normal.js";
export default {
name: 'Cameras',
components: {
TooltippedLabel,
CVselect,
CVnumberinput,
CVslider,
CVswitch,
CVimage
},
data() {
return {
calibrationDialog: false,
calibrationInProgress: false,
calibrationFailed: false,
filteredVideomodeIndex: 0,
settingsValid: true,
unfilteredStreamDivisors: [1, 2, 4],
snackbar: {
color: "success",
text: "",
},
snack: false,
}
},
computed: {
disallowCalibration() {
return !(this.calibrationData.boardType === 0 || this.calibrationData.boardType === 1) || !this.settingsValid;
},
checkCancellation() {
if (this.isCalibrating) {
return false
} else if (this.disallowCalibration) {
return true;
} else {
return true
}
},
currentCameraIndex: {
get() {
return this.$store.state.currentCameraIndex;
},
set(value) {
this.$store.commit('currentCameraIndex', value);
}
},
cameraGain: {
get() {
return parseInt(this.$store.getters.currentPipelineSettings.cameraGain)
},
set(val) {
this.$store.commit("mutatePipeline", {"cameraGain": parseInt(val)});
}
},
calibrationDivisors: {
get() {
return this.unfilteredStreamDivisors.filter(item => {
const res = this.stringResolutionList[this.selectedFilteredResIndex].split(" X ").map(it => parseInt(it));
// Realistically, we need more than 320x240, but lower than this is
// basically unusable. For now, don't allow decimations that take us
// below that
const ret = ((res[0] / item) >= 300 && (res[1] / item) >= 220) || (item === 1);
return ret;
})
}
},
// Makes sure there's only one entry per resolution
filteredResolutionList: {
get() {
let list = this.$store.getters.videoFormatList;
let filtered = [];
list.forEach((it, i) => {
if (!filtered.some(e => e.width === it.width && e.height === it.height)) {
it['index'] = i;
const calib = this.getCalibrationCoeffs(it);
if (calib != null) {
it['standardDeviation'] = calib.standardDeviation;
it['mean'] = calib.perViewErrors.reduce((a, b) => a + b) / calib.perViewErrors.length;
it['horizontalFOV'] = 2 * Math.atan2(it.width/2,calib.intrinsics[0]) * (180/Math.PI);
it['verticalFOV'] = 2 * Math.atan2(it.height/2,calib.intrinsics[4]) * (180/Math.PI);
it['diagonalFOV'] = 2 * Math.atan2(Math.sqrt(it.width**2 + (it.height/(calib.intrinsics[4]/calib.intrinsics[0]))**2)/2,calib.intrinsics[0]) * (180/Math.PI);
}
filtered.push(it);
}
});
filtered.sort((a, b) => (b.width + b.height) - (a.width + a.height));
return filtered
}
},
stringResolutionList: {
get() {
return this.filteredResolutionList.map(res => `${res['width']} X ${res['height']}`);
}
},
cameraSettings: {
get() {
return this.$store.getters.currentCameraSettings;
},
set(value) {
this.$store.commit('cameraSettings', value);
}
},
streamingFrameDivisor: {
get() {
return this.$store.getters.currentPipelineSettings.streamingFrameDivisor;
},
set(val) {
this.$store.commit("mutatePipeline", {"streamingFrameDivisor": val});
this.handlePipelineUpdate("streamingFrameDivisor", val);
}
},
boardType: {
get() {
return this.calibrationData.boardType
},
set(value) {
this.$store.commit('mutateCalibrationState', {['boardType']: value});
}
},
snapshotAmount: {
get() {
return this.calibrationData.count
}
},
minSnapshots: {
get() {
return this.calibrationData.minCount
}
},
hasEnough: {
get() {
return this.calibrationData.hasEnough
}
},
boardWidth: {
get() {
return this.calibrationData.patternWidth
},
set(value) {
this.$store.commit('mutateCalibrationState', {['patternWidth']: value})
}
},
boardHeight: {
get() {
return this.calibrationData.patternHeight
},
set(value) {
this.$store.commit('mutateCalibrationState', {['patternHeight']: value})
}
},
squareSizeIn: {
get() {
return this.calibrationData.squareSizeIn
},
set(value) {
this.$store.commit('mutateCalibrationState', {['squareSizeIn']: value})
}
},
calibrationData: {
get() {
return this.$store.state.calibrationData
}
},
isCalibrating: {
get() {
return this.$store.getters.currentPipelineIndex === -2;
}
},
selectedFilteredResIndex: {
get() {
return this.filteredVideomodeIndex
},
set(i) {
console.log(`Setting filtered index to ${i}`);
this.filteredVideomodeIndex = i;
this.$store.commit('mutateCalibrationState', {['videoModeIndex']: this.filteredResolutionList[i].index});
}
},
},
methods: {
readImportedCalibration(event) {
// let formData = new FormData();
// formData.append("zipData", event.target.files[0]);
const filename = event.target.files[0].name;
event.target.files[0].text().then(fileText => {
const data = {
"cameraIndex": this.$store.getters.currentCameraIndex,
"payload": fileText,
"filename": filename,
};
this.axios
.post("http://" + this.$address + "/api/calibration/importFromCalibDB", data, {
headers: { "Content-Type": "text/plain" },
})
.then((response) => {
this.snackbar = {
color: response.status === 200 ? "success" : "error",
text: response.data.text || response.data
}
this.snack = true;
})
.catch((err) => {
if (err.request) {
this.snackbar = {
color: "error",
text: "Error while uploading calibration file! The backend didn't respond to the upload attempt.",
};
} else {
this.snackbar = {
color: "error",
text: "Error while uploading calibration file!",
};
}
this.snack = true;
});
})
},
closeDialog() {
this.calibrationDialog = false;
this.calibrationInProgress = false;
this.calibrationFailed = false;
},
getCalibrationCoeffs(resolution) {
const calList = this.$store.getters.calibrationList;
let ret = null;
calList.forEach(cal => {
if (cal.width === resolution.width && cal.height === resolution.height) {
ret = cal
}
});
return ret;
},
downloadBoard() {
const config = {
type: this.boardType === 0 ? "chessboard" : "dotgrid",
boardWidthIn: this.boardWidth,
boardHeightIn: this.boardHeight,
patternSpacingIn: this.squareSizeIn
}
const doc = new jsPDF({ unit: "in", format: "letter" })
doc.setFont("Prompt-Regular")
doc.setFontSize(12)
const paperWidth = 8.5
const paperHeight = 11.0
// Draw the selected pattern to the document
switch (config.type) {
case "chessboard":
// eslint-disable-next-line no-case-declarations
const chessboardStartX = (paperWidth - config.boardWidthIn * config.patternSpacingIn) / 2
// eslint-disable-next-line no-case-declarations
const chessboardStartY = (paperHeight - config.boardWidthIn * config.patternSpacingIn) / 2
for (let squareY = 0; squareY < config.boardHeightIn; squareY++) {
for (let squareX = 0; squareX < config.boardWidthIn; squareX++) {
const xPos = chessboardStartX + squareX * config.patternSpacingIn
const yPos = chessboardStartY + squareY * config.patternSpacingIn
// Only draw the odd squares to create the chessboard pattern
if ((xPos + yPos + 0.25) % 2 === 0) {
doc.rect(xPos, yPos, config.patternSpacingIn, config.patternSpacingIn, "F")
}
}
}
break
case "dotgrid":
// eslint-disable-next-line no-case-declarations
const dotgridStartX = (paperWidth - (2 * (config.boardWidthIn - 1) + ((config.boardHeightIn - 1) % 2)) * config.patternSpacingIn) / 2.0
// eslint-disable-next-line no-case-declarations
const dotgridStartY = (paperHeight - (config.boardHeightIn - config.patternSpacingIn)) / 2
for (let squareY = 0; squareY < config.boardHeightIn; squareY++) {
for (let squareX = 0; squareX < config.boardWidthIn; squareX++) {
const xPos = dotgridStartX + (2 * squareX + (squareY % 2)) * config.patternSpacingIn
const yPos = dotgridStartY + squareY * config.patternSpacingIn
doc.circle(xPos, yPos, config.patternSpacingIn / 4, "F")
}
}
break
}
// Draw ruler pattern
const lineStartX = 1.0
const lineEndX = paperWidth - lineStartX
const lineY = paperHeight - 1.0
doc.setLineWidth(0.01)
doc.line(lineStartX, lineY, lineEndX, lineY)
for (let tickX = lineStartX; tickX <= lineEndX; tickX++) {
doc.line(tickX, lineY, tickX, lineY + 0.25)
doc.text(`${tickX - 1}${tickX - 1 === 0 ? " in" : ""}`, tickX + 0.1, lineY + 0.25)
}
// Add branding
const logoImage = new Image();
logoImage.src = require('@/assets/logos/logoMono.png');
doc.addImage(logoImage, 'PNG', 1.0, 0.75, 1.4, 0.5);
doc.text(`${config.boardWidthIn} x ${config.boardHeightIn} | ${config.patternSpacingIn}in`, paperWidth - 1, 1.0,
{
maxWidth: (paperWidth - 2.0) / 2,
align: "right",
}
)
doc.save(`calibrationTarget-${config.type}.pdf`)
},
sendCameraSettings() {
this.axios.post("http://" + this.$address + "/api/settings/camera", {"settings": this.cameraSettings, "index": this.$store.state.currentCameraIndex})
.then(response => {
this.snackbar = {
color: "success",
text: response.data.text || response.data
}
this.snack = true;
})
.catch(error => {
if(error.response) {
this.snackbar = {
color: "error",
text: error.response.data.text || error.response.data
}
} else if(error.request) {
this.snackbar = {
color: "error",
text: "Error while trying to process the request! The backend didn't respond.",
};
} else {
this.snackbar = {
color: "error",
text: "An error occurred while trying to process the request.",
};
}
this.snack = true;
})
},
isCalibrated(resolution) {
return this.$store.getters.currentCameraSettings.calibrations
.some(e => e.width === resolution.width && e.height === resolution.height);
},
sendCalibrationMode() {
let data = {
['cameraIndex']: this.$store.state.currentCameraIndex
};
if (this.isCalibrating) {
data['takeCalibrationSnapshot'] = true
} else {
// This store prevents an edge case of a user not selecting a different resolution, which causes the set logic to not be called
this.$store.commit('mutateCalibrationState', {['videoModeIndex']: this.filteredResolutionList[this.selectedFilteredResIndex].index});
const calData = this.calibrationData;
calData.isCalibrating = true;
data['startPnpCalibration'] = calData;
console.log("starting calibration with index " + calData.videoModeIndex);
}
this.$store.commit('currentPipelineIndex', -2);
this.$store.state.websocket.ws.send(this.$msgPack.encode(data));
},
sendCalibrationFinish() {
console.log("finishing calibration for index " + this.$store.getters.currentCameraIndex);
this.calibrationDialog = true;
this.calibrationInProgress = true;
this.axios.post("http://" + this.$address + "/api/calibration/end", {index: this.$store.getters.currentCameraIndex})
.then(() => {
// End calibration will always return a 200 code on success
this.calibrationInProgress = false;
}
).catch(() => {
this.calibrationFailed = true;
});
}
}
}
</script>
<style>
::-webkit-scrollbar{
height: 0.55em;
}
</style>
<style scoped>
.v-data-table {
text-align: center;
background-color: transparent !important;
width: 100%;
height: 100%;
overflow-y: auto;
}
.v-data-table th {
background-color: #006492 !important;
}
.v-data-table th, td {
font-size: 1rem !important;
}
</style>

View File

@@ -0,0 +1,70 @@
<script setup lang="ts">
import { computed } from "vue";
import CamerasCard from "@/components/dashboard/CamerasCard.vue";
import CameraAndPipelineSelectCard from "@/components/dashboard/CameraAndPipelineSelectCard.vue";
import StreamConfigCard from "@/components/dashboard/StreamConfigCard.vue";
import PipelineConfigCard from "@/components/dashboard/ConfigOptions.vue";
import { useCameraSettingsStore } from "@/stores/settings/CameraSettingsStore";
import { useStateStore } from "@/stores/StateStore";
const cameraViewType = computed<number[]>({
get: (): number[] => {
// Only show the input stream in Color Picking Mode
if(useStateStore().colorPickingMode) return [0];
// Only show the output stream in Driver Mode or Calibration Mode
if(useCameraSettingsStore().isDriverMode || useCameraSettingsStore().isCalibrationMode) return [1];
const ret: number[] = [];
if(useCameraSettingsStore().currentPipelineSettings.inputShouldShow) {
ret.push(0);
}
if(useCameraSettingsStore().currentPipelineSettings.outputShouldShow) {
ret.push(1);
}
if(ret.length === 0) return [0];
return ret;
},
set: v => {
useCameraSettingsStore().changeCurrentPipelineSetting({
inputShouldShow: v.includes(0),
outputShouldShow: v.includes(1)
}, true);
}
});
</script>
<template>
<v-container
class="pa-3"
fluid
>
<v-row
no-gutters
align="center"
justify="center"
>
<v-col
cols="12"
class="pb-3 pr-lg-3"
lg="8"
align-self="stretch"
>
<CamerasCard v-model="cameraViewType" />
</v-col>
<v-col
cols="12"
class="pb-3"
lg="4"
style="display: flex; flex-direction: column"
align-self="stretch"
>
<CameraAndPipelineSelectCard />
<StreamConfigCard v-model="cameraViewType" />
</v-col>
</v-row>
<PipelineConfigCard />
</v-container>
</template>

View File

@@ -1,12 +1,19 @@
<script setup lang="ts">
const devMode = process.env.NODE_ENV === "development";
</script>
<template>
<div
style="overflow:hidden; height:100%; width:100%"
>
<div
v-if="() => process.env.NODE_ENV === 'development'"
v-if="devMode"
style="width: 100%; height: 100%; padding: 16px"
>
<span style="color: white; font-weight: bold">PhotonClient is in development mode so the documentation page will not load. Please recompile in production mode with the documentation copied over after a full build.</span>
<span style="color: white; font-weight: bold">
PhotonClient is in development mode so the documentation page will not load.
Please recompile in production mode with the documentation copied over after a full build.
</span>
</div>
<div
v-else

View File

@@ -0,0 +1,16 @@
<script setup lang="ts">
import MetricsCard from "@/components/settings/MetricsCard.vue";
import DeviceControlCard from "@/components/settings/DeviceControlCard.vue";
import NetworkingCard from "@/components/settings/NetworkingCard.vue";
import LightingControlCard from "@/components/settings/LEDControlCard.vue";
import { useSettingsStore } from "@/stores/settings/GeneralSettingsStore";
</script>
<template>
<div class="pa-3">
<MetricsCard />
<DeviceControlCard />
<NetworkingCard />
<LightingControlCard v-if="useSettingsStore().lighting.supported" />
</div>
</template>

View File

@@ -1,140 +0,0 @@
<template>
<v-card
dark
class="pt-3"
color="primary"
flat
>
<v-card-title>
View Program Logs
<v-btn
color="secondary"
style="margin-left: auto;"
depressed
@click="$refs.exportLogFile.click()"
>
<v-icon left>
mdi-download
</v-icon>
Download Log
<!-- Special hidden link that gets 'clicked' when the user exports journalctl logs -->
<a
ref="exportLogFile"
style="color: black; text-decoration: none; display: none"
:href="'http://' + this.$address + '/api/utils/logs/photonvision-journalctl.txt'"
download="photonvision-journalctl.txt"
/>
</v-btn>
</v-card-title>
<div class="pr-6 pl-6">
<v-btn-toggle
v-model="logLevel"
dark
multiple
class="fill mb-4"
>
<v-btn
v-for="(level) in possibleLevelArray"
:key="level"
color="secondary"
class="fill"
>
{{ level }}
</v-btn>
</v-btn-toggle>
<!-- Logs -->
<v-virtual-scroll
:items="logMessageArray"
item-height="50"
height="600"
>
<template v-slot="{ item }">
<div :class="[getColor(item) + '--text', 'log-item']">
{{ item.message }}
</div>
</template>
</v-virtual-scroll>
</div>
<v-divider />
<v-card-actions>
<v-spacer />
<v-btn
color="white"
text
@click="$store.state.logsOverlay = false"
>
Close
</v-btn>
</v-card-actions>
</v-card>
</template>
<script>
export default {
name: "Logs",
components: {
},
data() {
return {
selectedLevel: [0, 1, 2],
possibleLevelArray: ['ERROR', 'WARN', 'INFO', 'DEBUG'],
colorArray: ['red', 'yellow','green', 'white'],
}
},
computed: {
rawLogs() {
return this.$store.state.logMessages;
},
logMessageArray() {
const logArray = this.$store.state.logMessages;
return logArray.filter(it => this.selectedLevel.includes(it.level));
},
logLevel: {
get() {
return this.selectedLevel
},
set(value) {
this.selectedLevel = value;
}
}
},
methods: {
getColor(message) {
return this.colorArray[message.level];
},
download(filename, text) {
const element = document.createElement('a');
element.setAttribute('href', 'data:text/plain;charset=utf-8,' + encodeURIComponent(text));
element.setAttribute('download', filename);
element.style.display = 'none';
document.body.appendChild(element);
element.click();
document.body.removeChild(element);
}
}
}
</script>
<style scoped>
.v-btn-toggle.fill {
width: 100%;
height: 100%;
}
.v-btn-toggle.fill > .v-btn {
width: 25%;
height: 100%;
}
</style>

View File

@@ -9,20 +9,13 @@
</v-card-subtitle>
</div>
<img
src="../assets/NotFound.webp"
src="@/assets/images/notfound.webp"
alt="missing-page"
style="margin-top: 64px"
>
</div>
</template>
<script>
export default {
name: 'NotFound'
}
</script>
<style>
.not-found-container {
width: 100%;

View File

@@ -1,532 +0,0 @@
<template>
<div>
<v-container
class="pa-3"
fluid
>
<v-row
no-gutters
align="center"
justify="center"
>
<v-col
cols="12"
:class="['pb-3 ', 'pr-lg-3']"
lg="8"
align-self="stretch"
>
<v-card
color="primary"
height="100%"
style="display: flex; flex-direction: column"
dark
>
<v-card-title
class="pb-0 mb-0 pl-4 pt-1"
style="min-height: 50px; justify-content: space-between; align-content: center"
>
<div class="pt-2">
<span class="mr-4">Cameras</span>
<v-chip
label
:color="fpsTooLow ? 'error' : 'transparent'"
:text-color="fpsTooLow ? '#C7EA46' : '#ff4d00'"
style="font-size: 1rem; padding: 0; margin: 0"
>
<span class="pr-1">Processing @ {{ Math.round($store.state.pipelineResults.fps) }}&nbsp;FPS &ndash;</span>
<span v-if="fpsTooLow && !$store.getters.currentPipelineSettings.inputShouldShow && $store.getters.pipelineType === 2">HSV thresholds are too broad; narrow them for better performance</span>
<span v-else-if="fpsTooLow && $store.getters.currentCameraSettings.inputShouldShow">stop viewing the raw stream for better performance</span>
<span v-else>{{ Math.min(Math.round($store.state.pipelineResults.latency), 9999) }} ms latency</span>
</v-chip>
</div>
<div>
<v-switch
v-model="driverMode"
label="Driver Mode"
style="margin-left: auto;"
color="accent"
class="pt-2"
/>
</div>
</v-card-title>
<v-divider />
<v-row
align="center"
class="pl-3 pr-3"
>
<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)"
style="height: 100%;"
>
<div style="position: relative; width: 100%; height: 100%;">
<cv-image
:id="idx === 0 ? 'raw-stream' : 'processed-stream'"
ref="streams"
:idx="idx"
:disconnected="!$store.state.backendConnected"
scale="95"
:max-height="$store.getters.isDriverMode ? '40vh' : '300px'"
:max-height-md="$store.getters.isDriverMode ? '50vh' : '380px'"
:max-height-lg="$store.getters.isDriverMode ? '55vh' : '390px'"
:max-height-xl="$store.getters.isDriverMode ? '60vh' : '450px'"
:alt="idx === 0 ? 'Raw stream' : 'Processed stream'"
:color-picking="$store.state.colorPicking && idx === 0"
style="padding-top: 22px;"
@click="onImageClick"
/>
</div>
</v-col>
</v-row>
</v-card>
</v-col>
<v-col
cols="12"
class="pb-3"
lg="4"
style="display: flex; flex-direction: column"
>
<v-card
color="primary"
>
<camera-and-pipeline-select />
</v-card>
<v-card
:disabled="$store.getters.isDriverMode || $store.state.colorPicking"
class="mt-3"
color="primary"
>
<v-row
align="center"
class="pa-sm-3"
>
<v-col lg="12">
<p style="color: white;">
Processing Mode
</p>
<v-btn-toggle
v-model="processingMode"
mandatory
dark
class="fill"
>
<v-btn
color="secondary"
>
<v-icon>mdi-square-outline</v-icon>
<span>2D</span>
</v-btn>
<v-btn
color="secondary"
@click="on3DClick"
>
<v-icon>mdi-cube-outline</v-icon>
<span>3D</span>
</v-btn>
</v-btn-toggle>
</v-col>
<v-col lg="12">
<p style="color: white;">
Stream Display
</p>
<v-btn-toggle
v-model="selectedOutputs"
:multiple="$vuetify.breakpoint.mdAndUp"
mandatory
dark
class="fill"
>
<v-btn
color="secondary"
class="fill"
>
<v-icon>mdi-import</v-icon>
<span>Raw</span>
</v-btn>
<v-btn
color="secondary"
class="fill"
>
<v-icon>mdi-export</v-icon>
<span>Processed</span>
</v-btn>
</v-btn-toggle>
</v-col>
</v-row>
</v-card>
</v-col>
</v-row>
<v-row no-gutters>
<v-col
v-for="(tabs, idx) in tabGroups"
:key="idx"
:cols="Math.floor(12 / tabGroups.length)"
:class="idx !== tabGroups.length - 1 ? 'pr-3' : ''"
align-self="stretch"
>
<v-card
color="primary"
height="100%"
class="pr-4 pl-4"
>
<v-tabs
v-if="!$store.getters.isDriverMode"
v-model="selectedTabs[idx]"
grow
background-color="primary"
dark
height="48"
slider-color="accent"
>
<v-tab
v-for="(tab, i) in tabs"
:key="i"
>
{{ tab.name }}
</v-tab>
</v-tabs>
<div class="pl-4 pr-4 pt-4 pb-2">
<keep-alive>
<component
:is="(tabs[selectedTabs[idx]] || tabs[0]).component"
:ref="(tabs[selectedTabs[idx]] || tabs[0]).name"
v-model="$store.getters.pipeline"
@update="$emit('save')"
/>
</keep-alive>
</div>
</v-card>
</v-col>
</v-row>
</v-container>
<v-snackbar
v-model="showNTWarning"
color="error"
timeout="-1"
top
>
{{ $store.state.settings.networkSettings.runNTServer ?
"NetworkTables server enabled! PhotonLib may not work." :
"NetworkTables not connected! Are you on a network with a robot?" }}
<template v-slot:action>
<v-btn
text
@click="hideNTWarning = true"
>
Hide
</v-btn>
</template>
</v-snackbar>
<v-dialog
v-model="dialog"
width="500"
>
<v-card
color="primary"
dark
>
<v-card-title>
Current resolution not calibrated
</v-card-title>
<v-card-text>
Because the current resolution {{ this.$store.getters.currentVideoFormat.width }} x {{ this.$store.getters.currentVideoFormat.height }} is not yet calibrated, 3D mode cannot be enabled. Please
<a
href="/cameras"
class="white--text"
@click="$emit('switch-to-cameras')"
> visit the Cameras tab</a> to calibrate this resolution. For now, SolvePNP will do nothing.
</v-card-text>
<v-divider />
<v-card-actions>
<v-spacer />
<v-btn
color="white"
text
@click="closeUncalibratedDialog"
>
OK
</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
</div>
</template>
<script>
import CameraAndPipelineSelect from "../components/pipeline/CameraAndPipelineSelect";
import cvImage from '../components/common/cv-image';
import InputTab from './PipelineViews/InputTab';
import ThresholdTab from './PipelineViews/ThresholdTab';
import ContoursTab from './PipelineViews/ContoursTab';
import OutputTab from './PipelineViews/OutputTab';
import TargetsTab from "./PipelineViews/TargetsTab";
import Map3DTab from './PipelineViews/Map3DTab';
import PnPTab from './PipelineViews/PnPTab';
import AprilTagTab from './PipelineViews/AprilTagTab';
import ArucoTab from './PipelineViews/ArucoTab';
export default {
name: 'Pipeline',
components: {
CameraAndPipelineSelect,
cvImage,
InputTab,
ThresholdTab,
ContoursTab,
OutputTab,
TargetsTab,
Map3DTab,
PnPTab,
AprilTagTab,
ArucoTab,
},
data() {
return {
selectedTabsData: [0, 0, 0, 0],
counterData: 0,
dialog: false,
processingModeOverride: false,
hideNTWarning: false,
}
},
computed: {
selectedTabs: {
get() {
return this.$store.getters.isDriverMode ? [0] : this.selectedTabsData;
},
set(value) {
this.selectedTabsData = value;
}
},
tabGroups: {
get() {
let tabs = {
input: {
name: "Input",
component: "InputTab",
},
threshold: {
name: "Threshold",
component: "ThresholdTab",
},
contours: {
name: "Contours",
component: "ContoursTab",
},
apriltag: {
name: "AprilTag",
component: "AprilTagTab",
},
aruco: {
name: "Aruco",
component: "ArucoTab",
},
output: {
name: "Output",
component: "OutputTab",
},
targets: {
name: "Targets",
component: "TargetsTab",
},
pnp: {
name: "PnP",
component: "PnPTab",
},
map3d: {
name: "3D",
component: "Map3DTab",
}
};
// If not in 3d, name "3D" is illegal
const allow3d = this.$store.getters.currentPipelineSettings.solvePNPEnabled;
// If in apriltag, "Threshold" and "Contours" are illegal -- otherwise "AprilTag" is
const isAprilTag = (this.$store.getters.currentPipelineSettings.pipelineType - 2) === 2;
const isAruco = (this.$store.getters.currentPipelineSettings.pipelineType - 2) === 3;
// 2D array of tab names and component names; each sub-array is a separate tab group
let ret = [];
if (this.$vuetify.breakpoint.smAndDown || this.$store.getters.isDriverMode || (this.$vuetify.breakpoint.mdAndDown && !this.$store.state.compactMode)) {
// One big tab group with all the tabs
ret[0] = Object.values(tabs);
} else if (this.$vuetify.breakpoint.mdAndDown || !this.$store.state.compactMode) {
// Two tab groups, one with "input, threshold, contours, output" and the other with "target info, 3D"
ret[0] = [tabs.input, tabs.threshold, tabs.contours, tabs.apriltag, tabs.aruco, tabs.output];
ret[1] = [tabs.targets, tabs.pnp, tabs.map3d];
} else if (this.$vuetify.breakpoint.lgAndDown) {
// Three tab groups, one with "input", one with "threshold, contours, output", and the other with "target info, 3D"
ret[0] = [tabs.input];
ret[1] = [tabs.threshold, tabs.contours, tabs.apriltag,tabs.aruco, tabs.output];
ret[2] = [tabs.targets, tabs.pnp, tabs.map3d];
} else if (this.$vuetify.breakpoint.xl) {
// Three tab groups, one with "input", one with "threshold, contours", and the other with "output, target info, 3D"
ret[0] = [tabs.input];
ret[1] = [tabs.threshold];
ret[2] = [tabs.contours, tabs.apriltag, tabs.aruco,tabs.output];
ret[3] = [tabs.targets, tabs.pnp, tabs.map3d];
}
for(let i = 0; i < ret.length; i++) {
const group = ret[i];
// All the tabs we allow
ret[i] = group.filter(it =>
!(!allow3d && it.name === "3D") //Filter out 3D tab any time 3D isn't calibrated
&& !((!allow3d || isAprilTag || isAruco) && it.name === "PnP") //Filter out the PnP config tab if 3D isn't available, or we're doing Apriltags
&& !((isAprilTag || isAruco) && (it.name === "Threshold")) //Filter out threshold tab if we're doing apriltags
&& !((isAprilTag || isAruco) && (it.name === "Contours")) //Filter out contours if we're doing Apriltag
&& !(!isAprilTag && it.name === "AprilTag") //Filter out apriltag unless we actually are doing Apriltags
&& !(!isAruco && it.name === "Aruco")
);
}
// One last filter to remove empty lists
return ret.filter(it => it !== undefined && it.length > 0);
}
},
processingMode: {
get() {
return (this.$store.getters.currentPipelineSettings.solvePNPEnabled || this.processingModeOverride) ? 1 : 0;
},
set(value) {
if (this.$store.getters.isCalibrated) {
this.$store.getters.currentPipelineSettings.solvePNPEnabled = value === 1;
this.handlePipelineUpdate("solvePNPEnabled", value === 1);
}
}
},
driverMode: {
get() {
return this.$store.getters.isDriverMode;
},
set(value) {
this.$store.getters.currentCameraSettings.currentPipelineIndex = value ? -1 : 0;
this.handleInputWithIndex('currentPipeline', value ? -1 : 0);
}
},
selectedOutputs: {
// All this logic exists to deal with the reality that the output select buttons sometimes need an array and sometimes need a number (depending on whether they're exclusive)
get() {
// We switch the selector to single-select only on sm-and-down size devices, so we have to return a Number instead of an Array in that state
let ret = [];
if (this.$store.state.colorPicking) {
ret = [0]; // We want the input stream only while color picking
} else if (this.$store.getters.isDriverMode) {
ret = [1]; // We want only the output stream in driver mode
} else {
if (this.$store.getters.currentPipelineSettings.inputShouldShow) ret = ret.concat([0]);
if (this.$store.getters.currentPipelineSettings.outputShouldShow) ret = ret.concat([1]);
if (!ret.length) ret = [0];
}
if (this.$vuetify.breakpoint.mdAndUp) {
return ret;
} else {
return ret[0] || 0;
}
},
set(value) {
let valToCommit = [0];
if (value instanceof Array) {
// Value is already an array, we don't need to do anything
valToCommit = value;
} else if (value) {
// Value is assumed to be a number, so we wrap it into an array
valToCommit = [value];
}
this.$store.commit("mutatePipeline", {"inputShouldShow": valToCommit.includes(0)});
this.$store.commit("mutatePipeline", {"outputShouldShow": valToCommit.includes(1)});
this.handlePipelineUpdate("inputShouldShow", valToCommit.includes(0));
}
},
fpsTooLow: {
get() {
// For now, we only show the FPS is too low warning when GPU acceleration is enabled, because we don't really trust the presented video modes otherwise
const currFPS = this.$store.state.pipelineResults.fps;
const targetFPS = this.$store.getters.currentVideoFormat.fps;
const driverMode = this.$store.getters.isDriverMode;
const gpuAccel = this.$store.state.settings.general.gpuAcceleration;
const isReflective = this.$store.getters.pipelineType === 2;
return (currFPS - targetFPS) < -5 && this.$store.state.pipelineResults.fps !== 0 && !driverMode && gpuAccel && isReflective;
}
},
latency: {
get() {
return this.$store.getters.currentPipelineResults.latency;
}
},
isCalibrated: {
get() {
const resolution = this.$store.getters.videoFormatList[this.$store.getters.currentPipelineSettings.cameraVideoModeIndex];
return this.$store.getters.currentCameraSettings.calibrations
.some(e => e.width === resolution.width && e.height === resolution.height)
}
},
isRobotConnected: {
get() {
// return this.$store.state.ntConnectionInfo.connected && this.$store.state.backendConnected;
return true;
}
},
showNTWarning: {
get() {
return (!this.$store.state.ntConnectionInfo.connected || this.$store.state.settings.networkSettings.runNTServer) && this.$store.state.settings.networkSettings.ntServerAddress != "" && this.$store.state.backendConnected && !this.hideNTWarning;
}
},
},
created() {
this.$store.state.connectedCallbacks.push(this.reloadStreams)
},
methods: {
reloadStreams() {
// Reload the streams as we technically close and reopen them
this.$refs.streams.forEach(it => it.reload())
},
onImageClick(event) {
// Get a reference to the threshold tab (if it is shown) and call its "onClick" method
let ref = this.$refs["Threshold"];
if (ref && ref[0])
ref[0].onClick(event)
},
on3DClick() {
if (!this.$store.getters.isCalibrated) {
this.dialog = true;
this.processingModeOverride = true;
}
},
closeUncalibratedDialog() {
this.dialog = false;
this.processingModeOverride = false;
// this.$store.getters.currentPipelineSettings.solvePNPEnabled = false;
this.handlePipelineUpdate("solvePNPEnabled", false);
}
}
}
</script>
<style scoped>
.v-btn-toggle.fill {
width: 100%;
height: 100%;
}
.v-btn-toggle.fill > .v-btn {
width: 50%;
height: 100%;
}
th {
width: 80px;
text-align: center;
}
</style>

View File

@@ -1,154 +0,0 @@
<template>
<div>
<CVselect
v-model="selectedFamily"
name="Target family"
:list="['AprilTag family 36h11', 'AprilTag family 25h9', 'AprilTag family 16h5']"
select-cols="8"
@input="handlePipelineUpdate('tagFamily', selectedFamily)"
/>
<CVslider
v-model="decimate"
class="pt-2"
slider-cols="8"
name="Decimate"
min="1"
max="8"
step="1.0"
tooltip="Increases FPS at the expense of range by reducing image resolution initially"
@input="handlePipelineData('decimate')"
/>
<CVslider
v-model="blur"
class="pt-2"
slider-cols="8"
name="Blur"
min="0"
max="5"
step=".01"
tooltip="Gaussian blur added to the image, high FPS cost for slightly decreased noise"
@input="handlePipelineData('blur')"
/>
<CVslider
v-model="threads"
class="pt-2"
slider-cols="8"
name="Threads"
min="1"
max="8"
step="1"
tooltip="Number of threads spawned by the AprilTag detector"
@input="handlePipelineData('threads')"
/>
<CVswitch
v-model="refineEdges"
class="pt-2"
slider-cols="8"
name="Refine Edges"
tooltip="Further refines the AprilTag corner position initial estimate, suggested left on"
@input="handlePipelineData('refineEdges')"
/>
<CVslider
v-model="decisionMargin"
class="pt-2 pb-4"
slider-cols="8"
name="Decision Margin Cutoff"
min="0"
max="250"
step="1"
tooltip="Tags with a 'margin' (decoding quality score) less than this wil be rejected. Increase this to reduce the number of false positive detections"
@input="handlePipelineData('decisionMargin')"
/>
<CVslider
v-model="numIterations"
class="pt-2 pb-4"
slider-cols="8"
name="Pose Estimation Iterations"
min="0"
max="500"
step="1"
tooltip="Number of iterations the pose estimation algorithm will run, 50-100 is a good starting point"
@input="handlePipelineData('numIterations')"
/>
</div>
</template>
<script>
import CVslider from '../../components/common/cv-slider'
import CVswitch from '../../components/common/cv-switch'
import CVselect from '../../components/common/cv-select'
export default {
name: "AprilTag",
components: {
CVslider,
CVswitch,
CVselect,
},
data() {
return {
familyList: ["AprilTag family 36h11", "AprilTag family 25h9", "AprilTag family 16h5"],
}
},
computed: {
selectedFamily: {
get() {
return this.$store.getters.currentPipelineSettings.tagFamily
},
set(val) {
this.$store.commit("mutatePipeline", {"tagFamily": val})
}
},
decimate: {
get() {
return this.$store.getters.currentPipelineSettings.decimate
},
set(val) {
this.$store.commit("mutatePipeline", {"decimate": val});
}
},
decisionMargin: {
get() {
return this.$store.getters.currentPipelineSettings.decisionMargin
},
set(val) {
this.$store.commit("mutatePipeline", {"decisionMargin": val});
}
},
numIterations: {
get() {
return this.$store.getters.currentPipelineSettings.numIterations
},
set(val) {
this.$store.commit("mutatePipeline", {"numIterations": val});
}
},
blur: {
get() {
return this.$store.getters.currentPipelineSettings.blur
},
set(val) {
this.$store.commit("mutatePipeline", {"blur": val});
}
},
threads: {
get() {
return this.$store.getters.currentPipelineSettings.threads
},
set(val) {
this.$store.commit("mutatePipeline", {"threads": val});
}
},
refineEdges: {
get() {
return this.$store.getters.currentPipelineSettings.refineEdges
},
set(val) {
this.$store.commit("mutatePipeline", {"refineEdges": val});
}
},
},
methods: {
}
}
</script>

View File

@@ -1,76 +0,0 @@
<template>
<div>
<CVslider
v-model="decimate"
class="pt-2"
slider-cols="8"
name="Decimate"
min="1"
max="8"
step="0.5"
tooltip="Increases FPS at the expense of range by reducing image resolution initially"
@input="handlePipelineData('decimate')"
/>
<CVslider
v-model="numIterations"
class="pt-2"
slider-cols="8"
name="Corner Iterations"
min="30"
max="1000"
step="5"
tooltip="How many iterations are going to be used in order to refine corners. Higher values are lead to more accuracy at the cost of performance"
@input="handlePipelineData('numIterations')"
/>
<CVslider
v-model="cornerAccuracy"
class="pt-2"
slider-cols="8"
name="Corner Accuracy"
min="0.01"
max="100"
step="0.01"
tooltip="Minimum accuracy for the corners, lower is better but more performance intensive "
@input="handlePipelineData('cornerAccuracy')"
/>
</div>
</template>
<script>
import CVslider from '../../components/common/cv-slider'
export default {
name: "Aruco",
components: {
CVslider
},
computed: {
decimate: {
get() {
return this.$store.getters.currentPipelineSettings.decimate
},
set(val) {
this.$store.commit("mutatePipeline", {"decimate": val});
},
},
numIterations: {
get() {
return this.$store.getters.currentPipelineSettings.numIterations
},
set(val) {
this.$store.commit("mutatePipeline", {"numIterations": val});
},
},
cornerAccuracy: {
get() {
return this.$store.getters.currentPipelineSettings.cornerDetectionAccuracyPercentage
},
set(val) {
this.$store.commit("mutatePipeline", {"cornerAccuracy": val});
},
},
},
methods: {
}
}
</script>

View File

@@ -1,344 +0,0 @@
<template>
<div>
<CVrangeSlider
v-model="contourArea"
name="Area"
min="0"
max="100"
step="0.01"
@input="handlePipelineData('contourArea')"
/>
<CVrangeSlider
v-if="currentPipelineType() !== 3"
v-model="contourRatio"
name="Ratio (W/H)"
tooltip="Min and max ratio between the width and height of a contour's bounding rectangle"
min="0"
max="100"
step="0.1"
@input="handlePipelineData('contourRatio')"
/>
<CVselect
v-model="contourTargetOrientation"
name="Target Orientation"
tooltip="Used to determine how to calculate target landmarks, as well as aspect ratio"
:list="['Portrait', 'Landscape']"
@input="handlePipelineData('contourTargetOrientation')"
@rollback="e=> rollback('contourTargetOrientation', e)"
/>
<CVrangeSlider
v-if="currentPipelineType() !== 3"
v-model="contourFullness"
name="Fullness"
tooltip="Min and max ratio between a contour's area and its bounding rectangle"
min="0"
max="100"
@input="handlePipelineData('contourFullness')"
/>
<CVrangeSlider
v-if="currentPipelineType() === 3"
v-model="contourPerimeter"
name="Perimeter"
tooltip="Min and max perimeter of the shape, in pixels"
min="0"
max="4000"
@input="handlePipelineData('contourPerimeter')"
/>
<CVslider
v-model="contourSpecklePercentage"
name="Speckle Rejection"
tooltip="Rejects contours whose average area is less than the given percentage of the average area of all the other contours"
min="0"
max="100"
:slider-cols="largeBox"
@input="handlePipelineData('contourSpecklePercentage')"
/>
<template v-if="currentPipelineType() !== 3">
<CVslider
v-model="contourFilterRangeX"
name="X filter tightness"
tooltip="Rejects contours whose center X is further than X standard deviations above/below the mean X location"
min="0.1"
max="4"
step="0.1"
:slider-cols="largeBox"
@input="handlePipelineData('contourFilterRangeX')"
/>
<CVslider
v-model="contourFilterRangeY"
name="Y filter tightness"
tooltip="Rejects contours whose center Y is further than X standard deviations above/below the mean Y location"
min="0.1"
max="4"
step="0.1"
:slider-cols="largeBox"
@input="handlePipelineData('contourFilterRangeY')"
/>
<CVselect
v-model="contourGroupingMode"
name="Target Grouping"
tooltip="Whether or not every two targets are paired with each other (good for e.g. 2019 targets)"
:select-cols="largeBox"
:list="['Single','Dual','2orMore']"
@input="handlePipelineData('contourGroupingMode')"
/>
<CVselect
v-model="contourIntersection"
name="Target Intersection"
tooltip="If target grouping is in dual mode it will use this dropdown to decide how targets are grouped with adjacent targets"
:select-cols="largeBox"
:list="['None','Up','Down','Left','Right']"
:disabled="contourGroupingMode === 0"
@input="handlePipelineData('contourIntersection')"
/>
</template>
<!-- If we arent not a shape, we are a shape-->
<template v-else>
<v-divider class="mt-3" />
<CVselect
v-model="contourShape"
name="Target Shape"
tooltip="The shape of targets to look for"
:select-cols="largeBox"
:list="['Circle', 'Polygon', 'Triangle', 'Quadrilateral']"
@input="handlePipelineData('contourShape')"
/>
<!-- Accuracy % is only for polygons-->
<CVslider
v-model="accuracyPercentage"
:disabled="currentPipelineSettings().contourShape < 1"
name="Shape Simplification"
tooltip="How much we should simply the input contour before checking how many sides it has"
min="0"
max="100"
:slider-cols="largeBox"
@input="handlePipelineData('accuracyPercentage')"
/>
<!-- Similarly, the threshold is only for circles -->
<CVslider
v-model="circleDetectThreshold"
:disabled="currentPipelineSettings().contourShape !== 0"
name="Circle match distance"
tooltip="How close the centroid of a contour must be to the center of a circle in order for them to be matched"
min="1"
max="100"
:slider-cols="largeBox"
@input="handlePipelineData('circleDetectThreshold')"
/>
<CVrangeSlider
v-model="contourRadius"
:disabled="currentPipelineSettings().contourShape !== 0"
name="Radius"
min="0"
max="100"
step="1"
label-cols="3"
@input="handlePipelineData('contourRadius')"
/>
<CVslider
v-model="maxCannyThresh"
:disabled="currentPipelineSettings().contourShape !== 0"
name="Max Canny Threshold"
min="1"
max="100"
:slider-cols="largeBox"
@input="handlePipelineData('maxCannyThresh')"
/>
<CVslider
v-model="circleAccuracy"
:disabled="currentPipelineSettings().contourShape !== 0"
name="Circle Accuracy"
min="1"
max="100"
:slider-cols="largeBox"
@input="handlePipelineData('circleAccuracy')"
/>
<v-divider class="mt-3" />
</template>
<CVselect
v-model="contourSortMode"
name="Target Sort"
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>
<script>
import CVrangeSlider from '../../components/common/cv-range-slider'
import CVselect from '../../components/common/cv-select'
import CVslider from '../../components/common/cv-slider'
export default {
name: 'Contours',
components: {
CVrangeSlider,
CVselect,
CVslider
},
// eslint-disable-next-line vue/require-prop-types
props: ['value'],
data() {
return {}
},
computed: {
largeBox: {
get() {
// Sliders and selectors should be fuller width if we're on screen size medium and
// up and either not in compact mode (because the tab will be 100% screen width),
// or in driver mode (where the card will also be 100% screen width).
return this.$vuetify.breakpoint.mdAndUp && (!this.$store.state.compactMode || this.$store.getters.isDriverMode) ? 10 : 8;
}
},
contourArea: {
get() {
return this.$store.getters.currentPipelineSettings.contourArea
},
set(val) {
this.$store.commit("mutatePipeline", {"contourArea": val});
}
},
contourRatio: {
get() {
return this.$store.getters.currentPipelineSettings.contourRatio
},
set(val) {
this.$store.commit("mutatePipeline", {"contourRatio": val});
}
},
contourTargetOrientation: {
get() {
return this.$store.getters.currentPipelineSettings.contourTargetOrientation
},
set(val) {
this.$store.commit("mutatePipeline", {"contourTargetOrientation": val});
}
},
contourFullness: {
get() {
return this.$store.getters.currentPipelineSettings.contourFullness
},
set(val) {
this.$store.commit("mutatePipeline", {"contourFullness": val});
}
},
contourPerimeter: {
get() {
return this.$store.getters.currentPipelineSettings.contourPerimeter
},
set(val) {
this.$store.commit("mutatePipeline", {"contourPerimeter": val});
}
},
contourSpecklePercentage: {
get() {
return this.$store.getters.currentPipelineSettings.contourSpecklePercentage
},
set(val) {
this.$store.commit("mutatePipeline", {"contourSpecklePercentage": val});
}
},
contourFilterRangeX: {
get() {
console.log(this.$store.getters.currentPipelineSettings.contourFilterRangeX)
return this.$store.getters.currentPipelineSettings.contourFilterRangeX
},
set(val) {
console.log("set")
console.log(val)
this.$store.commit("mutatePipeline", {"contourFilterRangeX": val});
}
},
contourFilterRangeY: {
get() {
return this.$store.getters.currentPipelineSettings.contourFilterRangeY
},
set(val) {
this.$store.commit("mutatePipeline", {"contourFilterRangeY": val});
}
},
contourGroupingMode: {
get() {
return this.$store.getters.currentPipelineSettings.contourGroupingMode
},
set(val) {
this.$store.commit("mutatePipeline", {"contourGroupingMode": val});
}
},
contourSortMode: {
get() {
return this.$store.getters.currentPipelineSettings.contourSortMode
},
set(val) {
this.$store.commit("mutatePipeline", {"contourSortMode": val});
}
},
contourShape: {
get() {
return this.$store.getters.currentPipelineSettings.contourShape
},
set(val) {
this.$store.commit("mutatePipeline", {"contourShape": val});
}
},
contourIntersection: {
get() {
return this.$store.getters.currentPipelineSettings.contourIntersection
},
set(val) {
this.$store.commit("mutatePipeline", {"contourIntersection": val});
}
},
accuracyPercentage: {
get() {
return this.$store.getters.currentPipelineSettings.accuracyPercentage
},
set(val) {
this.$store.commit("mutatePipeline", {"accuracyPercentage": val});
}
},
contourRadius: {
get() {
return this.$store.getters.currentPipelineSettings.contourRadius
},
set(val) {
this.$store.commit("mutatePipeline", {"contourRadius": val});
}
},
circleDetectThreshold: {
get() {
return this.$store.getters.currentPipelineSettings.circleDetectThreshold
},
set(val) {
this.$store.commit("mutatePipeline", {"circleDetectThreshold": val});
}
},
maxCannyThresh: {
get() {
return this.$store.getters.currentPipelineSettings.maxCannyThresh
},
set(val) {
this.$store.commit("mutatePipeline", {"maxCannyThresh": val});
}
},
circleAccuracy: {
get() {
return this.$store.getters.currentPipelineSettings.circleAccuracy
},
set(val) {
this.$store.commit("mutatePipeline", {"circleAccuracy": val});
}
},
},
methods: {},
}
</script>
<style lang="" scoped>
</style>

View File

@@ -1,248 +0,0 @@
<template>
<div>
<CVslider
v-model="cameraExposure"
:disabled="cameraAutoExposure"
name="Exposure"
min="0"
max="100"
step="0.1"
tooltip="Directly controls how much light is allowed to fall onto the sensor, which affects apparent brightness"
:slider-cols="largeBox"
@input="handlePipelineData('cameraExposure')"
@rollback="e => rollback('cameraExposure', e)"
/>
<CVslider
v-model="cameraBrightness"
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)"
/>
<CVswitch
v-model="cameraAutoExposure"
class="pt-2"
name="Auto Exposure"
tooltip="Enables or Disables camera automatic adjustment for current lighting conditions"
@input="handlePipelineData('cameraAutoExposure')"
/>
<CVslider
v-if="cameraGain >= 0"
v-model="cameraGain"
name="Camera Gain"
min="0"
max="100"
tooltip="Controls camera gain, similar to brightness"
:slider-cols="largeBox"
@input="handlePipelineData('cameraGain')"
@rollback="e => rollback('cameraGain', e)"
/>
<CVslider
v-if="cameraRedGain !== -1"
v-model="cameraRedGain"
name="Red Balance"
min="0"
max="100"
tooltip="Controls red automatic white balance gain, which affects how the camera captures colors in different conditions"
:slider-cols="largeBox"
@input="handlePipelineData('cameraRedGain')"
@rollback="e => rollback('cameraRedGain', e)"
/>
<CVslider
v-if="cameraBlueGain !== -1"
v-model="cameraBlueGain"
name="Blue Balance"
min="0"
max="100"
tooltip="Controls blue automatic white balance gain, which affects how the camera captures colors in different conditions"
:slider-cols="largeBox"
@input="handlePipelineData('cameraBlueGain')"
@rollback="e => rollback('cameraBlueGain', e)"
/>
<!-- TODO: stop filtering out the 90 degree rotation modes when we fix those in libcamera -->
<CVselect
v-model="inputImageRotationMode"
name="Orientation"
tooltip="Rotates the camera stream"
:list="['Normal','90° CW','180°','90° CCW']"
:filtered-indices="this.$store.state.settings.general.gpuAcceleration ? new Set([1, 3]) : undefined"
:select-cols="largeBox"
@input="handlePipelineData('inputImageRotationMode')"
@rollback="e => rollback('inputImageRotationMode',e)"
/>
<CVselect
v-model="cameraVideoModeIndex"
name="Resolution"
tooltip="Resolution and FPS the camera should directly capture at"
:list="resolutionList"
:select-cols="largeBox"
@input="handlePipelineData('cameraVideoModeIndex')"
@rollback="e => rollback('cameraVideoModeIndex', e)"
/>
<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"
@rollback="e => rollback('streamingFrameDivisor', e)"
/>
</div>
</template>
<script>
import CVslider from '../../components/common/cv-slider'
import CVselect from '../../components/common/cv-select'
import CVswitch from '../../components/common/cv-switch'
const unfilteredStreamDivisors = [1, 2, 4, 6];
export default {
name: 'Input',
components: {
CVslider,
CVselect,
CVswitch,
},
// eslint-disable-next-line vue/require-prop-types
props: ['value'],
computed: {
largeBox: {
get() {
// Sliders and selectors should be fuller width if we're on screen size medium and
// up and either not in compact mode (because the tab will be 100% screen width),
// or in driver mode (where the card will also be 100% screen width).
return this.$vuetify.breakpoint.mdAndUp && (!this.$store.state.compactMode || this.$store.getters.isDriverMode) ? 10 : 8;
}
},
cameraExposure: {
get() {
return parseFloat(this.$store.getters.currentPipelineSettings.cameraExposure);
},
set(val) {
this.$store.commit("mutatePipeline", {"cameraExposure": parseFloat(val)});
}
},
cameraAutoExposure: {
get() {
return this.$store.getters.currentPipelineSettings.cameraAutoExposure;
},
set(val) {
this.$store.commit("mutatePipeline", {"cameraAutoExposure": val});
}
},
cameraBrightness: {
get() {
return parseInt(this.$store.getters.currentPipelineSettings.cameraBrightness)
},
set(val) {
this.$store.commit("mutatePipeline", {"cameraBrightness": parseInt(val)});
}
},
cameraGain: {
get() {
return parseInt(this.$store.getters.currentPipelineSettings.cameraGain)
},
set(val) {
this.$store.commit("mutatePipeline", {"cameraGain": parseInt(val)});
}
},
cameraRedGain: {
get() {
return parseInt(this.$store.getters.currentPipelineSettings.cameraRedGain)
},
set(val) {
this.$store.commit("mutatePipeline", {"cameraRedGain": parseInt(val)});
}
},
cameraBlueGain: {
get() {
return parseInt(this.$store.getters.currentPipelineSettings.cameraBlueGain)
},
set(val) {
this.$store.commit("mutatePipeline", {"cameraBlueGain": parseInt(val)});
}
},
inputImageRotationMode: {
get() {
return this.$store.getters.currentPipelineSettings.inputImageRotationMode
},
set(val) {
this.$store.commit("mutatePipeline", {"inputImageRotationMode": val});
}
},
cameraVideoModeIndex: {
get() {
return this.$store.getters.currentPipelineSettings.cameraVideoModeIndex;
},
set(val) {
this.$store.commit("mutatePipeline", {"cameraVideoModeIndex": val});
this.handlePipelineUpdate("streamingFrameDivisor", this.getNumSkippedStreamDivisors());
this.$store.commit("mutatePipeline", {"streamingFrameDivisor": 0});
// If we don't have 3d mode calibrated at the new resolution either, we should disable it here
// (TODO) This probably belongs in the backend (Matt)
if (!this.$store.getters.isCalibrated) {
this.handlePipelineUpdate("solvePNPEnabled", false);
this.$store.commit("mutatePipeline", {"solvePNPEnabled": false});
}
}
},
streamingFrameDivisor: {
get() {
return this.$store.getters.currentPipelineSettings.streamingFrameDivisor;
},
set(val) {
this.$store.commit("mutatePipeline", {"streamingFrameDivisor": val});
this.handlePipelineUpdate("streamingFrameDivisor", this.getNumSkippedStreamDivisors() + val);
}
},
resolutionList: {
get() {
let tmp_list = [];
for (let i of this.$store.getters.videoFormatList) {
tmp_list.push(`${i['width']} X ${i['height']} at ${i['fps']} FPS, ${i['pixelFormat']}`)
}
return tmp_list;
}
},
streamResolutionList: {
get() {
const cam_res = this.$store.getters.videoFormatList[
this.$store.getters.currentCameraSettings.currentPipelineSettings.cameraVideoModeIndex];
let tmp_list = [];
for (const x of this.getRawStreamDivisors()) {
tmp_list.push(`${Math.floor(cam_res['width'] / x)} X ${Math.floor(cam_res['height'] / x)}`);
}
return tmp_list;
}
}
},
methods: {
getRawStreamDivisors() {
// Limit stream res when GPU acceleration is enabled because we *know* that we won't be able to get smooth streams above ~640x480
// It would probably be cleaner if this checked that we're on the Raspi 3 instead of checking for GPU accel status
const width = this.$store.getters.videoFormatList[
this.$store.getters.currentCameraSettings.currentPipelineSettings.cameraVideoModeIndex]['width'];
// If GPU acceleration is enabled, the downsized width must be below 400px
// This check should be skipped if we're currently in driver mode
return unfilteredStreamDivisors.filter((x) => this.$store.getters.isDriverMode
|| !this.$store.state.settings.general.gpuAcceleration || width / x < 400);
},
getNumSkippedStreamDivisors() {
return unfilteredStreamDivisors.length - this.getRawStreamDivisors().length;
}
}
}
</script>
<style scoped>
</style>

View File

@@ -1,53 +0,0 @@
<template>
<div>
<mini-map
class="miniMapClass"
:targets="targets"
:horizontal-f-o-v="horizontalFOV"
/>
</div>
</template>
<script>
import miniMap from '../../components/pipeline/3D/MiniMap';
export default {
name: "Map3D",
components: {
miniMap
},
data() {
return {
}
},
computed: {
targets: {
get() {
return this.$store.getters.currentPipelineResults.targets;
}
},
horizontalFOV: {
get() {
let index = this.$store.getters.currentPipelineSettings.cameraVideoModeIndex;
let FOV = this.$store.getters.currentCameraSettings.fov;
let resolution = this.$store.getters.videoFormatList[index];
let diagonalView = FOV * (Math.PI / 180);
let diagonalAspect = Math.hypot(resolution.width, resolution.height);
return Math.atan(Math.tan(diagonalView / 2) * (resolution.width / diagonalAspect)) * 2 * (180 / Math.PI)
}
},
},
methods: {
}
}
</script>
<style scoped>
.miniMapClass {
width: 400px !important;
height: 100% !important;
margin-left: auto;
margin-right: auto;
}
</style>

View File

@@ -1,158 +0,0 @@
<template>
<div>
<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)"
/>
<CVselect
v-if="!isTagPipeline"
v-model="contourTargetOrientation"
name="Target Orientation"
tooltip="Used to determine how to calculate target landmarks (e.g. the top, left, or bottom of the target)"
:list="['Portrait', 'Landscape']"
@input="handlePipelineData('contourTargetOrientation')"
@rollback="e=> rollback('contourTargetOrientation', e)"
/>
<CVswitch
v-model="outputShowMultipleTargets"
name="Show Multiple Targets"
tooltip="If enabled, up to five targets will be displayed and sent to user code, instead of just one"
:disabled="isTagPipeline"
text-cols="3"
@input="handlePipelineData('outputShowMultipleTargets')"
@rollback="e=> rollback('outputShowMultipleTargets', e)"
/>
<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)"
/>
<component
:is="selectedComponent"
:raw-point="rawPoint"
@update="doUpdate"
@snackbar="showSnackbar"
/>
<v-snackbar
v-model="snackbar"
:timeout="2000"
top
color="error"
>
<span style="color:#000">{{ snackbarText }}</span>
<v-btn
color="black"
text
@click="snackbar = false"
>
Close
</v-btn>
</v-snackbar>
</div>
</template>
<script>
import CVselect from '../../components/common/cv-select'
import CVswitch from '../../components/common/cv-switch'
import DualCalibration from "../../components/pipeline/OutputTab/DualCalibration";
import SingleCalibration from "../../components/pipeline/OutputTab/SingleCalibration";
export default {
name: 'Output',
components: {
CVselect,
CVswitch,
SingleCalibration,
DualCalibration,
},
// eslint-disable-next-line vue/require-prop-types
props: ['value'],
data() {
return {
snackbar: false,
snackbarText: ""
}
},
computed: {
contourTargetOffsetPointEdge: {
get() {
return this.$store.getters.currentPipelineSettings.contourTargetOffsetPointEdge
},
set(val) {
this.$store.commit("mutatePipeline", {"contourTargetOffsetPointEdge": val});
}
},
contourTargetOrientation: {
get() {
return this.$store.getters.currentPipelineSettings.contourTargetOrientation
},
set(val) {
this.$store.commit("mutatePipeline", {"contourTargetOrientation": val});
}
},
outputShowMultipleTargets: {
get() {
return this.$store.getters.currentPipelineSettings.outputShowMultipleTargets
},
set(val) {
this.$store.commit("mutatePipeline", {"outputShowMultipleTargets": val});
}
},
offsetRobotOffsetMode: {
get() {
return this.$store.getters.currentPipelineSettings.offsetRobotOffsetMode
},
set(val) {
this.$store.commit("mutatePipeline", {"offsetRobotOffsetMode": val});
}
},
selectedComponent: {
get() {
switch (this.offsetRobotOffsetMode) {
case 0:
return null;
case 1:
return SingleCalibration;
case 2:
return DualCalibration;
}
return ""
}
},
rawPoint: {
get() {
return undefined; // TODO fix
}
},
isTagPipeline: {
get() {
return this.$store.getters.currentPipelineSettings.pipelineType > 3;
}
}
},
methods: {
doUpdate() {
this.$emit('update')
},
showSnackbar(message) {
this.snackbarText = message;
this.snackbar = true;
},
}
}
</script>
<style scoped>
</style>

View File

@@ -1,135 +0,0 @@
<template>
<div>
<!-- Special hidden upload input that gets 'clicked' when the user selects the right dropdown item -->
<input
ref="file"
type="file"
accept=".csv"
style="display: none;"
@change="readFile"
>
<v-select
v-model="selectedModel"
dark
color="accent"
item-color="secondary"
label="Select a target model"
:items="targetList"
item-text="name"
item-value="data"
@input="handlePipelineUpdate('targetModel', targetList.indexOf(selectedModel))"
/>
<CVslider
v-model="cornerDetectionAccuracyPercentage"
class="pt-2"
slider-cols="12"
name="Contour simplification amount"
:disabled="selectedModel === null"
min="0"
max="100"
@input="handlePipelineData('cornerDetectionAccuracyPercentage')"
@rollback="e => rollback('cornerDetectionAccuracyPercentage', e)"
/>
<v-snackbar
v-model="snack"
top
:color="snackbar.color"
>
<span>{{ snackbar.text }}</span>
</v-snackbar>
</div>
</template>
<script>
import Papa from 'papaparse';
import CVslider from '../../components/common/cv-slider'
export default {
name: "PnP",
components: {
CVslider
},
data() {
return {
targetList: ['2020 High Goal Outer', '2020 High Goal Inner', '2019 Dual Target', '2020 Power Cell (7in)','2022 Cargo Ball (9.5in)', '2016 High Goal', '6.5in (36h11) AprilTag', '6in (16h5) AprilTag'], //Keep in sync with TargetModel.java
snackbar: {
color: "Success",
text: ""
},
snack: false,
}
},
computed: {
selectedModel: {
get() {
let ret = this.$store.getters.currentPipelineSettings.targetModel
return this.targetList[ret];
},
set(val) {
this.$store.commit("mutatePipeline", {"targetModel": this.targetList.indexOf(val)})
}
},
cornerDetectionAccuracyPercentage: {
get() {
return this.$store.getters.currentPipelineSettings.cornerDetectionAccuracyPercentage
},
set(val) {
this.$store.commit("mutatePipeline", {"cornerDetectionAccuracyPercentage": val});
}
},
},
methods: {
readFile(event) {
let file = event.target.files[0];
Papa.parse(file, {
complete: this.onParse,
skipEmptyLines: true
});
},
onParse(result) {
if (result.data.length > 0) {
let data = [];
for (let i = 0; i < result.data.length; i++) {
let item = result.data[i];
let tmp = [];
tmp.push(Number(item[0]));
tmp.push(Number(item[1]));
if (isNaN(tmp[0]) || isNaN(tmp[1])) {
this.snackbar = {
color: "error",
text: `Error: custom target CSV contained a non-numeric value on line ${i + 1}`
};
this.snack = true;
this.selectedModel = null;
return;
}
data.push(tmp);
}
this.uploadModel(data);
} else {
this.snackbar = {
color: "error",
text: "Error: custom target CSV was empty"
};
this.snack = true;
this.selectedModel = null;
}
},
}
}
</script>
<style scoped>
.miniMapClass {
width: 400px !important;
height: 100% !important;
margin-left: auto;
margin-right: auto;
}
</style>

View File

@@ -1,123 +0,0 @@
<template>
<div>
<v-row
align="start"
class="pb-4"
style="height: 300px;"
>
<!-- Simple table height must be set here and in the CSS for the fixed-header to work -->
<v-simple-table
fixed-header
height="100%"
dense
dark
>
<template v-slot:default>
<thead style="font-size: 1.25rem;">
<tr>
<th class="text-center">
Target Count
</th>
<th
v-if="$store.getters.pipelineType === 4 || (($store.getters.pipelineType - 2) === 3)"
class="text-center"
>
Fiducial ID
</th>
<template v-if="!$store.getters.currentPipelineSettings.solvePNPEnabled">
<th class="text-center">
Pitch &theta;&deg;
</th>
<th class="text-center">
Yaw &theta;&deg;
</th>
<th class="text-center">
Skew &theta;&deg;
</th>
<th class="text-center">
Area %
</th>
</template>
<template v-else>
<th class="text-center">
X meters
</th>
<th class="text-center">
Y meters
</th>
<th class="text-center">
Z Angle &theta;&deg;
</th>
</template>
<template v-if="$store.getters.pipelineType === 4 && $store.getters.currentPipelineSettings.solvePNPEnabled">
<th class="text-center">
Ambiguity %
</th>
</template>
</tr>
</thead>
<tbody>
<tr
v-for="(value, index) in $store.getters.currentPipelineResults.targets"
:key="index"
>
<td>{{ index }}</td>
<td v-if="$store.getters.pipelineType === 4 || (($store.getters.pipelineType - 2) === 3)">
{{ parseInt(value.fiducialId) }}
</td>
<template v-if="!$store.getters.currentPipelineSettings.solvePNPEnabled">
<td>{{ parseFloat(value.pitch).toFixed(2) }}</td>
<td>{{ parseFloat(value.yaw).toFixed(2) }}</td>
<td>{{ parseFloat(value.skew).toFixed(2) }}</td>
<td>{{ parseFloat(value.area).toFixed(2) }}</td>
</template>
<template v-else-if="$store.getters.currentPipelineSettings.solvePNPEnabled && $store.getters.pipelineType === 4">
<td>{{ parseFloat(value.pose.x).toFixed(2) }}&nbsp;m</td>
<td>{{ parseFloat(value.pose.y).toFixed(2) }}&nbsp;m</td>
<td>{{ (parseFloat(value.pose.angle_z) * 180 / Math.PI).toFixed(2) }}&deg;</td>
</template>
<template v-else-if="$store.getters.currentPipelineSettings.solvePNPEnabled">
<td>{{ parseFloat(value.pose.x).toFixed(2) }}&nbsp;m</td>
<td>{{ parseFloat(value.pose.y).toFixed(2) }}&nbsp;m</td>
<td>{{ (parseFloat(value.pose.angle_z) * 180 / Math.PI).toFixed(2) }}&deg;</td>
</template>
<template v-if="$store.getters.pipelineType === 4 && $store.getters.currentPipelineSettings.solvePNPEnabled">
<td>
{{ parseFloat(value.ambiguity).toFixed(2) }}
</td>
</template>
</tr>
</tbody>
</template>
</v-simple-table>
</v-row>
</div>
</template>
<script>
export default {
name: "TargetsTab",
}
</script>
<style scoped>
.v-data-table {
text-align: center;
background-color: transparent !important;
width: 100%;
height: 100%;
overflow-y: auto;
}
.v-data-table th {
background-color: #006492 !important;
}
.v-data-table th,td {
font-size: 1rem !important;
}
.v-data-table td {
font-family: monospace !important;
}
</style>

View File

@@ -1,276 +0,0 @@
<template>
<div :style="{'--averageHue': averageHue}">
<CVrangeSlider
id="hue-slider"
v-model="hsvHue"
:class="hueInverted ? 'inverted-slider' : 'normal-slider'"
name="Hue"
tooltip="Describes color"
:min="0"
:max="180"
:inverted="hueInverted"
@input="handlePipelineData('hsvHue')"
@rollback="e => rollback('hue',e)"
/>
<CVrangeSlider
id="sat-slider"
v-model="hsvSaturation"
class="normal-slider"
name="Saturation"
tooltip="Describes colorfulness; the smaller this value the 'whiter' the color becomes"
:min="0"
:max="255"
@input="handlePipelineData('hsvSaturation')"
@rollback="e => rollback('saturation',e)"
/>
<CVrangeSlider
id="value-slider"
v-model="hsvValue"
class="normal-slider"
name="Value"
tooltip="Describes lightness; the smaller this value the 'blacker' the color becomes"
:min="0"
:max="255"
@input="handlePipelineData('hsvValue')"
@rollback="e => rollback('value',e)"
/>
<CVSwitch
v-model="hueInverted"
name="Invert hue"
tooltip="Selects the hue range outside of the hue slider bounds instead of inside"
@input="handlePipelineData('hueInverted')"
@rollback="e => rollback('hueInverted',e)"
/>
<div class="pt-3 white--text">
Color Picker
</div>
<v-divider
class="mt-3"
/>
<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(hueInverted ? 2 : 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>
{{ hueInverted ? "Exclude" : "Set to" }} Average
</v-btn>
<v-btn
color="accent"
class="ma-2 black--text"
small
@click="setFunction(hueInverted ? 3: 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 class="mb-3" />
</div>
</template>
<script>
import CVrangeSlider from '../../components/common/cv-range-slider'
import CVSwitch from "@/components/common/cv-switch";
export default {
name: 'Threshold',
components: {
CVSwitch,
CVrangeSlider
},
// eslint-disable-next-line vue/require-prop-types
props: ['value'],
data() {
return {
currentFunction: undefined,
colorPicker: undefined,
showThresholdState: 0
}
},
computed: {
hsvHue: {
get() {
return this.$store.getters.currentPipelineSettings.hsvHue
},
set(val) {
this.$store.commit("mutatePipeline", {"hsvHue": val})
}
},
averageHue: {
get() {
const isInverted = this.$store.getters.currentPipelineSettings.hueInverted;
const arr = this.$store.getters.currentPipelineSettings.hsvHue;
let retVal = 0;
if (Array.isArray(arr)) {
retVal = (arr[0] + arr[1]);
} else {
retVal = (arr.first + arr.second);
}
if(isInverted){
retVal += 180;
}
if(retVal > 360){
retVal -= 360;
}
return retVal;
},
},
hueInverted: {
get() {
return this.$store.getters.currentPipelineSettings.hueInverted;
},
set(val) {
this.$store.commit("mutatePipeline", {"hueInverted": val});
}
},
hsvSaturation: {
get() {
return this.$store.getters.currentPipelineSettings.hsvSaturation;
},
set(val) {
this.$store.commit("mutatePipeline", {"hsvSaturation": val})
}
},
hsvValue: {
get() {
return this.$store.getters.currentPipelineSettings.hsvValue;
},
set(val) {
this.$store.commit("mutatePipeline", {"hsvValue": val});
}
},
},
mounted: function () {
const self = this;
this.colorPicker = require('../../plugins/ColorPicker').default;
this.$nextTick(() => {
self.colorPicker.initColorPicker();
});
},
methods: {
onClick(event) {
if (this.currentFunction !== undefined) {
this.colorPicker.initColorPicker();
let s = this.$store.getters.currentPipelineSettings;
let hsvArray = this.colorPicker.colorPickerClick(event, this.currentFunction,
[
[s.hsvHue[0], s.hsvSaturation[0], s.hsvValue[0]],
[s.hsvHue[1], s.hsvSaturation[1], s.hsvValue[1]]
].map(hsv => hsv.map(it => it || 0)));
// That `map` calls are to make sure that we don't let any undefined/null values slip in
this.currentFunction = undefined;
this.$store.state.colorPicking = false;
this.handlePipelineUpdate("outputShouldDraw", true);
s.hsvHue = [hsvArray[0][0], hsvArray[1][0]];
s.hsvSaturation = [hsvArray[0][1], hsvArray[1][1]];
s.hsvValue = [hsvArray[0][2], hsvArray[1][2]];
let msg = this.$msgPack.encode({
"changePipelineSetting": {
'hsvHue': s.hsvHue,
'hsvSaturation': s.hsvSaturation,
'hsvValue': s.hsvValue,
'cameraIndex': this.$store.state.currentCameraIndex
}
});
this.$store.state.websocket.ws.send(msg);
this.$emit('update');
}
},
setFunction(index) {
switch (index) {
case 0:
this.currentFunction = undefined;
this.$store.state.colorPicking = false;
this.handlePipelineUpdate("outputShouldDraw", true);
return;
case 1:
this.currentFunction = this.colorPicker.eyeDrop;
break;
case 2:
this.currentFunction = this.colorPicker.expand;
break;
case 3:
this.currentFunction = this.colorPicker.shrink;
break;
}
this.$store.state.colorPicking = true;
this.handlePipelineUpdate("outputShouldDraw", false);
this.$store.commit("mutatePipeline", {"inputShouldShow": true});
this.handlePipelineUpdate("inputShouldShow", true);
}
}
}
</script>
<style lang="css" scoped>
#hue-slider >>> .v-slider {
background: linear-gradient( to right, #f00 0%, #ff0 17%, #0f0 33%, #0ff 50%, #00f 67%, #f0f 83%, #f00 100% );
border-radius: 10px;
box-shadow: 0 0 5px #333, inset 0 0 3px #333;
}
#sat-slider >>> .v-slider {
background: linear-gradient( to right, #fff 0%, hsl(var(--averageHue), 100%, 50%) 100% );
border-radius: 10px;
box-shadow: 0 0 5px #333, inset 0 0 3px #333;
}
#value-slider >>> .v-slider {
background: linear-gradient( to right, #000 0%, hsl(var(--averageHue), 100%, 50%) 100% );
border-radius: 10px;
box-shadow: 0 0 5px #333, inset 0 0 3px #333;
}
>>> .v-slider__thumb {
outline: black solid thin;
}
.normal-slider >>> .v-slider__track-fill {
outline: black solid thin;
}
.inverted-slider >>> .v-slider__track-background {
outline: black solid thin;
}
</style>

View File

@@ -1,90 +0,0 @@
<template>
<div>
<v-row
class="pa-3"
no-gutters
>
<v-col
cols="12"
style="max-width: 1400px"
>
<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-col>
</v-row>
<v-snackbar
v-model="snack"
top
:color="snackbar.color"
>
<span>{{ snackbar.text }}</span>
</v-snackbar>
</div>
</template>
<script>
import Networking from './SettingsViews/Networking'
import Lighting from "./SettingsViews/Lighting";
import cvImage from '../components/common/cv-image'
import Stats from "./SettingsViews/Stats";
import DeviceControl from "./SettingsViews/DeviceControl";
export default {
name: 'SettingsTab',
components: {
cvImage,
// General,
},
data() {
return {
selectedTab: 0,
snack: false,
calibrationInProgress: false,
snackbar: {
color: "accent",
text: ""
},
}
},
computed: {
selectedComponent: {
get() {
return this.tabList[this.selectedTab];
}
},
settings: {
get() {
return this.$store.state.settings;
}
},
tabList: {
get() {
return [Stats, DeviceControl, Networking].concat(this.$store.state.settings.lighting.supported ? Lighting : []);
}
}
},
}
</script>
<style scoped>
.videoClass {
text-align: center;
}
.videoClass img {
padding-top: 10px;
height: auto !important;
vertical-align: middle;
}
</style>

View File

@@ -1,451 +0,0 @@
<template>
<div>
<v-row>
<v-col
cols="12"
lg="4"
md="6"
>
<v-btn
color="red"
@click="restartProgram()"
>
<v-icon left>
mdi-restart
</v-icon>
Restart PhotonVision
</v-btn>
</v-col>
<v-col
cols="12"
lg="4"
md="6"
>
<v-btn
color="red"
@click="restartDevice()"
>
<v-icon left>
mdi-restart-alert
</v-icon>
Restart Device
</v-btn>
</v-col>
<v-col
cols="12"
lg="4"
>
<v-btn
color="secondary"
@click="$refs.offlineUpdate.click()"
>
<v-icon left>
mdi-upload
</v-icon>
Offline Update
</v-btn>
</v-col>
</v-row>
<v-divider style="margin: 12px 0;" />
<v-row>
<v-col
cols="12"
sm="6"
>
<v-btn
color="secondary"
@click="() => showImportDialog = true"
>
<v-icon left>
mdi-import
</v-icon>
Import Settings
</v-btn>
</v-col>
<v-col
cols="12"
sm="6"
>
<v-btn
color="secondary"
@click="$refs.exportSettings.click()"
>
<v-icon left>
mdi-export
</v-icon>
Export Settings
</v-btn>
</v-col>
<v-col
cols="12"
sm="6"
>
<v-btn
color="secondary"
@click="$refs.exportLogFile.click()"
>
<v-icon left>
mdi-download
</v-icon>
Download Current Log
<!-- Special hidden link that gets 'clicked' when the user exports journalctl logs -->
<a
ref="exportLogFile"
style="color: black; text-decoration: none; display: none"
:href="'http://' + this.$address + '/api/utils/logs/photonvision-journalctl.txt'"
download="photonvision-journalctl.txt"
/>
</v-btn>
</v-col>
<v-col
cols="12"
sm="6"
>
<v-btn
color="secondary"
@click="showLogs()"
>
<v-icon left>
mdi-eye
</v-icon>
Show log viewer
</v-btn>
</v-col>
</v-row>
<v-snackbar
v-model="snack"
top
:color="snackbar.color"
:timeout="snackbarTimeout"
>
<span>{{ snackbar.text }}</span>
</v-snackbar>
<v-dialog
v-model="showImportDialog"
width="600"
@input="() => {
importType = undefined;
importFile = null;
}"
>
<v-card
color="primary"
dark
>
<v-card-title>Import Settings</v-card-title>
<v-card-text>
Upload and apply previously saved or exported PhotonVision settings to this device
<v-row
class="mt-6 ml-8"
>
<CVselect
v-model="importType"
name="Type"
tooltip="Select the type of settings file you are trying to upload"
:list="['All Settings', 'Hardware Config', 'Hardware Settings', 'Network Config']"
:select-cols="10"
/>
</v-row>
<v-row
class="mt-6 ml-8 mr-8"
>
<v-file-input
:disabled="importType === undefined"
:error-messages="importType === undefined ? 'Settings type not selected' : ''"
:accept="importType === 0 ? '.zip' : '.json'"
@change="(file) => importFile = file"
/>
</v-row>
<v-row
class="mt-12 ml-8 mr-8 mb-1"
style="display: flex; align-items: center; justify-content: center"
align="center"
>
<v-btn
color="secondary"
:disabled="importFile === null"
@click="uploadSettings"
>
<v-icon left>
mdi-import
</v-icon>
Import Settings
</v-btn>
</v-row>
</v-card-text>
</v-card>
</v-dialog>
<!-- Special hidden link that gets 'clicked' when the user exports settings -->
<a
ref="exportSettings"
style="color: black; text-decoration: none; display: none"
:href="`http://${this.$address}/api/settings/photonvision_config.zip`"
download="photonvision-settings.zip"
/>
<!-- Special hidden new jar upload input that gets 'clicked' when the user posts a new .jar -->
<input
ref="offlineUpdate"
type="file"
accept=".jar"
style="display: none;"
@change="doOfflineUpdate"
>
</div>
</template>
<script>
import CVselect from "../../components/common/cv-select";
export default {
// eslint-disable-next-line
name: "DeviceControl",
components: {
CVselect
},
data() {
return {
snack: false,
snackbarTimeout: 2000,
uploadPercentage: 0.0,
showImportDialog: false,
importType: undefined,
importFile: null,
snackbar: {
color: "success",
text: "",
}
};
},
computed: {
settings() {
return this.$store.state.settings.general;
},
version() {
return `${this.settings.version}`;
},
hwModel() {
if (this.settings.hardwareModel !== "") {
return `${this.settings.hardwareModel}`;
} else {
return `Unknown`;
}
},
platform() {
return `${this.settings.hardwarePlatform}`;
},
gpuAccel() {
return `${this.settings.gpuAcceleration ? "Enabled" : "Unsupported"} ${
this.settings.gpuAcceleration
? "(" + this.settings.gpuAcceleration + ")"
: ""
}`;
},
metrics() {
// console.log(this.$store.state.metrics);
return this.$store.state.metrics;
}
},
methods: {
restartProgram() {
this.axios.post("http://" + this.$address + "/api/utils/restartProgram")
.then(() => {
this.snackbar = {
color: "success",
text: "Successfully sent program restart request"
}
this.snack = true;
})
.catch(error => {
// This endpoint always return 204 regardless of outcome
if(error.request) {
this.snackbar = {
color: "error",
text: "Error while trying to process the request! The backend didn't respond.",
};
} else {
this.snackbar = {
color: "error",
text: "An error occurred while trying to process the request.",
};
}
this.snack = true;
})
},
restartDevice() {
this.axios.post("http://" + this.$address + "/api/utils/restartDevice")
.then(() => {
this.snackbar = {
color: "success",
text: "Successfully dispatched the restart command. It isn't confirmed if a device restart will occur."
}
this.snack = true;
})
.catch(error => {
if(error.response) {
this.snackbar = {
color: "error",
text: "The backend is unable to fulfil the request to restart the device."
}
} else if(error.request) {
this.snackbar = {
color: "error",
text: "Error while trying to process the request! The backend didn't respond.",
};
} else {
this.snackbar = {
color: "error",
text: "An error occurred while trying to process the request.",
};
}
this.snack = true;
})
},
uploadSettings() {
let formData = new FormData();
formData.append("data", this.importFile);
let settingsType
switch (this.importType) {
case 0:
settingsType = ""
break;
case 1:
settingsType = "/hardwareConfig"
break;
case 2:
settingsType = "/hardwareSettings"
break;
case 3:
settingsType = "/networkConfig"
break;
}
const requestUrl = `http://${this.$address}/api/settings${settingsType}`;
this.axios.post(requestUrl, formData, {
headers: { "Content-Type": "multipart/form-data" },
})
.then(response => {
this.snackbar = {
color: "success",
text: response.data.text || response.data
}
this.snack = true;
})
.catch(error => {
if(error.response) {
this.snackbar = {
color: "error",
text: error.response.data.text || error.response.data
}
} else if(error.request) {
this.snackbar = {
color: "error",
text: "Error while trying to process the request! The backend didn't respond.",
};
} else {
this.snackbar = {
color: "error",
text: "An error occurred while trying to process the request.",
};
}
this.snack = true;
})
this.showImportDialog = false
this.importType = undefined;
this.importFile = null;
},
doOfflineUpdate(event) {
this.snackbar = {
color: "secondary",
text: "New Software Upload in Process...",
};
this.snack = true;
this.snackbarTimeout = -1
let formData = new FormData();
formData.append("jarData", event.target.files[0]);
this.axios
.post(
"http://" + this.$address + "/api/utils/offlineUpdate",
formData,
{
headers: { "Content-Type": "multipart/form-data" },
onUploadProgress: function(progressEvent) {
this.uploadPercentage = parseInt(
Math.round((progressEvent.loaded / progressEvent.total) * 100)
);
if (this.uploadPercentage < 99.5) {
this.snackbar.text =
"New Software Upload in Process, " +
this.uploadPercentage +
"% complete";
} else {
this.snackbar.text = "Installing uploaded software...";
}
}.bind(this),
}
)
.then(response => {
this.snackbar = {
color: "success",
text: response.data.text || response.data
}
this.snack = true;
})
.catch(error => {
if(error.response) {
this.snackbar = {
color: "error",
text: error.response.data.text || error.response.data
};
} else if(error.request) {
this.snackbar = {
color: "error",
text: "Error while trying to process the request! The backend didn't respond.",
};
} else {
this.snackbar = {
color: "error",
text: "An error occurred while trying to process the request.",
};
}
this.snack = true;
})
// Reset the timeout after the loading bar
this.snackbarTimeout = 2000
},
showLogs() {
this.$store.state.logsOverlay = true;
},
},
};
</script>
<style lang="css" scoped>
.v-btn {
width: 100%;
}
.infoTable {
border: 1px solid;
border-collapse: separate;
border-spacing: 0;
border-radius: 5px;
text-align: left;
margin-bottom: 10px;
width: 100%;
display: block;
overflow-x: auto;
}
w
.infoElem {
padding: 1px 15px 1px 10px;
border-right: 1px solid;
}
</style>

View File

@@ -1,45 +0,0 @@
<template>
<div>
<CVslider
v-model="enabledLEDPercentage"
class="pt-2"
slider-cols="12"
name="Brightness"
min="0"
max="100"
@input="handleData('enabledLEDPercentage')"
/>
</div>
</template>
<script>
import CVslider from "../../components/common/cv-slider";
export default {
// eslint-disable-next-line
name: 'LEDControl',
components: {
CVslider,
},
computed: {
enabledLEDPercentage: {
get() {
return this.settings.brightness
},
set(value) {
this.$store.commit("mutateEnabledLEDPercentage", value)
}
},
isDHCP() {
return this.settings.connectionType === 0;
},
settings() {
return this.$store.state.settings.lighting;
}
},
}
</script>
<style lang="" scoped>
</style>

View File

@@ -1,298 +0,0 @@
<template>
<div>
<v-form
ref="form"
v-model="valid"
>
<CVinput
v-model="ntServerAddress"
:input-cols="inputCols - 1"
:label-cols="3"
:disabled="settings.runNTServer"
name="Team Number/NetworkTables Server Address"
tooltip="Enter the Team Number or the IP address of the NetworkTables Server"
:rules="[v => isValidTeamNumber(v) || 'The NetworkTables Server Address must be a non blank team number, IP address, or hostname']"
/>
<v-banner
v-show="!isValidTeamNumber(ntServerAddress) && !runNTServer"
rounded
color="red"
text-color="white"
style="margin: 8px 0"
icon="mdi-alert-circle-outline"
>
NetworkTables Server Address is unset or invalid. NetworkTables is unable to connect
</v-banner>
<CVradio
v-show="$store.state.settings.networkSettings.shouldManage"
v-model="connectionType"
:input-cols="inputCols"
name="IP Assignment Mode"
tooltip="DHCP will make the radio (router) automatically assign an IP address; this may result in an IP address that changes across reboots. Static IP assignment means that you pick the IP address and it won't change."
:list="['DHCP','Static']"
/>
<CVinput
v-if="!isDHCP"
v-model="staticIp"
:input-cols="inputCols"
:rules="[v => isIPv4(v) || 'Invalid IPv4 address']"
name="IP"
/>
<CVinput
v-show="$store.state.settings.networkSettings.shouldManage"
v-model="hostname"
:input-cols="inputCols"
:rules="[v => isHostname(v) || 'Invalid hostname']"
name="Hostname"
/>
<CVSwitch
v-model="runNTServer"
name="Run NetworkTables Server (Debugging Only)"
tooltip="If enabled, this device will create a NT server. This is useful for home debugging, but should be disabled on-robot."
class="mt-3 mb-3"
:text-cols="$vuetify.breakpoint.mdAndUp ? undefined : 5"
/>
<v-banner
v-show="runNTServer"
rounded
color="red"
text-color="white"
icon="mdi-information-outline"
>
This mode is intended for debugging; it should be off for proper usage. PhotonLib will NOT work!
</v-banner>
</v-form>
<v-btn
color="accent"
:class="runNTServer ? 'mt-3' : ''"
style="color: black; width: 100%;"
:disabled="!valid && !runNTServer"
@click="sendGeneralSettings()"
>
Save
</v-btn>
<v-snackbar
v-model="snack"
top
:color="snackbar.color"
timeout="2000"
>
<span>{{ snackbar.text }}</span>
</v-snackbar>
<template v-if="$store.state.settings.networkSettings.shouldManage && false">
<!-- Advanced controls for changing DHCP settings and stuff -->
<v-divider class="mt-4 mb-4" />
<v-card-title> Advanced </v-card-title>
<CVinput
:input-cols="inputCols"
name="Set DHCP command"
/>
<CVinput
:input-cols="inputCols"
name="Set static command"
/>
<CVinput
:input-cols="inputCols"
name="NetworkManager interface"
/>
<CVinput
:input-cols="inputCols"
name="Physical interface"
/>
</template>
</div>
</template>
<script>
import CVradio from '../../components/common/cv-radio'
import CVinput from '../../components/common/cv-input'
import CVSwitch from "@/components/common/cv-switch";
// https://stackoverflow.com/a/17871737
const ipv4Regex = /^((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])$/;
// https://stackoverflow.com/a/18494710
const hostnameRegex = /^([a-zA-Z0-9]+(-[a-zA-Z0-9]+)*)+(\.([a-zA-Z0-9]+(-[a-zA-Z0-9]+)*))*$/;
const teamNumberRegex = /^[1-9][0-9]{0,3}$/;
const badTeamNumberRegex = /^[0-9]{5,}$/;
export default {
name: 'Networking',
components: {
CVSwitch,
CVradio,
CVinput
},
data() {
return {
file: undefined,
snackbar: {
color: "success",
text: ""
},
snack: false,
isLoading: false,
valid: true, // Are all settings valid
}
},
computed: {
inputCols() {
return this.$vuetify.breakpoint.mdAndUp ? 10 : 7;
},
isDHCP() {
return this.settings.connectionType === 0;
},
settings() {
return this.$store.state.settings.networkSettings;
},
ntServerAddress: {
get() {
return this.settings.ntServerAddress
},
set(value) {
this.$store.commit('mutateNetworkSettings', {['ntServerAddress']: value || ""});
}
},
runNTServer: {
get() {
return this.settings.runNTServer
},
set(value) {
this.$store.commit('mutateNetworkSettings', {['runNTServer']: value});
}
},
connectionType: {
get() {
return this.settings.connectionType
},
set(value) {
this.$store.commit('mutateNetworkSettings', {['connectionType']: value});
}
},
staticIp: {
get() {
return this.settings.staticIp
},
set(value) {
this.$store.commit('mutateNetworkSettings', {['staticIp']: value});
}
},
hostname: {
get() {
return this.settings.hostname
},
set(value) {
this.$store.commit('mutateNetworkSettings', {['hostname']: value});
}
},
},
methods: {
isValidTeamNumber(v) {
if (teamNumberRegex.test(v)) return true;
if (ipv4Regex.test(v)) return true;
// need to check these before the hostname. "0" and "99999" are valid hostnames,
// but we don't want to allow then
if (v == '0') return false;
if (badTeamNumberRegex.test(v)) return false;
if (hostnameRegex.test(v)) return true;
return false;
},
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;
},
sendGeneralSettings() {
const changingStaticIp = !this.isDHCP;
this.snackbar = {
color: "secondary",
text: "Updating settings..."
};
this.snack = true;
this.axios.post("http://" + this.$address + "/api/settings/general", this.settings)
.then(response => {
this.snackbar = {
color: "success",
text: response.data.text || response.data
}
this.snack = true;
})
.catch(error => {
if(error.response) {
if (error.status === 504 || changingStaticIp) {
this.snackbar = {
color: "error",
text: `Connection lost! Try the new static IP at ${this.staticIp}:5800 or ${this.hostname}:5800?`
};
} else {
this.snackbar = {
color: "error",
text: error.response.data.text || error.response.data
}
}
} else if(error.request) {
this.snackbar = {
color: "error",
text: "Error while trying to process the request! The backend didn't respond.",
};
} else {
this.snackbar = {
color: "error",
text: "An error occurred while trying to process the request.",
};
}
this.snack = true;
})
},
},
}
</script>
<style scoped>
.v-data-table {
/*text-align: center;*/
background-color: transparent !important;
width: 100%;
height: 100%;
overflow-y: auto;
}
.v-data-table th {
background-color: #006492 !important;
}
.v-data-table th, td {
font-size: 1rem !important;
}
.v-data-table td {
font-family: monospace !important;
}
</style>
<style>
.v-banner__wrapper {
padding: 6px !important;
}
</style>

View File

@@ -1,183 +0,0 @@
<template>
<div>
<v-row class="pa-4">
<table class="infoTable">
<tr>
<th class="infoElem infoElemTitle">
Version
</th>
<th class="infoElem infoElemTitle">
Hardware Model
</th>
<th class="infoElem infoElemTitle">
Platform
</th>
<th class="infoElem infoElemTitle">
GPU Acceleration
</th>
</tr>
<tr>
<td class="infoElem">
{{ version.replace(" ", "") }}
</td>
<td class="infoElem">
{{ hwModel.replace(" ", "") }}
</td>
<td class="infoElem">
{{ platform.replace(" ", "") }}
</td>
<td class="infoElem">
{{ gpuAccel.replace(" ", "") }}
</td>
</tr>
</table>
<table class="infoTable">
<tr>
<th class="infoElem infoElemTitle">
CPU Usage
</th>
<th class="infoElem infoElemTitle">
CPU Temp
</th>
<th class="infoElem infoElemTitle">
CPU Memory Usage
</th>
<th class="infoElem infoElemTitle">
GPU Memory Usage
</th>
<th class="infoElem infoElemTitle">
Disk Usage
</th>
<th class="infoElem">
<v-tooltip top>
<template v-slot:activator="{ on, attrs }">
<span
v-bind="attrs"
class="infoElemTitle"
v-on="on"
>
CPU Throttling
</span>
</template>
<span>
Current or Previous Reason for the cpu being held back from maximum performance.
</span>
</v-tooltip>
</th>
<th class="infoElem infoElemTitle">
CPU Uptime
</th>
</tr>
<tr v-if="metrics.cpuUtil !== 'N/A'">
<td class="infoElem">
{{ metrics.cpuUtil }}%
</td>
<td class="infoElem">
{{ parseInt(metrics.cpuTemp) }}&deg;&nbsp;C
</td>
<td class="infoElem">
{{ metrics.ramUtil }}MB of {{ metrics.cpuMem }}MB
</td>
<td class="infoElem">
{{ metrics.gpuMemUtil }}MB of {{ metrics.gpuMem }}MB
</td>
<td class="infoElem">
{{ metrics.diskUtilPct }}
</td>
<td class="infoElem">
{{ metrics.cpuThr }}
</td>
<td class="infoElem">
{{ metrics.cpuUptime }}
</td>
</tr>
<tr v-if="metrics.cpuUtil === 'N/A'">
<td class="infoElem">
---
</td>
<td class="infoElem">
---
</td>
<td class="infoElem">
---
</td>
<td class="infoElem">
---
</td>
<td class="infoElem">
---
</td>
<td class="infoElem">
---
</td>
<td class="infoElem">
---
</td>
</tr>
</table>
</v-row>
</div>
</template>
<script>
export default {
name: 'Stats',
computed: {
settings() {
return this.$store.state.settings.general;
},
version() {
return `${this.settings.version}`;
},
hwModel() {
if (this.settings.hardwareModel !== '') {
return `${this.settings.hardwareModel}`;
} else {
return `Unknown`;
}
},
platform() {
return `${this.settings.hardwarePlatform}`;
},
gpuAccel() {
return `${this.settings.gpuAcceleration ? "Enabled" : "Unsupported"} ${this.settings.gpuAcceleration ? "(" + this.settings.gpuAcceleration + ")" : ""}`
},
metrics() {
// console.log(this.$store.state.metrics);
return this.$store.state.metrics;
}
},
}
</script>
<style lang="css" scoped>
.v-btn {
width: 100%;
}
.infoTable{
border: 1px solid;
border-collapse: separate;
border-spacing: 0;
border-radius: 5px;
text-align: left;
margin-bottom: 10px;
width: 100%;
display: block;
overflow-x: auto;
}
.infoElem {
padding: 1px 15px 1px 10px;
border-right: 1px solid;
font-weight: normal;
}
.infoElemTitle {
font-size: 18px;
text-decoration: underline;
text-decoration-color: #ffd843;
}
</style>