UI patches (#905)

- Show 0 clients when NT server props are undefined
- Add Prettier 

---------

Co-authored-by: Matthew Morley <matthew.morley.ca@gmail.com>
This commit is contained in:
Sriman Achanta
2023-08-31 16:56:58 -04:00
committed by GitHub
parent de394418f6
commit 08892b9e68
55 changed files with 3323 additions and 2808 deletions

View File

@@ -67,8 +67,11 @@ jobs:
- name: Install Dependencies
run: npm ci
- name: Check Formatting
- name: Check Linting
run: npm run lint
- name: Check Formatting
run: npm run check-format
photon-build-examples:
runs-on: ubuntu-22.04
name: "Build Examples"

View File

@@ -1,9 +1,10 @@
{
"root": true,
"extends": [
"plugin:vue/vue3-recommended",
"eslint:recommended",
"plugin:vue/recommended",
"@vue/eslint-config-typescript/recommended"
"@vue/eslint-config-typescript",
"@vue/eslint-config-prettier/skip-formatting"
],
"rules": {
"quotes": ["error", "double"],

View File

@@ -0,0 +1 @@
src/assets/fonts/PromptRegular.ts

View File

@@ -0,0 +1,8 @@
{
"$schema": "https://json.schemastore.org/prettierrc",
"semi": true,
"tabWidth": 2,
"singleQuote": false,
"printWidth": 120,
"trailingComma": "none"
}

File diff suppressed because it is too large Load Diff

View File

@@ -7,7 +7,9 @@
"build": "run-p build-only",
"preview": "vite preview --port 4173",
"build-only": "vite build",
"lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix --ignore-path .gitignore"
"lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix --ignore-path .gitignore",
"format": "prettier --write src/",
"check-format": "prettier --check src/"
},
"dependencies": {
"@fontsource/prompt": "^5.0.5",
@@ -22,10 +24,13 @@
"vuetify": "^2.6.15"
},
"devDependencies": {
"@rushstack/eslint-patch": "^1.3.2",
"@vue/eslint-config-prettier": "^8.0.0",
"@vue/eslint-config-typescript": "^11.0.3",
"prettier": "^3.0.0",
"@types/node": "^16.11.45",
"@types/three": "^0.154.0",
"@vitejs/plugin-vue2": "^2.2.0",
"@vue/eslint-config-typescript": "^11.0.3",
"@vue/tsconfig": "^0.1.3",
"deepmerge": "^4.3.1",
"eslint": "^8.45.0",

View File

@@ -9,39 +9,39 @@ import PhotonLogView from "@/components/app/photon-log-view.vue";
import PhotonErrorSnackbar from "@/components/app/photon-error-snackbar.vue";
const websocket = new AutoReconnectingWebsocket(
`ws://${inject("backendHost")}/websocket_data`,
() => {
useStateStore().$patch({ backendConnected: true });
},
(data) => {
if(data.log !== undefined) {
useStateStore().addLogFromWebsocket(data.log);
}
if(data.settings !== undefined) {
useSettingsStore().updateGeneralSettingsFromWebsocket(data.settings);
}
if(data.cameraSettings !== undefined) {
useCameraSettingsStore().updateCameraSettingsFromWebsocket(data.cameraSettings);
}
if(data.ntConnectionInfo !== undefined) {
useStateStore().updateNTConnectionStatusFromWebsocket(data.ntConnectionInfo);
}
if(data.metrics !== undefined) {
useSettingsStore().updateMetricsFromWebsocket(data.metrics);
}
if(data.updatePipelineResult !== undefined) {
useStateStore().updatePipelineResultsFromWebsocket(data.updatePipelineResult);
}
if(data.mutatePipelineSettings !== undefined && data.cameraIndex !== undefined) {
useCameraSettingsStore().changePipelineSettingsInStore(data.mutatePipelineSettings, data.cameraIndex);
}
if(data.calibrationData !== undefined) {
useStateStore().updateCalibrationStateValuesFromWebsocket(data.calibrationData);
}
},
() => {
useStateStore().$patch({ backendConnected: false });
`ws://${inject("backendHost")}/websocket_data`,
() => {
useStateStore().$patch({ backendConnected: true });
},
(data) => {
if (data.log !== undefined) {
useStateStore().addLogFromWebsocket(data.log);
}
if (data.settings !== undefined) {
useSettingsStore().updateGeneralSettingsFromWebsocket(data.settings);
}
if (data.cameraSettings !== undefined) {
useCameraSettingsStore().updateCameraSettingsFromWebsocket(data.cameraSettings);
}
if (data.ntConnectionInfo !== undefined) {
useStateStore().updateNTConnectionStatusFromWebsocket(data.ntConnectionInfo);
}
if (data.metrics !== undefined) {
useSettingsStore().updateMetricsFromWebsocket(data.metrics);
}
if (data.updatePipelineResult !== undefined) {
useStateStore().updatePipelineResultsFromWebsocket(data.updatePipelineResult);
}
if (data.mutatePipelineSettings !== undefined && data.cameraIndex !== undefined) {
useCameraSettingsStore().changePipelineSettingsInStore(data.mutatePipelineSettings, data.cameraIndex);
}
if (data.calibrationData !== undefined) {
useStateStore().updateCalibrationStateValuesFromWebsocket(data.calibrationData);
}
},
() => {
useStateStore().$patch({ backendConnected: false });
}
);
useStateStore().$patch({ websocket: websocket });
@@ -51,11 +51,7 @@ useStateStore().$patch({ websocket: websocket });
<v-app>
<photon-sidebar />
<v-main>
<v-container
class="main-container"
fluid
fill-height
>
<v-container class="main-container" fluid fill-height>
<v-layout>
<v-flex>
<router-view />
@@ -70,7 +66,7 @@ useStateStore().$patch({ websocket: websocket });
</template>
<style lang="scss">
@import 'vuetify/src/styles/settings/_variables';
@import "vuetify/src/styles/settings/_variables";
@media #{map-get($display-breakpoints, 'md-and-down')} {
html {

View File

@@ -18,7 +18,7 @@ import { TrackballControls } from "three/examples/jsm/controls/TrackballControls
import { type Object3D } from "three";
const props = defineProps<{
targets: PhotonTarget[]
targets: PhotonTarget[];
}>();
let scene: Scene | undefined;
@@ -29,25 +29,20 @@ let controls: TrackballControls | undefined;
let previousTargets: Object3D[] = [];
const drawTargets = (targets: PhotonTarget[]) => {
// Check here, since if we check in watchEffect this never gets called
if(scene === undefined || camera === undefined || renderer === undefined || controls === undefined) {
if (scene === undefined || camera === undefined || renderer === undefined || controls === undefined) {
return;
}
scene.remove(...previousTargets);
previousTargets = [];
targets.forEach(target => {
if(target.pose === undefined) return;
targets.forEach((target) => {
if (target.pose === undefined) return;
const geometry = new BoxGeometry(0.3 / 5, 0.2, 0.2);
const material = new MeshNormalMaterial();
const quaternion = new Quaternion(
target.pose.qx,
target.pose.qy,
target.pose.qz,
target.pose.qw
);
const quaternion = new Quaternion(target.pose.qx, target.pose.qy, target.pose.qz, target.pose.qw);
const cube = new Mesh(geometry, material);
cube.position.set(target.pose.x, target.pose.y, target.pose.z);
@@ -72,7 +67,7 @@ const drawTargets = (targets: PhotonTarget[]) => {
previousTargets.push(arrow);
});
if(previousTargets.length > 0) {
if (previousTargets.length > 0) {
scene.add(...previousTargets);
}
};
@@ -80,7 +75,7 @@ const onWindowResize = () => {
const container = document.getElementById("container");
const canvas = document.getElementById("view");
if(container === null || canvas === null || camera === undefined || renderer === undefined) {
if (container === null || canvas === null || camera === undefined || renderer === undefined) {
return;
}
@@ -91,7 +86,7 @@ const onWindowResize = () => {
renderer.setSize(canvas.clientWidth, canvas.clientHeight);
};
const resetCamFirstPerson = () => {
if(scene === undefined || camera === undefined || controls === undefined) {
if (scene === undefined || camera === undefined || controls === undefined) {
return;
}
@@ -100,12 +95,12 @@ const resetCamFirstPerson = () => {
camera.up.set(0, 0, 1);
controls.target.set(4.0, 0.0, 0.0);
controls.update();
if(previousTargets.length > 0) {
if (previousTargets.length > 0) {
scene.add(...previousTargets);
}
};
const resetCamThirdPerson = () => {
if(scene === undefined || camera === undefined || controls === undefined) {
if (scene === undefined || camera === undefined || controls === undefined) {
return;
}
@@ -114,7 +109,7 @@ const resetCamThirdPerson = () => {
camera.up.set(0, 0, 1);
controls.target.set(4.0, 0.0, 0.0);
controls.update();
if(previousTargets.length > 0) {
if (previousTargets.length > 0) {
scene.add(...previousTargets);
}
};
@@ -124,7 +119,7 @@ onMounted(() => {
camera = new PerspectiveCamera(75, 800 / 800, 0.1, 1000);
const canvas = document.getElementById("view");
if(canvas === null) return;
if (canvas === null) return;
renderer = new WebGLRenderer({ canvas: canvas });
scene.background = new Color(0xa9a9a9);
@@ -133,14 +128,20 @@ onMounted(() => {
window.addEventListener("resize", onWindowResize);
const referenceFrameCues: Object3D[] = [];
referenceFrameCues.push(new ArrowHelper(new Vector3(1, 0, 0).normalize(), new Vector3(0, 0, 0), 1, 0xff0000, 0.1, 0.1));
referenceFrameCues.push(new ArrowHelper(new Vector3(0, 1, 0).normalize(), new Vector3(0, 0, 0), 1, 0x00ff00, 0.1, 0.1));
referenceFrameCues.push(new ArrowHelper(new Vector3(0, 0, 1).normalize(), new Vector3(0, 0, 0), 1, 0x0000ff, 0.1, 0.1));
referenceFrameCues.push(
new ArrowHelper(new Vector3(1, 0, 0).normalize(), new Vector3(0, 0, 0), 1, 0xff0000, 0.1, 0.1)
);
referenceFrameCues.push(
new ArrowHelper(new Vector3(0, 1, 0).normalize(), new Vector3(0, 0, 0), 1, 0x00ff00, 0.1, 0.1)
);
referenceFrameCues.push(
new ArrowHelper(new Vector3(0, 0, 1).normalize(), new Vector3(0, 0, 0), 1, 0x0000ff, 0.1, 0.1)
);
// Draw the Camera Body
const camSize = 0.2;
const camBodyGeometry = new BoxGeometry(camSize, camSize, camSize);
const camLensGeometry = new ConeGeometry(camSize*0.4, camSize*0.8, 30);
const camLensGeometry = new ConeGeometry(camSize * 0.4, camSize * 0.8, 30);
const camMaterial = new MeshNormalMaterial();
const camBody = new Mesh(camBodyGeometry, camMaterial);
const camLens = new Mesh(camLensGeometry, camMaterial);
@@ -150,10 +151,7 @@ onMounted(() => {
referenceFrameCues.push(camBody);
referenceFrameCues.push(camLens);
controls = new TrackballControls(
camera,
renderer.domElement
);
controls = new TrackballControls(camera, renderer.domElement);
controls.rotateSpeed = 1.0;
controls.zoomSpeed = 1.2;
controls.panSpeed = 0.8;
@@ -168,7 +166,7 @@ onMounted(() => {
controls.update();
const animate = () => {
if(scene === undefined || camera === undefined || renderer === undefined || controls === undefined) {
if (scene === undefined || camera === undefined || renderer === undefined || controls === undefined) {
return;
}
@@ -185,41 +183,23 @@ onBeforeUnmount(() => {
window.removeEventListener("resize", onWindowResize);
});
watchEffect(() => {
drawTargets(props.targets);
drawTargets(props.targets);
});
</script>
<template>
<div
id="container"
style="width: 100%"
>
<div id="container" style="width: 100%">
<v-row>
<v-col
align-self="stretch"
style="display: flex; justify-content: center"
>
<canvas
id="view"
/>
<v-col align-self="stretch" style="display: flex; justify-content: center">
<canvas id="view" />
</v-col>
</v-row>
<v-row style="margin-bottom: 24px">
<v-col style="display: flex; justify-content: center">
<v-btn
color="secondary"
@click="resetCamFirstPerson"
>
First Person
</v-btn>
<v-btn color="secondary" @click="resetCamFirstPerson"> First Person </v-btn>
</v-col>
<v-col style="display: flex; justify-content: center">
<v-btn
color="secondary"
@click="resetCamThirdPerson"
>
Third Person
</v-btn>
<v-btn color="secondary" @click="resetCamThirdPerson"> Third Person </v-btn>
</v-col>
</v-row>
</div>

View File

@@ -6,14 +6,15 @@ import loadingImage from "@/assets/images/loading.svg";
import type { StyleValue } from "vue/types/jsx";
const props = defineProps<{
streamType: "Raw" | "Processed",
id?: string
streamType: "Raw" | "Processed";
id?: string;
}>();
const src = computed<string>(() => {
const port = useCameraSettingsStore().currentCameraSettings.stream[props.streamType === "Raw" ? "inputPort" : "outputPort"];
const port =
useCameraSettingsStore().currentCameraSettings.stream[props.streamType === "Raw" ? "inputPort" : "outputPort"];
if(!useStateStore().backendConnected || port === 0) {
if (!useStateStore().backendConnected || port === 0) {
return loadingImage;
}
@@ -22,9 +23,9 @@ const src = computed<string>(() => {
const alt = computed<string>(() => `${props.streamType} Stream View`);
const style = computed<StyleValue>(() => {
if(useStateStore().colorPickingMode) {
if (useStateStore().colorPickingMode) {
return { cursor: "crosshair" };
} else if(src.value !== loadingImage) {
} else if (src.value !== loadingImage) {
return { cursor: "pointer" };
}
@@ -32,19 +33,12 @@ const style = computed<StyleValue>(() => {
});
const handleClick = () => {
if(!useStateStore().colorPickingMode && src.value !== loadingImage) {
if (!useStateStore().colorPickingMode && src.value !== loadingImage) {
window.open(src.value);
}
};
</script>
<template>
<img
:id="id"
crossorigin="anonymous"
:src="src"
:alt="alt"
:style="style"
@click="handleClick"
>
<img :id="id" crossorigin="anonymous" :src="src" :alt="alt" :style="style" @click="handleClick" />
</template>

View File

@@ -5,7 +5,9 @@ import { useStateStore } from "@/stores/StateStore";
const selectedLogLevels = ref<LogLevel[]>([LogLevel.ERROR, LogLevel.WARN, LogLevel.INFO]);
const logs = computed<LogMessage[]>(() => useStateStore().logMessages.filter(message => selectedLogLevels.value.includes(message.level)));
const logs = computed<LogMessage[]>(() =>
useStateStore().logMessages.filter((message) => selectedLogLevels.value.includes(message.level))
);
const backendHost = inject<string>("backendHost");
@@ -33,38 +35,22 @@ const handleLogExport = () => {
exportLogFile.value.click();
};
document.addEventListener("keydown", e => {
document.addEventListener("keydown", (e) => {
switch (e.key) {
case "`":
useStateStore().$patch(state => state.showLogModal = !state.showLogModal);
useStateStore().$patch((state) => (state.showLogModal = !state.showLogModal));
break;
}
});
</script>
<template>
<v-dialog
v-model="useStateStore().showLogModal"
width="1500"
dark
>
<v-card
dark
class="pt-3"
color="primary"
flat
>
<v-dialog v-model="useStateStore().showLogModal" width="1500" dark>
<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="handleLogExport"
>
<v-icon left>
mdi-download
</v-icon>
<v-btn color="secondary" style="margin-left: auto" depressed @click="handleLogExport">
<v-icon left> mdi-download </v-icon>
Download Current Log
<!-- Special hidden link that gets 'clicked' when the user exports journalctl logs -->
@@ -79,34 +65,16 @@ document.addEventListener("keydown", e => {
</v-card-title>
<div class="pr-6 pl-6">
<v-btn-toggle
v-model="selectedLogLevels"
dark
multiple
class="fill mb-4"
>
<v-btn
v-for="(level) in [0, 1, 2, 3]"
:key="level"
color="secondary"
class="fill"
>
<v-btn-toggle v-model="selectedLogLevels" dark multiple class="fill mb-4">
<v-btn v-for="level in [0, 1, 2, 3]" :key="level" color="secondary" class="fill">
{{ getLogLevelFromIndex(level) }}
</v-btn>
</v-btn-toggle>
<v-card-text
v-if="logs.length === 0"
style="font-size: 18px; font-weight: 600"
>
<v-card-text v-if="logs.length === 0" style="font-size: 18px; font-weight: 600">
There are no Logs to show
</v-card-text>
<v-virtual-scroll
v-else
:items="logs"
item-height="50"
height="600"
>
<template #default="{item}">
<v-virtual-scroll v-else :items="logs" item-height="50" height="600">
<template #default="{ item }">
<div :class="[getLogColor(item.level) + '--text', 'log-item']">
{{ item.message }}
</div>
@@ -118,13 +86,7 @@ document.addEventListener("keydown", e => {
<v-card-actions>
<v-spacer />
<v-btn
color="white"
text
@click="() => useStateStore().showLogModal = false"
>
Close
</v-btn>
<v-btn color="white" text @click="() => (useStateStore().showLogModal = false)"> Close </v-btn>
</v-card-actions>
</v-card>
</v-dialog>

View File

@@ -4,8 +4,12 @@ import { useSettingsStore } from "@/stores/settings/GeneralSettingsStore";
import { useStateStore } from "@/stores/StateStore";
const compact = computed<boolean>({
get: () => { return useStateStore().sidebarFolded; },
set: (val) => { useStateStore().setSidebarFolded(val); }
get: () => {
return useStateStore().sidebarFolded;
},
set: (val) => {
useStateStore().setSidebarFolded(val);
}
});
// Vuetify2 doesn't yet support the useDisplay API so this is required to access the prop when using the Composition API
@@ -13,39 +17,17 @@ const mdAndUp = computed<boolean>(() => getCurrentInstance()?.proxy.$vuetify.bre
</script>
<template>
<v-navigation-drawer
dark
app
permanent
:mini-variant="compact || !mdAndUp"
color="primary"
>
<v-navigation-drawer dark app permanent :mini-variant="compact || !mdAndUp" color="primary">
<v-list>
<!-- List item for the heading; note that there are some tricks in setting padding and image width make things look right -->
<v-list-item
:class="(compact || !mdAndUp) ? 'pr-0 pl-0' : ''"
style="display: flex; justify-content: center"
>
<v-list-item :class="compact || !mdAndUp ? 'pr-0 pl-0' : ''" style="display: flex; justify-content: center">
<v-list-item-icon class="mr-0">
<img
v-if="!(compact || !mdAndUp)"
class="logo"
src="@/assets/images/logoLarge.svg"
alt="large logo"
>
<img
v-else
class="logo"
src="@/assets/images/logoSmall.svg"
alt="small logo"
>
<img v-if="!(compact || !mdAndUp)" class="logo" src="@/assets/images/logoLarge.svg" alt="large logo" />
<img v-else class="logo" src="@/assets/images/logoSmall.svg" alt="small logo" />
</v-list-item-icon>
</v-list-item>
<v-list-item
link
to="/dashboard"
>
<v-list-item link to="/dashboard">
<v-list-item-icon>
<v-icon>mdi-view-dashboard</v-icon>
</v-list-item-icon>
@@ -53,11 +35,7 @@ const mdAndUp = computed<boolean>(() => getCurrentInstance()?.proxy.$vuetify.bre
<v-list-item-title>Dashboard</v-list-item-title>
</v-list-item-content>
</v-list-item>
<v-list-item
ref="camerasTabOpener"
link
to="/cameras"
>
<v-list-item ref="camerasTabOpener" link to="/cameras">
<v-list-item-icon>
<v-icon>mdi-camera</v-icon>
</v-list-item-icon>
@@ -65,10 +43,7 @@ const mdAndUp = computed<boolean>(() => getCurrentInstance()?.proxy.$vuetify.bre
<v-list-item-title>Cameras</v-list-item-title>
</v-list-item-content>
</v-list-item>
<v-list-item
link
to="/settings"
>
<v-list-item link to="/settings">
<v-list-item-icon>
<v-icon>mdi-cog</v-icon>
</v-list-item-icon>
@@ -76,10 +51,7 @@ const mdAndUp = computed<boolean>(() => getCurrentInstance()?.proxy.$vuetify.bre
<v-list-item-title>Settings</v-list-item-title>
</v-list-item-content>
</v-list-item>
<v-list-item
link
to="/docs"
>
<v-list-item link to="/docs">
<v-list-item-icon>
<v-icon>mdi-bookshelf</v-icon>
</v-list-item-icon>
@@ -87,46 +59,27 @@ const mdAndUp = computed<boolean>(() => getCurrentInstance()?.proxy.$vuetify.bre
<v-list-item-title>Documentation</v-list-item-title>
</v-list-item-content>
</v-list-item>
<v-list-item
v-if="mdAndUp"
link
@click="() => compact = !compact"
>
<v-list-item v-if="mdAndUp" link @click="() => (compact = !compact)">
<v-list-item-icon>
<v-icon v-if="compact || !mdAndUp">
mdi-chevron-right
</v-icon>
<v-icon v-else>
mdi-chevron-left
</v-icon>
<v-icon v-if="compact || !mdAndUp"> mdi-chevron-right </v-icon>
<v-icon v-else> mdi-chevron-left </v-icon>
</v-list-item-icon>
<v-list-item-content>
<v-list-item-title>Compact Mode</v-list-item-title>
</v-list-item-content>
</v-list-item>
<div style="position: absolute; bottom: 0; left: 0;">
<div style="position: absolute; bottom: 0; left: 0">
<v-list-item>
<v-list-item-icon>
<v-icon v-if="useSettingsStore().network.runNTServer">
mdi-server
</v-icon>
<v-icon v-else-if="useStateStore().ntConnectionStatus.connected">
mdi-robot
</v-icon>
<v-icon
v-else
style="border-radius: 100%"
>
mdi-robot-off
</v-icon>
<v-icon v-if="useSettingsStore().network.runNTServer"> mdi-server </v-icon>
<v-icon v-else-if="useStateStore().ntConnectionStatus.connected"> mdi-robot </v-icon>
<v-icon v-else style="border-radius: 100%"> mdi-robot-off </v-icon>
</v-list-item-icon>
<v-list-item-content>
<v-list-item-title
v-if="useSettingsStore().network.runNTServer"
class="text-wrap"
>
NetworkTables server running for <span class="accent--text">{{ useStateStore().ntConnectionStatus.clients }}</span> clients
<v-list-item-title v-if="useSettingsStore().network.runNTServer" class="text-wrap">
NetworkTables server running for
<span class="accent--text">{{ useStateStore().ntConnectionStatus.clients || 0 }}</span> clients
</v-list-item-title>
<v-list-item-title
v-else-if="useStateStore().ntConnectionStatus.connected && useStateStore().backendConnected"
@@ -134,17 +87,11 @@ const mdAndUp = computed<boolean>(() => getCurrentInstance()?.proxy.$vuetify.bre
style="flex-direction: column; display: flex"
>
NetworkTables Server Connected!
<span
class="accent--text"
>
<span class="accent--text">
{{ useStateStore().ntConnectionStatus.address }}
</span>
</v-list-item-title>
<v-list-item-title
v-else
class="text-wrap"
style="flex-direction: column; display: flex"
>
<v-list-item-title v-else class="text-wrap" style="flex-direction: column; display: flex">
Not connected to NetworkTables Server!
</v-list-item-title>
</v-list-item-content>
@@ -152,15 +99,8 @@ const mdAndUp = computed<boolean>(() => getCurrentInstance()?.proxy.$vuetify.bre
<v-list-item>
<v-list-item-icon>
<v-icon v-if="useStateStore().backendConnected">
mdi-server-network
</v-icon>
<v-icon
v-else
style="border-radius: 100%;"
>
mdi-server-network-off
</v-icon>
<v-icon v-if="useStateStore().backendConnected"> mdi-server-network </v-icon>
<v-icon v-else style="border-radius: 100%"> mdi-server-network-off </v-icon>
</v-list-item-icon>
<v-list-item-content>
<v-list-item-title class="text-wrap">

View File

@@ -12,41 +12,60 @@ import CvSelect from "@/components/common/cv-select.vue";
import CvNumberInput from "@/components/common/cv-number-input.vue";
import { WebsocketPipelineType } from "@/types/WebsocketDataTypes";
const settingsValid = ref(true);
const getCalibrationCoeffs = (resolution: Resolution) => {
return useCameraSettingsStore().currentCameraSettings.completeCalibrations.find(cal => cal.resolution.width === resolution.width && cal.resolution.height === resolution.height);
return useCameraSettingsStore().currentCameraSettings.completeCalibrations.find(
(cal) => cal.resolution.width === resolution.width && cal.resolution.height === resolution.height
);
};
const getUniqueVideoResolutions = (): VideoFormat[] => {
const uniqueResolutions: VideoFormat[] = [];
useCameraSettingsStore().currentCameraSettings.validVideoFormats.forEach((format, index) => {
if(!uniqueResolutions.some(v => v.resolution.width === format.resolution.width && v.resolution.height === format.resolution.height)) {
if (
!uniqueResolutions.some(
(v) => v.resolution.width === format.resolution.width && v.resolution.height === format.resolution.height
)
) {
format.index = index;
const calib = getCalibrationCoeffs(format.resolution);
if(calib !== undefined) {
if (calib !== undefined) {
format.standardDeviation = calib.standardDeviation;
format.mean = calib.perViewErrors.reduce((a, b) => a + b) / calib.perViewErrors.length;
format.horizontalFOV = 2 * Math.atan2(format.resolution.width/2, calib.intrinsics[0]) * (180/Math.PI);
format.verticalFOV = 2 * Math.atan2(format.resolution.height/2, calib.intrinsics[4]) * (180/Math.PI);
format.diagonalFOV = 2 * Math.atan2(Math.sqrt(format.resolution.width**2 + (format.resolution.height/(calib.intrinsics[4]/calib.intrinsics[0]))**2)/2, calib.intrinsics[0]) * (180/Math.PI);
format.horizontalFOV = 2 * Math.atan2(format.resolution.width / 2, calib.intrinsics[0]) * (180 / Math.PI);
format.verticalFOV = 2 * Math.atan2(format.resolution.height / 2, calib.intrinsics[4]) * (180 / Math.PI);
format.diagonalFOV =
2 *
Math.atan2(
Math.sqrt(
format.resolution.width ** 2 +
(format.resolution.height / (calib.intrinsics[4] / calib.intrinsics[0])) ** 2
) / 2,
calib.intrinsics[0]
) *
(180 / Math.PI);
}
uniqueResolutions.push(format);
}
});
uniqueResolutions.sort((a, b) => (b.resolution.width + b.resolution.height) - (a.resolution.width + a.resolution.height));
uniqueResolutions.sort(
(a, b) => b.resolution.width + b.resolution.height - (a.resolution.width + a.resolution.height)
);
return uniqueResolutions;
};
const getUniqueVideoResolutionStrings = () => getUniqueVideoResolutions().map<{name: string, value: number}>(f => ({
name: `${f.resolution.width} X ${f.resolution.height}`,
// Index won't ever be undefined
value: f.index || 0
}));
const calibrationDivisors = computed(() => [1, 2, 4].filter(v => {
const currentRes = useCameraSettingsStore().currentVideoFormat.resolution;
return ((currentRes.width / v) >= 300 && (currentRes.height / v) >= 220) || (v === 1);
}));
const getUniqueVideoResolutionStrings = () =>
getUniqueVideoResolutions().map<{ name: string; value: number }>((f) => ({
name: `${f.resolution.width} X ${f.resolution.height}`,
// Index won't ever be undefined
value: f.index || 0
}));
const calibrationDivisors = computed(() =>
[1, 2, 4].filter((v) => {
const currentRes = useCameraSettingsStore().currentVideoFormat.resolution;
return (currentRes.width / v >= 300 && currentRes.height / v >= 220) || v === 1;
})
);
const squareSizeIn = ref(1);
const patternWidth = ref(8);
@@ -85,7 +104,8 @@ const downloadCalibBoard = () => {
break;
case CalibrationBoardTypes.DotBoard:
// eslint-disable-next-line no-case-declarations
const dotgridStartX = (paperWidth - (2 * (patternWidth.value - 1) + ((patternHeight.value - 1) % 2)) * squareSizeIn.value) / 2.0;
const dotgridStartX =
(paperWidth - (2 * (patternWidth.value - 1) + ((patternHeight.value - 1) % 2)) * squareSizeIn.value) / 2.0;
// eslint-disable-next-line no-case-declarations
const dotgridStartY = (paperHeight - (patternHeight.value - squareSizeIn.value)) / 2;
@@ -118,12 +138,10 @@ const downloadCalibBoard = () => {
logoImage.src = MonoLogo;
doc.addImage(logoImage, "PNG", 1.0, 0.75, 1.4, 0.5);
doc.text(`${patternWidth.value} x ${patternHeight.value} | ${squareSizeIn.value}in`, paperWidth - 1, 1.0,
{
maxWidth: (paperWidth - 2.0) / 2,
align: "right"
}
);
doc.text(`${patternWidth.value} x ${patternHeight.value} | ${squareSizeIn.value}in`, paperWidth - 1, 1.0, {
maxWidth: (paperWidth - 2.0) / 2,
align: "right"
});
doc.save(`calibrationTarget-${CalibrationBoardTypes[boardType.value]}.pdf`);
};
@@ -132,28 +150,29 @@ const importCalibrationFromCalibDB = ref();
const openCalibUploadPrompt = () => {
importCalibrationFromCalibDB.value.click();
};
const readImportedCalibration = ({ files } : { files: FileList}) => {
files[0].text().then(text => {
useCameraSettingsStore().importCalibDB({ payload: text, filename: files[0].name })
.then((response) => {
useStateStore().showSnackbarMessage({
message: response.data.text || response.data,
color: response.status === 200 ? "success" : "error"
});
})
.catch(err => {
if (err.request) {
useStateStore().showSnackbarMessage({
message: "Error while uploading calibration file! The backend didn't respond to the upload attempt.",
color: "error"
});
} else {
useStateStore().showSnackbarMessage({
message: "Error while uploading calibration file!",
color: "error"
});
}
const readImportedCalibration = ({ files }: { files: FileList }) => {
files[0].text().then((text) => {
useCameraSettingsStore()
.importCalibDB({ payload: text, filename: files[0].name })
.then((response) => {
useStateStore().showSnackbarMessage({
message: response.data.text || response.data,
color: response.status === 200 ? "success" : "error"
});
})
.catch((err) => {
if (err.request) {
useStateStore().showSnackbarMessage({
message: "Error while uploading calibration file! The backend didn't respond to the upload attempt.",
color: "error"
});
} else {
useStateStore().showSnackbarMessage({
message: "Error while uploading calibration file!",
color: "error"
});
}
});
});
};
@@ -174,13 +193,14 @@ const showCalibEndDialog = ref(false);
const calibCanceled = ref(false);
const calibSuccess = ref<boolean | undefined>(undefined);
const endCalibration = () => {
if(!useStateStore().calibrationData.hasEnoughImages) {
if (!useStateStore().calibrationData.hasEnoughImages) {
calibCanceled.value = true;
}
showCalibEndDialog.value = true;
// Check if calibration finished cleanly or was canceled
useCameraSettingsStore().endPnPCalibration()
useCameraSettingsStore()
.endPnPCalibration()
.then(() => {
calibSuccess.value = true;
})
@@ -195,22 +215,12 @@ const endCalibration = () => {
<template>
<div>
<v-card
class="pr-6 pb-3"
color="primary"
dark
>
<v-card class="pr-6 pb-3" color="primary" dark>
<v-card-title>Camera Calibration</v-card-title>
<div class="ml-5">
<v-row>
<v-col
cols="12"
md="6"
>
<v-form
ref="form"
v-model="settingsValid"
>
<v-col cols="12" md="6">
<v-form ref="form" v-model="settingsValid">
<cv-select
v-model="useStateStore().calibrationData.videoFormatIndex"
label="Resolution"
@@ -225,7 +235,9 @@ const endCalibration = () => {
tooltip="Resolution to which camera frames are downscaled for detection. Calibration still uses full-res"
:items="calibrationDivisors"
:select-cols="7"
@input="v => useCameraSettingsStore().changeCurrentPipelineSetting({streamingFrameDivisor: v}, false)"
@input="
(v) => useCameraSettingsStore().changeCurrentPipelineSetting({ streamingFrameDivisor: v }, false)
"
/>
<cv-select
v-model="boardType"
@@ -240,7 +252,7 @@ const endCalibration = () => {
label="Pattern Spacing (in)"
tooltip="Spacing between pattern features in inches"
:disabled="isCalibrating"
:rules="[v => (v > 0) || 'Size must be positive']"
:rules="[(v) => v > 0 || 'Size must be positive']"
:label-cols="5"
/>
<cv-number-input
@@ -248,7 +260,7 @@ const endCalibration = () => {
label="Board Width (in)"
tooltip="Width of the board in dots or chessboard squares"
:disabled="isCalibrating"
:rules="[v => (v >= 4) || 'Width must be at least 4']"
:rules="[(v) => v >= 4 || 'Width must be at least 4']"
:label-cols="5"
/>
<cv-number-input
@@ -256,54 +268,31 @@ const endCalibration = () => {
label="Board Height (in)"
tooltip="Height of the board in dots or chessboard squares"
:disabled="isCalibrating"
:rules="[v => (v >= 4) || 'Height must be at least 4']"
:rules="[(v) => v >= 4 || 'Height must be at least 4']"
:label-cols="5"
/>
</v-form>
</v-col>
<v-col
cols="12"
md="6"
>
<v-row
align="start"
class="pb-4 pt-2"
>
<v-simple-table
fixed-header
height="100%"
dense
>
<v-col cols="12" md="6">
<v-row align="start" class="pb-4 pt-2">
<v-simple-table fixed-header height="100%" dense>
<thead>
<tr>
<th>
Resolution
</th>
<th>
Mean Error
</th>
<th>
Standard Deviation
</th>
<th>
Horizontal FOV
</th>
<th>
Vertical FOV
</th>
<th>
Diagonal FOV
</th>
<th>Resolution</th>
<th>Mean Error</th>
<th>Standard Deviation</th>
<th>Horizontal FOV</th>
<th>Vertical FOV</th>
<th>Diagonal FOV</th>
</tr>
</thead>
<tbody>
<tr
v-for="(value, index) in getUniqueVideoResolutions()"
:key="index"
>
<tr v-for="(value, index) in getUniqueVideoResolutions()" :key="index">
<td>{{ value.resolution.width }} X {{ value.resolution.height }}</td>
<td>{{ value.mean !== undefined ? value.mean.toFixed(2) + "px" : "-" }}</td>
<td>{{ value.standardDeviation !== undefined ? value.standardDeviation.toFixed(2) + "px" : "-" }}</td>
<td>
{{ value.standardDeviation !== undefined ? value.standardDeviation.toFixed(2) + "px" : "-" }}
</td>
<td>{{ value.horizontalFOV !== undefined ? value.horizontalFOV.toFixed(2) + "°" : "-" }}</td>
<td>{{ value.verticalFOV !== undefined ? value.verticalFOV.toFixed(2) + "°" : "-" }}</td>
<td>{{ value.diagonalFOV !== undefined ? value.diagonalFOV.toFixed(2) + "°" : "-" }}</td>
@@ -317,16 +306,14 @@ const endCalibration = () => {
label
:color="useStateStore().calibrationData.hasEnoughImages ? 'secondary' : 'gray'"
>
Snapshots: {{ useStateStore().calibrationData.imageCount }} of at least {{ useStateStore().calibrationData.minimumImageCount }}
Snapshots: {{ useStateStore().calibrationData.imageCount }} of at least
{{ useStateStore().calibrationData.minimumImageCount }}
</v-chip>
</v-row>
</v-col>
</v-row>
<v-row v-if="isCalibrating">
<v-col
cols="12"
class="pt-0"
>
<v-col cols="12" class="pt-0">
<cv-slider
v-model="useCameraSettingsStore().currentPipelineSettings.cameraExposure"
:disabled="useCameraSettingsStore().currentCameraSettings.pipelineSettings.cameraAutoExposure"
@@ -336,7 +323,7 @@ const endCalibration = () => {
:max="100"
:slider-cols="8"
:step="0.1"
@input="args => useCameraSettingsStore().changeCurrentPipelineSetting({cameraExposure: args}, false)"
@input="(args) => useCameraSettingsStore().changeCurrentPipelineSetting({ cameraExposure: args }, false)"
/>
<cv-slider
v-model="useCameraSettingsStore().currentPipelineSettings.cameraBrightness"
@@ -344,7 +331,9 @@ const endCalibration = () => {
:min="0"
:max="100"
:slider-cols="8"
@input="args => useCameraSettingsStore().changeCurrentPipelineSetting({cameraBrightness: args}, false)"
@input="
(args) => useCameraSettingsStore().changeCurrentPipelineSetting({ cameraBrightness: args }, false)
"
/>
<cv-switch
v-model="useCameraSettingsStore().currentPipelineSettings.cameraAutoExposure"
@@ -352,7 +341,9 @@ const endCalibration = () => {
label="Auto Exposure"
:label-cols="4"
tooltip="Enables or Disables camera automatic adjustment for current lighting conditions"
@input="args => useCameraSettingsStore().changeCurrentPipelineSetting({cameraAutoExposure: args}, false)"
@input="
(args) => useCameraSettingsStore().changeCurrentPipelineSetting({ cameraAutoExposure: args }, false)
"
/>
<cv-slider
v-if="useCameraSettingsStore().currentPipelineSettings.cameraGain >= 0"
@@ -361,7 +352,7 @@ const endCalibration = () => {
tooltip="Controls camera gain, similar to brightness"
:min="0"
:max="100"
@input="args => useCameraSettingsStore().changeCurrentPipelineSetting({cameraGain: args}, false)"
@input="(args) => useCameraSettingsStore().changeCurrentPipelineSetting({ cameraGain: args }, false)"
/>
<cv-slider
v-if="useCameraSettingsStore().currentPipelineSettings.cameraRedGain !== -1"
@@ -370,7 +361,7 @@ const endCalibration = () => {
:min="0"
:max="100"
tooltip="Controls red automatic white balance gain, which affects how the camera captures colors in different conditions"
@input="args => useCameraSettingsStore().changeCurrentPipelineSetting({cameraRedGain: args}, false)"
@input="(args) => useCameraSettingsStore().changeCurrentPipelineSetting({ cameraRedGain: args }, false)"
/>
<cv-slider
v-if="useCameraSettingsStore().currentPipelineSettings.cameraBlueGain !== -1"
@@ -379,7 +370,7 @@ const endCalibration = () => {
:min="0"
:max="100"
tooltip="Controls blue automatic white balance gain, which affects how the camera captures colors in different conditions"
@input="args => useCameraSettingsStore().changeCurrentPipelineSetting({cameraBlueGain: args}, false)"
@input="(args) => useCameraSettingsStore().changeCurrentPipelineSetting({ cameraBlueGain: args }, false)"
/>
</v-col>
</v-row>
@@ -388,7 +379,7 @@ const endCalibration = () => {
<v-btn
small
color="secondary"
style="width: 100%;"
style="width: 100%"
:disabled="!settingsValid"
@click="isCalibrating ? useCameraSettingsStore().takeCalibrationSnapshot(true) : startCalibration()"
>
@@ -400,7 +391,7 @@ const endCalibration = () => {
small
:color="useStateStore().calibrationData.hasEnoughImages ? 'accent' : 'red'"
:class="useStateStore().calibrationData.hasEnoughImages ? 'black--text' : 'white---text'"
style="width: 100%;"
style="width: 100%"
:disabled="!isCalibrating || !settingsValid"
@click="endCalibration"
>
@@ -414,104 +405,70 @@ const endCalibration = () => {
color="accent"
small
outlined
style="width: 100%;"
style="width: 100%"
:disabled="!settingsValid"
@click="downloadCalibBoard"
>
<v-icon left>
mdi-download
</v-icon>
<v-icon left> mdi-download </v-icon>
Generate Board
</v-btn>
</v-col>
<v-col :cols="6">
<v-btn
color="secondary"
:disabled="isCalibrating"
small
style="width: 100%;"
@click="openCalibUploadPrompt"
>
<v-icon left>
mdi-upload
</v-icon>
<v-btn color="secondary" :disabled="isCalibrating" small style="width: 100%" @click="openCalibUploadPrompt">
<v-icon left> mdi-upload </v-icon>
Import From CalibDB
</v-btn>
<input
ref="importCalibrationFromCalibDB"
type="file"
accept=".json"
style="display: none;"
style="display: none"
@change="readImportedCalibration"
>
/>
</v-col>
</v-row>
</div>
</v-card>
<v-dialog
v-model="showCalibEndDialog"
width="500px"
:persistent="true"
>
<v-card
color="primary"
dark
>
<v-card-title class="pb-8">
Camera Calibration
</v-card-title>
<v-dialog v-model="showCalibEndDialog" width="500px" :persistent="true">
<v-card color="primary" dark>
<v-card-title class="pb-8"> Camera Calibration </v-card-title>
<div class="ml-3">
<v-col style="text-align: center">
<template v-if="calibCanceled">
<v-icon
color="blue"
size="70"
<v-icon color="blue" size="70"> mdi-cancel </v-icon>
<v-card-text
>Camera Calibration has been Canceled, the backend is attempting to cleanly cancel the calibration
process.</v-card-text
>
mdi-cancel
</v-icon>
<v-card-text>Camera Calibration has been Canceled, the backend is attempting to cleanly cancel the calibration process.</v-card-text>
</template>
<template v-else-if="isCalibrating">
<v-progress-circular
indeterminate
:size="70"
:width="8"
color="accent"
/>
<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="calibSuccess">
<v-icon
color="green"
size="70"
>
mdi-check-bold
</v-icon>
<v-icon color="green" size="70"> mdi-check-bold </v-icon>
<v-card-text>
Camera has been successfully calibrated for {{ getUniqueVideoResolutionStrings().find(v => v.value === useStateStore().calibrationData.videoFormatIndex).name }}!
Camera has been successfully calibrated for
{{
getUniqueVideoResolutionStrings().find(
(v) => v.value === useStateStore().calibrationData.videoFormatIndex
).name
}}!
</v-card-text>
</template>
<template v-else>
<v-icon
color="red"
size="70"
<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
>
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="!isCalibrating"
color="white"
text
@click="showCalibEndDialog = false"
>
OK
</v-btn>
<v-btn v-if="!isCalibrating" color="white" text @click="showCalibEndDialog = false"> OK </v-btn>
</v-card-actions>
</v-card>
</v-dialog>
@@ -522,7 +479,8 @@ const endCalibration = () => {
.v-data-table {
text-align: center;
th, td {
th,
td {
background-color: #006492 !important;
font-size: 1rem !important;
}

View File

@@ -8,43 +8,40 @@ import { ref } from "vue";
const currentFov = ref(useCameraSettingsStore().currentCameraSettings.fov.value);
const saveCameraSettings = () => {
useCameraSettingsStore().updateCameraSettings({ fov: currentFov.value }, true)
.then((response) => {
useStateStore().showSnackbarMessage({
color: "success",
message: response.data.text || response.data
});
})
.catch(error => {
if(error.response) {
useStateStore().showSnackbarMessage({
color: "error",
message: error.response.data.text || error.response.data
});
} else if(error.request) {
useStateStore().showSnackbarMessage({
color: "error",
message: "Error while trying to process the request! The backend didn't respond."
});
} else {
useStateStore().showSnackbarMessage({
color: "error",
message: "An error occurred while trying to process the request."
});
}
})
.finally(() => {
useCameraSettingsStore().currentCameraSettings.fov.value = currentFov.value;
useCameraSettingsStore()
.updateCameraSettings({ fov: currentFov.value }, true)
.then((response) => {
useStateStore().showSnackbarMessage({
color: "success",
message: response.data.text || response.data
});
})
.catch((error) => {
if (error.response) {
useStateStore().showSnackbarMessage({
color: "error",
message: error.response.data.text || error.response.data
});
} else if (error.request) {
useStateStore().showSnackbarMessage({
color: "error",
message: "Error while trying to process the request! The backend didn't respond."
});
} else {
useStateStore().showSnackbarMessage({
color: "error",
message: "An error occurred while trying to process the request."
});
}
})
.finally(() => {
useCameraSettingsStore().currentCameraSettings.fov.value = currentFov.value;
});
};
</script>
<template>
<v-card
class="mb-3 pr-6 pb-3"
color="primary"
dark
>
<v-card class="mb-3 pr-6 pb-3" color="primary" dark>
<v-card-title>Camera Settings</v-card-title>
<div class="ml-5">
<cv-select
@@ -52,29 +49,33 @@ const saveCameraSettings = () => {
label="Camera"
:items="useCameraSettingsStore().cameraNames"
:select-cols="8"
@input="args => {
currentFov = useCameraSettingsStore().cameras[args].fov.value;
useCameraSettingsStore().setCurrentCameraIndex(args);
}"
@input="
(args) => {
currentFov = useCameraSettingsStore().cameras[args].fov.value;
useCameraSettingsStore().setCurrentCameraIndex(args);
}
"
/>
<cv-number-input
v-model="currentFov"
:tooltip="!useCameraSettingsStore().currentCameraSettings.fov.managedByVendor ? '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'"
:tooltip="
!useCameraSettingsStore().currentCameraSettings.fov.managedByVendor
? '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'
"
label="Maximum Diagonal FOV"
:disabled="useCameraSettingsStore().currentCameraSettings.fov.managedByVendor"
:label-cols="4"
/>
<br>
<br />
<v-btn
style="margin-top:10px"
style="margin-top: 10px"
small
color="secondary"
:disabled="currentFov === useCameraSettingsStore().currentCameraSettings.fov.value"
@click="saveCameraSettings"
>
<v-icon left>
mdi-content-save
</v-icon>
<v-icon left> mdi-content-save </v-icon>
Save Changes
</v-btn>
</div>

View File

@@ -8,22 +8,25 @@ import { useSettingsStore } from "@/stores/settings/GeneralSettingsStore";
const props = defineProps<{
// TODO fully update v-model usage in custom components on Vue3 update
value: number[]
value: number[];
}>();
const emit = defineEmits<{
(e: "input", value: number[]): void
(e: "input", value: number[]): void;
}>();
const localValue = computed({
get: () => props.value,
set: v => emit("input", v)
set: (v) => emit("input", v)
});
const driverMode = computed<boolean>({
get: () => useCameraSettingsStore().isDriverMode,
set: v => useCameraSettingsStore().changeCurrentPipelineIndex(v ? -1 : useCameraSettingsStore().currentCameraSettings.lastPipelineIndex || 0, true)
set: (v) =>
useCameraSettingsStore().changeCurrentPipelineIndex(
v ? -1 : useCameraSettingsStore().currentCameraSettings.lastPipelineIndex || 0,
true
)
});
const fpsTooLow = computed<boolean>(() => {
@@ -33,38 +36,30 @@ const fpsTooLow = computed<boolean>(() => {
const gpuAccel = useSettingsStore().general.gpuAcceleration !== undefined;
const isReflective = useCameraSettingsStore().currentPipelineSettings.pipelineType === PipelineType.Reflective;
return (currFPS - targetFPS) < -5 && currFPS !== 0 && !driverMode && gpuAccel && isReflective;
return currFPS - targetFPS < -5 && currFPS !== 0 && !driverMode && gpuAccel && isReflective;
});
</script>
<template>
<v-card
class="mb-3 pr-6 pb-3 pa-4"
color="primary"
dark
>
<v-card class="mb-3 pr-6 pb-3 pa-4" color="primary" dark>
<v-card-title
class="pb-0 mb-2 pl-4 pt-1"
style="min-height: 50px; justify-content: space-between; align-content: center"
>
<div style="display: flex; flex-wrap: wrap">
<div>
<span
class="mr-4"
style="white-space: nowrap"
>
Cameras
</span>
<span class="mr-4" style="white-space: nowrap"> Cameras </span>
</div>
<div>
<v-chip
label
:color="fpsTooLow ? 'error' : 'transparent'"
:text-color="fpsTooLow ? '#C7EA46' : '#ff4d00'"
style="font-size: 1rem; padding: 0; margin: 0;"
style="font-size: 1rem; padding: 0; margin: 0"
>
<span class="pr-1">
{{ Math.round(useStateStore().pipelineResults?.fps || 0) }}&nbsp;FPS &ndash; {{ Math.min(Math.round(useStateStore().pipelineResults?.latency || 0), 9999) }} ms latency
{{ Math.round(useStateStore().pipelineResults?.fps || 0) }}&nbsp;FPS &ndash;
{{ Math.min(Math.round(useStateStore().pipelineResults?.latency || 0), 9999) }} ms latency
</span>
</v-chip>
</div>
@@ -75,43 +70,24 @@ const fpsTooLow = computed<boolean>(() => {
v-model="driverMode"
:disabled="useCameraSettingsStore().isCalibrationMode"
label="Driver Mode"
style="margin-left: auto;"
style="margin-left: auto"
color="accent"
class="pt-2"
/>
</div>
</v-card-title>
<div
class="stream-container pb-4"
>
<div class="stream-container pb-4">
<div class="stream">
<photon-camera-stream
v-show="value.includes(0)"
stream-type="Raw"
style="max-width: 100%"
/>
<photon-camera-stream v-show="value.includes(0)" stream-type="Raw" style="max-width: 100%" />
</div>
<div class="stream">
<photon-camera-stream
v-show="value.includes(1)"
stream-type="Processed"
style="max-width: 100%"
/>
<photon-camera-stream v-show="value.includes(1)" stream-type="Processed" style="max-width: 100%" />
</div>
</div>
<v-divider />
<div class="pt-4">
<p style="color: white;">
Stream Display
</p>
<v-btn-toggle
v-model="localValue"
:multiple="true"
mandatory
dark
class="fill"
style="width: 100%"
>
<p style="color: white">Stream Display</p>
<v-btn-toggle v-model="localValue" :multiple="true" mandatory dark class="fill" style="width: 100%">
<v-btn
color="secondary"
class="fill"

View File

@@ -1,34 +1,30 @@
<script setup lang="ts">
const props = withDefaults(defineProps<{
iconName: string,
color?: string,
tooltip?: string,
right?: boolean,
hover?: boolean
}>(), {
right: false,
hover: false
});
const props = withDefaults(
defineProps<{
iconName: string;
color?: string;
tooltip?: string;
right?: boolean;
hover?: boolean;
}>(),
{
right: false,
hover: false
}
);
defineEmits<{
(e: "click"): void;
}>();
const hoverClass = props.hover ? "hover" : "";
</script>
<template>
<div>
<v-tooltip
:right="right"
:bottom="!right"
nudge-right="10"
:disabled="tooltip === undefined"
>
<v-tooltip :right="right" :bottom="!right" nudge-right="10" :disabled="tooltip === undefined">
<template #activator="{ on, attrs }">
<v-icon
:class="hoverClass"
:color="color"
v-bind="attrs"
v-on="on"
@click="$emit('click')"
>
<v-icon :class="hoverClass" :color="color" v-bind="attrs" v-on="on" @click="$emit('click')">
{{ iconName }}
</v-icon>
</template>
@@ -38,7 +34,7 @@ const hoverClass = props.hover ? "hover" : "";
</template>
<style scoped>
.hover:hover {
color: white !important;
}
.hover:hover {
color: white !important;
}
</style>

View File

@@ -2,38 +2,40 @@
import { computed } from "vue";
import TooltippedLabel from "@/components/common/cv-tooltipped-label.vue";
const props = withDefaults(defineProps<{
label?: string,
tooltip?: string,
// TODO fully update v-model usage in custom components on Vue3 update
value: string,
disabled?: boolean,
errorMessage?: string,
placeholder?: string,
labelCols?: number,
inputCols?: number,
rules?: ((v: string) => boolean | string)[]
}>(), {
disabled: false,
inputCols: 8
});
const props = withDefaults(
defineProps<{
label?: string;
tooltip?: string;
// TODO fully update v-model usage in custom components on Vue3 update
value: string;
disabled?: boolean;
errorMessage?: string;
placeholder?: string;
labelCols?: number;
inputCols?: number;
rules?: ((v: string) => boolean | string)[];
}>(),
{
disabled: false,
inputCols: 8
}
);
const emit = defineEmits<{
(e: "input", value: string): void
(e: "onEnter", value: string): void
(e: "onEscape"): void
(e: "input", value: string): void;
(e: "onEnter", value: string): void;
(e: "onEscape"): void;
}>();
const localValue = computed({
get: () => props.value,
set: v => emit("input", v)
set: (v) => emit("input", v)
});
const handleKeydown = ({ key }) => {
switch (key) {
case "Enter":
if(!(props.rules || []).some(v => v(localValue.value) === false || typeof v(localValue.value) === "string")) {
if (!(props.rules || []).some((v) => v(localValue.value) === false || typeof v(localValue.value) === "string")) {
emit("onEnter", localValue.value);
}
break;
@@ -42,20 +44,13 @@ const handleKeydown = ({ key }) => {
break;
}
};
</script>
<template>
<div>
<v-row
dense
align="center"
>
<v-col :cols="labelCols || (12 - inputCols)">
<tooltipped-label
:tooltip="tooltip"
:label="label"
/>
<v-row dense align="center">
<v-col :cols="labelCols || 12 - inputCols">
<tooltipped-label :tooltip="tooltip" :label="label" />
</v-col>
<v-col :cols="inputCols">

View File

@@ -2,42 +2,39 @@
import TooltippedLabel from "@/components/common/cv-tooltipped-label.vue";
import { computed } from "vue";
const props = withDefaults(defineProps<{
label?: string,
tooltip?: string,
// TODO fully update v-model usage in custom components on Vue3 update
value: number,
disabled?: boolean,
labelCols?: number,
rules?: ((v: number) => boolean | string)[],
step?: number
}>(), {
disabled: false,
labelCols: 2,
step: 1
});
const props = withDefaults(
defineProps<{
label?: string;
tooltip?: string;
// TODO fully update v-model usage in custom components on Vue3 update
value: number;
disabled?: boolean;
labelCols?: number;
rules?: ((v: number) => boolean | string)[];
step?: number;
}>(),
{
disabled: false,
labelCols: 2,
step: 1
}
);
const emit = defineEmits<{
(e: "input", value: number): void
(e: "input", value: number): void;
}>();
const localValue = computed({
get: () => props.value,
set: v => emit("input", parseFloat(v as unknown as string))
set: (v) => emit("input", parseFloat(v as unknown as string))
});
</script>
<template>
<div>
<v-row
dense
align="center"
>
<v-row dense align="center">
<v-col :cols="labelCols">
<tooltipped-label
:tooltip="tooltip"
:label="label"
/>
<tooltipped-label :tooltip="tooltip" :label="label" />
</v-col>
<v-col>
<v-text-field

View File

@@ -2,48 +2,40 @@
import { computed } from "vue";
import TooltippedLabel from "@/components/common/cv-tooltipped-label.vue";
const props = withDefaults(defineProps<{
label?: string,
tooltip?: string,
// TODO fully update v-model usage in custom components on Vue3 update
value: number,
disabled?: boolean,
inputCols?: number,
list: string[]
}>(), {
disabled: false,
inputCols: 8
});
const props = withDefaults(
defineProps<{
label?: string;
tooltip?: string;
// TODO fully update v-model usage in custom components on Vue3 update
value: number;
disabled?: boolean;
inputCols?: number;
list: string[];
}>(),
{
disabled: false,
inputCols: 8
}
);
const emit = defineEmits<{
(e: "input", value: number): void
(e: "input", value: number): void;
}>();
const localValue = computed({
get: () => props.value,
set: v => emit("input", v)
set: (v) => emit("input", v)
});
</script>
<template>
<div>
<v-row
dense
align="center"
>
<v-row dense align="center">
<v-col :cols="12 - inputCols">
<tooltipped-label
:tooltip="tooltip"
:label="label"
/>
<tooltipped-label :tooltip="tooltip" :label="label" />
</v-col>
<v-col :cols="inputCols">
<v-radio-group
v-model="localValue"
row
dark
:mandatory="true"
>
<v-radio-group v-model="localValue" row dark :mandatory="true">
<v-radio
v-for="(radioName, index) in list"
:key="index"

View File

@@ -2,34 +2,37 @@
import { computed } from "vue";
import TooltippedLabel from "@/components/common/cv-tooltipped-label.vue";
const props = withDefaults(defineProps<{
label?: string,
tooltip?: string,
// TODO fully update v-model usage in custom components on Vue3 update
// value: [number, number] | WebsocketNumberPair, // Vue doesnt like Union types for the value prop for some reason.
value: [number, number],
min: number,
max: number,
step?: number,
sliderCols?: number,
disabled?: boolean,
inverted?: boolean,
}>(), {
step: 1,
disabled: false,
inverted: false,
sliderCols: 10
});
const props = withDefaults(
defineProps<{
label?: string;
tooltip?: string;
// TODO fully update v-model usage in custom components on Vue3 update
// value: [number, number] | WebsocketNumberPair, // Vue doesnt like Union types for the value prop for some reason.
value: [number, number];
min: number;
max: number;
step?: number;
sliderCols?: number;
disabled?: boolean;
inverted?: boolean;
}>(),
{
step: 1,
disabled: false,
inverted: false,
sliderCols: 10
}
);
const emit = defineEmits<{
(e: "input", value: [number, number]): void
(e: "input", value: [number, number]): void;
}>();
const localValue = computed<[number, number]>({
get: ():[number, number] => {
get: (): [number, number] => {
return Object.values(props.value) as [number, number];
},
set: v => emit("input", v)
set: (v) => emit("input", v)
});
const changeFromSlot = (v: number, i: number) => {
@@ -42,15 +45,9 @@ const changeFromSlot = (v: number, i: number) => {
<template>
<div>
<v-row
dense
align="center"
>
<v-row dense align="center">
<v-col :cols="12 - sliderCols">
<tooltipped-label
:tooltip="tooltip"
:label="label"
/>
<tooltipped-label :tooltip="tooltip" :label="label" />
</v-col>
<v-col :cols="sliderCols">
<v-range-slider
@@ -79,7 +76,7 @@ const changeFromSlot = (v: number, i: number) => {
:step="step"
type="number"
style="width: 60px"
@input="v => changeFromSlot(v, 0)"
@input="(v) => changeFromSlot(v, 0)"
/>
</template>
<template #append>
@@ -95,7 +92,7 @@ const changeFromSlot = (v: number, i: number) => {
:step="step"
type="number"
style="width: 60px"
@input="v => changeFromSlot(v, 1)"
@input="(v) => changeFromSlot(v, 1)"
/>
</template>
</v-range-slider>

View File

@@ -3,37 +3,40 @@ import { computed } from "vue";
import TooltippedLabel from "@/components/common/cv-tooltipped-label.vue";
interface SelectItem {
name: string | number,
value: string | number,
disabled?: boolean
name: string | number;
value: string | number;
disabled?: boolean;
}
const props = withDefaults(defineProps<{
label?: string,
tooltip?: string,
selectCols?: number,
// TODO fully update v-model usage in custom components on Vue3 update
value: number,
disabled?: boolean,
items: string[] | number[] | SelectItem[]
}>(), {
selectCols: 9,
disabled: false
});
const props = withDefaults(
defineProps<{
label?: string;
tooltip?: string;
selectCols?: number;
// TODO fully update v-model usage in custom components on Vue3 update
value: number;
disabled?: boolean;
items: string[] | number[] | SelectItem[];
}>(),
{
selectCols: 9,
disabled: false
}
);
const emit = defineEmits<{
(e: "input", value: number): void
(e: "input", value: number): void;
}>();
const localValue = computed({
get: () => props.value,
set: v => emit("input", v)
set: (v) => emit("input", v)
});
// Computed in case items changes
const items = computed<SelectItem[]>(() => {
// Check if the prop exists on the object to infer object type
if((props.items[0] as SelectItem).name) {
if ((props.items[0] as SelectItem).name) {
return props.items as SelectItem[];
}
return props.items.map((v, i) => ({ name: v, value: i }));
@@ -42,15 +45,9 @@ const items = computed<SelectItem[]>(() => {
<template>
<div>
<v-row
dense
align="center"
>
<v-row dense align="center">
<v-col :cols="12 - selectCols">
<tooltipped-label
:tooltip="tooltip"
:label="label"
/>
<tooltipped-label :tooltip="tooltip" :label="label" />
</v-col>
<v-col :cols="selectCols">
<v-select

View File

@@ -2,43 +2,40 @@
import { computed } from "vue";
import TooltippedLabel from "@/components/common/cv-tooltipped-label.vue";
const props = withDefaults(defineProps<{
label?: string,
tooltip?: string,
// TODO fully update v-model usage in custom components on Vue3 update
value: number,
min: number,
max: number,
step?: number
disabled?: boolean,
sliderCols?: number,
}>(), {
step: 1,
disabled: false,
sliderCols: 8
});
const props = withDefaults(
defineProps<{
label?: string;
tooltip?: string;
// TODO fully update v-model usage in custom components on Vue3 update
value: number;
min: number;
max: number;
step?: number;
disabled?: boolean;
sliderCols?: number;
}>(),
{
step: 1,
disabled: false,
sliderCols: 8
}
);
const emit = defineEmits<{
(e: "input", value: number): void
(e: "input", value: number): void;
}>();
const localValue = computed({
get: () => props.value,
set: v => emit("input", v)
set: (v) => emit("input", v)
});
</script>
<template>
<div>
<v-row
dense
align="center"
>
<v-row dense align="center">
<v-col :cols="12 - sliderCols">
<tooltipped-label
:tooltip="tooltip"
:label="label"
/>
<tooltipped-label :tooltip="tooltip" :label="label" />
</v-col>
<v-col :cols="sliderCols">
<v-slider

View File

@@ -2,48 +2,40 @@
import TooltippedLabel from "@/components/common/cv-tooltipped-label.vue";
import { computed } from "vue";
const props = withDefaults(defineProps<{
label?: string,
tooltip?: string,
// TODO fully update v-model usage in custom components on Vue3 update
value: boolean,
disabled?: boolean,
labelCols?: number,
switchCols?: number
}>(), {
disabled: false,
labelCols: 2
});
const props = withDefaults(
defineProps<{
label?: string;
tooltip?: string;
// TODO fully update v-model usage in custom components on Vue3 update
value: boolean;
disabled?: boolean;
labelCols?: number;
switchCols?: number;
}>(),
{
disabled: false,
labelCols: 2
}
);
const emit = defineEmits<{
(e: "input", value: boolean): void
(e: "input", value: boolean): void;
}>();
const localValue = computed({
get: () => props.value,
set: v => emit("input", v)
set: (v) => emit("input", v)
});
</script>
<template>
<div>
<v-row
dense
align="center"
>
<v-col :cols="(12 - switchCols) || labelCols">
<tooltipped-label
:tooltip="tooltip"
:label="label"
/>
<v-row dense align="center">
<v-col :cols="12 - switchCols || labelCols">
<tooltipped-label :tooltip="tooltip" :label="label" />
</v-col>
<v-col :cols="switchCols || (12 - labelCols)">
<v-switch
v-model="localValue"
dark
:disabled="disabled"
color="#ffd843"
/>
<v-col :cols="switchCols || 12 - labelCols">
<v-switch v-model="localValue" dark :disabled="disabled" color="#ffd843" />
</v-col>
</v-row>
</div>

View File

@@ -1,24 +1,15 @@
<script setup lang="ts">
defineProps<{
label?: string,
tooltip?: string
label?: string;
tooltip?: string;
}>();
</script>
<template>
<div>
<v-tooltip
:disabled="tooltip === undefined"
right
open-delay="300"
>
<v-tooltip :disabled="tooltip === undefined" right open-delay="300">
<template #activator="{ on, attrs }">
<span
style="cursor: text !important;"
class="white--text"
v-bind="attrs"
v-on="on"
>{{ label }}</span>
<span style="cursor: text !important" class="white--text" v-bind="attrs" v-on="on">{{ label }}</span>
</template>
<span>{{ tooltip }}</span>
</v-tooltip>

View File

@@ -38,40 +38,43 @@ const startCameraNameEdit = () => {
isCameraNameEdit.value = true;
};
const checkCameraName = (name: string): string | boolean => {
if(!nameChangeRegex.test(name)) return "A camera name can only contain letters, numbers, spaces, underscores, hyphens, parenthesis, and periods";
if(useCameraSettingsStore().cameraNames.some(cameraName => cameraName === name)) return "This camera name has already been used";
if (!nameChangeRegex.test(name))
return "A camera name can only contain letters, numbers, spaces, underscores, hyphens, parenthesis, and periods";
if (useCameraSettingsStore().cameraNames.some((cameraName) => cameraName === name))
return "This camera name has already been used";
return true;
};
const saveCameraNameEdit = (newName: string) => {
useCameraSettingsStore().changeCameraNickname(newName, false)
.then(response => {
useCameraSettingsStore()
.changeCameraNickname(newName, false)
.then((response) => {
useStateStore().showSnackbarMessage({
color: "success",
message: response.data.text || response.data
});
useCameraSettingsStore().currentCameraSettings.nickname = newName;
})
.catch((error) => {
if (error.response) {
useStateStore().showSnackbarMessage({
color: "success",
message: response.data.text || response.data
color: "error",
message: error.response.data.text || error.response.data
});
useCameraSettingsStore().currentCameraSettings.nickname = newName;
})
.catch(error => {
if(error.response) {
useStateStore().showSnackbarMessage({
color: "error",
message: error.response.data.text || error.response.data
});
} else if(error.request) {
useStateStore().showSnackbarMessage({
color: "error",
message: "Error while trying to process the request! The backend didn't respond."
});
} else {
useStateStore().showSnackbarMessage({
color: "error",
message: "An error occurred while trying to process the request."
});
}
currentCameraName.value = useCameraSettingsStore().currentCameraSettings.nickname;
})
.finally(() => isCameraNameEdit.value = false);
} else if (error.request) {
useStateStore().showSnackbarMessage({
color: "error",
message: "Error while trying to process the request! The backend didn't respond."
});
} else {
useStateStore().showSnackbarMessage({
color: "error",
message: "An error occurred while trying to process the request."
});
}
currentCameraName.value = useCameraSettingsStore().currentCameraSettings.nickname;
})
.finally(() => (isCameraNameEdit.value = false));
};
const cancelCameraNameEdit = () => {
isCameraNameEdit.value = false;
@@ -79,13 +82,13 @@ const cancelCameraNameEdit = () => {
};
// Pipeline Name Edit
const pipelineNamesWrapper = computed<{name: string, value: number}[]>(() => {
const pipelineNamesWrapper = computed<{ name: string; value: number }[]>(() => {
const pipelineNames = useCameraSettingsStore().pipelineNames.map((name, index) => ({ name: name, value: index }));
if(useCameraSettingsStore().isDriverMode) {
if (useCameraSettingsStore().isDriverMode) {
pipelineNames.push({ name: "Driver Mode", value: WebsocketPipelineType.DriverMode });
}
if(useCameraSettingsStore().isCalibrationMode) {
if (useCameraSettingsStore().isCalibrationMode) {
pipelineNames.push({ name: "3D Calibration Mode", value: WebsocketPipelineType.Calib3d });
}
@@ -98,8 +101,10 @@ const startPipelineNameEdit = () => {
isPipelineNameEdit.value = true;
};
const checkPipelineName = (name: string): string | boolean => {
if(!nameChangeRegex.test(name)) return "A pipeline name can only contain letters, numbers, spaces, underscores, hyphens, parenthesis, and periods";
if(useCameraSettingsStore().pipelineNames.some(pipelineName => pipelineName === name)) return "This pipeline name has already been used";
if (!nameChangeRegex.test(name))
return "A pipeline name can only contain letters, numbers, spaces, underscores, hyphens, parenthesis, and periods";
if (useCameraSettingsStore().pipelineNames.some((pipelineName) => pipelineName === name))
return "This pipeline name has already been used";
return true;
};
@@ -123,7 +128,7 @@ const showCreatePipelineDialog = () => {
};
const createNewPipeline = () => {
const type = newPipelineType.value;
if(type === WebsocketPipelineType.DriverMode || type === WebsocketPipelineType.Calib3d) return;
if (type === WebsocketPipelineType.DriverMode || type === WebsocketPipelineType.Calib3d) return;
useCameraSettingsStore().createNewPipeline(newPipelineName.value, type);
showPipelineCreationDialog.value = false;
};
@@ -142,18 +147,18 @@ const confirmDeleteCurrentPipeline = () => {
// Pipeline Type Change
const showPipelineTypeChangeDialog = ref(false);
const pipelineTypesWrapper = computed<{name: string, value: number}[]>(() => {
const pipelineTypes =[
const pipelineTypesWrapper = computed<{ name: string; value: number }[]>(() => {
const pipelineTypes = [
{ name: "Reflective", value: WebsocketPipelineType.Reflective },
{ name: "Colored Shape", value: WebsocketPipelineType.ColoredShape },
{ name: "AprilTag", value: WebsocketPipelineType.AprilTag }
// { name: "Aruco", value: WebsocketPipelineType.Aruco }
];
if(useCameraSettingsStore().isDriverMode) {
if (useCameraSettingsStore().isDriverMode) {
pipelineTypes.push({ name: "Driver Mode", value: WebsocketPipelineType.DriverMode });
}
if(useCameraSettingsStore().isCalibrationMode) {
if (useCameraSettingsStore().isCalibrationMode) {
pipelineTypes.push({ name: "3D Calibration Mode", value: WebsocketPipelineType.Calib3d });
}
@@ -162,17 +167,17 @@ const pipelineTypesWrapper = computed<{name: string, value: number}[]>(() => {
const pipelineType = ref<WebsocketPipelineType>(useCameraSettingsStore().currentWebsocketPipelineType);
const currentPipelineType = computed<WebsocketPipelineType>({
get: () => {
if(useCameraSettingsStore().isDriverMode) return WebsocketPipelineType.DriverMode;
if(useCameraSettingsStore().isCalibrationMode) return WebsocketPipelineType.Calib3d;
if (useCameraSettingsStore().isDriverMode) return WebsocketPipelineType.DriverMode;
if (useCameraSettingsStore().isCalibrationMode) return WebsocketPipelineType.Calib3d;
return pipelineType.value;
},
set: v => {
set: (v) => {
pipelineType.value = v;
}
});
const confirmChangePipelineType = () => {
const type = currentPipelineType.value;
if(type === WebsocketPipelineType.DriverMode || type === WebsocketPipelineType.Calib3d) return;
if (type === WebsocketPipelineType.DriverMode || type === WebsocketPipelineType.Calib3d) return;
useCameraSettingsStore().changeCurrentPipelineType(type);
showPipelineTypeChangeDialog.value = false;
};
@@ -203,14 +208,9 @@ useCameraSettingsStore().$subscribe((mutation, state) => {
</script>
<template>
<v-card
color="primary"
>
<v-card color="primary">
<v-row style="padding: 12px 12px 0 24px">
<v-col
cols="10"
class="pa-0"
>
<v-col cols="10" class="pa-0">
<cv-select
v-if="!isCameraNameEdit"
v-model="useStateStore().currentCameraIndex"
@@ -222,164 +222,101 @@ useCameraSettingsStore().$subscribe((mutation, state) => {
v-else
v-model="currentCameraName"
class="pt-2"
:input-cols="12-3"
:rules="[v => checkCameraName(v)]"
:input-cols="12 - 3"
:rules="[(v) => checkCameraName(v)]"
label="Camera"
@onEnter="saveCameraNameEdit"
@onEscape="cancelCameraNameEdit"
@on-enter="saveCameraNameEdit"
@on-escape="cancelCameraNameEdit"
/>
</v-col>
<v-col
cols="2"
style="display: flex; align-items: center; justify-content: center"
>
<cv-icon
color="#c5c5c5"
icon-name="mdi-pencil"
tooltip="Edit Camera Name"
@click="startCameraNameEdit"
/>
<v-col cols="2" style="display: flex; align-items: center; justify-content: center">
<cv-icon color="#c5c5c5" icon-name="mdi-pencil" tooltip="Edit Camera Name" @click="startCameraNameEdit" />
</v-col>
</v-row>
<v-row style="padding: 0 12px 0 24px;">
<v-col
cols="10"
class="pa-0"
>
<v-row style="padding: 0 12px 0 24px">
<v-col cols="10" class="pa-0">
<cv-select
v-if="!isPipelineNameEdit"
:value="useCameraSettingsStore().currentCameraSettings.currentPipelineIndex"
label="Pipeline"
tooltip="Each pipeline runs on a camera output and stores a unique set of processing settings"
:disabled="useCameraSettingsStore().isDriverMode
|| useCameraSettingsStore().isCalibrationMode"
:disabled="useCameraSettingsStore().isDriverMode || useCameraSettingsStore().isCalibrationMode"
:items="pipelineNamesWrapper"
@input="args => useCameraSettingsStore().changeCurrentPipelineIndex(args, true)"
@input="(args) => useCameraSettingsStore().changeCurrentPipelineIndex(args, true)"
/>
<cv-input
v-else
v-model="currentPipelineName"
:input-cols="12-3"
:rules="[v => checkPipelineName(v)]"
:input-cols="12 - 3"
:rules="[(v) => checkPipelineName(v)]"
label="Pipeline"
@onEnter="v => savePipelineNameEdit(v)"
@onEscape="cancelPipelineNameEdit"
@on-enter="(v) => savePipelineNameEdit(v)"
@on-escape="cancelPipelineNameEdit"
/>
</v-col>
<v-col
cols="2"
class="pa-0"
style="display: flex; align-items: center; justify-content: center"
>
<v-menu
v-if="!useCameraSettingsStore().isDriverMode"
offset-y
nudge-bottom="7"
auto
>
<v-col cols="2" class="pa-0" style="display: flex; align-items: center; justify-content: center">
<v-menu v-if="!useCameraSettingsStore().isDriverMode" offset-y nudge-bottom="7" auto>
<template #activator="{ on }">
<v-icon
color="#c5c5c5"
v-on="on"
@click="cancelPipelineNameEdit"
>
mdi-menu
</v-icon>
<v-icon color="#c5c5c5" v-on="on" @click="cancelPipelineNameEdit"> mdi-menu </v-icon>
</template>
<v-list
dark
dense
color="primary"
>
<v-list dark dense color="primary">
<v-list-item @click="startPipelineNameEdit">
<v-list-item-title>
<cv-icon
color="#c5c5c5"
:right="true"
icon-name="mdi-pencil"
tooltip="Edit pipeline name"
/>
<cv-icon color="#c5c5c5" :right="true" icon-name="mdi-pencil" tooltip="Edit pipeline name" />
</v-list-item-title>
</v-list-item>
<v-list-item @click="showCreatePipelineDialog">
<v-list-item-title>
<cv-icon
color="#c5c5c5"
:right="true"
icon-name="mdi-plus"
tooltip="Add new pipeline"
/>
<cv-icon color="#c5c5c5" :right="true" icon-name="mdi-plus" tooltip="Add new pipeline" />
</v-list-item-title>
</v-list-item>
<v-list-item @click="showPipelineDeletionConfirmationDialog = true">
<v-list-item-title>
<cv-icon
color="red darken-2"
:right="true"
icon-name="mdi-delete"
tooltip="Delete pipeline"
/>
<cv-icon color="red darken-2" :right="true" icon-name="mdi-delete" tooltip="Delete pipeline" />
</v-list-item-title>
</v-list-item>
<v-list-item @click="useCameraSettingsStore().duplicatePipeline(useCameraSettingsStore().currentCameraSettings.currentPipelineIndex)">
<v-list-item
@click="
useCameraSettingsStore().duplicatePipeline(
useCameraSettingsStore().currentCameraSettings.currentPipelineIndex
)
"
>
<v-list-item-title>
<cv-icon
color="#c5c5c5"
:right="true"
icon-name="mdi-content-copy"
tooltip="Duplicate pipeline"
/>
<cv-icon color="#c5c5c5" :right="true" icon-name="mdi-content-copy" tooltip="Duplicate pipeline" />
</v-list-item-title>
</v-list-item>
</v-list>
</v-menu>
</v-col>
</v-row>
<v-row style="padding: 0 12px 12px 24px;">
<v-col
cols="10"
class="pa-0"
>
<v-row style="padding: 0 12px 12px 24px">
<v-col cols="10" class="pa-0">
<cv-select
v-model="currentPipelineType"
label="Type"
tooltip="Changes the pipeline type, which changes the type of processing that will happen on input frames"
:disabled="useCameraSettingsStore().isDriverMode
|| useCameraSettingsStore().isCalibrationMode"
:disabled="useCameraSettingsStore().isDriverMode || useCameraSettingsStore().isCalibrationMode"
:items="pipelineTypesWrapper"
@input="showPipelineTypeChangeDialog = true"
/>
</v-col>
</v-row>
<v-dialog
v-model="showPipelineCreationDialog"
dark
persistent
width="500"
>
<v-card
dark
color="primary"
>
<v-card-title
class="headline"
style="font-family: 'Prompt', sans-serif !important;"
primary-title
>
Create New Pipeline
</v-card-title>
<v-dialog v-model="showPipelineCreationDialog" dark persistent width="500">
<v-card dark color="primary">
<v-card-title> Create New Pipeline </v-card-title>
<v-card-text>
<cv-input
v-model="newPipelineName"
placeholder="Pipeline Name"
:label-cols="3"
:input-cols="12-3"
:input-cols="12 - 3"
label="Pipeline Name"
:rules="[v => checkPipelineName(v)]"
:rules="[(v) => checkPipelineName(v)]"
/>
<cv-select
v-model="newPipelineType"
:select-cols="12-3"
:select-cols="12 - 3"
label="Tracking Type"
tooltip="Pipeline type, which changes the type of processing that will happen on input frames"
:items="[
@@ -393,87 +330,38 @@ useCameraSettingsStore().$subscribe((mutation, state) => {
<v-divider />
<v-card-actions>
<v-spacer />
<v-btn
color="#ffd843"
:disabled="checkPipelineName(newPipelineName) !== true"
@click="createNewPipeline"
>
<v-btn color="#ffd843" :disabled="checkPipelineName(newPipelineName) !== true" @click="createNewPipeline">
Save
</v-btn>
<v-btn
color="error"
@click="cancelPipelineCreation"
>
Cancel
</v-btn>
<v-btn color="error" @click="cancelPipelineCreation"> Cancel </v-btn>
</v-card-actions>
</v-card>
</v-dialog>
<v-dialog
v-model="showPipelineDeletionConfirmationDialog"
dark
width="500"
>
<v-card
dark
color="primary"
>
<v-card-title
class="headline"
style="font-family: 'Prompt', sans-serif !important;"
primary-title
>
Pipeline Deletion Confirmation
</v-card-title>
<v-card-text>
Are you sure you want to delete this pipeline? This cannot be undone.
</v-card-text>
<v-dialog v-model="showPipelineDeletionConfirmationDialog" dark width="500">
<v-card dark color="primary">
<v-card-title> Pipeline Deletion Confirmation </v-card-title>
<v-card-text> Are you sure you want to delete this pipeline? This cannot be undone. </v-card-text>
<v-divider />
<v-card-actions>
<v-spacer />
<v-btn
color="error"
@click="confirmDeleteCurrentPipeline"
>
Yes, I'm sure
</v-btn>
<v-btn
color="#ffd843"
@click="showPipelineDeletionConfirmationDialog = false"
>
No, take me back
</v-btn>
<v-btn color="error" @click="confirmDeleteCurrentPipeline"> Yes, I'm sure </v-btn>
<v-btn color="#ffd843" @click="showPipelineDeletionConfirmationDialog = false"> No, take me back </v-btn>
</v-card-actions>
</v-card>
</v-dialog>
<v-dialog
v-model="showPipelineTypeChangeDialog"
persistent
width="600"
>
<v-card
color="primary"
dark
>
<v-dialog v-model="showPipelineTypeChangeDialog" persistent width="600">
<v-card color="primary" dark>
<v-card-title>Change Pipeline Type</v-card-title>
<v-card-text>
Are you sure you want to change the current pipeline type? This will cause all the pipeline settings to be overwritten and they will be lost. If this isn't what you want, duplicate this pipeline first or export settings.
Are you sure you want to change the current pipeline type? This will cause all the pipeline settings to be
overwritten and they will be lost. If this isn't what you want, duplicate this pipeline first or export
settings.
</v-card-text>
<v-divider />
<v-card-actions>
<v-spacer />
<v-btn
color="error"
@click="confirmChangePipelineType"
>
Yes, I'm sure
</v-btn>
<v-btn
color="#ffd843"
@click="cancelChangePipelineType"
>
No, take me back
</v-btn>
<v-btn color="error" @click="confirmChangePipelineType"> Yes, I'm sure </v-btn>
<v-btn color="#ffd843" @click="cancelChangePipelineType"> No, take me back </v-btn>
</v-card-actions>
</v-card>
</v-dialog>

View File

@@ -8,12 +8,16 @@ import PhotonCameraStream from "@/components/app/photon-camera-stream.vue";
defineProps<{
// TODO fully update v-model usage in custom components on Vue3 update
value: number[]
value: number[];
}>();
const driverMode = computed<boolean>({
get: () => useCameraSettingsStore().isDriverMode,
set: v => useCameraSettingsStore().changeCurrentPipelineIndex(v ? -1 : useCameraSettingsStore().currentCameraSettings.lastPipelineIndex || 0, true)
set: (v) =>
useCameraSettingsStore().changeCurrentPipelineIndex(
v ? -1 : useCameraSettingsStore().currentCameraSettings.lastPipelineIndex || 0,
true
)
});
const fpsTooLow = computed<boolean>(() => {
@@ -23,17 +27,12 @@ const fpsTooLow = computed<boolean>(() => {
const gpuAccel = useSettingsStore().general.gpuAcceleration !== undefined;
const isReflective = useCameraSettingsStore().currentPipelineSettings.pipelineType === PipelineType.Reflective;
return (currFPS - targetFPS) < -5 && currFPS !== 0 && !driverMode && gpuAccel && isReflective;
return currFPS - targetFPS < -5 && currFPS !== 0 && !driverMode && gpuAccel && isReflective;
});
</script>
<template>
<v-card
color="primary"
height="100%"
style="display: flex; flex-direction: column"
dark
>
<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"
@@ -49,7 +48,13 @@ const fpsTooLow = computed<boolean>(() => {
<span class="pr-1">
Processing @ {{ Math.round(useStateStore().pipelineResults?.fps || 0) }}&nbsp;FPS &ndash;
</span>
<span v-if="fpsTooLow && !useCameraSettingsStore().currentPipelineSettings.inputShouldShow && useCameraSettingsStore().currentPipelineSettings.pipelineType === PipelineType.Reflective">
<span
v-if="
fpsTooLow &&
!useCameraSettingsStore().currentPipelineSettings.inputShouldShow &&
useCameraSettingsStore().currentPipelineSettings.pipelineType === PipelineType.Reflective
"
>
HSV thresholds are too broad; narrow them for better performance
</span>
<span v-else-if="fpsTooLow && useCameraSettingsStore().currentPipelineSettings.inputShouldShow">
@@ -61,39 +66,16 @@ const fpsTooLow = computed<boolean>(() => {
</v-chip>
</div>
<div>
<v-switch
v-model="driverMode"
label="Driver Mode"
style="margin-left: auto;"
color="accent"
class="pt-2"
/>
<v-switch v-model="driverMode" label="Driver Mode" style="margin-left: auto" color="accent" class="pt-2" />
</div>
</v-card-title>
<v-divider style="border-color: white" />
<v-row
class="pl-3 pr-3 pt-3 pb-3"
style="flex-wrap: nowrap; justify-content: center"
>
<v-col
v-show="value.includes(0)"
style="max-width: 500px; display: flex; align-items: center"
>
<photon-camera-stream
id="input-camera-stream"
stream-type="Raw"
style="width: 100%; height: auto"
/>
<v-row class="pl-3 pr-3 pt-3 pb-3" style="flex-wrap: nowrap; justify-content: center">
<v-col v-show="value.includes(0)" style="max-width: 500px; display: flex; align-items: center">
<photon-camera-stream id="input-camera-stream" stream-type="Raw" style="width: 100%; height: auto" />
</v-col>
<v-col
v-show="value.includes(1)"
style="max-width: 500px; display: flex; align-items: center"
>
<photon-camera-stream
id="output-camera-stream"
stream-type="Processed"
style="width: 100%; height: auto"
/>
<v-col v-show="value.includes(1)" style="max-width: 500px; display: flex; align-items: center">
<photon-camera-stream id="output-camera-stream" stream-type="Processed" style="width: 100%; height: auto" />
</v-col>
</v-row>
</v-card>

View File

@@ -15,8 +15,8 @@ import Map3DTab from "@/components/dashboard/tabs/Map3DTab.vue";
import { WebsocketPipelineType } from "@/types/WebsocketDataTypes";
interface ConfigOption {
tabName: string,
component: Component
tabName: string;
component: Component;
}
const allTabs = Object.freeze({
@@ -65,20 +65,27 @@ const getTabGroups = (): ConfigOption[][] => {
const lgAndDown = getCurrentInstance()?.proxy.$vuetify.breakpoint.lgAndDown || false;
const xl = getCurrentInstance()?.proxy.$vuetify.breakpoint.xl || false;
if(smAndDown || useCameraSettingsStore().isDriverMode || (mdAndDown && !useStateStore().sidebarFolded)) {
if (smAndDown || useCameraSettingsStore().isDriverMode || (mdAndDown && !useStateStore().sidebarFolded)) {
return [Object.values(allTabs)];
} else if(mdAndDown || !useStateStore().sidebarFolded) {
} else if (mdAndDown || !useStateStore().sidebarFolded) {
return [
[allTabs.inputTab, allTabs.thresholdTab, allTabs.contoursTab, allTabs.apriltagTab, allTabs.arucoTab, allTabs.outputTab],
[allTabs.targetsTab, allTabs.pnpTab, allTabs.map3dTab]
[
allTabs.inputTab,
allTabs.thresholdTab,
allTabs.contoursTab,
allTabs.apriltagTab,
allTabs.arucoTab,
allTabs.outputTab
],
[allTabs.targetsTab, allTabs.pnpTab, allTabs.map3dTab]
];
} else if(lgAndDown) {
} else if (lgAndDown) {
return [
[allTabs.inputTab],
[allTabs.thresholdTab, allTabs.contoursTab, allTabs.apriltagTab, allTabs.arucoTab, allTabs.outputTab],
[allTabs.targetsTab, allTabs.pnpTab, allTabs.map3dTab]
[allTabs.inputTab],
[allTabs.thresholdTab, allTabs.contoursTab, allTabs.apriltagTab, allTabs.arucoTab, allTabs.outputTab],
[allTabs.targetsTab, allTabs.pnpTab, allTabs.map3dTab]
];
} else if(xl) {
} else if (xl) {
return [
[allTabs.inputTab],
[allTabs.thresholdTab],
@@ -91,45 +98,41 @@ const getTabGroups = (): ConfigOption[][] => {
};
const tabGroups = computed<ConfigOption[][]>(() => {
// Just return the input tab because we know that is always the case in driver mode
if(useCameraSettingsStore().isDriverMode) return [[allTabs.inputTab]];
if (useCameraSettingsStore().isDriverMode) return [[allTabs.inputTab]];
const allow3d = useCameraSettingsStore().currentPipelineSettings.solvePNPEnabled;
const isAprilTag = useCameraSettingsStore().currentWebsocketPipelineType === WebsocketPipelineType.AprilTag;
const isAruco = useCameraSettingsStore().currentWebsocketPipelineType === WebsocketPipelineType.Aruco;
return getTabGroups().map(tabGroup => tabGroup.filter(tabConfig =>
!(!allow3d && tabConfig.tabName === "3D") //Filter out 3D tab any time 3D isn't calibrated
&& !((!allow3d || isAprilTag || isAruco) && tabConfig.tabName === "PnP") //Filter out the PnP config tab if 3D isn't available, or we're doing AprilTags
&& !((isAprilTag || isAruco) && (tabConfig.tabName === "Threshold")) //Filter out threshold tab if we're doing AprilTags
&& !((isAprilTag || isAruco) && (tabConfig.tabName === "Contours")) //Filter out contours if we're doing AprilTags
&& !(!isAprilTag && tabConfig.tabName === "AprilTag") //Filter out apriltag unless we actually are doing AprilTags
&& !(!isAruco && tabConfig.tabName === "Aruco") //Filter out aruco unless we actually are doing Aruco
));
return getTabGroups().map((tabGroup) =>
tabGroup.filter(
(tabConfig) =>
!(!allow3d && tabConfig.tabName === "3D") && //Filter out 3D tab any time 3D isn't calibrated
!((!allow3d || isAprilTag || isAruco) && tabConfig.tabName === "PnP") && //Filter out the PnP config tab if 3D isn't available, or we're doing AprilTags
!((isAprilTag || isAruco) && tabConfig.tabName === "Threshold") && //Filter out threshold tab if we're doing AprilTags
!((isAprilTag || isAruco) && tabConfig.tabName === "Contours") && //Filter out contours if we're doing AprilTags
!(!isAprilTag && tabConfig.tabName === "AprilTag") && //Filter out apriltag unless we actually are doing AprilTags
!(!isAruco && tabConfig.tabName === "Aruco") //Filter out aruco unless we actually are doing Aruco
)
);
});
onBeforeUpdate(() => {
// Force the current tab to the input tab on driver mode change
if(useCameraSettingsStore().isDriverMode) {
if (useCameraSettingsStore().isDriverMode) {
selectedTabs.value[0] = 0;
}
});
</script>
<template>
<v-row
no-gutters
class="tabGroups"
>
<v-row no-gutters class="tabGroups">
<v-col
v-for="(tabGroupData, tabGroupIndex) in tabGroups"
:key="tabGroupIndex"
:class="tabGroupIndex !== tabGroups.length - 1 && 'pr-3'"
>
<v-card
color="primary"
height="100%"
class="pr-4 pl-4"
>
<v-card color="primary" height="100%" class="pr-4 pl-4">
<v-tabs
v-model="selectedTabs[tabGroupIndex]"
grow
@@ -138,10 +141,7 @@ onBeforeUpdate(() => {
height="48"
slider-color="accent"
>
<v-tab
v-for="(tabConfig, index) in tabGroupData"
:key="index"
>
<v-tab v-for="(tabConfig, index) in tabGroupData" :key="index">
{{ tabConfig.tabName }}
</v-tab>
</v-tabs>
@@ -156,7 +156,8 @@ onBeforeUpdate(() => {
</template>
<style>
.v-slide-group__next--disabled, .v-slide-group__prev--disabled {
.v-slide-group__next--disabled,
.v-slide-group__prev--disabled {
display: none !important;
}
</style>

View File

@@ -5,23 +5,22 @@ import { useStateStore } from "@/stores/StateStore";
const props = defineProps<{
// TODO fully update v-model usage in custom components on Vue3 update
value: number[]
value: number[];
}>();
const emit = defineEmits<{
(e: "input", value: number[]): void
(e: "input", value: number[]): void;
}>();
const localValue = computed({
get: () => props.value,
set: v => emit("input", v)
set: (v) => emit("input", v)
});
const processingMode = computed<number>({
get: () => useCameraSettingsStore().currentPipelineSettings.solvePNPEnabled ? 1: 0,
set: v => {
if(useCameraSettingsStore().isCurrentVideoFormatCalibrated) {
get: () => (useCameraSettingsStore().currentPipelineSettings.solvePNPEnabled ? 1 : 0),
set: (v) => {
if (useCameraSettingsStore().isCurrentVideoFormatCalibrated) {
useCameraSettingsStore().changeCurrentPipelineSetting({ solvePNPEnabled: v === 1 }, true);
}
}
@@ -35,62 +34,30 @@ const processingMode = computed<number>({
color="primary"
style="height: 100%; display: flex; flex-direction: column"
>
<v-row
align="center"
class="pa-3 pb-0"
>
<v-row align="center" class="pa-3 pb-0">
<v-col>
<p style="color: white;">
Processing Mode
</p>
<v-btn-toggle
v-model="processingMode"
mandatory
dark
class="fill"
>
<v-btn
color="secondary"
>
<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"
:disabled="!useCameraSettingsStore().isCurrentVideoFormatCalibrated"
>
<v-btn color="secondary" :disabled="!useCameraSettingsStore().isCurrentVideoFormatCalibrated">
<v-icon>mdi-cube-outline</v-icon>
<span>3D</span>
</v-btn>
</v-btn-toggle>
</v-col>
</v-row>
<v-row
align="center"
class="pa-3 pt-0"
>
<v-row align="center" class="pa-3 pt-0">
<v-col>
<p style="color: white;">
Stream Display
</p>
<v-btn-toggle
v-model="localValue"
:multiple="true"
mandatory
dark
class="fill"
>
<v-btn
color="secondary"
class="fill"
>
<p style="color: white">Stream Display</p>
<v-btn-toggle v-model="localValue" :multiple="true" 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-btn color="secondary" class="fill">
<v-icon>mdi-export</v-icon>
<span>Processed</span>
</v-btn>

View File

@@ -11,7 +11,13 @@ import { useStateStore } from "@/stores/StateStore";
// Defer reference to store access method
const currentPipelineSettings = useCameraSettingsStore().currentPipelineSettings;
const interactiveCols = computed(() => (getCurrentInstance()?.proxy.$vuetify.breakpoint.mdAndDown || false) && (!useStateStore().sidebarFolded || useCameraSettingsStore().isDriverMode)) ? 9 : 8;
const interactiveCols = computed(
() =>
(getCurrentInstance()?.proxy.$vuetify.breakpoint.mdAndDown || false) &&
(!useStateStore().sidebarFolded || useCameraSettingsStore().isDriverMode)
)
? 9
: 8;
</script>
<template>
@@ -21,7 +27,7 @@ const interactiveCols = computed(() => (getCurrentInstance()?.proxy.$vuetify.bre
label="Target family"
:items="['AprilTag Family 36h11', 'AprilTag Family 25h9', 'AprilTag Family 16h5']"
:select-cols="interactiveCols"
@input="value => useCameraSettingsStore().changeCurrentPipelineSetting({tagFamily: value}, false)"
@input="(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ tagFamily: value }, false)"
/>
<cv-slider
v-model="currentPipelineSettings.decimate"
@@ -31,7 +37,7 @@ const interactiveCols = computed(() => (getCurrentInstance()?.proxy.$vuetify.bre
tooltip="Increases FPS at the expense of range by reducing image resolution initially"
:min="1"
:max="8"
@input="value => useCameraSettingsStore().changeCurrentPipelineSetting({decimate: value}, false)"
@input="(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ decimate: value }, false)"
/>
<cv-slider
v-model="currentPipelineSettings.blur"
@@ -42,7 +48,7 @@ const interactiveCols = computed(() => (getCurrentInstance()?.proxy.$vuetify.bre
:min="0"
:max="5"
:step="0.1"
@input="value => useCameraSettingsStore().changeCurrentPipelineSetting({blur: value}, false)"
@input="(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ blur: value }, false)"
/>
<cv-slider
v-model="currentPipelineSettings.threads"
@@ -52,14 +58,14 @@ const interactiveCols = computed(() => (getCurrentInstance()?.proxy.$vuetify.bre
tooltip="Number of threads spawned by the AprilTag detector"
:min="1"
:max="8"
@input="value => useCameraSettingsStore().changeCurrentPipelineSetting({threads: value}, false)"
@input="(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ threads: value }, false)"
/>
<cv-switch
v-model="currentPipelineSettings.refineEdges"
class="pt-2"
label="Refine Edges"
tooltip="Further refines the AprilTag corner position initial estimate, suggested left on"
@input="value => useCameraSettingsStore().changeCurrentPipelineSetting({refineEdges: value}, false)"
@input="(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ refineEdges: value }, false)"
/>
<cv-slider
v-model="currentPipelineSettings.decisionMargin"
@@ -69,7 +75,7 @@ const interactiveCols = computed(() => (getCurrentInstance()?.proxy.$vuetify.bre
tooltip="Tags with a 'margin' (decoding quality score) less than this wil be rejected. Increase this to reduce the number of false positive detections"
:min="0"
:max="250"
@input="value => useCameraSettingsStore().changeCurrentPipelineSetting({decisionMargin: value}, false)"
@input="(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ decisionMargin: value }, false)"
/>
<cv-slider
v-model="currentPipelineSettings.numIterations"
@@ -79,7 +85,7 @@ const interactiveCols = computed(() => (getCurrentInstance()?.proxy.$vuetify.bre
tooltip="Number of iterations the pose estimation algorithm will run, 50-100 is a good starting point"
:min="0"
:max="500"
@input="value => useCameraSettingsStore().changeCurrentPipelineSetting({numIterations: value}, false)"
@input="(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ numIterations: value }, false)"
/>
</div>
</template>

View File

@@ -9,7 +9,13 @@ import { useStateStore } from "@/stores/StateStore";
// Defer reference to store access method
const currentPipelineSettings = useCameraSettingsStore().currentPipelineSettings;
const interactiveCols = computed(() => (getCurrentInstance()?.proxy.$vuetify.breakpoint.mdAndDown || false) && (!useStateStore().sidebarFolded || useCameraSettingsStore().isDriverMode)) ? 9 : 8;
const interactiveCols = computed(
() =>
(getCurrentInstance()?.proxy.$vuetify.breakpoint.mdAndDown || false) &&
(!useStateStore().sidebarFolded || useCameraSettingsStore().isDriverMode)
)
? 9
: 8;
</script>
<template>
@@ -22,7 +28,7 @@ const interactiveCols = computed(() => (getCurrentInstance()?.proxy.$vuetify.bre
tooltip="Increases FPS at the expense of range by reducing image resolution initially"
:min="1"
:max="8"
@input="value => useCameraSettingsStore().changeCurrentPipelineSetting({decimate: value}, false)"
@input="(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ decimate: value }, false)"
/>
<cv-slider
v-model="currentPipelineSettings.numIterations"
@@ -33,7 +39,7 @@ const interactiveCols = computed(() => (getCurrentInstance()?.proxy.$vuetify.bre
:min="30"
:max="1000"
:step="5"
@input="value => useCameraSettingsStore().changeCurrentPipelineSetting({numIterations: value}, false)"
@input="(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ numIterations: value }, false)"
/>
<cv-slider
v-model="currentPipelineSettings.cornerAccuracy"
@@ -44,7 +50,7 @@ const interactiveCols = computed(() => (getCurrentInstance()?.proxy.$vuetify.bre
:min="0.01"
:max="100"
:step="0.01"
@input="value => useCameraSettingsStore().changeCurrentPipelineSetting({cornerAccuracy: value}, false)"
@input="(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ cornerAccuracy: value }, false)"
/>
</div>
</template>

View File

@@ -14,34 +14,46 @@ const currentPipelineSettings = useCameraSettingsStore().currentPipelineSettings
// TODO fix cv-range-slider so that store access doesn't need to be deferred
const contourArea = computed<[number, number]>({
get: () => Object.values(useCameraSettingsStore().currentPipelineSettings.contourArea) as [number, number],
set: v => useCameraSettingsStore().currentPipelineSettings.contourArea = v
set: (v) => (useCameraSettingsStore().currentPipelineSettings.contourArea = v)
});
const contourRatio = computed<[number, number]>({
get: () => Object.values(useCameraSettingsStore().currentPipelineSettings.contourRatio) as [number, number],
set: v => useCameraSettingsStore().currentPipelineSettings.contourRatio = v
set: (v) => (useCameraSettingsStore().currentPipelineSettings.contourRatio = v)
});
const contourFullness = computed<[number, number]>({
get: () => Object.values(useCameraSettingsStore().currentPipelineSettings.contourFullness) as [number, number],
set: v => useCameraSettingsStore().currentPipelineSettings.contourFullness = v
set: (v) => (useCameraSettingsStore().currentPipelineSettings.contourFullness = v)
});
const contourPerimeter = computed<[number, number]>({
get: () => currentPipelineSettings.pipelineType === PipelineType.ColoredShape ? Object.values(currentPipelineSettings.contourPerimeter) as [number, number] : [0, 0] as [number, number],
set: v => {
if(currentPipelineSettings.pipelineType === PipelineType.ColoredShape) {
get: () =>
currentPipelineSettings.pipelineType === PipelineType.ColoredShape
? (Object.values(currentPipelineSettings.contourPerimeter) as [number, number])
: ([0, 0] as [number, number]),
set: (v) => {
if (currentPipelineSettings.pipelineType === PipelineType.ColoredShape) {
currentPipelineSettings.contourPerimeter = v;
}
}
});
const contourRadius = computed<[number, number]>({
get: () => currentPipelineSettings.pipelineType === PipelineType.ColoredShape ? Object.values(currentPipelineSettings.contourRadius) as [number, number] : [0, 0] as [number, number],
set: v => {
if(currentPipelineSettings.pipelineType === PipelineType.ColoredShape) {
get: () =>
currentPipelineSettings.pipelineType === PipelineType.ColoredShape
? (Object.values(currentPipelineSettings.contourRadius) as [number, number])
: ([0, 0] as [number, number]),
set: (v) => {
if (currentPipelineSettings.pipelineType === PipelineType.ColoredShape) {
currentPipelineSettings.contourRadius = v;
}
}
});
const interactiveCols = computed(() => (getCurrentInstance()?.proxy.$vuetify.breakpoint.mdAndDown || false) && (!useStateStore().sidebarFolded || useCameraSettingsStore().isDriverMode)) ? 9 : 8;
const interactiveCols = computed(
() =>
(getCurrentInstance()?.proxy.$vuetify.breakpoint.mdAndDown || false) &&
(!useStateStore().sidebarFolded || useCameraSettingsStore().isDriverMode)
)
? 9
: 8;
</script>
<template>
@@ -53,7 +65,7 @@ const interactiveCols = computed(() => (getCurrentInstance()?.proxy.$vuetify.bre
:max="100"
:slider-cols="interactiveCols"
:step="0.01"
@input="value => useCameraSettingsStore().changeCurrentPipelineSetting({contourArea: value}, false)"
@input="(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ contourArea: value }, false)"
/>
<cv-range-slider
v-if="useCameraSettingsStore().currentPipelineType !== PipelineType.ColoredShape"
@@ -64,7 +76,7 @@ const interactiveCols = computed(() => (getCurrentInstance()?.proxy.$vuetify.bre
:max="100"
:slider-cols="interactiveCols"
:step="0.1"
@input="value => useCameraSettingsStore().changeCurrentPipelineSetting({contourRatio: value}, false)"
@input="(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ contourRatio: value }, false)"
/>
<cv-select
v-model="useCameraSettingsStore().currentPipelineSettings.contourTargetOrientation"
@@ -72,7 +84,9 @@ const interactiveCols = computed(() => (getCurrentInstance()?.proxy.$vuetify.bre
tooltip="Used to determine how to calculate target landmarks, as well as aspect ratio"
:items="['Portrait', 'Landscape']"
:select-cols="interactiveCols"
@input="value => useCameraSettingsStore().changeCurrentPipelineSetting({contourTargetOrientation: value}, false)"
@input="
(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ contourTargetOrientation: value }, false)
"
/>
<cv-range-slider
v-if="useCameraSettingsStore().currentPipelineType === PipelineType.ColoredShape"
@@ -82,7 +96,7 @@ const interactiveCols = computed(() => (getCurrentInstance()?.proxy.$vuetify.bre
:min="0"
:max="100"
:slider-cols="interactiveCols"
@input="value => useCameraSettingsStore().changeCurrentPipelineSetting({contourFullness: value}, false)"
@input="(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ contourFullness: value }, false)"
/>
<cv-range-slider
v-if="currentPipelineSettings.pipelineType === PipelineType.ColoredShape"
@@ -92,7 +106,7 @@ const interactiveCols = computed(() => (getCurrentInstance()?.proxy.$vuetify.bre
min="0"
max="4000"
:slider-cols="interactiveCols"
@input="value => useCameraSettingsStore().changeCurrentPipelineSetting({contourPerimeter: value}, false)"
@input="(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ contourPerimeter: value }, false)"
/>
<cv-slider
v-model="useCameraSettingsStore().currentPipelineSettings.contourSpecklePercentage"
@@ -101,7 +115,9 @@ const interactiveCols = computed(() => (getCurrentInstance()?.proxy.$vuetify.bre
:min="0"
:max="100"
:slider-cols="interactiveCols"
@input="value => useCameraSettingsStore().changeCurrentPipelineSetting({contourSpecklePercentage: value}, false)"
@input="
(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ contourSpecklePercentage: value }, false)
"
/>
<template v-if="currentPipelineSettings.pipelineType === PipelineType.Reflective">
<cv-slider
@@ -112,7 +128,7 @@ const interactiveCols = computed(() => (getCurrentInstance()?.proxy.$vuetify.bre
:max="4"
:step="0.1"
:slider-cols="interactiveCols"
@input="value => useCameraSettingsStore().changeCurrentPipelineSetting({contourFilterRangeX: value}, false)"
@input="(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ contourFilterRangeX: value }, false)"
/>
<cv-slider
v-model="currentPipelineSettings.contourFilterRangeY"
@@ -122,24 +138,24 @@ const interactiveCols = computed(() => (getCurrentInstance()?.proxy.$vuetify.bre
:max="4"
:step="0.1"
:slider-cols="interactiveCols"
@input="value => useCameraSettingsStore().changeCurrentPipelineSetting({contourFilterRangeY: value}, false)"
@input="(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ contourFilterRangeY: value }, false)"
/>
<cv-select
v-model="useCameraSettingsStore().currentPipelineSettings.contourGroupingMode"
label="Target Grouping"
tooltip="Whether or not every two targets are paired with each other (good for e.g. 2019 targets)"
:select-cols="interactiveCols"
:items="['Single','Dual','Two or More']"
@input="value => useCameraSettingsStore().changeCurrentPipelineSetting({contourGroupingMode: value}, false)"
:items="['Single', 'Dual', 'Two or More']"
@input="(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ contourGroupingMode: value }, false)"
/>
<cv-select
v-model="useCameraSettingsStore().currentPipelineSettings.contourIntersection"
label="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="interactiveCols"
:items="['None','Up','Down','Left','Right']"
:items="['None', 'Up', 'Down', 'Left', 'Right']"
:disabled="useCameraSettingsStore().currentPipelineSettings.contourGroupingMode === 0"
@input="value => useCameraSettingsStore().changeCurrentPipelineSetting({contourIntersection: value}, false)"
@input="(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ contourIntersection: value }, false)"
/>
</template>
<template v-else-if="currentPipelineSettings.pipelineType === PipelineType.ColoredShape">
@@ -150,7 +166,7 @@ const interactiveCols = computed(() => (getCurrentInstance()?.proxy.$vuetify.bre
tooltip="The shape of targets to look for"
:select-cols="interactiveCols"
:items="['Circle', 'Polygon', 'Triangle', 'Quadrilateral']"
@input="value => useCameraSettingsStore().changeCurrentPipelineSetting({contourShape: value}, false)"
@input="(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ contourShape: value }, false)"
/>
<cv-slider
v-model="currentPipelineSettings.accuracyPercentage"
@@ -160,7 +176,7 @@ const interactiveCols = computed(() => (getCurrentInstance()?.proxy.$vuetify.bre
:min="0"
:max="100"
:slider-cols="interactiveCols"
@input="value => useCameraSettingsStore().changeCurrentPipelineSetting({accuracyPercentage: value}, false)"
@input="(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ accuracyPercentage: value }, false)"
/>
<cv-slider
v-model="currentPipelineSettings.circleDetectThreshold"
@@ -170,7 +186,9 @@ const interactiveCols = computed(() => (getCurrentInstance()?.proxy.$vuetify.bre
:min="1"
:max="100"
:slider-cols="interactiveCols"
@input="value => useCameraSettingsStore().changeCurrentPipelineSetting({circleDetectThreshold: value}, false)"
@input="
(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ circleDetectThreshold: value }, false)
"
/>
<cv-range-slider
v-model="contourRadius"
@@ -179,7 +197,7 @@ const interactiveCols = computed(() => (getCurrentInstance()?.proxy.$vuetify.bre
:min="0"
:max="100"
:slider-cols="interactiveCols"
@input="value => useCameraSettingsStore().changeCurrentPipelineSetting({contourRadius: value}, false)"
@input="(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ contourRadius: value }, false)"
/>
<cv-slider
v-model="currentPipelineSettings.maxCannyThresh"
@@ -188,7 +206,7 @@ const interactiveCols = computed(() => (getCurrentInstance()?.proxy.$vuetify.bre
:min="1"
:max="100"
:slider-cols="interactiveCols"
@input="value => useCameraSettingsStore().changeCurrentPipelineSetting({maxCannyThresh: value}, false)"
@input="(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ maxCannyThresh: value }, false)"
/>
<cv-slider
v-model="currentPipelineSettings.circleAccuracy"
@@ -197,7 +215,7 @@ const interactiveCols = computed(() => (getCurrentInstance()?.proxy.$vuetify.bre
:min="1"
:max="100"
:slider-cols="interactiveCols"
@input="value => useCameraSettingsStore().changeCurrentPipelineSetting({circleAccuracy: value}, false)"
@input="(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ circleAccuracy: value }, false)"
/>
<v-divider class="mt-3" />
</template>
@@ -206,8 +224,8 @@ const interactiveCols = computed(() => (getCurrentInstance()?.proxy.$vuetify.bre
label="Target Sort"
tooltip="Chooses the sorting mode used to determine the 'best' targets to provide to user code"
:select-cols="interactiveCols"
:items="['Largest','Smallest','Highest','Lowest','Rightmost','Leftmost','Centermost']"
@input="value => useCameraSettingsStore().changeCurrentPipelineSetting({contourSortMode: value}, false)"
:items="['Largest', 'Smallest', 'Highest', 'Lowest', 'Rightmost', 'Leftmost', 'Centermost']"
@input="(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ contourSortMode: value }, false)"
/>
</div>
</template>

View File

@@ -8,26 +8,38 @@ import { useSettingsStore } from "@/stores/settings/GeneralSettingsStore";
import { useStateStore } from "@/stores/StateStore";
// Due to something with libcamera or something else IDK much about, the 90° rotations need to be disabled if the libcamera drivers are being used.
const cameraRotations = computed(() => ["Normal", "90° CW", "180°", "90° CCW"].map((v, i) => ({ name: v, value: i, disabled: useSettingsStore().gpuAccelerationEnabled ? [1, 3].includes(i) : false })));
const cameraRotations = computed(() =>
["Normal", "90° CW", "180°", "90° CCW"].map((v, i) => ({
name: v,
value: i,
disabled: useSettingsStore().gpuAccelerationEnabled ? [1, 3].includes(i) : false
}))
);
const streamDivisors = [1, 2, 4, 6];
const getFilteredStreamDivisors = (): number[] => {
const currentResolutionWidth = useCameraSettingsStore().currentVideoFormat.resolution.width;
return streamDivisors.filter(x =>
useCameraSettingsStore().isDriverMode
|| !useSettingsStore().gpuAccelerationEnabled
|| currentResolutionWidth / x < 400);
return streamDivisors.filter(
(x) =>
useCameraSettingsStore().isDriverMode ||
!useSettingsStore().gpuAccelerationEnabled ||
currentResolutionWidth / x < 400
);
};
const getNumberOfSkippedDivisors = () => streamDivisors.length - getFilteredStreamDivisors().length;
const cameraResolutions = computed(() => useCameraSettingsStore().currentCameraSettings.validVideoFormats.map(f => `${f.resolution.width} X ${f.resolution.height} at ${f.fps} FPS, ${f.pixelFormat}`));
const cameraResolutions = computed(() =>
useCameraSettingsStore().currentCameraSettings.validVideoFormats.map(
(f) => `${f.resolution.width} X ${f.resolution.height} at ${f.fps} FPS, ${f.pixelFormat}`
)
);
const handleResolutionChange = (value: number) => {
useCameraSettingsStore().changeCurrentPipelineSetting({ cameraVideoModeIndex: value }, false);
useCameraSettingsStore().changeCurrentPipelineSetting({ streamingFrameDivisor: getNumberOfSkippedDivisors() }, false);
useCameraSettingsStore().currentPipelineSettings.streamingFrameDivisor = 0;
if(!useCameraSettingsStore().isCurrentVideoFormatCalibrated) {
if (!useCameraSettingsStore().isCurrentVideoFormatCalibrated) {
useCameraSettingsStore().changeCurrentPipelineSetting({ solvePNPEnabled: false }, true);
}
};
@@ -35,14 +47,24 @@ const handleResolutionChange = (value: number) => {
const streamResolutions = computed(() => {
const streamDivisors = getFilteredStreamDivisors();
const currentResolution = useCameraSettingsStore().currentVideoFormat.resolution;
return streamDivisors
.map(x => `${Math.floor(currentResolution.width / x)} X ${Math.floor(currentResolution.height / x)}`);
return streamDivisors.map(
(x) => `${Math.floor(currentResolution.width / x)} X ${Math.floor(currentResolution.height / x)}`
);
});
const handleStreamResolutionChange = (value: number) => {
useCameraSettingsStore().changeCurrentPipelineSetting({ streamingFrameDivisor: value + getNumberOfSkippedDivisors() }, false);
useCameraSettingsStore().changeCurrentPipelineSetting(
{ streamingFrameDivisor: value + getNumberOfSkippedDivisors() },
false
);
};
const interactiveCols = computed(() => (getCurrentInstance()?.proxy.$vuetify.breakpoint.mdAndDown || false) && (!useStateStore().sidebarFolded || useCameraSettingsStore().isDriverMode)) ? 9 : 8;
const interactiveCols = computed(
() =>
(getCurrentInstance()?.proxy.$vuetify.breakpoint.mdAndDown || false) &&
(!useStateStore().sidebarFolded || useCameraSettingsStore().isDriverMode)
)
? 9
: 8;
</script>
<template>
@@ -56,7 +78,7 @@ const interactiveCols = computed(() => (getCurrentInstance()?.proxy.$vuetify.bre
:max="100"
:slider-cols="interactiveCols"
:step="0.1"
@input="args => useCameraSettingsStore().changeCurrentPipelineSetting({cameraExposure: args}, false)"
@input="(args) => useCameraSettingsStore().changeCurrentPipelineSetting({ cameraExposure: args }, false)"
/>
<cv-slider
v-model="useCameraSettingsStore().currentPipelineSettings.cameraBrightness"
@@ -64,7 +86,7 @@ const interactiveCols = computed(() => (getCurrentInstance()?.proxy.$vuetify.bre
:min="0"
:max="100"
:slider-cols="interactiveCols"
@input="args => useCameraSettingsStore().changeCurrentPipelineSetting({cameraBrightness: args}, false)"
@input="(args) => useCameraSettingsStore().changeCurrentPipelineSetting({ cameraBrightness: args }, false)"
/>
<cv-switch
v-model="useCameraSettingsStore().currentPipelineSettings.cameraAutoExposure"
@@ -72,7 +94,7 @@ const interactiveCols = computed(() => (getCurrentInstance()?.proxy.$vuetify.bre
label="Auto Exposure"
:switch-cols="interactiveCols"
tooltip="Enables or Disables camera automatic adjustment for current lighting conditions"
@input="args => useCameraSettingsStore().changeCurrentPipelineSetting({cameraAutoExposure: args}, false)"
@input="(args) => useCameraSettingsStore().changeCurrentPipelineSetting({ cameraAutoExposure: args }, false)"
/>
<cv-slider
v-if="useCameraSettingsStore().currentPipelineSettings.cameraGain >= 0"
@@ -82,7 +104,7 @@ const interactiveCols = computed(() => (getCurrentInstance()?.proxy.$vuetify.bre
:min="0"
:max="100"
:slider-cols="interactiveCols"
@input="args => useCameraSettingsStore().changeCurrentPipelineSetting({cameraGain: args}, false)"
@input="(args) => useCameraSettingsStore().changeCurrentPipelineSetting({ cameraGain: args }, false)"
/>
<cv-slider
v-if="useCameraSettingsStore().currentPipelineSettings.cameraRedGain !== -1"
@@ -92,7 +114,7 @@ const interactiveCols = computed(() => (getCurrentInstance()?.proxy.$vuetify.bre
:max="100"
:slider-cols="interactiveCols"
tooltip="Controls red automatic white balance gain, which affects how the camera captures colors in different conditions"
@input="args => useCameraSettingsStore().changeCurrentPipelineSetting({cameraRedGain: args}, false)"
@input="(args) => useCameraSettingsStore().changeCurrentPipelineSetting({ cameraRedGain: args }, false)"
/>
<cv-slider
v-if="useCameraSettingsStore().currentPipelineSettings.cameraBlueGain !== -1"
@@ -102,7 +124,7 @@ const interactiveCols = computed(() => (getCurrentInstance()?.proxy.$vuetify.bre
:max="100"
:slider-cols="interactiveCols"
tooltip="Controls blue automatic white balance gain, which affects how the camera captures colors in different conditions"
@input="args => useCameraSettingsStore().changeCurrentPipelineSetting({cameraBlueGain: args}, false)"
@input="(args) => useCameraSettingsStore().changeCurrentPipelineSetting({ cameraBlueGain: args }, false)"
/>
<cv-select
v-model="useCameraSettingsStore().currentPipelineSettings.inputImageRotationMode"
@@ -110,7 +132,7 @@ const interactiveCols = computed(() => (getCurrentInstance()?.proxy.$vuetify.bre
tooltip="Rotates the camera stream"
:items="cameraRotations"
:select-cols="interactiveCols"
@input="args => useCameraSettingsStore().changeCurrentPipelineSetting({inputImageRotationMode: args}, false)"
@input="(args) => useCameraSettingsStore().changeCurrentPipelineSetting({ inputImageRotationMode: args }, false)"
/>
<cv-select
v-model="useCameraSettingsStore().currentPipelineSettings.cameraVideoModeIndex"
@@ -118,7 +140,7 @@ const interactiveCols = computed(() => (getCurrentInstance()?.proxy.$vuetify.bre
tooltip="Resolution and FPS the camera should directly capture at"
:items="cameraResolutions"
:select-cols="interactiveCols"
@input="args => handleResolutionChange(args)"
@input="(args) => handleResolutionChange(args)"
/>
<cv-select
v-model="useCameraSettingsStore().currentPipelineSettings.streamingFrameDivisor"
@@ -126,7 +148,7 @@ const interactiveCols = computed(() => (getCurrentInstance()?.proxy.$vuetify.bre
tooltip="Resolution to which camera frames are downscaled for streaming to the dashboard"
:items="streamResolutions"
:select-cols="interactiveCols"
@input="args => handleStreamResolutionChange(args)"
@input="(args) => handleStreamResolutionChange(args)"
/>
</div>
</template>

View File

@@ -16,9 +16,7 @@ const trackedTargets = computed<PhotonTarget[]>(() => useStateStore().pipelineRe
</v-row>
<v-row style="width: 100%">
<v-col style="display: flex; align-items: center; justify-content: center">
<photon3d-visualizer
:targets="trackedTargets"
/>
<photon3d-visualizer :targets="trackedTargets" />
</v-col>
</v-row>
</div>

View File

@@ -7,11 +7,15 @@ import { computed, getCurrentInstance } from "vue";
import { RobotOffsetType } from "@/types/SettingTypes";
import { useStateStore } from "@/stores/StateStore";
const isTagPipeline = computed(() => useCameraSettingsStore().currentPipelineType === PipelineType.AprilTag || useCameraSettingsStore().currentPipelineType === PipelineType.Aruco);
const isTagPipeline = computed(
() =>
useCameraSettingsStore().currentPipelineType === PipelineType.AprilTag ||
useCameraSettingsStore().currentPipelineType === PipelineType.Aruco
);
interface MetricItem {
header: string,
value?: string
header: string;
value?: string;
}
const offsetPoints = computed<MetricItem[]>(() => {
@@ -24,10 +28,11 @@ const offsetPoints = computed<MetricItem[]>(() => {
const firstPointArea = useCameraSettingsStore().currentPipelineSettings.offsetDualPointAArea;
const secondPoint = Object.values(useCameraSettingsStore().currentPipelineSettings.offsetDualPointB);
const secondPointArea = useCameraSettingsStore().currentPipelineSettings.offsetDualPointBArea;
return [{ header: "First Offset Point", value: `(${firstPoint[0].toFixed(2)}°, ${firstPoint[1].toFixed(2)}°)` },
{ header: "First Offset Point Area", value: `${firstPointArea.toFixed(2)}%` },
{ header: "Second Offset Point", value: `(${secondPoint[0].toFixed(2)}°, ${secondPoint[1].toFixed(2)}°)` },
{ header: "Second Offset Point Area", value: `${secondPointArea.toFixed(2)}%` }
return [
{ header: "First Offset Point", value: `(${firstPoint[0].toFixed(2)}°, ${firstPoint[1].toFixed(2)}°)` },
{ header: "First Offset Point Area", value: `${firstPointArea.toFixed(2)}%` },
{ header: "Second Offset Point", value: `(${secondPoint[0].toFixed(2)}°, ${secondPoint[1].toFixed(2)}°)` },
{ header: "Second Offset Point Area", value: `${secondPointArea.toFixed(2)}%` }
];
default:
case RobotOffsetPointMode.None:
@@ -35,7 +40,13 @@ const offsetPoints = computed<MetricItem[]>(() => {
}
});
const interactiveCols = computed(() => (getCurrentInstance()?.proxy.$vuetify.breakpoint.mdAndDown || false) && (!useStateStore().sidebarFolded || useCameraSettingsStore().isDriverMode)) ? 9 : 8;
const interactiveCols = computed(
() =>
(getCurrentInstance()?.proxy.$vuetify.breakpoint.mdAndDown || false) &&
(!useStateStore().sidebarFolded || useCameraSettingsStore().isDriverMode)
)
? 9
: 8;
</script>
<template>
@@ -44,9 +55,11 @@ const interactiveCols = computed(() => (getCurrentInstance()?.proxy.$vuetify.bre
v-model="useCameraSettingsStore().currentPipelineSettings.contourTargetOffsetPointEdge"
label="Target Offset Point"
tooltip="Changes where the 'center' of the target is (used for calculating e.g. pitch and yaw)"
:items="['Center','Top','Bottom','Left','Right']"
:items="['Center', 'Top', 'Bottom', 'Left', 'Right']"
:select-cols="interactiveCols"
@input="value => useCameraSettingsStore().changeCurrentPipelineSetting({contourTargetOffsetPointEdge: value}, false)"
@input="
(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ contourTargetOffsetPointEdge: value }, false)
"
/>
<cv-select
v-if="!isTagPipeline"
@@ -55,7 +68,9 @@ const interactiveCols = computed(() => (getCurrentInstance()?.proxy.$vuetify.bre
tooltip="Used to determine how to calculate target landmarks (e.g. the top, left, or bottom of the target)"
:items="['Portrait', 'Landscape']"
:select-cols="interactiveCols"
@input="value => useCameraSettingsStore().changeCurrentPipelineSetting({contourTargetOrientation: value}, false)"
@input="
(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ contourTargetOrientation: value }, false)
"
/>
<cv-switch
v-model="useCameraSettingsStore().currentPipelineSettings.outputShowMultipleTargets"
@@ -63,7 +78,9 @@ const interactiveCols = computed(() => (getCurrentInstance()?.proxy.$vuetify.bre
tooltip="If enabled, up to five targets will be displayed and sent to user code, instead of just one"
:disabled="isTagPipeline"
:switch-cols="interactiveCols"
@input="value => useCameraSettingsStore().changeCurrentPipelineSetting({outputShowMultipleTargets: value}, false)"
@input="
(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ outputShowMultipleTargets: value }, false)
"
/>
<v-divider />
<table
@@ -71,20 +88,12 @@ const interactiveCols = computed(() => (getCurrentInstance()?.proxy.$vuetify.bre
class="metrics-table mt-3 mb-3"
>
<tr>
<th
v-for="(item, itemIndex) in offsetPoints"
:key="itemIndex"
class="metric-item metric-item-title"
>
<th v-for="(item, itemIndex) in offsetPoints" :key="itemIndex" class="metric-item metric-item-title">
{{ item.header }}
</th>
</tr>
<tr>
<td
v-for="(item, itemIndex) in offsetPoints"
:key="itemIndex"
class="metric-item"
>
<td v-for="(item, itemIndex) in offsetPoints" :key="itemIndex" class="metric-item">
{{ item.value }}
</td>
</tr>
@@ -93,21 +102,23 @@ const interactiveCols = computed(() => (getCurrentInstance()?.proxy.$vuetify.bre
v-model="useCameraSettingsStore().currentPipelineSettings.offsetRobotOffsetMode"
label="Robot Offset Mode"
tooltip="Used to add an arbitrary offset to the location of the targeting crosshair"
:items="['None','Single Point','Dual Point']"
:items="['None', 'Single Point', 'Dual Point']"
:select-cols="interactiveCols"
@input="value => useCameraSettingsStore().changeCurrentPipelineSetting({offsetRobotOffsetMode: value}, false)"
@input="(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ offsetRobotOffsetMode: value }, false)"
/>
<v-row
v-if="useCameraSettingsStore().currentPipelineSettings.offsetRobotOffsetMode !== RobotOffsetPointMode.None"
align="center"
justify="start"
>
<v-row v-if="useCameraSettingsStore().currentPipelineSettings.offsetRobotOffsetMode === RobotOffsetPointMode.Single">
<v-row
v-if="useCameraSettingsStore().currentPipelineSettings.offsetRobotOffsetMode === RobotOffsetPointMode.Single"
>
<v-col>
<v-btn
small
color="accent"
style="width: 100%;"
style="width: 100%"
class="black--text"
@click="useCameraSettingsStore().takeRobotOffsetPoint(RobotOffsetType.Single)"
>
@@ -115,12 +126,14 @@ const interactiveCols = computed(() => (getCurrentInstance()?.proxy.$vuetify.bre
</v-btn>
</v-col>
</v-row>
<v-row v-else-if="useCameraSettingsStore().currentPipelineSettings.offsetRobotOffsetMode === RobotOffsetPointMode.Dual">
<v-row
v-else-if="useCameraSettingsStore().currentPipelineSettings.offsetRobotOffsetMode === RobotOffsetPointMode.Dual"
>
<v-col>
<v-btn
small
color="accent"
style="width: 100%;"
style="width: 100%"
class="black--text"
@click="useCameraSettingsStore().takeRobotOffsetPoint(RobotOffsetType.DualFirst)"
>
@@ -131,7 +144,7 @@ const interactiveCols = computed(() => (getCurrentInstance()?.proxy.$vuetify.bre
<v-btn
small
color="accent"
style="width: 100%;"
style="width: 100%"
class="black--text"
@click="useCameraSettingsStore().takeRobotOffsetPoint(RobotOffsetType.DualSecond)"
>
@@ -143,7 +156,7 @@ const interactiveCols = computed(() => (getCurrentInstance()?.proxy.$vuetify.bre
<v-btn
small
color="yellow darken-3"
style="width: 100%;"
style="width: 100%"
@click="useCameraSettingsStore().takeRobotOffsetPoint(RobotOffsetType.Clear)"
>
Clear All Points
@@ -154,7 +167,7 @@ const interactiveCols = computed(() => (getCurrentInstance()?.proxy.$vuetify.bre
</template>
<style scoped>
.metrics-table{
.metrics-table {
border-collapse: separate;
border-spacing: 0;
border-radius: 5px;

View File

@@ -6,7 +6,13 @@ import CvSlider from "@/components/common/cv-slider.vue";
import { computed, getCurrentInstance } from "vue";
import { useStateStore } from "@/stores/StateStore";
const interactiveCols = computed(() => (getCurrentInstance()?.proxy.$vuetify.breakpoint.mdAndDown || false) && (!useStateStore().sidebarFolded || useCameraSettingsStore().isDriverMode)) ? 9 : 8;
const interactiveCols = computed(
() =>
(getCurrentInstance()?.proxy.$vuetify.breakpoint.mdAndDown || false) &&
(!useStateStore().sidebarFolded || useCameraSettingsStore().isDriverMode)
)
? 9
: 8;
</script>
<template>
@@ -15,18 +21,18 @@ const interactiveCols = computed(() => (getCurrentInstance()?.proxy.$vuetify.bre
v-model="useCameraSettingsStore().currentPipelineSettings.targetModel"
label="Target Model"
:items="[
{name: '2020 High Goal Outer', value: TargetModel.InfiniteRechargeHighGoalOuter},
{name: '2020 High Goal Inner', value: TargetModel.InfiniteRechargeHighGoalInner},
{name: '2019 Dual Target', value: TargetModel.DeepSpaceDualTarget},
{name: '2020 Power Cell (7in)', value: TargetModel.CircularPowerCell7in},
{name: '2022 Cargo Ball (9.5in)', value: TargetModel.RapidReactCircularCargoBall},
{name: '2016 High Goal', value: TargetModel.StrongholdHighGoal},
{name: '200mm AprilTag', value: TargetModel.Apriltag_200mm},
{name: '6in (16h5) Aruco', value: TargetModel.Aruco6in_16h5},
{name: '6in (16h5) AprilTag', value: TargetModel.Apriltag6in_16h5}
{ name: '2020 High Goal Outer', value: TargetModel.InfiniteRechargeHighGoalOuter },
{ name: '2020 High Goal Inner', value: TargetModel.InfiniteRechargeHighGoalInner },
{ name: '2019 Dual Target', value: TargetModel.DeepSpaceDualTarget },
{ name: '2020 Power Cell (7in)', value: TargetModel.CircularPowerCell7in },
{ name: '2022 Cargo Ball (9.5in)', value: TargetModel.RapidReactCircularCargoBall },
{ name: '2016 High Goal', value: TargetModel.StrongholdHighGoal },
{ name: '200mm AprilTag', value: TargetModel.Apriltag_200mm },
{ name: '6in (16h5) Aruco', value: TargetModel.Aruco6in_16h5 },
{ name: '6in (16h5) AprilTag', value: TargetModel.Apriltag6in_16h5 }
]"
:select-cols="interactiveCols"
@input="value => useCameraSettingsStore().changeCurrentPipelineSetting({targetModel: value}, false)"
@input="(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ targetModel: value }, false)"
/>
<cv-slider
v-model="useCameraSettingsStore().currentPipelineSettings.cornerDetectionAccuracyPercentage"
@@ -35,7 +41,10 @@ const interactiveCols = computed(() => (getCurrentInstance()?.proxy.$vuetify.bre
label="Contour simplification Percentage"
:min="0"
:max="100"
@input="value => useCameraSettingsStore().changeCurrentPipelineSetting({cornerDetectionAccuracyPercentage: value}, false)"
@input="
(value) =>
useCameraSettingsStore().changeCurrentPipelineSetting({ cornerDetectionAccuracyPercentage: value }, false)
"
/>
</div>
</template>

View File

@@ -6,69 +6,52 @@ import { useStateStore } from "@/stores/StateStore";
<template>
<div>
<v-row
align="start"
class="pb-4"
style="height: 300px;"
>
<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
>
<v-simple-table fixed-header height="100%" dense dark>
<template #default>
<thead style="font-size: 1.25rem;">
<thead style="font-size: 1.25rem">
<tr>
<th class="text-center">
Target Count
</th>
<th class="text-center">Target Count</th>
<th
v-if="useCameraSettingsStore().currentPipelineType === PipelineType.AprilTag || useCameraSettingsStore().currentPipelineType === PipelineType.Aruco"
v-if="
useCameraSettingsStore().currentPipelineType === PipelineType.AprilTag ||
useCameraSettingsStore().currentPipelineType === PipelineType.Aruco
"
class="text-center"
>
Fiducial ID
</th>
<template v-if="!useCameraSettingsStore().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>
<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>
<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="useCameraSettingsStore().currentPipelineType === PipelineType.AprilTag && useCameraSettingsStore().currentPipelineSettings.solvePNPEnabled">
<th class="text-center">
Ambiguity %
</th>
<template
v-if="
useCameraSettingsStore().currentPipelineType === PipelineType.AprilTag &&
useCameraSettingsStore().currentPipelineSettings.solvePNPEnabled
"
>
<th class="text-center">Ambiguity %</th>
</template>
</tr>
</thead>
<tbody>
<tr
v-for="(target, index) in useStateStore().pipelineResults?.targets"
:key="index"
>
<tr v-for="(target, index) in useStateStore().pipelineResults?.targets" :key="index">
<td>{{ index }}</td>
<td v-if="useCameraSettingsStore().currentPipelineType === PipelineType.AprilTag || useCameraSettingsStore().currentPipelineType === PipelineType.Aruco">
<td
v-if="
useCameraSettingsStore().currentPipelineType === PipelineType.AprilTag ||
useCameraSettingsStore().currentPipelineType === PipelineType.Aruco
"
>
{{ target.fiducialId }}
</td>
<template v-if="!useCameraSettingsStore().currentPipelineSettings.solvePNPEnabled">
@@ -80,9 +63,14 @@ import { useStateStore } from "@/stores/StateStore";
<template v-else-if="useCameraSettingsStore().currentPipelineSettings.solvePNPEnabled">
<td>{{ target.pose?.x.toFixed(2) }}&nbsp;m</td>
<td>{{ target.pose?.y.toFixed(2) }}&nbsp;m</td>
<td>{{ (target.pose?.angle_z * 180.0 / Math.PI).toFixed(2) }}&deg;</td>
<td>{{ ((target.pose?.angle_z * 180.0) / Math.PI).toFixed(2) }}&deg;</td>
</template>
<template v-if="useCameraSettingsStore().currentPipelineType === PipelineType.AprilTag && useCameraSettingsStore().currentPipelineSettings.solvePNPEnabled">
<template
v-if="
useCameraSettingsStore().currentPipelineType === PipelineType.AprilTag &&
useCameraSettingsStore().currentPipelineSettings.solvePNPEnabled
"
>
<td>{{ target.ambiguity?.toFixed(2) }}%</td>
</template>
</tr>
@@ -100,7 +88,8 @@ import { useStateStore } from "@/stores/StateStore";
text-align: center;
background-color: #006492 !important;
th, td {
th,
td {
background-color: #006492 !important;
font-size: 1rem !important;
}

View File

@@ -10,7 +10,7 @@ const averageHue = computed<number>(() => {
const isHueInverted = useCameraSettingsStore().currentPipelineSettings.hueInverted;
let val = Object.values(useCameraSettingsStore().currentPipelineSettings.hsvHue).reduce((a, b) => a + b, 0);
if(isHueInverted) val += 180;
if (isHueInverted) val += 180;
if (val > 360) val -= 360;
return val;
@@ -19,23 +19,23 @@ const averageHue = computed<number>(() => {
// TODO fix cv-range-slider so that store access doesn't need to be deferred
const hsvHue = computed<[number, number]>({
get: () => Object.values(useCameraSettingsStore().currentPipelineSettings.hsvHue) as [number, number],
set: v => useCameraSettingsStore().currentPipelineSettings.hsvHue = v
set: (v) => (useCameraSettingsStore().currentPipelineSettings.hsvHue = v)
});
const hsvSaturation = computed<[number, number]>({
get: () => Object.values(useCameraSettingsStore().currentPipelineSettings.hsvSaturation) as [number, number],
set: v => useCameraSettingsStore().currentPipelineSettings.hsvSaturation = v
set: (v) => (useCameraSettingsStore().currentPipelineSettings.hsvSaturation = v)
});
const hsvValue = computed<[number, number]>({
get: () => Object.values(useCameraSettingsStore().currentPipelineSettings.hsvValue) as [number, number],
set: v => useCameraSettingsStore().currentPipelineSettings.hsvValue = v
set: (v) => (useCameraSettingsStore().currentPipelineSettings.hsvValue = v)
});
let selectedEventMode: 0 | 1 | 2 | 3 = 0;
const handleStreamClick = (event: MouseEvent) => {
if(!useStateStore().colorPickingMode || selectedEventMode === 0) return;
if (!useStateStore().colorPickingMode || selectedEventMode === 0) return;
const cameraStream = document.getElementById("input-camera-stream");
if(cameraStream === null) return;
if (cameraStream === null) return;
const canvas = document.createElement("canvas");
canvas.width = cameraStream.clientWidth;
@@ -43,18 +43,21 @@ const handleStreamClick = (event: MouseEvent) => {
// Get the (x, y) position of the click with (0, 0) in the top left corner
const rect = cameraStream.getBoundingClientRect();
const x = Math.round((event.clientX - rect.left) / rect.width * cameraStream.clientWidth);
const y = Math.round((event.clientY - rect.top) / rect.height * cameraStream.clientHeight);
const x = Math.round(((event.clientX - rect.left) / rect.width) * cameraStream.clientWidth);
const y = Math.round(((event.clientY - rect.top) / rect.height) * cameraStream.clientHeight);
const context = canvas.getContext("2d");
if(context === null) return;
if (context === null) return;
context.drawImage(cameraStream as CanvasImageSource, 0, 0, cameraStream.clientWidth, cameraStream.clientHeight);
const colorPicker = new ColorPicker(context.getImageData(x, y, 1, 1).data);
// Calculate HSV values based on the mode
let selectedHSVData: [HSV, HSV] = [[0, 0, 0], [0, 0, 0]];
if(selectedEventMode === 1) {
let selectedHSVData: [HSV, HSV] = [
[0, 0, 0],
[0, 0, 0]
];
if (selectedEventMode === 1) {
selectedHSVData = colorPicker.selectedColorRange();
} else {
const currentHue = Object.values(useCameraSettingsStore().currentPipelineSettings.hsvHue);
@@ -66,19 +69,22 @@ const handleStreamClick = (event: MouseEvent) => {
[currentHue[1], currentSaturation[1], currentValue[1]]
];
if(selectedEventMode === 2) {
if (selectedEventMode === 2) {
selectedHSVData = colorPicker.expandColorRange(currentData);
} else if(selectedEventMode === 3) {
} else if (selectedEventMode === 3) {
selectedHSVData = colorPicker.shrinkColorRange(currentData);
}
}
// Update the store and backend with the new HSV values
useCameraSettingsStore().changeCurrentPipelineSetting({
hsvHue: [selectedHSVData[0][0], selectedHSVData[1][0]],
hsvSaturation: [selectedHSVData[0][1], selectedHSVData[1][1]],
hsvValue: [selectedHSVData[0][2], selectedHSVData[1][2]]
}, true);
useCameraSettingsStore().changeCurrentPipelineSetting(
{
hsvHue: [selectedHSVData[0][0], selectedHSVData[1][0]],
hsvSaturation: [selectedHSVData[0][1], selectedHSVData[1][1]],
hsvValue: [selectedHSVData[0][2], selectedHSVData[1][2]]
},
true
);
disableColorPicking();
};
@@ -90,36 +96,45 @@ const enableColorPicking = (mode: 1 | 2 | 3) => {
useStateStore().colorPickingMode = true;
inputShowing = useCameraSettingsStore().currentPipelineSettings.inputShouldShow;
outputShowing = useCameraSettingsStore().currentPipelineSettings.outputShouldShow;
useCameraSettingsStore().changeCurrentPipelineSetting({ outputShouldDraw: false, inputShouldShow: true, outputShouldShow: false }, true);
useCameraSettingsStore().changeCurrentPipelineSetting(
{ outputShouldDraw: false, inputShouldShow: true, outputShouldShow: false },
true
);
selectedEventMode = mode;
};
const disableColorPicking = () => {
useStateStore().colorPickingMode = false;
useCameraSettingsStore().changeCurrentPipelineSetting({ outputShouldDraw: true, inputShouldShow: inputShowing, outputShouldShow: outputShowing }, true);
useCameraSettingsStore().changeCurrentPipelineSetting(
{ outputShouldDraw: true, inputShouldShow: inputShowing, outputShouldShow: outputShowing },
true
);
selectedEventMode = 0;
};
onMounted(() => {
const cameraStream = document.getElementById("input-camera-stream");
if(cameraStream === null) return;
if (cameraStream === null) return;
cameraStream.addEventListener("click", handleStreamClick);
});
onBeforeUnmount(() => {
const cameraStream = document.getElementById("input-camera-stream");
if(cameraStream === null) return;
if (cameraStream === null) return;
cameraStream.removeEventListener("click", handleStreamClick);
});
const interactiveCols = computed(() => (getCurrentInstance()?.proxy.$vuetify.breakpoint.mdAndDown || false) && (!useStateStore().sidebarFolded || useCameraSettingsStore().isDriverMode)) ? 9 : 8;
const interactiveCols = computed(
() =>
(getCurrentInstance()?.proxy.$vuetify.breakpoint.mdAndDown || false) &&
(!useStateStore().sidebarFolded || useCameraSettingsStore().isDriverMode)
)
? 9
: 8;
</script>
<template>
<div
class="threshold-modifiers"
:style="{'--averageHue': averageHue}"
>
<div class="threshold-modifiers" :style="{ '--averageHue': averageHue }">
<cv-range-slider
id="hue-slider"
v-model="hsvHue"
@@ -130,7 +145,7 @@ const interactiveCols = computed(() => (getCurrentInstance()?.proxy.$vuetify.bre
:max="180"
:slider-cols="interactiveCols"
:inverted="useCameraSettingsStore().currentPipelineSettings.hueInverted"
@input="value => useCameraSettingsStore().changeCurrentPipelineSetting({hsvHue: value}, false)"
@input="(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ hsvHue: value }, false)"
/>
<cv-range-slider
id="sat-slider"
@@ -141,7 +156,7 @@ const interactiveCols = computed(() => (getCurrentInstance()?.proxy.$vuetify.bre
:min="0"
:max="255"
:slider-cols="interactiveCols"
@input="value => useCameraSettingsStore().changeCurrentPipelineSetting({hsvSaturation: value}, false)"
@input="(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ hsvSaturation: value }, false)"
/>
<cv-range-slider
id="value-slider"
@@ -152,26 +167,19 @@ const interactiveCols = computed(() => (getCurrentInstance()?.proxy.$vuetify.bre
:min="0"
:max="255"
:slider-cols="interactiveCols"
@input="value => useCameraSettingsStore().changeCurrentPipelineSetting({hsvValue: value}, false)"
@input="(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ hsvValue: value }, false)"
/>
<cv-switch
v-model="useCameraSettingsStore().currentPipelineSettings.hueInverted"
label="Invert Hue"
:switch-cols="interactiveCols"
tooltip="Selects the hue range outside of the hue slider bounds instead of inside"
@input="value => useCameraSettingsStore().changeCurrentPipelineSetting({hueInverted: value}, false)"
/>
<v-divider
class="mt-3"
@input="(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ hueInverted: value }, false)"
/>
<v-divider class="mt-3" />
<div>
<div class="pt-3 white--text">
Color Picker
</div>
<v-row
justify="center"
class="mt-3 mb-3"
>
<div class="pt-3 white--text">Color Picker</div>
<v-row justify="center" class="mt-3 mb-3">
<template v-if="!useStateStore().colorPickingMode">
<v-btn
color="accent"
@@ -179,42 +187,25 @@ const interactiveCols = computed(() => (getCurrentInstance()?.proxy.$vuetify.bre
small
@click="enableColorPicking(useCameraSettingsStore().currentPipelineSettings.hueInverted ? 2 : 3)"
>
<v-icon left>
mdi-minus
</v-icon>
<v-icon left> mdi-minus </v-icon>
Shrink Range
</v-btn>
<v-btn
color="accent"
class="ma-2 black--text"
small
@click="enableColorPicking(1)"
>
<v-icon left>
mdi-plus-minus
</v-icon>
<v-btn color="accent" class="ma-2 black--text" small @click="enableColorPicking(1)">
<v-icon left> mdi-plus-minus </v-icon>
{{ useCameraSettingsStore().currentPipelineSettings.hueInverted ? "Exclude" : "Set to" }} Average
</v-btn>
<v-btn
color="accent"
class="ma-2 black--text"
small
@click="enableColorPicking(useCameraSettingsStore().currentPipelineSettings.hueInverted ? 3: 2)"
@click="enableColorPicking(useCameraSettingsStore().currentPipelineSettings.hueInverted ? 3 : 2)"
>
<v-icon left>
mdi-plus
</v-icon>
<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="disableColorPicking"
>
<v-btn color="accent" class="ma-2 black--text" style="width: 30%" small @click="disableColorPicking">
Cancel
</v-btn>
</template>
@@ -228,18 +219,21 @@ const interactiveCols = computed(() => (getCurrentInstance()?.proxy.$vuetify.bre
--averageHue: 0;
}
#hue-slider >>> .v-slider {
background: linear-gradient( to right, #f00 0%, #ff0 17%, #0f0 33%, #0ff 50%, #00f 67%, #f0f 83%, #f00 100% );
background: linear-gradient(to right, #f00 0%, #ff0 17%, #0f0 33%, #0ff 50%, #00f 67%, #f0f 83%, #f00 100%);
border-radius: 10px;
/* prettier-ignore */
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% );
background: linear-gradient(to right, #fff 0%, hsl(var(--averageHue), 100%, 50%) 100%);
border-radius: 10px;
/* prettier-ignore */
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% );
background: linear-gradient(to right, #000 0%, hsl(var(--averageHue), 100%, 50%) 100%);
border-radius: 10px;
/* prettier-ignore */
box-shadow: 0 0 5px #333, inset 0 0 3px #333;
}
>>> .v-slider__thumb {

View File

@@ -5,54 +5,56 @@ import CvSelect from "@/components/common/cv-select.vue";
import axios from "axios";
const restartProgram = () => {
axios.post("/utils/restartProgram")
.then(() => {
useStateStore().showSnackbarMessage({
message: "Successfully sent program restart request",
color: "success"
});
})
.catch(error => {
// This endpoint always return 204 regardless of outcome
if (error.request) {
useStateStore().showSnackbarMessage({
message: "Error while trying to process the request! The backend didn't respond.",
color: "error"
});
} else {
useStateStore().showSnackbarMessage({
message: "An error occurred while trying to process the request.",
color: "error"
});
}
axios
.post("/utils/restartProgram")
.then(() => {
useStateStore().showSnackbarMessage({
message: "Successfully sent program restart request",
color: "success"
});
})
.catch((error) => {
// This endpoint always return 204 regardless of outcome
if (error.request) {
useStateStore().showSnackbarMessage({
message: "Error while trying to process the request! The backend didn't respond.",
color: "error"
});
} else {
useStateStore().showSnackbarMessage({
message: "An error occurred while trying to process the request.",
color: "error"
});
}
});
};
const restartDevice = () => {
axios.post("/utils/restartDevice")
.then(() => {
useStateStore().showSnackbarMessage({
message: "Successfully dispatched the restart command. It isn't confirmed if a device restart will occur.",
color: "success"
});
})
.catch(error => {
if (error.response) {
useStateStore().showSnackbarMessage({
message: "The backend is unable to fulfil the request to restart the device.",
color: "error"
});
} else if (error.request) {
useStateStore().showSnackbarMessage({
message: "Error while trying to process the request! The backend didn't respond.",
color: "error"
});
} else {
useStateStore().showSnackbarMessage({
message: "An error occurred while trying to process the request.",
color: "error"
});
}
axios
.post("/utils/restartDevice")
.then(() => {
useStateStore().showSnackbarMessage({
message: "Successfully dispatched the restart command. It isn't confirmed if a device restart will occur.",
color: "success"
});
})
.catch((error) => {
if (error.response) {
useStateStore().showSnackbarMessage({
message: "The backend is unable to fulfil the request to restart the device.",
color: "error"
});
} else if (error.request) {
useStateStore().showSnackbarMessage({
message: "Error while trying to process the request! The backend didn't respond.",
color: "error"
});
} else {
useStateStore().showSnackbarMessage({
message: "An error occurred while trying to process the request.",
color: "error"
});
}
});
};
const address = inject<string>("backendHost");
@@ -61,55 +63,60 @@ const offlineUpdate = ref();
const openOfflineUpdatePrompt = () => {
offlineUpdate.value.click();
};
const handleOfflineUpdate = ({ files } : { files: FileList}) => {
useStateStore().showSnackbarMessage({ message: "New Software Upload in Progress...", color: "secondary", timeout: -1 });
const handleOfflineUpdate = ({ files }: { files: FileList }) => {
useStateStore().showSnackbarMessage({
message: "New Software Upload in Progress...",
color: "secondary",
timeout: -1
});
const formData = new FormData();
formData.append("jarData", files[0]);
axios.post("/utils/offlineUpdate", formData, {
headers: { "Content-Type": "multipart/form-data" },
onUploadProgress: ({ progress }) => {
const uploadPercentage = ((progress || 0) * 100.0);
if (uploadPercentage < 99.5) {
useStateStore().showSnackbarMessage({
message: "New Software Upload in Process, " + uploadPercentage.toFixed(2) + "% complete",
color: "secondary",
timeout: -1
});
} else {
useStateStore().showSnackbarMessage({
message: "Installing uploaded software...",
color: "secondary",
timeout: -1
});
}
}
})
.then(response => {
useStateStore().showSnackbarMessage({
message: response.data.text || response.data,
color: "success"
});
})
.catch(error => {
if (error.response) {
axios
.post("/utils/offlineUpdate", formData, {
headers: { "Content-Type": "multipart/form-data" },
onUploadProgress: ({ progress }) => {
const uploadPercentage = (progress || 0) * 100.0;
if (uploadPercentage < 99.5) {
useStateStore().showSnackbarMessage({
color: "error",
message: error.response.data.text || error.response.data
});
} else if (error.request) {
useStateStore().showSnackbarMessage({
color: "error",
message: "Error while trying to process the request! The backend didn't respond."
message: "New Software Upload in Process, " + uploadPercentage.toFixed(2) + "% complete",
color: "secondary",
timeout: -1
});
} else {
useStateStore().showSnackbarMessage({
color: "error",
message: "An error occurred while trying to process the request."
message: "Installing uploaded software...",
color: "secondary",
timeout: -1
});
}
}
})
.then((response) => {
useStateStore().showSnackbarMessage({
message: response.data.text || response.data,
color: "success"
});
})
.catch((error) => {
if (error.response) {
useStateStore().showSnackbarMessage({
color: "error",
message: error.response.data.text || error.response.data
});
} else if (error.request) {
useStateStore().showSnackbarMessage({
color: "error",
message: "Error while trying to process the request! The backend didn't respond."
});
} else {
useStateStore().showSnackbarMessage({
color: "error",
message: "An error occurred while trying to process the request."
});
}
});
};
const exportLogFile = ref();
@@ -154,33 +161,34 @@ const handleSettingsImport = () => {
break;
}
axios.post(`/settings${settingsEndpoint}`, formData, {
headers: { "Content-Type": "multipart/form-data" }
})
.then(response => {
useStateStore().showSnackbarMessage({
message: response.data.text || response.data,
color: "success"
});
})
.catch(error => {
if (error.response) {
useStateStore().showSnackbarMessage({
color: "error",
message: error.response.data.text || error.response.data
});
} else if (error.request) {
useStateStore().showSnackbarMessage({
color: "error",
message: "Error while trying to process the request! The backend didn't respond."
});
} else {
useStateStore().showSnackbarMessage({
color: "error",
message: "An error occurred while trying to process the request."
});
}
axios
.post(`/settings${settingsEndpoint}`, formData, {
headers: { "Content-Type": "multipart/form-data" }
})
.then((response) => {
useStateStore().showSnackbarMessage({
message: response.data.text || response.data,
color: "success"
});
})
.catch((error) => {
if (error.response) {
useStateStore().showSnackbarMessage({
color: "error",
message: error.response.data.text || error.response.data
});
} else if (error.request) {
useStateStore().showSnackbarMessage({
color: "error",
message: "Error while trying to process the request! The backend didn't respond."
});
} else {
useStateStore().showSnackbarMessage({
color: "error",
message: "An error occurred while trying to process the request."
});
}
});
showImportDialog.value = false;
importType.value = -1;
@@ -189,99 +197,52 @@ const handleSettingsImport = () => {
</script>
<template>
<v-card
dark
class="mb-3 pr-6 pb-3"
style="background-color: #006492;"
>
<v-card dark class="mb-3 pr-6 pb-3" style="background-color: #006492">
<v-card-title>Device Control</v-card-title>
<div class="ml-5">
<v-row>
<v-col
cols="12"
lg="4"
md="6"
>
<v-btn
color="red"
@click="restartProgram"
>
<v-icon left>
mdi-restart
</v-icon>
<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>
<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="openOfflineUpdatePrompt"
>
<v-icon left>
mdi-upload
</v-icon>
<v-col cols="12" lg="4">
<v-btn color="secondary" @click="openOfflineUpdatePrompt">
<v-icon left> mdi-upload </v-icon>
Offline Update
</v-btn>
<input
ref="offlineUpdate"
type="file"
accept=".jar"
style="display: none;"
@change="handleOfflineUpdate"
>
<input ref="offlineUpdate" type="file" accept=".jar" style="display: none" @change="handleOfflineUpdate" />
</v-col>
</v-row>
<v-divider style="margin: 12px 0;" />
<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>
<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-dialog
v-model="showImportDialog"
width="600"
@input="() => {
importType = -1;
importFile = null;
}"
@input="
() => {
importType = -1;
importFile = null;
}
"
>
<v-card
color="primary"
dark
>
<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-4"
>
<v-row class="mt-6 ml-4">
<cv-select
v-model="importType"
label="Type"
@@ -290,14 +251,12 @@ const handleSettingsImport = () => {
:select-cols="10"
/>
</v-row>
<v-row
class="mt-6 ml-4 mr-8"
>
<v-row class="mt-6 ml-4 mr-8">
<v-file-input
:disabled="importType === -1"
:error-messages="importType === -1 ? 'Settings type not selected' : ''"
:accept="importType === ImportType.AllSettings ? '.zip' : '.json'"
@change="(file) => importFile = file"
@change="(file) => (importFile = file)"
/>
</v-row>
<v-row
@@ -305,14 +264,8 @@ const handleSettingsImport = () => {
style="display: flex; align-items: center; justify-content: center"
align="center"
>
<v-btn
color="secondary"
:disabled="importFile === null"
@click="handleSettingsImport"
>
<v-icon left>
mdi-import
</v-icon>
<v-btn color="secondary" :disabled="importFile === null" @click="handleSettingsImport">
<v-icon left> mdi-import </v-icon>
Import Settings
</v-btn>
</v-row>
@@ -320,17 +273,9 @@ const handleSettingsImport = () => {
</v-card>
</v-dialog>
</v-col>
<v-col
cols="12"
sm="6"
>
<v-btn
color="secondary"
@click="openExportSettingsPrompt"
>
<v-icon left>
mdi-export
</v-icon>
<v-col cols="12" sm="6">
<v-btn color="secondary" @click="openExportSettingsPrompt">
<v-icon left> mdi-export </v-icon>
Export Settings
</v-btn>
<a
@@ -341,17 +286,9 @@ const handleSettingsImport = () => {
target="_blank"
/>
</v-col>
<v-col
cols="12"
sm="6"
>
<v-btn
color="secondary"
@click="openExportLogsPrompt"
>
<v-icon left>
mdi-download
</v-icon>
<v-col cols="12" sm="6">
<v-btn color="secondary" @click="openExportLogsPrompt">
<v-icon left> mdi-download </v-icon>
Download Current Log
<!-- Special hidden link that gets 'clicked' when the user exports journalctl logs -->
@@ -364,17 +301,9 @@ const handleSettingsImport = () => {
/>
</v-btn>
</v-col>
<v-col
cols="12"
sm="6"
>
<v-btn
color="secondary"
@click="useStateStore().showLogModal = true"
>
<v-icon left>
mdi-eye
</v-icon>
<v-col cols="12" sm="6">
<v-btn color="secondary" @click="useStateStore().showLogModal = true">
<v-icon left> mdi-eye </v-icon>
Show log viewer
</v-btn>
</v-col>

View File

@@ -4,11 +4,7 @@ import { useSettingsStore } from "@/stores/settings/GeneralSettingsStore";
</script>
<template>
<v-card
dark
class="mb-3 pr-6 pb-3"
style="background-color: #006492;"
>
<v-card dark class="mb-3 pr-6 pb-3" style="background-color: #006492">
<v-card-title>LED Control</v-card-title>
<div class="ml-5">
<cv-slider
@@ -18,7 +14,7 @@ import { useSettingsStore } from "@/stores/settings/GeneralSettingsStore";
:slider-cols="12"
:min="0"
:max="100"
@input="args => useSettingsStore().changeLEDBrightness(args)"
@input="(args) => useSettingsStore().changeLEDBrightness(args)"
/>
</div>
</v-card>

View File

@@ -5,27 +5,28 @@ import { useStateStore } from "@/stores/StateStore";
import CvIcon from "@/components/common/cv-icon.vue";
interface MetricItem {
header: string,
value?: string
header: string;
value?: string;
}
const generalMetrics = computed<MetricItem[]>(() => [
{
header: "Version",
value: useSettingsStore().general.version || "Unknown"
},
{
header: "Hardware Model",
value: useSettingsStore().general.hardwareModel || "Unknown"
},
{
header: "Platform",
value: useSettingsStore().general.hardwarePlatform || "Unknown"
},
{
header: "GPU Acceleration",
value: useSettingsStore().general.gpuAcceleration || "Unknown"
}]);
{
header: "Version",
value: useSettingsStore().general.version || "Unknown"
},
{
header: "Hardware Model",
value: useSettingsStore().general.hardwareModel || "Unknown"
},
{
header: "Platform",
value: useSettingsStore().general.hardwarePlatform || "Unknown"
},
{
header: "GPU Acceleration",
value: useSettingsStore().general.gpuAcceleration || "Unknown"
}
]);
const platformMetrics = computed<MetricItem[]>(() => [
{
header: "CPU Temp",
@@ -33,15 +34,21 @@ const platformMetrics = computed<MetricItem[]>(() => [
},
{
header: "CPU Usage",
value: useSettingsStore().metrics.cpuUtil === undefined ? "Unknown" :`${useSettingsStore().metrics.cpuUtil}%`
value: useSettingsStore().metrics.cpuUtil === undefined ? "Unknown" : `${useSettingsStore().metrics.cpuUtil}%`
},
{
header: "CPU Memory Usage",
value: useSettingsStore().metrics.ramUtil === undefined || useSettingsStore().metrics.cpuMem === undefined ? "Unknown" : `${useSettingsStore().metrics.ramUtil || "Unknown"}MB of ${useSettingsStore().metrics.cpuMem}MB`
value:
useSettingsStore().metrics.ramUtil === undefined || useSettingsStore().metrics.cpuMem === undefined
? "Unknown"
: `${useSettingsStore().metrics.ramUtil || "Unknown"}MB of ${useSettingsStore().metrics.cpuMem}MB`
},
{
header: "GPU Memory Usage",
value: useSettingsStore().metrics.gpuMemUtil === undefined || useSettingsStore().metrics.gpuMem === undefined ? "Unknown" : `${useSettingsStore().metrics.gpuMemUtil}MB of ${useSettingsStore().metrics.gpuMem}MB`
value:
useSettingsStore().metrics.gpuMemUtil === undefined || useSettingsStore().metrics.gpuMem === undefined
? "Unknown"
: `${useSettingsStore().metrics.gpuMemUtil}MB of ${useSettingsStore().metrics.gpuMem}MB`
},
{
header: "CPU Throttling",
@@ -60,28 +67,28 @@ const platformMetrics = computed<MetricItem[]>(() => [
const metricsLastFetched = ref("Never");
const fetchMetrics = () => {
useSettingsStore()
.requestMetricsUpdate()
.catch(error => {
if(error.request) {
useStateStore().showSnackbarMessage({
color: "error",
message: "Unable to fetch Metrics! The backend didn't respond."
});
} else {
useStateStore().showSnackbarMessage({
color: "error",
message: "An error occurred while trying to fetch Metrics."
});
}
})
.finally(() => {
const pad = (num: number): string => {
return String(num).padStart(2, "0");
};
.requestMetricsUpdate()
.catch((error) => {
if (error.request) {
useStateStore().showSnackbarMessage({
color: "error",
message: "Unable to fetch Metrics! The backend didn't respond."
});
} else {
useStateStore().showSnackbarMessage({
color: "error",
message: "An error occurred while trying to fetch Metrics."
});
}
})
.finally(() => {
const pad = (num: number): string => {
return String(num).padStart(2, "0");
};
const date = new Date();
metricsLastFetched.value = `${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}`;
});
const date = new Date();
metricsLastFetched.value = `${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}`;
});
};
onBeforeMount(() => {
@@ -90,47 +97,24 @@ onBeforeMount(() => {
</script>
<template>
<v-card
dark
class="mb-3 pr-6 pb-3"
style="background-color: #006492;"
>
<v-card dark class="mb-3 pr-6 pb-3" style="background-color: #006492">
<v-card-title style="display: flex; justify-content: space-between">
<span>Stats</span>
<cv-icon
icon-name="mdi-reload"
color="white"
tooltip="Reload Metrics"
hover
@click="fetchMetrics"
/>
<cv-icon icon-name="mdi-reload" color="white" tooltip="Reload Metrics" hover @click="fetchMetrics" />
</v-card-title>
<v-row class="pt-2 pa-4 ma-0 ml-5 pb-1">
<v-card-subtitle
class="ma-0 pa-0 pb-2"
style="font-size: 16px"
>
General Metrics
</v-card-subtitle>
<v-card-subtitle class="ma-0 pa-0 pb-2" style="font-size: 16px"> General Metrics </v-card-subtitle>
<v-simple-table class="metrics-table">
<thead>
<tr>
<th
v-for="(item, itemIndex) in generalMetrics"
:key="itemIndex"
class="metric-item metric-item-title"
>
<th v-for="(item, itemIndex) in generalMetrics" :key="itemIndex" class="metric-item metric-item-title">
{{ item.header }}
</th>
</tr>
</thead>
<tbody>
<tr>
<td
v-for="(item, itemIndex) in generalMetrics"
:key="itemIndex"
class="metric-item"
>
<td v-for="(item, itemIndex) in generalMetrics" :key="itemIndex" class="metric-item">
{{ item.value }}
</td>
</tr>
@@ -138,31 +122,18 @@ onBeforeMount(() => {
</v-simple-table>
</v-row>
<v-row class="pa-4 ma-0 ml-5">
<v-card-subtitle
class="ma-0 pa-0 pb-2"
style="font-size: 16px"
>
Hardware Metrics
</v-card-subtitle>
<v-card-subtitle class="ma-0 pa-0 pb-2" style="font-size: 16px"> Hardware Metrics </v-card-subtitle>
<v-simple-table class="metrics-table">
<thead>
<tr>
<th
v-for="(item, itemIndex) in platformMetrics"
:key="itemIndex"
class="metric-item metric-item-title"
>
<th v-for="(item, itemIndex) in platformMetrics" :key="itemIndex" class="metric-item metric-item-title">
{{ item.header }}
</th>
</tr>
</thead>
<tbody>
<tr>
<td
v-for="(item, itemIndex) in platformMetrics"
:key="itemIndex"
class="metric-item"
>
<td v-for="(item, itemIndex) in platformMetrics" :key="itemIndex" class="metric-item">
<span v-if="useSettingsStore().metrics.cpuUtil !== undefined">{{ item.value }}</span>
<span v-else>---</span>
</td>
@@ -177,7 +148,7 @@ onBeforeMount(() => {
</template>
<style scoped lang="scss">
.metrics-table{
.metrics-table {
border-collapse: separate;
border-spacing: 0;
border-radius: 5px;
@@ -203,7 +174,8 @@ onBeforeMount(() => {
}
.v-data-table {
thead, tbody {
thead,
tbody {
background-color: #006492;
}

View File

@@ -14,7 +14,7 @@ const isValidNetworkTablesIP = (v: string | undefined): boolean => {
// Check if it is a team number longer than 5 digits
const badTeamNumberRegex = /^[0-9]{5,}$/;
if(v === undefined) return false;
if (v === undefined) return false;
if (teamNumberRegex.test(v)) return true;
if (isValidIPv4(v)) return true;
// need to check these before the hostname. "0" and "99999" are valid hostnames, but we don't want to allow then
@@ -26,77 +26,80 @@ const isValidIPv4 = (v: string | undefined) => {
// 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])$/;
if(v === undefined) return false;
if (v === undefined) return false;
return ipv4Regex.test(v);
};
const isValidHostname = (v: string | undefined) => {
// https://stackoverflow.com/a/18494710
const hostnameRegex = /^([a-zA-Z0-9]+(-[a-zA-Z0-9]+)*)+(\.([a-zA-Z0-9]+(-[a-zA-Z0-9]+)*))*$/;
if(v === undefined) return false;
if (v === undefined) return false;
return hostnameRegex.test(v);
};
const saveGeneralSettings = () => {
const changingStaticIp = useSettingsStore().network.connectionType === NetworkConnectionType.Static;
useSettingsStore().saveGeneralSettings()
.then(response => {
useStateStore().showSnackbarMessage({
message: response.data.text || response.data,
color: "success"
});
})
.catch(error => {
if(error.response) {
if (error.status === 504 || changingStaticIp) {
useStateStore().showSnackbarMessage({
color: "error",
message: `Connection lost! Try the new static IP at ${useSettingsStore().network.staticIp}:5800 or ${useSettingsStore().network.hostname}:5800?`
});
} else {
useStateStore().showSnackbarMessage({
color: "error",
message: error.response.data.text || error.response.data
});
}
} else if(error.request) {
useSettingsStore()
.saveGeneralSettings()
.then((response) => {
useStateStore().showSnackbarMessage({
message: response.data.text || response.data,
color: "success"
});
})
.catch((error) => {
if (error.response) {
if (error.status === 504 || changingStaticIp) {
useStateStore().showSnackbarMessage({
color: "error",
message: "Error while trying to process the request! The backend didn't respond."
message: `Connection lost! Try the new static IP at ${useSettingsStore().network.staticIp}:5800 or ${
useSettingsStore().network.hostname
}:5800?`
});
} else {
useStateStore().showSnackbarMessage({
color: "error",
message: "An error occurred while trying to process the request."
message: error.response.data.text || error.response.data
});
}
});
} else if (error.request) {
useStateStore().showSnackbarMessage({
color: "error",
message: "Error while trying to process the request! The backend didn't respond."
});
} else {
useStateStore().showSnackbarMessage({
color: "error",
message: "An error occurred while trying to process the request."
});
}
});
};
</script>
<template>
<v-card
dark
class="mb-3 pr-6 pb-3"
style="background-color: #006492;"
>
<v-card dark class="mb-3 pr-6 pb-3" style="background-color: #006492">
<v-card-title>Networking</v-card-title>
<div class="ml-5">
<v-form
ref="form"
v-model="settingsValid"
>
<v-form ref="form" v-model="settingsValid">
<cv-input
v-model="useSettingsStore().network.ntServerAddress"
label="Team Number/NetworkTables Server Address"
tooltip="Enter the Team Number or the IP address of the NetworkTables Server"
:label-cols="3"
:disabled="useSettingsStore().network.runNTServer"
:rules="[v => isValidNetworkTablesIP(v) || 'The NetworkTables Server Address must be a valid Team Number, IP address, or Hostname']"
:rules="[
(v) =>
isValidNetworkTablesIP(v) ||
'The NetworkTables Server Address must be a valid Team Number, IP address, or Hostname'
]"
/>
<v-banner
v-show="!isValidNetworkTablesIP(useSettingsStore().network.ntServerAddress) && !useSettingsStore().network.runNTServer"
v-show="
!isValidNetworkTablesIP(useSettingsStore().network.ntServerAddress) &&
!useSettingsStore().network.runNTServer
"
rounded
color="red"
text-color="white"
@@ -110,22 +113,22 @@ const saveGeneralSettings = () => {
v-model="useSettingsStore().network.connectionType"
label="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."
:input-cols="12-3"
:list="['DHCP','Static']"
:input-cols="12 - 3"
:list="['DHCP', 'Static']"
/>
<cv-input
v-if="useSettingsStore().network.connectionType === NetworkConnectionType.Static"
v-model="useSettingsStore().network.staticIp"
:input-cols="12-3"
:input-cols="12 - 3"
label="Static IP"
:rules="[v => isValidIPv4(v) || 'Invalid IPv4 address']"
:rules="[(v) => isValidIPv4(v) || 'Invalid IPv4 address']"
/>
<cv-input
v-show="useSettingsStore().network.shouldManage"
v-model="useSettingsStore().network.hostname"
label="Hostname"
:input-cols="12-3"
:rules="[v => isValidHostname(v) || 'Invalid hostname']"
:input-cols="12 - 3"
:rules="[(v) => isValidHostname(v) || 'Invalid hostname']"
/>
<cv-switch
v-model="useSettingsStore().network.runNTServer"
@@ -147,7 +150,7 @@ const saveGeneralSettings = () => {
<v-btn
color="accent"
:class="useSettingsStore().network.runNTServer ? 'mt-3' : ''"
style="color: black; width: 100%;"
style="color: black; width: 100%"
:disabled="!settingsValid && !useSettingsStore().network.runNTServer"
@click="saveGeneralSettings"
>

View File

@@ -21,7 +21,12 @@ export class AutoReconnectingWebsocket {
* @param onData decoded websocket message data consumer. The data is automatically decoded by msgpack.
* @param onDisconnect action to run on websocket disconnection (when the websocket changes to the CLOSED state)
*/
constructor(serverAddress: string | URL, onConnect: () => void, onData: (data: IncomingWebsocketData) => void, onDisconnect: () => void) {
constructor(
serverAddress: string | URL,
onConnect: () => void,
onData: (data: IncomingWebsocketData) => void,
onDisconnect: () => void
) {
this.serverAddress = serverAddress;
this.onConnect = onConnect;
@@ -41,8 +46,8 @@ export class AutoReconnectingWebsocket {
*/
send(data, encodeData = true) {
// Only send data if the websocket is open
if(this.isConnected()) {
if(encodeData) {
if (this.isConnected()) {
if (encodeData) {
this.websocket?.send(encode(data));
} else {
this.websocket?.send(data);

View File

@@ -1,98 +1,101 @@
export type HSV = [number, number, number]
export type RGBA = [number, number, number, number] | Uint8ClampedArray
export type HSV = [number, number, number];
export type RGBA = [number, number, number, number] | Uint8ClampedArray;
export class ColorPicker {
public hsvData: HSV;
public hsvData: HSV;
constructor(pixelData: RGBA) {
this.hsvData = this.RGBtoHSV(pixelData);
constructor(pixelData: RGBA) {
this.hsvData = this.RGBtoHSV(pixelData);
}
public selectedColorRange() {
return this.widenRange([[...this.hsvData], [...this.hsvData]]);
}
public expandColorRange(currentRange: [HSV, HSV]) {
const widenedHSV = this.widenRange([[...this.hsvData], [...this.hsvData]]);
return this.createRange(currentRange.concat(widenedHSV));
}
public shrinkColorRange(currentRange: [HSV, HSV]) {
const widenedHSV = this.widenRange([[...this.hsvData], [...this.hsvData]]);
//Tries to shrink the lower part of to widened HSV
if (!this.shrinkRange(currentRange, widenedHSV[0])) {
//If the prev attempt failed, try to shrink the higher part of to widened HSV
this.shrinkRange(currentRange, widenedHSV[1]);
}
public selectedColorRange() {
return this.widenRange([[...this.hsvData], [...this.hsvData]]);
return currentRange;
}
private createRange(range: HSV[]): [HSV, HSV] {
const newRange: [HSV, HSV] = [
[0, 0, 0],
[0, 0, 0]
];
for (let i = 0; i < 3; i++) {
newRange[0][i] = range[0][i];
newRange[1][i] = range[0][i];
for (let j = range.length - 1; j >= 0; j--) {
newRange[0][i] = Math.min(range[j][i], newRange[0][i]);
newRange[1][i] = Math.max(range[j][i], newRange[1][i]);
}
}
return newRange;
}
private widenRange(range: [HSV, HSV]): [HSV, HSV] {
const expanded: [HSV, HSV] = [
[0, 0, 0],
[0, 0, 0]
];
for (let i = 0; i < 3; i++) {
//Expanding the range by 10
expanded[0][i] = Math.max(0, range[0][i] - 10);
expanded[1][i] = Math.min(255, range[1][i] + 10);
}
expanded[1][0] = Math.min(180, expanded[1][0]); //h is up to 180
return expanded;
}
private shrinkRange(range: [HSV, HSV], color: HSV): boolean {
for (let i = 0; i < color.length; i++) {
if (!(range[0][i] <= color[i] && color[i] <= range[1][i])) return false;
}
public expandColorRange(currentRange: [HSV, HSV]) {
const widenedHSV = this.widenRange([[...this.hsvData], [...this.hsvData]]);
return this.createRange(currentRange.concat(widenedHSV));
for (let i = 0; i < color.length; i++) {
if (color[i] - range[0][i] < range[1][i] - color[i]) {
//shrink from min side
range[0][i] = Math.min(range[0][i] + 10, range[1][i]);
} else {
//shrink from max side
range[1][i] = Math.max(range[1][i] - 10, range[0][i]);
}
}
public shrinkColorRange(currentRange: [HSV, HSV]) {
const widenedHSV = this.widenRange([[...this.hsvData], [...this.hsvData]]);
return true;
}
//Tries to shrink the lower part of to widened HSV
if(!this.shrinkRange(currentRange, widenedHSV[0])) {
//If the prev attempt failed, try to shrink the higher part of to widened HSV
this.shrinkRange(currentRange, widenedHSV[1]);
}
private RGBtoHSV(rgba: RGBA): HSV {
// Normalize RGB ranges
let r = rgba[0],
g = rgba[1],
b = rgba[2];
r = r / 255;
g = g / 255;
b = b / 255;
return currentRange;
}
private createRange(range: HSV[]): [HSV, HSV] {
const newRange: [HSV, HSV] = [[0, 0, 0], [0, 0, 0]];
for (let i = 0; i < 3; i++) {
newRange[0][i] = range[0][i];
newRange[1][i] = range[0][i];
for (let j = range.length - 1; j >= 0; j--) {
newRange[0][i] = Math.min(range[j][i], newRange[0][i]);
newRange[1][i] = Math.max(range[j][i], newRange[1][i]);
}
}
return newRange;
}
private widenRange(range: [HSV, HSV]): [HSV, HSV] {
const expanded: [HSV, HSV] = [[0, 0, 0], [0, 0, 0]];
for (let i = 0; i < 3; i++) {
//Expanding the range by 10
expanded[0][i] = Math.max(0, range[0][i] - 10);
expanded[1][i] = Math.min(255, range[1][i] + 10);
}
expanded[1][0] = Math.min(180, expanded[1][0]); //h is up to 180
return expanded;
}
private shrinkRange(range: [HSV, HSV], color: HSV): boolean {
for (let i = 0; i < color.length; i++) {
if (!(range[0][i] <= color[i] && color[i] <= range[1][i])) return false;
}
for (let i = 0; i < color.length; i++) {
if (color[i] - range[0][i] < range[1][i] - color[i]) {
//shrink from min side
range[0][i] = Math.min(range[0][i] + 10, range[1][i]);
} else {
//shrink from max side
range[1][i] = Math.max(range[1][i] - 10, range[0][i]);
}
}
return true;
}
private RGBtoHSV(rgba: RGBA ): HSV {
// Normalize RGB ranges
let r = rgba[0],
g = rgba[1],
b = rgba[2];
r = r / 255;
g = g / 255;
b = b / 255;
const minRGB = Math.min(r, Math.min(g, b));
const maxRGB = Math.max(r, Math.max(g, b));
const d = (r === minRGB) ? g - b : ((b === minRGB) ? r - g : b - r);
const h = (r === minRGB) ? 3 : ((b === minRGB) ? 1 : 5);
let H = 30 * (h - d / (maxRGB - minRGB));
let S = 255 * (maxRGB - minRGB) / maxRGB;
let V = 255 * maxRGB;
if (isNaN(H))
H = 0;
if (isNaN(S))
S = 0;
if (isNaN(V))
V = 0;
return [Math.round(H), Math.round(S), Math.round(V)];
}
const minRGB = Math.min(r, Math.min(g, b));
const maxRGB = Math.max(r, Math.max(g, b));
const d = r === minRGB ? g - b : b === minRGB ? r - g : b - r;
const h = r === minRGB ? 3 : b === minRGB ? 1 : 5;
let H = 30 * (h - d / (maxRGB - minRGB));
let S = (255 * (maxRGB - minRGB)) / maxRGB;
let V = 255 * maxRGB;
if (isNaN(H)) H = 0;
if (isNaN(S)) S = 0;
if (isNaN(V)) V = 0;
return [Math.round(H), Math.round(S), Math.round(V)];
}
}

View File

@@ -7,39 +7,38 @@ import type { VuetifyThemeVariant } from "vuetify/types/services/theme";
Vue.use(Vuetify);
const darkTheme: VuetifyThemeVariant = Object.freeze({
primary: "#006492",
secondary: "#39A4D5",
accent: "#FFD843",
background: "#232C37",
error: "#FF5252",
info: "#2196F3",
success: "#4CAF50",
warning: "#FFC107"
primary: "#006492",
secondary: "#39A4D5",
accent: "#FFD843",
background: "#232C37",
error: "#FF5252",
info: "#2196F3",
success: "#4CAF50",
warning: "#FFC107"
});
const lightTheme: VuetifyThemeVariant = Object.freeze({
primary: "#006492",
secondary: "#39A4D5",
accent: "#FFD843",
background: "#232C37",
error: "#FF5252",
info: "#2196F3",
success: "#4CAF50",
warning: "#FFC107"
primary: "#006492",
secondary: "#39A4D5",
accent: "#FFD843",
background: "#232C37",
error: "#FF5252",
info: "#2196F3",
success: "#4CAF50",
warning: "#FFC107"
});
export default new Vuetify({
theme: {
themes: {
light: lightTheme,
dark: darkTheme
}
},
breakpoint: {
thresholds: {
md: 1460,
lg: 2000
}
theme: {
themes: {
light: lightTheme,
dark: darkTheme
}
},
breakpoint: {
thresholds: {
md: 1460,
lg: 2000
}
}
});

View File

@@ -3,123 +3,120 @@ import type { LogMessage } from "@/types/SettingTypes";
import type { AutoReconnectingWebsocket } from "@/lib/AutoReconnectingWebsocket";
import type { PipelineResult } from "@/types/PhotonTrackingTypes";
import type {
WebsocketCalibrationData,
WebsocketLogMessage,
WebsocketNTUpdate,
WebsocketPipelineResultUpdate
WebsocketCalibrationData,
WebsocketLogMessage,
WebsocketNTUpdate,
WebsocketPipelineResultUpdate
} from "@/types/WebsocketDataTypes";
export interface NTConnectionStatus {
connected: boolean,
address?: string,
clients?: number
connected: boolean;
address?: string;
clients?: number;
}
interface StateStore {
backendConnected: boolean,
websocket?: AutoReconnectingWebsocket,
ntConnectionStatus: NTConnectionStatus,
showLogModal: boolean,
sidebarFolded: boolean,
logMessages: LogMessage[]
currentCameraIndex: number,
backendConnected: boolean;
websocket?: AutoReconnectingWebsocket;
ntConnectionStatus: NTConnectionStatus;
showLogModal: boolean;
sidebarFolded: boolean;
logMessages: LogMessage[];
currentCameraIndex: number;
pipelineResults?: PipelineResult,
pipelineResults?: PipelineResult;
colorPickingMode: boolean,
colorPickingMode: boolean;
calibrationData: {
imageCount: number,
videoFormatIndex: number,
minimumImageCount: number,
hasEnoughImages: boolean
},
calibrationData: {
imageCount: number;
videoFormatIndex: number;
minimumImageCount: number;
hasEnoughImages: boolean;
};
snackbarData: {
show: boolean,
message: string,
color: string,
timeout: number
}
snackbarData: {
show: boolean;
message: string;
color: string;
timeout: number;
};
}
export const useStateStore = defineStore("state", {
state: (): StateStore => {
return {
backendConnected: false,
websocket: undefined,
ntConnectionStatus: {
connected: false
},
showLogModal: false,
// Ignored if the display is too small
sidebarFolded: localStorage.getItem("sidebarFolded") === null ? false : localStorage.getItem("sidebarFolded") === "true",
logMessages: [],
currentCameraIndex: 0,
state: (): StateStore => {
return {
backendConnected: false,
websocket: undefined,
ntConnectionStatus: {
connected: false
},
showLogModal: false,
// Ignored if the display is too small
sidebarFolded:
localStorage.getItem("sidebarFolded") === null ? false : localStorage.getItem("sidebarFolded") === "true",
logMessages: [],
currentCameraIndex: 0,
pipelineResults: undefined,
pipelineResults: undefined,
colorPickingMode: false,
colorPickingMode: false,
calibrationData: {
imageCount: 0,
videoFormatIndex: 0,
minimumImageCount: 12,
hasEnoughImages: false
},
calibrationData: {
imageCount: 0,
videoFormatIndex: 0,
minimumImageCount: 12,
hasEnoughImages: false
},
snackbarData: {
show: false,
message: "No Message",
color: "info",
timeout: 2000
}
};
snackbarData: {
show: false,
message: "No Message",
color: "info",
timeout: 2000
}
};
},
actions: {
setSidebarFolded(value: boolean) {
this.sidebarFolded = value;
localStorage.setItem("sidebarFolded", Boolean(value).toString());
},
actions: {
setSidebarFolded(value: boolean) {
this.sidebarFolded = value;
localStorage.setItem("sidebarFolded", Boolean(value).toString());
},
addLogFromWebsocket(data: WebsocketLogMessage) {
this.logMessages.push({
level: data.logMessage.logLevel,
message: data.logMessage.logMessage
});
},
updateNTConnectionStatusFromWebsocket(data: WebsocketNTUpdate) {
this.ntConnectionStatus = {
connected: data.connected,
address: data.address,
clients: data.clients
};
},
updatePipelineResultsFromWebsocket(data: WebsocketPipelineResultUpdate) {
for(const cameraIndex in data) {
if(parseInt(cameraIndex) === this.currentCameraIndex) {
this.pipelineResults = data[cameraIndex];
}
}
},
updateCalibrationStateValuesFromWebsocket(data: WebsocketCalibrationData) {
this.calibrationData = {
imageCount: data.count,
videoFormatIndex: data.videoModeIndex,
minimumImageCount: data.minCount,
hasEnoughImages: data.hasEnough
};
},
showSnackbarMessage(data: {
message: string,
color: string,
timeout?: number
}) {
this.snackbarData = {
show: true,
message: data.message,
color: data.color,
timeout: data.timeout || 2000
};
addLogFromWebsocket(data: WebsocketLogMessage) {
this.logMessages.push({
level: data.logMessage.logLevel,
message: data.logMessage.logMessage
});
},
updateNTConnectionStatusFromWebsocket(data: WebsocketNTUpdate) {
this.ntConnectionStatus = {
connected: data.connected,
address: data.address,
clients: data.clients
};
},
updatePipelineResultsFromWebsocket(data: WebsocketPipelineResultUpdate) {
for (const cameraIndex in data) {
if (parseInt(cameraIndex) === this.currentCameraIndex) {
this.pipelineResults = data[cameraIndex];
}
}
},
updateCalibrationStateValuesFromWebsocket(data: WebsocketCalibrationData) {
this.calibrationData = {
imageCount: data.count,
videoFormatIndex: data.videoModeIndex,
minimumImageCount: data.minCount,
hasEnoughImages: data.hasEnough
};
},
showSnackbarMessage(data: { message: string; color: string; timeout?: number }) {
this.snackbarData = {
show: true,
message: data.message,
color: data.color,
timeout: data.timeout || 2000
};
}
}
});

View File

@@ -1,11 +1,11 @@
import { defineStore } from "pinia";
import type {
CalibrationBoardTypes,
CameraCalibrationResult,
CameraSettings,
ConfigurableCameraSettings,
RobotOffsetType,
VideoFormat
CalibrationBoardTypes,
CameraCalibrationResult,
CameraSettings,
ConfigurableCameraSettings,
RobotOffsetType,
VideoFormat
} from "@/types/SettingTypes";
import { PlaceholderCameraSettings } from "@/types/SettingTypes";
import { useStateStore } from "@/stores/StateStore";
@@ -16,334 +16,372 @@ import type { PipelineType } from "@/types/PipelineTypes";
import axios from "axios";
interface CameraSettingsStore {
cameras: CameraSettings[]
cameras: CameraSettings[];
}
export const useCameraSettingsStore = defineStore("cameraSettings", {
state: (): CameraSettingsStore => ({
cameras: [
PlaceholderCameraSettings
]
}),
getters: {
// TODO update types to update this value being undefined. This would be a decently large change.
currentCameraSettings(): CameraSettings {
return this.cameras[useStateStore().currentCameraIndex];
},
currentPipelineSettings(): ActivePipelineSettings {
return this.currentCameraSettings.pipelineSettings;
},
currentPipelineType(): PipelineType {
return this.currentPipelineSettings.pipelineType;
},
// This method only exists due to just how lazy I am and my dislike of consolidating the pipeline type enums (which mind you, suck as is)
currentWebsocketPipelineType(): WebsocketPipelineType {
return this.currentPipelineType - 2;
},
currentVideoFormat(): VideoFormat {
return this.currentCameraSettings.validVideoFormats[this.currentPipelineSettings.cameraVideoModeIndex];
},
isCurrentVideoFormatCalibrated(): boolean {
return this.currentCameraSettings.completeCalibrations.some(v =>
v.resolution.width === this.currentVideoFormat.resolution.width
&& v.resolution.height === this.currentVideoFormat.resolution.height);
},
cameraNames(): string[] {
return this.cameras.map(c => c.nickname);
},
pipelineNames(): string[] {
return this.currentCameraSettings.pipelineNicknames;
},
isDriverMode(): boolean {
return this.currentCameraSettings.currentPipelineIndex === WebsocketPipelineType.DriverMode;
},
isCalibrationMode(): boolean {
return this.currentCameraSettings.currentPipelineIndex == WebsocketPipelineType.Calib3d;
}
state: (): CameraSettingsStore => ({
cameras: [PlaceholderCameraSettings]
}),
getters: {
// TODO update types to update this value being undefined. This would be a decently large change.
currentCameraSettings(): CameraSettings {
return this.cameras[useStateStore().currentCameraIndex];
},
actions: {
updateCameraSettingsFromWebsocket(data: WebsocketCameraSettingsUpdate[]) {
this.cameras = data.map<CameraSettings>((d) => ({
nickname: d.nickname,
fov: {
value: d.fov,
managedByVendor: !d.isFovConfigurable
},
stream: {
inputPort: d.inputStreamPort,
outputPort: d.outputStreamPort
},
validVideoFormats: Object.entries(d.videoFormatList)
.sort(([firstKey], [secondKey]) => parseInt(firstKey) - parseInt(secondKey))
// eslint-disable-next-line @typescript-eslint/no-unused-vars
.map<VideoFormat>(([k, v], i) => ({
resolution: {
width: v.width,
height: v.height
},
fps: v.fps,
pixelFormat: v.pixelFormat,
index: v.index || i,
diagonalFOV: v.diagonalFOV,
horizontalFOV: v.horizontalFOV,
verticalFOV: v.verticalFOV,
standardDeviation: v.standardDeviation,
mean: v.mean
})),
completeCalibrations: d.calibrations.map<CameraCalibrationResult>(calib => ({
resolution: {
height: calib.height,
width: calib.width
},
distCoeffs: calib.distCoeffs,
standardDeviation: calib.standardDeviation,
perViewErrors: calib.perViewErrors,
intrinsics: calib.intrinsics
})),
pipelineNicknames: d.pipelineNicknames,
currentPipelineIndex: d.currentPipelineIndex,
pipelineSettings: d.currentPipelineSettings
}));
},
/**
* Update the configurable camera settings.
*
* @param data camera settings to save.
* @param updateStore whether or not to update the store. This is useful if the input field already models the store reference.
* @param cameraIndex the index of the camera.
*/
updateCameraSettings(data: ConfigurableCameraSettings, updateStore = true, cameraIndex: number = useStateStore().currentCameraIndex) {
// The camera settings endpoint doesn't actually require all data, instead, it needs key data such as the FOV
const payload = {
settings: {
...data
},
index: cameraIndex
};
if(updateStore) {
this.currentCameraSettings.fov.value = data.fov;
}
return axios.post("/settings/camera", payload);
},
/**
* Create a new Pipeline for the provided camera.
*
* @param newPipelineName the name of the new pipeline.
* @param pipelineType the type of the new pipeline. Cannot be {@link WebsocketPipelineType.Calib3d} or {@link WebsocketPipelineType.DriverMode}.
* @param cameraIndex the index of the camera
*/
createNewPipeline(newPipelineName: string, pipelineType: Exclude<WebsocketPipelineType, WebsocketPipelineType.Calib3d | WebsocketPipelineType.DriverMode>, cameraIndex: number = useStateStore().currentCameraIndex) {
const payload = {
addNewPipeline: [newPipelineName, pipelineType],
cameraIndex: cameraIndex
};
useStateStore().websocket?.send(payload, true);
},
/**
* Modify the settings of the currently selected pipeline of the provided camera.
*
* @param settings settings to modify. The type of the settings should match the currently selected pipeline type.
* @param updateStore whether or not to update the store. This is useful if the input field already models the store reference.
* @param cameraIndex the index of the camera
*/
changeCurrentPipelineSetting(settings: ActiveConfigurablePipelineSettings, updateStore = true, cameraIndex: number = useStateStore().currentCameraIndex) {
const payload = {
changePipelineSetting: {
...settings,
cameraIndex: cameraIndex
}
};
if(updateStore) {
this.changePipelineSettingsInStore(settings, cameraIndex);
}
useStateStore().websocket?.send(payload, true);
},
changePipelineSettingsInStore(settings: Partial<ActivePipelineSettings>, cameraIndex: number = useStateStore().currentCameraIndex) {
Object.entries(settings).forEach(([k, v]) => {
this.cameras[cameraIndex].pipelineSettings[k] = v;
});
},
/**
* Change the nickname of the currently selected pipeline of the provided camera.
*
* @param newName the new nickname for the camera.
* @param updateStore whether or not to update the store. This is useful if the input field already models the store reference.
* @param cameraIndex the index of the camera
*/
changeCurrentPipelineNickname(newName: string, updateStore = true, cameraIndex: number = useStateStore().currentCameraIndex) {
const payload = {
changePipelineName: newName,
cameraIndex: cameraIndex
};
if(updateStore) {
this.cameras[cameraIndex].pipelineSettings.pipelineNickname = newName;
}
useStateStore().websocket?.send(payload, true);
},
/**
* Modify the Pipeline type of the currently selected pipeline of the provided camera. This overwrites the current pipeline's settings when the backend resets the current pipeline settings.
*
* @param type the pipeline type to set. Cannot be {@link WebsocketPipelineType.Calib3d} or {@link WebsocketPipelineType.DriverMode}.
* @param cameraIndex the index of the camera.
*/
changeCurrentPipelineType(type: Exclude<WebsocketPipelineType, WebsocketPipelineType.Calib3d | WebsocketPipelineType.DriverMode>, cameraIndex: number = useStateStore().currentCameraIndex) {
const payload = {
pipelineType: type,
cameraIndex: cameraIndex
};
useStateStore().websocket?.send(payload, true);
},
/**
* Change the index of the pipeline of the currently selected camera.
*
* @param index pipeline index to set.
* @param updateStore whether or not to update the store. This is useful if the input field already models the store reference.
* @param cameraIndex the index of the camera.
*/
changeCurrentPipelineIndex(index: number, updateStore = true, cameraIndex: number = useStateStore().currentCameraIndex) {
const payload = {
currentPipeline: index,
cameraIndex: cameraIndex
};
if(updateStore) {
if(this.cameras[cameraIndex].currentPipelineIndex !== -1
&& this.cameras[cameraIndex].currentPipelineIndex !== -2) {
this.cameras[cameraIndex].lastPipelineIndex = this.cameras[cameraIndex].currentPipelineIndex;
}
this.cameras[cameraIndex].currentPipelineIndex = index;
}
useStateStore().websocket?.send(payload, true);
},
/**
* Change the currently selected pipeline of the provided camera.
*
* @param cameraIndex the index of the camera's pipeline to change.
*/
deleteCurrentPipeline(cameraIndex: number = useStateStore().currentCameraIndex) {
const payload = {
deleteCurrentPipeline: {},
cameraIndex: cameraIndex
};
useStateStore().websocket?.send(payload, true);
},
/**
* Duplicate the pipeline at the provided index.
*
* @param pipelineIndex index of the pipeline to duplicate.
* @param cameraIndex the index of the camera.
*/
duplicatePipeline(pipelineIndex: number, cameraIndex: number = useStateStore().currentCameraIndex) {
const payload = {
duplicatePipeline: pipelineIndex,
cameraIndex: cameraIndex
};
useStateStore().websocket?.send(payload, true);
},
/**
* Change the currently set camera
*
* @param cameraIndex the index of the camera to set as the current camera.
* @param updateStore whether or not to update the store. This is useful if the input field already models the store reference.
*/
setCurrentCameraIndex(cameraIndex: number, updateStore = true) {
const payload = {
currentCamera: cameraIndex
};
if(updateStore) {
useStateStore().currentCameraIndex = cameraIndex;
}
useStateStore().websocket?.send(payload, true);
},
/**
* Change the nickname of the provided camera.
*
* @param newName the new nickname of the camera.
* @param updateStore whether or not to update the store. This is useful if the input field already models the store reference.
* @param cameraIndex the index of the camera.
* @return HTTP request promise to the backend
*/
changeCameraNickname(newName: string, updateStore = true, cameraIndex: number = useStateStore().currentCameraIndex) {
const payload = {
name: newName,
cameraIndex: cameraIndex
};
if(updateStore) {
this.currentCameraSettings.nickname = newName;
}
return axios.post("/settings/camera/setNickname", payload);
},
/**
* Start the 3D calibration process for the provided camera.
*
* @param calibrationInitData initialization calibration data.
* @param cameraIndex the index of the camera.
*/
startPnPCalibration(calibrationInitData: {
squareSizeIn: number,
patternWidth: number,
patternHeight: number,
boardType: CalibrationBoardTypes
}, cameraIndex: number = useStateStore().currentCameraIndex) {
const stateCalibData = useStateStore().calibrationData;
const payload = {
startPnpCalibration: {
count: stateCalibData.imageCount,
minCount: stateCalibData.minimumImageCount,
hasEnough: stateCalibData.hasEnoughImages,
videoModeIndex: stateCalibData.videoFormatIndex,
...calibrationInitData
},
cameraIndex: cameraIndex
};
useStateStore().websocket?.send(payload, true);
},
/**
* End the 3D calibration process for the provided camera.
*
* @param cameraIndex the index of the camera
* @return HTTP request promise to the backend
*/
endPnPCalibration(cameraIndex: number = useStateStore().currentCameraIndex) {
return axios.post("/calibration/end", { index: cameraIndex });
},
/**
* Import calibration data that was computed using CalibDB.
*
* @param data Data from the uploaded CalibDB config
* @param cameraIndex the index of the camera
*/
importCalibDB(data: { payload: string, filename: string }, cameraIndex: number = useStateStore().currentCameraIndex) {
const payload = {
...data,
cameraIndex: cameraIndex
};
return axios.post("/calibration/importFromCalibDB", payload, { headers: { "Content-Type": "text/plain" } });
},
/**
* Take a snapshot for the calibration processes
*
* @param takeSnapshot whether or not to take a snapshot. Defaults to true
* @param cameraIndex the index of the camera that is currently in the calibration process
*/
takeCalibrationSnapshot(takeSnapshot = true, cameraIndex: number = useStateStore().currentCameraIndex) {
const payload = {
takeCalibrationSnapshot: takeSnapshot,
cameraIndex: cameraIndex
};
useStateStore().websocket?.send(payload, true);
},
/**
* Set the robot offset mode type.
*
* @param type Offset type to take.
* @param cameraIndex the index of the camera.
*/
takeRobotOffsetPoint(type: RobotOffsetType, cameraIndex: number = useStateStore().currentCameraIndex) {
const payload = {
robotOffsetPoint: type,
cameraIndex: cameraIndex
};
useStateStore().websocket?.send(payload, true);
}
currentPipelineSettings(): ActivePipelineSettings {
return this.currentCameraSettings.pipelineSettings;
},
currentPipelineType(): PipelineType {
return this.currentPipelineSettings.pipelineType;
},
// This method only exists due to just how lazy I am and my dislike of consolidating the pipeline type enums (which mind you, suck as is)
currentWebsocketPipelineType(): WebsocketPipelineType {
return this.currentPipelineType - 2;
},
currentVideoFormat(): VideoFormat {
return this.currentCameraSettings.validVideoFormats[this.currentPipelineSettings.cameraVideoModeIndex];
},
isCurrentVideoFormatCalibrated(): boolean {
return this.currentCameraSettings.completeCalibrations.some(
(v) =>
v.resolution.width === this.currentVideoFormat.resolution.width &&
v.resolution.height === this.currentVideoFormat.resolution.height
);
},
cameraNames(): string[] {
return this.cameras.map((c) => c.nickname);
},
pipelineNames(): string[] {
return this.currentCameraSettings.pipelineNicknames;
},
isDriverMode(): boolean {
return this.currentCameraSettings.currentPipelineIndex === WebsocketPipelineType.DriverMode;
},
isCalibrationMode(): boolean {
return this.currentCameraSettings.currentPipelineIndex == WebsocketPipelineType.Calib3d;
}
},
actions: {
updateCameraSettingsFromWebsocket(data: WebsocketCameraSettingsUpdate[]) {
this.cameras = data.map<CameraSettings>((d) => ({
nickname: d.nickname,
fov: {
value: d.fov,
managedByVendor: !d.isFovConfigurable
},
stream: {
inputPort: d.inputStreamPort,
outputPort: d.outputStreamPort
},
validVideoFormats: Object.entries(d.videoFormatList)
.sort(([firstKey], [secondKey]) => parseInt(firstKey) - parseInt(secondKey))
// eslint-disable-next-line @typescript-eslint/no-unused-vars
.map<VideoFormat>(([k, v], i) => ({
resolution: {
width: v.width,
height: v.height
},
fps: v.fps,
pixelFormat: v.pixelFormat,
index: v.index || i,
diagonalFOV: v.diagonalFOV,
horizontalFOV: v.horizontalFOV,
verticalFOV: v.verticalFOV,
standardDeviation: v.standardDeviation,
mean: v.mean
})),
completeCalibrations: d.calibrations.map<CameraCalibrationResult>((calib) => ({
resolution: {
height: calib.height,
width: calib.width
},
distCoeffs: calib.distCoeffs,
standardDeviation: calib.standardDeviation,
perViewErrors: calib.perViewErrors,
intrinsics: calib.intrinsics
})),
pipelineNicknames: d.pipelineNicknames,
currentPipelineIndex: d.currentPipelineIndex,
pipelineSettings: d.currentPipelineSettings
}));
},
/**
* Update the configurable camera settings.
*
* @param data camera settings to save.
* @param updateStore whether or not to update the store. This is useful if the input field already models the store reference.
* @param cameraIndex the index of the camera.
*/
updateCameraSettings(
data: ConfigurableCameraSettings,
updateStore = true,
cameraIndex: number = useStateStore().currentCameraIndex
) {
// The camera settings endpoint doesn't actually require all data, instead, it needs key data such as the FOV
const payload = {
settings: {
...data
},
index: cameraIndex
};
if (updateStore) {
this.currentCameraSettings.fov.value = data.fov;
}
return axios.post("/settings/camera", payload);
},
/**
* Create a new Pipeline for the provided camera.
*
* @param newPipelineName the name of the new pipeline.
* @param pipelineType the type of the new pipeline. Cannot be {@link WebsocketPipelineType.Calib3d} or {@link WebsocketPipelineType.DriverMode}.
* @param cameraIndex the index of the camera
*/
createNewPipeline(
newPipelineName: string,
pipelineType: Exclude<WebsocketPipelineType, WebsocketPipelineType.Calib3d | WebsocketPipelineType.DriverMode>,
cameraIndex: number = useStateStore().currentCameraIndex
) {
const payload = {
addNewPipeline: [newPipelineName, pipelineType],
cameraIndex: cameraIndex
};
useStateStore().websocket?.send(payload, true);
},
/**
* Modify the settings of the currently selected pipeline of the provided camera.
*
* @param settings settings to modify. The type of the settings should match the currently selected pipeline type.
* @param updateStore whether or not to update the store. This is useful if the input field already models the store reference.
* @param cameraIndex the index of the camera
*/
changeCurrentPipelineSetting(
settings: ActiveConfigurablePipelineSettings,
updateStore = true,
cameraIndex: number = useStateStore().currentCameraIndex
) {
const payload = {
changePipelineSetting: {
...settings,
cameraIndex: cameraIndex
}
};
if (updateStore) {
this.changePipelineSettingsInStore(settings, cameraIndex);
}
useStateStore().websocket?.send(payload, true);
},
changePipelineSettingsInStore(
settings: Partial<ActivePipelineSettings>,
cameraIndex: number = useStateStore().currentCameraIndex
) {
Object.entries(settings).forEach(([k, v]) => {
this.cameras[cameraIndex].pipelineSettings[k] = v;
});
},
/**
* Change the nickname of the currently selected pipeline of the provided camera.
*
* @param newName the new nickname for the camera.
* @param updateStore whether or not to update the store. This is useful if the input field already models the store reference.
* @param cameraIndex the index of the camera
*/
changeCurrentPipelineNickname(
newName: string,
updateStore = true,
cameraIndex: number = useStateStore().currentCameraIndex
) {
const payload = {
changePipelineName: newName,
cameraIndex: cameraIndex
};
if (updateStore) {
this.cameras[cameraIndex].pipelineSettings.pipelineNickname = newName;
}
useStateStore().websocket?.send(payload, true);
},
/**
* Modify the Pipeline type of the currently selected pipeline of the provided camera. This overwrites the current pipeline's settings when the backend resets the current pipeline settings.
*
* @param type the pipeline type to set. Cannot be {@link WebsocketPipelineType.Calib3d} or {@link WebsocketPipelineType.DriverMode}.
* @param cameraIndex the index of the camera.
*/
changeCurrentPipelineType(
type: Exclude<WebsocketPipelineType, WebsocketPipelineType.Calib3d | WebsocketPipelineType.DriverMode>,
cameraIndex: number = useStateStore().currentCameraIndex
) {
const payload = {
pipelineType: type,
cameraIndex: cameraIndex
};
useStateStore().websocket?.send(payload, true);
},
/**
* Change the index of the pipeline of the currently selected camera.
*
* @param index pipeline index to set.
* @param updateStore whether or not to update the store. This is useful if the input field already models the store reference.
* @param cameraIndex the index of the camera.
*/
changeCurrentPipelineIndex(
index: number,
updateStore = true,
cameraIndex: number = useStateStore().currentCameraIndex
) {
const payload = {
currentPipeline: index,
cameraIndex: cameraIndex
};
if (updateStore) {
if (
this.cameras[cameraIndex].currentPipelineIndex !== -1 &&
this.cameras[cameraIndex].currentPipelineIndex !== -2
) {
this.cameras[cameraIndex].lastPipelineIndex = this.cameras[cameraIndex].currentPipelineIndex;
}
this.cameras[cameraIndex].currentPipelineIndex = index;
}
useStateStore().websocket?.send(payload, true);
},
/**
* Change the currently selected pipeline of the provided camera.
*
* @param cameraIndex the index of the camera's pipeline to change.
*/
deleteCurrentPipeline(cameraIndex: number = useStateStore().currentCameraIndex) {
const payload = {
deleteCurrentPipeline: {},
cameraIndex: cameraIndex
};
useStateStore().websocket?.send(payload, true);
},
/**
* Duplicate the pipeline at the provided index.
*
* @param pipelineIndex index of the pipeline to duplicate.
* @param cameraIndex the index of the camera.
*/
duplicatePipeline(pipelineIndex: number, cameraIndex: number = useStateStore().currentCameraIndex) {
const payload = {
duplicatePipeline: pipelineIndex,
cameraIndex: cameraIndex
};
useStateStore().websocket?.send(payload, true);
},
/**
* Change the currently set camera
*
* @param cameraIndex the index of the camera to set as the current camera.
* @param updateStore whether or not to update the store. This is useful if the input field already models the store reference.
*/
setCurrentCameraIndex(cameraIndex: number, updateStore = true) {
const payload = {
currentCamera: cameraIndex
};
if (updateStore) {
useStateStore().currentCameraIndex = cameraIndex;
}
useStateStore().websocket?.send(payload, true);
},
/**
* Change the nickname of the provided camera.
*
* @param newName the new nickname of the camera.
* @param updateStore whether or not to update the store. This is useful if the input field already models the store reference.
* @param cameraIndex the index of the camera.
* @return HTTP request promise to the backend
*/
changeCameraNickname(
newName: string,
updateStore = true,
cameraIndex: number = useStateStore().currentCameraIndex
) {
const payload = {
name: newName,
cameraIndex: cameraIndex
};
if (updateStore) {
this.currentCameraSettings.nickname = newName;
}
return axios.post("/settings/camera/setNickname", payload);
},
/**
* Start the 3D calibration process for the provided camera.
*
* @param calibrationInitData initialization calibration data.
* @param cameraIndex the index of the camera.
*/
startPnPCalibration(
calibrationInitData: {
squareSizeIn: number;
patternWidth: number;
patternHeight: number;
boardType: CalibrationBoardTypes;
},
cameraIndex: number = useStateStore().currentCameraIndex
) {
const stateCalibData = useStateStore().calibrationData;
const payload = {
startPnpCalibration: {
count: stateCalibData.imageCount,
minCount: stateCalibData.minimumImageCount,
hasEnough: stateCalibData.hasEnoughImages,
videoModeIndex: stateCalibData.videoFormatIndex,
...calibrationInitData
},
cameraIndex: cameraIndex
};
useStateStore().websocket?.send(payload, true);
},
/**
* End the 3D calibration process for the provided camera.
*
* @param cameraIndex the index of the camera
* @return HTTP request promise to the backend
*/
endPnPCalibration(cameraIndex: number = useStateStore().currentCameraIndex) {
return axios.post("/calibration/end", { index: cameraIndex });
},
/**
* Import calibration data that was computed using CalibDB.
*
* @param data Data from the uploaded CalibDB config
* @param cameraIndex the index of the camera
*/
importCalibDB(
data: { payload: string; filename: string },
cameraIndex: number = useStateStore().currentCameraIndex
) {
const payload = {
...data,
cameraIndex: cameraIndex
};
return axios.post("/calibration/importFromCalibDB", payload, { headers: { "Content-Type": "text/plain" } });
},
/**
* Take a snapshot for the calibration processes
*
* @param takeSnapshot whether or not to take a snapshot. Defaults to true
* @param cameraIndex the index of the camera that is currently in the calibration process
*/
takeCalibrationSnapshot(takeSnapshot = true, cameraIndex: number = useStateStore().currentCameraIndex) {
const payload = {
takeCalibrationSnapshot: takeSnapshot,
cameraIndex: cameraIndex
};
useStateStore().websocket?.send(payload, true);
},
/**
* Set the robot offset mode type.
*
* @param type Offset type to take.
* @param cameraIndex the index of the camera.
*/
takeRobotOffsetPoint(type: RobotOffsetType, cameraIndex: number = useStateStore().currentCameraIndex) {
const payload = {
robotOffsetPoint: type,
cameraIndex: cameraIndex
};
useStateStore().websocket?.send(payload, true);
}
}
});

View File

@@ -1,111 +1,106 @@
import { defineStore } from "pinia";
import type {
GeneralSettings,
LightingSettings,
MetricData,
NetworkSettings
} from "@/types/SettingTypes";
import type { GeneralSettings, LightingSettings, MetricData, NetworkSettings } from "@/types/SettingTypes";
import { NetworkConnectionType } from "@/types/SettingTypes";
import { useStateStore } from "@/stores/StateStore";
import axios from "axios";
import type { WebsocketSettingsUpdate } from "@/types/WebsocketDataTypes";
interface GeneralSettingsStore {
general: GeneralSettings,
network: NetworkSettings,
lighting: LightingSettings,
metrics: MetricData
general: GeneralSettings;
network: NetworkSettings;
lighting: LightingSettings;
metrics: MetricData;
}
export const useSettingsStore = defineStore("settings", {
state: (): GeneralSettingsStore => ({
general: {
version: undefined,
gpuAcceleration: undefined,
hardwareModel: undefined,
hardwarePlatform: undefined
},
network: {
ntServerAddress: "",
shouldManage: true,
connectionType: NetworkConnectionType.DHCP,
staticIp: "",
hostname: "photonvision",
runNTServer: false
},
lighting: {
supported: true,
brightness: 0
},
metrics: {
cpuTemp: undefined,
cpuUtil: undefined,
cpuMem: undefined,
gpuMem: undefined,
ramUtil: undefined,
gpuMemUtil: undefined,
cpuThr: undefined,
cpuUptime: undefined,
diskUtilPct: undefined
}
}),
getters: {
gpuAccelerationEnabled(): boolean {
return this.general.gpuAcceleration !== undefined;
}
state: (): GeneralSettingsStore => ({
general: {
version: undefined,
gpuAcceleration: undefined,
hardwareModel: undefined,
hardwarePlatform: undefined
},
actions: {
requestMetricsUpdate() {
return axios.post("/utils/publishMetrics");
},
updateMetricsFromWebsocket(data: Required<MetricData>) {
this.metrics = {
cpuTemp: data.cpuTemp || undefined,
cpuUtil: data.cpuUtil || undefined,
cpuMem: data.cpuMem || undefined,
gpuMem: data.gpuMem || undefined,
ramUtil: data.ramUtil || undefined,
gpuMemUtil: data.gpuMemUtil || undefined,
cpuThr: data.cpuThr || undefined,
cpuUptime: data.cpuUptime || undefined,
diskUtilPct: data.diskUtilPct || undefined
};
},
updateGeneralSettingsFromWebsocket(data: WebsocketSettingsUpdate) {
this.general = {
version: data.general.version || undefined,
hardwareModel: data.general.hardwareModel || undefined,
hardwarePlatform: data.general.hardwarePlatform || undefined,
gpuAcceleration: data.general.gpuAcceleration || undefined
};
this.lighting = data.lighting;
this.network = data.networkSettings;
},
saveGeneralSettings() {
const payload: Required<NetworkSettings> = {
connectionType: this.network.connectionType,
hostname: this.network.hostname,
networkManagerIface: this.network.networkManagerIface || "",
ntServerAddress: this.network.ntServerAddress,
physicalInterface: this.network.physicalInterface || "",
runNTServer: this.network.runNTServer,
setDHCPcommand: this.network.setDHCPcommand || "",
setStaticCommand: this.network.setStaticCommand || "",
shouldManage: this.network.shouldManage,
staticIp: this.network.staticIp
};
return axios.post("/settings/general", payload);
},
/**
* Modify the brightness of the LEDs.
*
* @param brightness brightness to set [0, 100]
*/
changeLEDBrightness(brightness: number) {
const payload = {
enabledLEDPercentage: brightness
};
useStateStore().websocket?.send(payload, true);
}
network: {
ntServerAddress: "",
shouldManage: true,
connectionType: NetworkConnectionType.DHCP,
staticIp: "",
hostname: "photonvision",
runNTServer: false
},
lighting: {
supported: true,
brightness: 0
},
metrics: {
cpuTemp: undefined,
cpuUtil: undefined,
cpuMem: undefined,
gpuMem: undefined,
ramUtil: undefined,
gpuMemUtil: undefined,
cpuThr: undefined,
cpuUptime: undefined,
diskUtilPct: undefined
}
}),
getters: {
gpuAccelerationEnabled(): boolean {
return this.general.gpuAcceleration !== undefined;
}
},
actions: {
requestMetricsUpdate() {
return axios.post("/utils/publishMetrics");
},
updateMetricsFromWebsocket(data: Required<MetricData>) {
this.metrics = {
cpuTemp: data.cpuTemp || undefined,
cpuUtil: data.cpuUtil || undefined,
cpuMem: data.cpuMem || undefined,
gpuMem: data.gpuMem || undefined,
ramUtil: data.ramUtil || undefined,
gpuMemUtil: data.gpuMemUtil || undefined,
cpuThr: data.cpuThr || undefined,
cpuUptime: data.cpuUptime || undefined,
diskUtilPct: data.diskUtilPct || undefined
};
},
updateGeneralSettingsFromWebsocket(data: WebsocketSettingsUpdate) {
this.general = {
version: data.general.version || undefined,
hardwareModel: data.general.hardwareModel || undefined,
hardwarePlatform: data.general.hardwarePlatform || undefined,
gpuAcceleration: data.general.gpuAcceleration || undefined
};
this.lighting = data.lighting;
this.network = data.networkSettings;
},
saveGeneralSettings() {
const payload: Required<NetworkSettings> = {
connectionType: this.network.connectionType,
hostname: this.network.hostname,
networkManagerIface: this.network.networkManagerIface || "",
ntServerAddress: this.network.ntServerAddress,
physicalInterface: this.network.physicalInterface || "",
runNTServer: this.network.runNTServer,
setDHCPcommand: this.network.setDHCPcommand || "",
setStaticCommand: this.network.setStaticCommand || "",
shouldManage: this.network.shouldManage,
staticIp: this.network.staticIp
};
return axios.post("/settings/general", payload);
},
/**
* Modify the brightness of the LEDs.
*
* @param brightness brightness to set [0, 100]
*/
changeLEDBrightness(brightness: number) {
const payload = {
enabledLEDPercentage: brightness
};
useStateStore().websocket?.send(payload, true);
}
}
});

View File

@@ -1,29 +1,29 @@
export interface Pose {
x: number,
y: number,
z: number,
angle_z: number,
qw: number,
qx: number,
qy: number,
qz: number
x: number;
y: number;
z: number;
angle_z: number;
qw: number;
qx: number;
qy: number;
qz: number;
}
export interface PhotonTarget {
yaw: number,
pitch: number,
skew: number,
area: number,
// -1 if not set
ambiguity: number,
// -1 if not set
fiducialId: number,
// undefined if 3d isn't enabled
pose?: Pose
yaw: number;
pitch: number;
skew: number;
area: number;
// -1 if not set
ambiguity: number;
// -1 if not set
fiducialId: number;
// undefined if 3d isn't enabled
pose?: Pose;
}
export interface PipelineResult {
fps: number,
latency: number,
targets: PhotonTarget[]
fps: number;
latency: number;
targets: PhotonTarget[];
}

View File

@@ -1,244 +1,277 @@
import type { WebsocketNumberPair } from "@/types/WebsocketDataTypes";
export enum PipelineType {
DriverMode=1,
Reflective=2,
ColoredShape=3,
AprilTag=4,
Aruco=5
DriverMode = 1,
Reflective = 2,
ColoredShape = 3,
AprilTag = 4,
Aruco = 5
}
export enum AprilTagFamily {
Family36h11=0,
Family25h9=1,
Family16h5=2
Family36h11 = 0,
Family25h9 = 1,
Family16h5 = 2
}
export enum RobotOffsetPointMode {
None=0,
Single=1,
Dual=2
None = 0,
Single = 1,
Dual = 2
}
export enum TargetModel {
InfiniteRechargeHighGoalOuter=0,
InfiniteRechargeHighGoalInner=1,
DeepSpaceDualTarget=2,
CircularPowerCell7in=3,
RapidReactCircularCargoBall=4,
StrongholdHighGoal=5,
Apriltag_200mm=6,
Aruco6in_16h5=7,
Apriltag6in_16h5=8
InfiniteRechargeHighGoalOuter = 0,
InfiniteRechargeHighGoalInner = 1,
DeepSpaceDualTarget = 2,
CircularPowerCell7in = 3,
RapidReactCircularCargoBall = 4,
StrongholdHighGoal = 5,
Apriltag_200mm = 6,
Aruco6in_16h5 = 7,
Apriltag6in_16h5 = 8
}
export interface PipelineSettings {
offsetRobotOffsetMode: RobotOffsetPointMode
streamingFrameDivisor: number
offsetDualPointBArea: number
contourGroupingMode: number
hsvValue: WebsocketNumberPair | [number, number]
cameraGain: number
cameraBlueGain: number
cameraRedGain: number
cornerDetectionSideCount: number
contourRatio: WebsocketNumberPair | [number, number]
contourTargetOffsetPointEdge: number
pipelineNickname: string
inputImageRotationMode: number
contourArea: WebsocketNumberPair | [number, number]
solvePNPEnabled: boolean
contourFullness: WebsocketNumberPair | [number, number]
pipelineIndex: number
inputShouldShow: boolean
cameraAutoExposure: boolean
contourSpecklePercentage: number
contourTargetOrientation: number
targetModel: TargetModel
cornerDetectionUseConvexHulls: boolean
outputShouldShow: boolean
outputShouldDraw: boolean
offsetDualPointA: {x: number, y: number}
offsetDualPointB: {x: number, y: number}
hsvHue: WebsocketNumberPair | [number, number]
ledMode: boolean
hueInverted: boolean
outputShowMultipleTargets: boolean
contourSortMode: number
cameraExposure: number
offsetSinglePoint: {x: number, y: number}
cameraBrightness: number
offsetDualPointAArea: number
cornerDetectionExactSideCount: boolean
cameraVideoModeIndex: number
cornerDetectionStrategy: number
cornerDetectionAccuracyPercentage: number
hsvSaturation: WebsocketNumberPair | [number, number]
pipelineType: PipelineType
contourIntersection: number
offsetRobotOffsetMode: RobotOffsetPointMode;
streamingFrameDivisor: number;
offsetDualPointBArea: number;
contourGroupingMode: number;
hsvValue: WebsocketNumberPair | [number, number];
cameraGain: number;
cameraBlueGain: number;
cameraRedGain: number;
cornerDetectionSideCount: number;
contourRatio: WebsocketNumberPair | [number, number];
contourTargetOffsetPointEdge: number;
pipelineNickname: string;
inputImageRotationMode: number;
contourArea: WebsocketNumberPair | [number, number];
solvePNPEnabled: boolean;
contourFullness: WebsocketNumberPair | [number, number];
pipelineIndex: number;
inputShouldShow: boolean;
cameraAutoExposure: boolean;
contourSpecklePercentage: number;
contourTargetOrientation: number;
targetModel: TargetModel;
cornerDetectionUseConvexHulls: boolean;
outputShouldShow: boolean;
outputShouldDraw: boolean;
offsetDualPointA: { x: number; y: number };
offsetDualPointB: { x: number; y: number };
hsvHue: WebsocketNumberPair | [number, number];
ledMode: boolean;
hueInverted: boolean;
outputShowMultipleTargets: boolean;
contourSortMode: number;
cameraExposure: number;
offsetSinglePoint: { x: number; y: number };
cameraBrightness: number;
offsetDualPointAArea: number;
cornerDetectionExactSideCount: boolean;
cameraVideoModeIndex: number;
cornerDetectionStrategy: number;
cornerDetectionAccuracyPercentage: number;
hsvSaturation: WebsocketNumberPair | [number, number];
pipelineType: PipelineType;
contourIntersection: number;
}
export type ConfigurablePipelineSettings = Partial<Omit<PipelineSettings, "offsetDualPointAArea" | "cornerDetectionSideCount" | "pipelineNickname" | "pipelineIndex" | "pipelineType" | "cornerDetectionUseConvexHulls" | "offsetDualPointA" | "offsetDualPointB" | "ledMode" | "offsetSinglePoint" | "offsetDualPointBArea" | "cornerDetectionExactSideCount" | "cornerDetectionStrategy">>
export type ConfigurablePipelineSettings = Partial<
Omit<
PipelineSettings,
| "offsetDualPointAArea"
| "cornerDetectionSideCount"
| "pipelineNickname"
| "pipelineIndex"
| "pipelineType"
| "cornerDetectionUseConvexHulls"
| "offsetDualPointA"
| "offsetDualPointB"
| "ledMode"
| "offsetSinglePoint"
| "offsetDualPointBArea"
| "cornerDetectionExactSideCount"
| "cornerDetectionStrategy"
>
>;
export const DefaultPipelineSettings: PipelineSettings = {
offsetRobotOffsetMode: RobotOffsetPointMode.None,
streamingFrameDivisor: 0,
offsetDualPointBArea: 0,
contourGroupingMode: 0,
hsvValue: { first: 50, second: 255 },
cameraBlueGain: 20,
cameraRedGain: 11,
cornerDetectionSideCount: 4,
contourRatio: { first: 0, second: 20 },
contourTargetOffsetPointEdge: 0,
pipelineNickname: "Placeholder Pipeline",
inputImageRotationMode: 0,
contourArea: { first: 0, second: 100 },
solvePNPEnabled: false,
contourFullness: { first: 0, second: 100 },
pipelineIndex: 0,
inputShouldShow: false,
cameraAutoExposure: false,
contourSpecklePercentage: 5,
contourTargetOrientation: 1,
cornerDetectionUseConvexHulls: true,
outputShouldShow: true,
outputShouldDraw: true,
offsetDualPointA: { x: 0, y: 0 },
offsetDualPointB: { x: 0, y: 0 },
hsvHue: { first: 50, second: 180 },
hueInverted: false,
contourSortMode: 0,
offsetSinglePoint: { x: 0, y: 0 },
cameraBrightness: 50,
offsetDualPointAArea: 0,
cornerDetectionExactSideCount: false,
cameraVideoModeIndex: 0,
cornerDetectionStrategy: 0,
cornerDetectionAccuracyPercentage: 10,
hsvSaturation: { first: 50, second: 255 },
contourIntersection: 1,
offsetRobotOffsetMode: RobotOffsetPointMode.None,
streamingFrameDivisor: 0,
offsetDualPointBArea: 0,
contourGroupingMode: 0,
hsvValue: { first: 50, second: 255 },
cameraBlueGain: 20,
cameraRedGain: 11,
cornerDetectionSideCount: 4,
contourRatio: { first: 0, second: 20 },
contourTargetOffsetPointEdge: 0,
pipelineNickname: "Placeholder Pipeline",
inputImageRotationMode: 0,
contourArea: { first: 0, second: 100 },
solvePNPEnabled: false,
contourFullness: { first: 0, second: 100 },
pipelineIndex: 0,
inputShouldShow: false,
cameraAutoExposure: false,
contourSpecklePercentage: 5,
contourTargetOrientation: 1,
cornerDetectionUseConvexHulls: true,
outputShouldShow: true,
outputShouldDraw: true,
offsetDualPointA: { x: 0, y: 0 },
offsetDualPointB: { x: 0, y: 0 },
hsvHue: { first: 50, second: 180 },
hueInverted: false,
contourSortMode: 0,
offsetSinglePoint: { x: 0, y: 0 },
cameraBrightness: 50,
offsetDualPointAArea: 0,
cornerDetectionExactSideCount: false,
cameraVideoModeIndex: 0,
cornerDetectionStrategy: 0,
cornerDetectionAccuracyPercentage: 10,
hsvSaturation: { first: 50, second: 255 },
contourIntersection: 1,
// These settings will be overridden by different pipeline types
cameraGain: -1,
targetModel: -1,
ledMode: false,
outputShowMultipleTargets: false,
cameraExposure: -1,
pipelineType: -1
// These settings will be overridden by different pipeline types
cameraGain: -1,
targetModel: -1,
ledMode: false,
outputShowMultipleTargets: false,
cameraExposure: -1,
pipelineType: -1
};
export interface ReflectivePipelineSettings extends PipelineSettings {
pipelineType: PipelineType.Reflective
contourFilterRangeY: number
contourFilterRangeX: number
pipelineType: PipelineType.Reflective;
contourFilterRangeY: number;
contourFilterRangeX: number;
}
export type ConfigurableReflectivePipelineSettings = Partial<Omit<ReflectivePipelineSettings, "pipelineType">> & ConfigurablePipelineSettings
export type ConfigurableReflectivePipelineSettings = Partial<Omit<ReflectivePipelineSettings, "pipelineType">> &
ConfigurablePipelineSettings;
export const DefaultReflectivePipelineSettings: ReflectivePipelineSettings = {
...DefaultPipelineSettings,
cameraGain: 20,
targetModel: TargetModel.InfiniteRechargeHighGoalOuter,
ledMode: true,
outputShowMultipleTargets: false,
cameraExposure: 6,
pipelineType: PipelineType.Reflective,
...DefaultPipelineSettings,
cameraGain: 20,
targetModel: TargetModel.InfiniteRechargeHighGoalOuter,
ledMode: true,
outputShowMultipleTargets: false,
cameraExposure: 6,
pipelineType: PipelineType.Reflective,
contourFilterRangeY: 2,
contourFilterRangeX: 2
contourFilterRangeY: 2,
contourFilterRangeX: 2
};
export interface ColoredShapePipelineSettings extends PipelineSettings {
pipelineType: PipelineType.ColoredShape
erode: boolean
cameraCalibration: null
dilate: boolean
circleAccuracy: number
contourRadius: WebsocketNumberPair | [number, number]
circleDetectThreshold: number
accuracyPercentage: number
contourShape: number
contourPerimeter: WebsocketNumberPair | [number, number]
minDist: number
maxCannyThresh: number
pipelineType: PipelineType.ColoredShape;
erode: boolean;
cameraCalibration: null;
dilate: boolean;
circleAccuracy: number;
contourRadius: WebsocketNumberPair | [number, number];
circleDetectThreshold: number;
accuracyPercentage: number;
contourShape: number;
contourPerimeter: WebsocketNumberPair | [number, number];
minDist: number;
maxCannyThresh: number;
}
export type ConfigurableColoredShapePipelineSettings = Partial<Omit<ColoredShapePipelineSettings, "pipelineType" | "erode" | "cameraCalibration" | "dilate" | "minDist" >> & ConfigurablePipelineSettings
export type ConfigurableColoredShapePipelineSettings = Partial<
Omit<ColoredShapePipelineSettings, "pipelineType" | "erode" | "cameraCalibration" | "dilate" | "minDist">
> &
ConfigurablePipelineSettings;
export const DefaultColoredShapePipelineSettings: ColoredShapePipelineSettings = {
...DefaultPipelineSettings,
cameraGain: 75,
targetModel: TargetModel.InfiniteRechargeHighGoalOuter,
ledMode: true,
outputShowMultipleTargets: false,
cameraExposure: 20,
pipelineType: PipelineType.ColoredShape,
...DefaultPipelineSettings,
cameraGain: 75,
targetModel: TargetModel.InfiniteRechargeHighGoalOuter,
ledMode: true,
outputShowMultipleTargets: false,
cameraExposure: 20,
pipelineType: PipelineType.ColoredShape,
erode: false,
cameraCalibration: null,
dilate: false,
circleAccuracy: 20,
contourRadius: { first: 0, second: 100 },
circleDetectThreshold: 5,
accuracyPercentage: 10,
contourShape: 2,
contourPerimeter: { first: 0, second: 1.7976931348623157e+308 },
minDist: 20,
maxCannyThresh: 90
erode: false,
cameraCalibration: null,
dilate: false,
circleAccuracy: 20,
contourRadius: { first: 0, second: 100 },
circleDetectThreshold: 5,
accuracyPercentage: 10,
contourShape: 2,
contourPerimeter: { first: 0, second: 1.7976931348623157e308 },
minDist: 20,
maxCannyThresh: 90
};
export interface AprilTagPipelineSettings extends PipelineSettings {
pipelineType: PipelineType.AprilTag
hammingDist: number
numIterations: number
decimate: number
blur: number
decisionMargin: number
refineEdges: boolean
debug: boolean
threads: number
tagFamily: AprilTagFamily
pipelineType: PipelineType.AprilTag;
hammingDist: number;
numIterations: number;
decimate: number;
blur: number;
decisionMargin: number;
refineEdges: boolean;
debug: boolean;
threads: number;
tagFamily: AprilTagFamily;
}
export type ConfigurableAprilTagPipelineSettings = Partial<Omit<AprilTagPipelineSettings, "pipelineType" | "hammingDist" | "debug">> & ConfigurablePipelineSettings
export type ConfigurableAprilTagPipelineSettings = Partial<
Omit<AprilTagPipelineSettings, "pipelineType" | "hammingDist" | "debug">
> &
ConfigurablePipelineSettings;
export const DefaultAprilTagPipelineSettings: AprilTagPipelineSettings = {
...DefaultPipelineSettings,
cameraGain: 75,
targetModel: TargetModel.Apriltag6in_16h5,
ledMode: false,
outputShowMultipleTargets: true,
cameraExposure: 20,
pipelineType: PipelineType.AprilTag,
...DefaultPipelineSettings,
cameraGain: 75,
targetModel: TargetModel.Apriltag6in_16h5,
ledMode: false,
outputShowMultipleTargets: true,
cameraExposure: 20,
pipelineType: PipelineType.AprilTag,
hammingDist: 0,
numIterations: 40,
decimate: 1,
blur: 0,
decisionMargin: 35,
refineEdges: true,
debug: false,
threads: 4,
tagFamily: AprilTagFamily.Family16h5
hammingDist: 0,
numIterations: 40,
decimate: 1,
blur: 0,
decisionMargin: 35,
refineEdges: true,
debug: false,
threads: 4,
tagFamily: AprilTagFamily.Family16h5
};
export interface ArucoPipelineSettings extends PipelineSettings {
pipelineType: PipelineType.Aruco
decimate: number
threads: number
numIterations: number
cornerAccuracy: number
useAruco3: boolean
pipelineType: PipelineType.Aruco;
decimate: number;
threads: number;
numIterations: number;
cornerAccuracy: number;
useAruco3: boolean;
}
export type ConfigurableArucoPipelineSettings = Partial<Omit<ArucoPipelineSettings, "pipelineType">> & ConfigurablePipelineSettings
export type ConfigurableArucoPipelineSettings = Partial<Omit<ArucoPipelineSettings, "pipelineType">> &
ConfigurablePipelineSettings;
export const DefaultArucoPipelineSettings: ArucoPipelineSettings = {
...DefaultPipelineSettings,
outputShowMultipleTargets: true,
targetModel: TargetModel.Aruco6in_16h5,
cameraExposure: -1,
cameraAutoExposure: true,
ledMode: false,
pipelineType: PipelineType.Aruco,
...DefaultPipelineSettings,
outputShowMultipleTargets: true,
targetModel: TargetModel.Aruco6in_16h5,
cameraExposure: -1,
cameraAutoExposure: true,
ledMode: false,
pipelineType: PipelineType.Aruco,
decimate: 1,
threads: 2,
numIterations: 100,
cornerAccuracy: 25,
useAruco3: true
decimate: 1,
threads: 2,
numIterations: 100,
cornerAccuracy: 25,
useAruco3: true
};
export type ActivePipelineSettings = ReflectivePipelineSettings | ColoredShapePipelineSettings | AprilTagPipelineSettings | ArucoPipelineSettings
export type ActiveConfigurablePipelineSettings = ConfigurableReflectivePipelineSettings | ConfigurableColoredShapePipelineSettings | ConfigurableAprilTagPipelineSettings | ConfigurableArucoPipelineSettings
export type ActivePipelineSettings =
| ReflectivePipelineSettings
| ColoredShapePipelineSettings
| AprilTagPipelineSettings
| ArucoPipelineSettings;
export type ActiveConfigurablePipelineSettings =
| ConfigurableReflectivePipelineSettings
| ConfigurableColoredShapePipelineSettings
| ConfigurableAprilTagPipelineSettings
| ConfigurableArucoPipelineSettings;

View File

@@ -1,152 +1,152 @@
import { type ActivePipelineSettings, DefaultAprilTagPipelineSettings } from "@/types/PipelineTypes";
export interface GeneralSettings {
version?: string
gpuAcceleration?: string
hardwareModel?: string
hardwarePlatform?: string
version?: string;
gpuAcceleration?: string;
hardwareModel?: string;
hardwarePlatform?: string;
}
export interface MetricData {
cpuTemp?: string,
cpuUtil?: string,
cpuMem?: string,
gpuMem?: string,
ramUtil?: string
gpuMemUtil?: string,
cpuThr?: string,
cpuUptime?: string,
diskUtilPct?: string,
cpuTemp?: string;
cpuUtil?: string;
cpuMem?: string;
gpuMem?: string;
ramUtil?: string;
gpuMemUtil?: string;
cpuThr?: string;
cpuUptime?: string;
diskUtilPct?: string;
}
export enum NetworkConnectionType {
DHCP = 0,
Static = 1
DHCP = 0,
Static = 1
}
export interface NetworkSettings {
ntServerAddress: string
connectionType: NetworkConnectionType,
staticIp: string,
hostname: string,
runNTServer: boolean
shouldManage: boolean,
networkManagerIface?: string,
physicalInterface?: string,
setStaticCommand?: string,
setDHCPcommand?: string
ntServerAddress: string;
connectionType: NetworkConnectionType;
staticIp: string;
hostname: string;
runNTServer: boolean;
shouldManage: boolean;
networkManagerIface?: string;
physicalInterface?: string;
setStaticCommand?: string;
setDHCPcommand?: string;
}
export interface LightingSettings {
supported: boolean,
brightness: number
supported: boolean;
brightness: number;
}
export enum LogLevel {
ERROR=0,
WARN=1,
INFO=2,
DEBUG=3,
TRACE=4
ERROR = 0,
WARN = 1,
INFO = 2,
DEBUG = 3,
TRACE = 4
}
export interface LogMessage {
level: LogLevel,
message: string
level: LogLevel;
message: string;
}
export interface Resolution {
width: number,
height: number
width: number;
height: number;
}
export interface VideoFormat {
resolution: Resolution
fps: number,
pixelFormat: string,
index?: number,
diagonalFOV?: number,
horizontalFOV?: number,
verticalFOV?: number,
standardDeviation?: number,
mean?: number
resolution: Resolution;
fps: number;
pixelFormat: string;
index?: number;
diagonalFOV?: number;
horizontalFOV?: number;
verticalFOV?: number;
standardDeviation?: number;
mean?: number;
}
export interface CameraCalibrationResult {
resolution: Resolution
distCoeffs: number[],
standardDeviation: number,
perViewErrors: number[],
intrinsics: number[],
resolution: Resolution;
distCoeffs: number[];
standardDeviation: number;
perViewErrors: number[];
intrinsics: number[];
}
export interface ConfigurableCameraSettings {
fov: number
fov: number;
}
export interface CameraSettings {
nickname: string
nickname: string;
fov: {
value: number,
managedByVendor: boolean
}
stream: {
inputPort: number,
outputPort: number
}
fov: {
value: number;
managedByVendor: boolean;
};
stream: {
inputPort: number;
outputPort: number;
};
validVideoFormats: VideoFormat[]
completeCalibrations: CameraCalibrationResult[]
validVideoFormats: VideoFormat[];
completeCalibrations: CameraCalibrationResult[];
lastPipelineIndex?: number,
currentPipelineIndex: number,
pipelineNicknames: string[],
pipelineSettings: ActivePipelineSettings
lastPipelineIndex?: number;
currentPipelineIndex: number;
pipelineNicknames: string[];
pipelineSettings: ActivePipelineSettings;
}
export const PlaceholderCameraSettings: CameraSettings = {
nickname: "Placeholder Camera",
fov: {
value: 70,
managedByVendor: true
nickname: "Placeholder Camera",
fov: {
value: 70,
managedByVendor: true
},
stream: {
inputPort: 0,
outputPort: 0
},
validVideoFormats: [
{
resolution: { width: 1920, height: 1080 },
fps: 60,
pixelFormat: "RGB"
},
stream: {
inputPort: 0,
outputPort: 0
{
resolution: { width: 1280, height: 720 },
fps: 60,
pixelFormat: "RGB"
},
validVideoFormats: [
{
resolution: { width: 1920, height: 1080 },
fps: 60,
pixelFormat: "RGB"
},
{
resolution: { width: 1280, height: 720 },
fps: 60,
pixelFormat: "RGB"
},
{
resolution: { width: 640, height: 480 },
fps: 30,
pixelFormat: "RGB"
}
],
completeCalibrations: [],
pipelineNicknames: ["Placeholder Pipeline"],
lastPipelineIndex: 0,
currentPipelineIndex: 0,
pipelineSettings: DefaultAprilTagPipelineSettings
{
resolution: { width: 640, height: 480 },
fps: 30,
pixelFormat: "RGB"
}
],
completeCalibrations: [],
pipelineNicknames: ["Placeholder Pipeline"],
lastPipelineIndex: 0,
currentPipelineIndex: 0,
pipelineSettings: DefaultAprilTagPipelineSettings
};
export enum CalibrationBoardTypes {
Chessboard=0,
DotBoard=1
Chessboard = 0,
DotBoard = 1
}
export enum RobotOffsetType {
Clear=0,
Single=1,
DualFirst=2,
DualSecond=3
Clear = 0,
Single = 1,
DualFirst = 2,
DualSecond = 3
}

View File

@@ -3,116 +3,122 @@ import type { ActivePipelineSettings } from "@/types/PipelineTypes";
import type { LogLevel } from "@/types/SettingTypes";
export interface WebsocketLogMessage {
logMessage: {
logLevel: LogLevel,
logMessage: string
}
logMessage: {
logLevel: LogLevel;
logMessage: string;
};
}
export interface WebsocketSettingsUpdate {
general: Required<GeneralSettings>,
lighting: Required<LightingSettings>,
networkSettings: NetworkSettings
general: Required<GeneralSettings>;
lighting: Required<LightingSettings>;
networkSettings: NetworkSettings;
}
export interface WebsocketNumberPair {
first: number,
second: number
first: number;
second: number;
}
export interface WebsocketCompleteCalib {
distCoeffs: number[],
height: number,
width: number,
standardDeviation: number,
perViewErrors: number[],
intrinsics: number[]
distCoeffs: number[];
height: number;
width: number;
standardDeviation: number;
perViewErrors: number[];
intrinsics: number[];
}
export type WebsocketVideoFormat = Record<number, {
fps: number,
height: number,
width: number,
pixelFormat: string,
index?: number,
diagonalFOV?: number,
horizontalFOV?: number,
verticalFOV?: number,
standardDeviation?: number,
mean?: number
}>
export type WebsocketVideoFormat = Record<
number,
{
fps: number;
height: number;
width: number;
pixelFormat: string;
index?: number;
diagonalFOV?: number;
horizontalFOV?: number;
verticalFOV?: number;
standardDeviation?: number;
mean?: number;
}
>;
export interface WebsocketCameraSettingsUpdate {
calibrations: WebsocketCompleteCalib[],
currentPipelineIndex: number,
currentPipelineSettings: ActivePipelineSettings,
fov: number,
inputStreamPort: number,
isFovConfigurable: boolean,
nickname: string,
outputStreamPort: number,
pipelineNicknames: string[],
videoFormatList: WebsocketVideoFormat
calibrations: WebsocketCompleteCalib[];
currentPipelineIndex: number;
currentPipelineSettings: ActivePipelineSettings;
fov: number;
inputStreamPort: number;
isFovConfigurable: boolean;
nickname: string;
outputStreamPort: number;
pipelineNicknames: string[];
videoFormatList: WebsocketVideoFormat;
}
export interface WebsocketNTUpdate {
connected: boolean,
address?: string,
clients?: number
connected: boolean;
address?: string;
clients?: number;
}
export type WebsocketPipelineResultUpdate = Record<number, {
fps: number,
latency: number,
export type WebsocketPipelineResultUpdate = Record<
number,
{
fps: number;
latency: number;
targets: {
yaw: number,
pitch: number,
skew: number,
area: number,
ambiguity: number,
fiducialId: number,
pose: {
"angle_z": number,
"qw": number,
"qx": number,
"x": number,
"qy": number,
"y": number,
"qz": number,
"z": number
},
}[]
}>
yaw: number;
pitch: number;
skew: number;
area: number;
ambiguity: number;
fiducialId: number;
pose: {
angle_z: number;
qw: number;
qx: number;
x: number;
qy: number;
y: number;
qz: number;
z: number;
};
}[];
}
>;
export interface WebsocketCalibrationData {
"patternWidth": number,
"boardType": number,
"hasEnough": boolean,
"count": number,
"minCount": number,
"videoModeIndex": number,
"patternHeight": number,
"squareSizeIn": number
patternWidth: number;
boardType: number;
hasEnough: boolean;
count: number;
minCount: number;
videoModeIndex: number;
patternHeight: number;
squareSizeIn: number;
}
export interface IncomingWebsocketData {
log?: WebsocketLogMessage,
settings?: WebsocketSettingsUpdate,
cameraSettings?: WebsocketCameraSettingsUpdate[],
ntConnectionInfo?: WebsocketNTUpdate,
metrics?: Required<MetricData>,
updatePipelineResult?: WebsocketPipelineResultUpdate,
networkInfo?: {
possibleRios: string[],
deviceips: string[]
}
mutatePipelineSettings?: Partial<ActivePipelineSettings>,
cameraIndex?: number // Sent when mutating pipeline settings to check against currently active
calibrationData?: WebsocketCalibrationData,
log?: WebsocketLogMessage;
settings?: WebsocketSettingsUpdate;
cameraSettings?: WebsocketCameraSettingsUpdate[];
ntConnectionInfo?: WebsocketNTUpdate;
metrics?: Required<MetricData>;
updatePipelineResult?: WebsocketPipelineResultUpdate;
networkInfo?: {
possibleRios: string[];
deviceips: string[];
};
mutatePipelineSettings?: Partial<ActivePipelineSettings>;
cameraIndex?: number; // Sent when mutating pipeline settings to check against currently active
calibrationData?: WebsocketCalibrationData;
}
export enum WebsocketPipelineType {
Calib3d=-2,
DriverMode=-1,
Reflective=0,
ColoredShape=1,
AprilTag=2,
Aruco=3
Calib3d = -2,
DriverMode = -1,
Reflective = 0,
ColoredShape = 1,
AprilTag = 2,
Aruco = 3
}

View File

@@ -9,24 +9,24 @@ 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];
if (useStateStore().colorPickingMode) return [0];
// Only show the output stream in Driver Mode or Calibration Mode
if(useCameraSettingsStore().isDriverMode || useCameraSettingsStore().isCalibrationMode) return [1];
if (useCameraSettingsStore().isDriverMode || useCameraSettingsStore().isCalibrationMode) return [1];
const ret: number[] = [];
if(useCameraSettingsStore().currentPipelineSettings.inputShouldShow) {
if (useCameraSettingsStore().currentPipelineSettings.inputShouldShow) {
ret.push(0);
}
if(useCameraSettingsStore().currentPipelineSettings.outputShouldShow) {
if (useCameraSettingsStore().currentPipelineSettings.outputShouldShow) {
ret.push(1);
}
if(ret.length === 0) return [0];
if (ret.length === 0) return [0];
return ret;
},
set: v => {
set: (v) => {
useCameraSettingsStore().currentPipelineSettings.inputShouldShow = v.includes(0);
useCameraSettingsStore().currentPipelineSettings.outputShouldShow = v.includes(1);
useCameraSettingsStore().changeCurrentPipelineSetting({ inputShouldShow: v.includes(0) }, false);
@@ -36,22 +36,12 @@ const cameraViewType = computed<number[]>({
<template>
<div>
<v-row
no-gutters
class="pa-3"
>
<v-col
cols="12"
md="7"
>
<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"
>
<v-col class="pl-md-3 pt-3 pt-md-0" cols="12" md="5">
<CamerasView v-model="cameraViewType" />
</v-col>
</v-row>

View File

@@ -10,57 +10,42 @@ 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];
if (useStateStore().colorPickingMode) return [0];
// Only show the output stream in Driver Mode or Calibration Mode
if(useCameraSettingsStore().isDriverMode || useCameraSettingsStore().isCalibrationMode) return [1];
if (useCameraSettingsStore().isDriverMode || useCameraSettingsStore().isCalibrationMode) return [1];
const ret: number[] = [];
if(useCameraSettingsStore().currentPipelineSettings.inputShouldShow) {
if (useCameraSettingsStore().currentPipelineSettings.inputShouldShow) {
ret.push(0);
}
if(useCameraSettingsStore().currentPipelineSettings.outputShouldShow) {
if (useCameraSettingsStore().currentPipelineSettings.outputShouldShow) {
ret.push(1);
}
if(ret.length === 0) return [0];
if (ret.length === 0) return [0];
return ret;
},
set: v => {
useCameraSettingsStore().changeCurrentPipelineSetting({
inputShouldShow: v.includes(0),
outputShouldShow: v.includes(1)
}, true);
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"
>
<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"
>
<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>

View File

@@ -3,27 +3,16 @@ const devMode = process.env.NODE_ENV === "development";
</script>
<template>
<div
style="overflow:hidden; height:100%; width:100%"
>
<div
v-if="devMode"
style="width: 100%; height: 100%; padding: 16px"
>
<div style="overflow: hidden; height: 100%; width: 100%">
<div 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.
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
style="width: 100%; height: 100%"
>
<div v-else style="width: 100%; height: 100%">
<!--suppress HtmlUnknownTarget -->
<iframe
src="docs/index.html"
style="overflow:hidden; height:100%; width:100%; border: 0"
/>
<iframe src="docs/index.html" style="overflow: hidden; height: 100%; width: 100%; border: 0" />
</div>
</div>
</template>

View File

@@ -1,18 +1,14 @@
<template>
<div class="not-found-container">
<div>
<v-card-title style="color: white; padding-bottom: 36px; font-size: 42px;">
<v-card-title style="color: white; padding-bottom: 36px; font-size: 42px">
The page you are looking for isn't here
</v-card-title>
<v-card-subtitle style="color: white; font-size: 18px;">
<v-card-subtitle style="color: white; font-size: 18px">
Please use the sidebar to find what you are looking for
</v-card-subtitle>
</div>
<img
src="@/assets/images/notfound.webp"
alt="missing-page"
style="margin-top: 64px"
>
<img src="@/assets/images/notfound.webp" alt="missing-page" style="margin-top: 64px" />
</div>
</template>

View File

@@ -91,7 +91,7 @@ public class NetworkTablesManager {
subMap.put("connected", ntInstance.isConnected());
if (ntInstance.isConnected()) {
var connections = getInstance().ntInstance.getConnections();
var connections = ntInstance.getConnections();
if (connections.length > 0) {
subMap.put("address", connections[0].remote_ip + ":" + connections[0].remote_port);
}