TypeCheck Frontend (#2394)

We recently had an error that would've been caught by type checking in the frontend (see #2393). This PR implements type checking so that future errors will be caught.

Additionally, this PR contains miscellaneous frontend cleanup that's tangentially related to type-checking.
This commit is contained in:
Sam Freund
2026-05-05 10:24:19 -05:00
committed by GitHub
parent d587cd19bb
commit 2372e110f9
43 changed files with 578 additions and 388 deletions

View File

@@ -3,7 +3,7 @@ import type { PhotonTarget } from "@/types/PhotonTrackingTypes";
// @ts-expect-error Intellisense says these conflict with the dynamic imports below
import type { Mesh, Object3D, PerspectiveCamera, Scene, WebGLRenderer } from "three";
// @ts-expect-error Intellisense says these conflict with the dynamic imports below
import type { TrackballControls } from "three/examples/jsm/controls/TrackballControls";
import type { TrackballControls } from "three/examples/jsm/controls/TrackballControls.js";
import { onBeforeUnmount, onMounted, watchEffect } from "vue";
const {
ArrowHelper,
@@ -20,7 +20,7 @@ const {
Scene,
WebGLRenderer
} = await import("three");
const { TrackballControls } = await import("three/examples/jsm/controls/TrackballControls");
const { TrackballControls } = await import("three/examples/jsm/controls/TrackballControls.js");
import { useCameraSettingsStore } from "@/stores/settings/CameraSettingsStore";
import { createPerspectiveCamera } from "@/lib/ThreeUtils";
@@ -213,14 +213,14 @@ onMounted(async () => {
renderer.render(scene, camera);
};
drawTargets(props.targets);
await drawTargets(props.targets);
animate();
});
onBeforeUnmount(() => {
window.removeEventListener("resize", onWindowResize);
});
watchEffect(() => {
drawTargets(props.targets);
void drawTargets(props.targets);
});
</script>

View File

@@ -1,5 +1,13 @@
<script setup lang="ts">
import { onBeforeUnmount, onMounted, ref, watch, watchEffect, type Ref } from "vue";
import type {
Scene as SceneType,
PerspectiveCamera as PerspectiveCameraType,
WebGLRenderer as WebGLRendererType,
Group as GroupType,
Object3D
} from "three";
import type { TrackballControls as TrackballControlsType } from "three/examples/jsm/controls/TrackballControls.js";
const {
AmbientLight,
AxesHelper,
@@ -16,7 +24,7 @@ const {
SphereGeometry,
WebGLRenderer
} = await import("three");
const { TrackballControls } = await import("three/examples/jsm/controls/TrackballControls");
const { TrackballControls } = await import("three/examples/jsm/controls/TrackballControls.js");
import type { BoardObservation, CameraCalibrationResult } from "@/types/SettingTypes";
import axios from "axios";
import { useCameraSettingsStore } from "@/stores/settings/CameraSettingsStore";
@@ -31,12 +39,12 @@ const props = defineProps<{
title: string;
}>();
let scene: Scene | undefined;
let camera: PerspectiveCamera | undefined;
let renderer: WebGLRenderer | undefined;
let controls: TrackballControls | undefined;
let scene: SceneType | undefined;
let camera: PerspectiveCameraType | undefined;
let renderer: WebGLRendererType | undefined;
let controls: TrackballControlsType | undefined;
const createChessboard = (obs: BoardObservation, cal: CameraCalibrationResult): Group => {
const createChessboard = (obs: BoardObservation, cal: CameraCalibrationResult): GroupType => {
const group = new Group();
if (obs.locationInImageSpace.length === 0) return group;
@@ -194,9 +202,6 @@ const resetCamThirdPerson = () => {
let animationFrameId: number | null = null;
onMounted(async () => {
// Grab data first off
fetchCalibrationData();
scene = new Scene();
camera = new PerspectiveCamera(75, 800 / 800, 0.1, 1000);
@@ -256,6 +261,10 @@ onMounted(async () => {
controls.update();
// Fetch calibration only after the scene is ready so the initial draw
// can happen immediately when the data arrives.
await fetchCalibrationData();
const animate = () => {
if (!scene || !camera || !renderer || !controls) {
return;
@@ -318,7 +327,7 @@ if (import.meta.hot) {
}
watchEffect(() => {
drawCalibration(calibrationData.value);
void drawCalibration(calibrationData.value);
});
watch(
@@ -328,9 +337,9 @@ watch(
props.resolution.height,
useCameraSettingsStore().getCalibrationCoeffs(props.resolution)
],
() => {
async () => {
console.log("Camera or resolution changed, refetching calibration");
fetchCalibrationData();
await fetchCalibrationData();
}
);
</script>

View File

@@ -1,5 +1,5 @@
<script setup lang="ts">
import { computed, inject, ref, onBeforeUnmount } from "vue";
import { computed, inject, onBeforeUnmount, useTemplateRef } from "vue";
import { useStateStore } from "@/stores/StateStore";
import { useCameraSettingsStore } from "@/stores/settings/CameraSettingsStore";
import type { StyleValue } from "vue";
@@ -13,6 +13,7 @@ const props = defineProps<{
cameraSettings: UiCameraConfiguration;
}>();
const backendHostname = inject<string>("backendHostname");
const emptyStreamSrc = "//:0";
const streamSrc = computed<string>(() => {
const port = props.cameraSettings.stream[props.streamType === "Raw" ? "inputPort" : "outputPort"];
@@ -21,7 +22,7 @@ const streamSrc = computed<string>(() => {
return emptyStreamSrc;
}
return `http://${inject("backendHostname")}:${port}/stream.mjpg`;
return `http://${backendHostname}:${port}/stream.mjpg`;
});
const streamDesc = computed<string>(() => `${props.streamType} Stream View`);
const streamStyle = computed<StyleValue>(() => {
@@ -67,26 +68,26 @@ const handleCaptureClick = () => {
const handlePopoutClick = () => {
window.open(streamSrc.value);
};
const handleFullscreenRequest = () => {
const handleFullscreenRequest = async () => {
const stream = document.getElementById(props.id);
if (!stream) return;
stream.requestFullscreen();
await stream.requestFullscreen();
};
const mjpgStream: any = ref(null);
const mjpgStream = useTemplateRef("mjpgStream");
const handleStreamError = () => {
if (streamSrc.value && streamSrc.value !== emptyStreamSrc) {
console.error("Error loading stream:", streamSrc.value, " Trying again.");
setTimeout(() => {
mjpgStream.value.src = streamSrc.value;
mjpgStream.value!.src = streamSrc.value;
}, 100);
}
};
onBeforeUnmount(() => {
if (!mjpgStream.value) return;
mjpgStream.value["src"] = emptyStreamSrc;
mjpgStream.value.src = emptyStreamSrc;
});
</script>

View File

@@ -1,5 +1,5 @@
<script setup lang="ts">
import { computed, inject, ref, watch } from "vue";
import { computed, inject, ref, useTemplateRef, watch } from "vue";
import { LogLevel, type LogMessage } from "@/types/SettingTypes";
import { useStateStore } from "@/stores/StateStore";
import LogEntry from "@/components/app/photon-log-entry.vue";
@@ -10,10 +10,10 @@ const backendHost = inject<string>("backendHost");
const searchQuery = ref("");
const timeInput = ref<string>();
const autoScroll = ref(true);
const logList = ref();
const logList = useTemplateRef<InstanceType<typeof VirtualList>>("logList"); // this needs to be typed in the definition since vue has trouble inferring it
const logKeeps = ref(40);
const exportLogFile = ref();
const selectedLogLevels = ref({
const exportLogFile = useTemplateRef("exportLogFile");
const selectedLogLevels = ref<Record<number, boolean>>({
[LogLevel.ERROR]: true,
[LogLevel.WARN]: true,
[LogLevel.INFO]: true,
@@ -48,7 +48,7 @@ watch(logs, () => {
);
autoScroll.value = bottomOffset < 50;
if (autoScroll.value) logList.value.scrollToBottom();
if (autoScroll.value) logList.value?.scrollToBottom();
});
const getLogLevelFromIndex = (index: number): string => {
@@ -56,7 +56,7 @@ const getLogLevelFromIndex = (index: number): string => {
};
const handleLogExport = () => {
exportLogFile.value.click();
exportLogFile.value?.click();
};
const handleLogClear = () => {

View File

@@ -89,7 +89,7 @@ const calibrationDivisors = computed(() =>
})
);
const uniqueVideoResolutionString = ref("");
const uniqueVideoResolutionIndex = ref(getUniqueVideoResolutionStrings()?.[0]?.value);
// Use a watchEffect so the value is populated/reacts when the stores become available or update.
// This avoids trying to index into an array that may be empty during page reload.
@@ -106,7 +106,7 @@ watchEffect(() => {
? currentFormatIndex
: names.length - 1;
useStateStore().calibrationData.videoFormatIndex = currentIndex;
uniqueVideoResolutionString.value = names[currentIndex] ?? "";
uniqueVideoResolutionIndex.value = currentIndex;
});
const squareSizeIn = ref(1);
const markerSizeIn = ref(0.75);
@@ -191,7 +191,7 @@ const downloadCalibBoard = async () => {
};
const isCalibrating = computed(
() => useCameraSettingsStore().currentCameraSettings.currentPipelineIndex === WebsocketPipelineType.Calib3d
() => useCameraSettingsStore().currentCameraSettings.currentPipelineIndex === WebsocketPipelineType.Calib3d.valueOf()
);
const startCalibration = () => {
@@ -310,23 +310,23 @@ const setSelectedVideoFormat = (format: VideoFormat) => {
>
<v-form v-model="settingsValid">
<pv-select
v-model="uniqueVideoResolutionString"
v-model="uniqueVideoResolutionIndex"
label="Resolution"
:select-cols="8"
:disabled="isCalibrating"
tooltip="Resolution to calibrate at (you will have to calibrate every resolution you use 3D mode on)"
:items="getUniqueVideoResolutionStrings()"
@update:model-value="
useStateStore().calibrationData.videoFormatIndex =
getUniqueVideoResolutionStrings().find((v) => v.value === $event)?.value || 0
"
@update:model-value="(value) => (useStateStore().calibrationData.videoFormatIndex = value)"
/>
<pv-select
v-model="boardType"
label="Board Type"
tooltip="Calibration board pattern to use"
:select-cols="8"
:items="['Chessboard', 'ChArUco']"
:items="[
{ value: CalibrationBoardTypes.Charuco, name: 'ChArUco' },
{ value: CalibrationBoardTypes.Chessboard, name: 'Chessboard' }
]"
:disabled="isCalibrating"
/>
<v-alert
@@ -356,7 +356,12 @@ const setSelectedVideoFormat = (format: VideoFormat) => {
label="Tag Family"
tooltip="Dictionary of ArUco markers on the ChArUco board"
:select-cols="8"
:items="['Dict_4X4_1000', 'Dict_5X5_1000', 'Dict_6X6_1000', 'Dict_7X7_1000']"
:items="[
{ value: CalibrationTagFamilies.Dict_4X4_1000, name: 'Dict_4X4_1000' },
{ value: CalibrationTagFamilies.Dict_5X5_1000, name: 'Dict_5X5_1000' },
{ value: CalibrationTagFamilies.Dict_6X6_1000, name: 'Dict_6X6_1000' },
{ value: CalibrationTagFamilies.Dict_7X7_1000, name: 'Dict_7X7_1000' }
]"
:disabled="isCalibrating"
/>
<pv-number-input

View File

@@ -3,7 +3,7 @@ import PhotonCalibrationVisualizer from "@/components/app/photon-calibration-vis
import type { CameraCalibrationResult, VideoFormat } from "@/types/SettingTypes";
import { useCameraSettingsStore } from "@/stores/settings/CameraSettingsStore";
import { useStateStore } from "@/stores/StateStore";
import { computed, inject, ref } from "vue";
import { computed, inject, ref, useTemplateRef } from "vue";
import { axiosPost, getResolutionString, parseJsonFile } from "@/lib/PhotonUtils";
import { useTheme } from "vuetify";
import PvDeleteModal from "@/components/common/pv-delete-modal.vue";
@@ -13,28 +13,28 @@ const props = defineProps<{
videoFormat: VideoFormat;
}>();
const confirmRemoveDialog = ref({ show: false, vf: props.videoFormat as VideoFormat });
const confirmRemoveDialog = ref({ show: false, vf: props.videoFormat });
const removeCalibration = (vf: VideoFormat) => {
axiosPost("/calibration/remove", "delete a camera calibration", {
const removeCalibration = async (vf: VideoFormat) => {
await axiosPost("/calibration/remove", "delete a camera calibration", {
cameraUniqueName: useCameraSettingsStore().currentCameraSettings.uniqueName,
width: vf.resolution.width,
height: vf.resolution.height
});
};
const exportCalibration = ref();
const exportCalibration = useTemplateRef("exportCalibration");
const openExportCalibrationPrompt = () => {
exportCalibration.value.click();
exportCalibration.value?.click();
};
const importCalibrationFromPhotonJson = ref();
const importCalibrationFromPhotonJson = useTemplateRef("importCalibrationFromPhotonJson");
const openUploadPhotonCalibJsonPrompt = () => {
importCalibrationFromPhotonJson.value.click();
importCalibrationFromPhotonJson.value?.click();
};
const importCalibration = async () => {
const files = importCalibrationFromPhotonJson.value.files;
if (files.length === 0) return;
const files = importCalibrationFromPhotonJson.value?.files;
if (!files?.length) return;
const uploadedJson = files[0];
const data = await parseJsonFile<CameraCalibrationResult>(uploadedJson);

View File

@@ -53,7 +53,7 @@ const fetchSnapshots = () => {
.get("/utils/getImageSnapshots")
.then((response) => {
imgData.value = response.data.map(
(snapshotData: { snapshotName: string; cameraUniqueName: string; snapshotData: string }, index) => {
(snapshotData: { snapshotName: string; cameraUniqueName: string; snapshotData: string }, index: number) => {
const metadata = getSnapshotMetadataFromName(snapshotData.snapshotName);
return {
@@ -99,7 +99,7 @@ const expanded = ref([]);
<v-card-text class="pt-0">
<v-btn
color="buttonPassive"
:variant="theme.global.current.value.dark ? 'outlined' : 'tonal'"
:variant="theme.global.current.value.dark ? 'outlined' : 'elevated'"
@click="fetchSnapshots"
>
<v-icon start class="open-icon" size="large"> mdi-folder </v-icon>

View File

@@ -9,6 +9,7 @@ import { computed, ref, watchEffect } from "vue";
import { type CameraSettingsChangeRequest, ValidQuirks } from "@/types/SettingTypes";
import { useTheme } from "vuetify";
import { axiosPost } from "@/lib/PhotonUtils";
import { WebsocketPipelineType } from "@/types/WebsocketDataTypes";
const theme = useTheme();
@@ -20,7 +21,7 @@ const focusMode = computed<boolean>({
get: () => useCameraSettingsStore().isFocusMode,
set: (v) =>
useCameraSettingsStore().changeCurrentPipelineIndex(
v ? -3 : useCameraSettingsStore().currentCameraSettings.lastPipelineIndex || 0,
v ? WebsocketPipelineType.FocusCamera : useCameraSettingsStore().currentCameraSettings.lastPipelineIndex || 0,
true
)
});
@@ -65,8 +66,8 @@ const settingsHaveChanged = (): boolean => {
const a = tempSettingsStruct.value;
const b = useCameraSettingsStore().currentCameraSettings;
for (const q in ValidQuirks) {
if (a.quirksToChange[q] !== b.cameraQuirks.quirks[q]) return true;
for (const quirk of Object.values(ValidQuirks)) {
if (a.quirksToChange[quirk] !== b.cameraQuirks.quirks[quirk]) return true;
}
return a.fov !== b.fov.value;
@@ -120,12 +121,12 @@ watchEffect(() => {
});
const showDeleteCamera = ref(false);
const deleteThisCamera = () => {
axiosPost("/utils/nukeOneCamera", "delete this camera", {
const deleteThisCamera = async () => {
await axiosPost("/utils/nukeOneCamera", "delete this camera", {
cameraUniqueName: useStateStore().currentCameraUniqueName
});
};
const wrappedCameras = computed<SelectItem[]>(() =>
const wrappedCameras = computed<SelectItem<string>[]>(() =>
Object.keys(useCameraSettingsStore().cameras).map((cameraUniqueName) => ({
name: useCameraSettingsStore().cameras[cameraUniqueName].nickname,
value: cameraUniqueName
@@ -159,7 +160,7 @@ const wrappedCameras = computed<SelectItem[]>(() =>
v-model="arducamSelectWrapper"
label="Arducam Model"
:items="[
{ name: 'None', value: 0, disabled: true },
{ name: 'None', value: 0 },
{ name: 'OV9281', value: 1 },
{ name: 'OV2311', value: 2 },
{ name: 'OV9782', value: 3 }

View File

@@ -6,6 +6,7 @@ import { PipelineType } from "@/types/PipelineTypes";
import { useStateStore } from "@/stores/StateStore";
import { useSettingsStore } from "@/stores/settings/GeneralSettingsStore";
import { useTheme } from "vuetify";
import { WebsocketPipelineType } from "@/types/WebsocketDataTypes";
const theme = useTheme();
@@ -15,7 +16,7 @@ const driverMode = computed<boolean>({
get: () => useCameraSettingsStore().isDriverMode,
set: (v) =>
useCameraSettingsStore().changeCurrentPipelineIndex(
v ? -1 : useCameraSettingsStore().currentCameraSettings.lastPipelineIndex || 0,
v ? WebsocketPipelineType.DriverMode : useCameraSettingsStore().currentCameraSettings.lastPipelineIndex || 0,
true
)
});

View File

@@ -1,5 +1,6 @@
<script setup lang="ts">
import { PVCameraInfo } from "@/types/SettingTypes";
import { cameraInfoFor } from "@/lib/PhotonUtils";
const { camera } = defineProps({
camera: {
@@ -7,19 +8,6 @@ const { camera } = defineProps({
required: true
}
});
const cameraInfoFor: any = (camera: PVCameraInfo) => {
if (camera.PVUsbCameraInfo) {
return camera.PVUsbCameraInfo;
}
if (camera.PVCSICameraInfo) {
return camera.PVCSICameraInfo;
}
if (camera.PVFileCameraInfo) {
return camera.PVFileCameraInfo;
}
return {};
};
</script>
<template>

View File

@@ -1,5 +1,6 @@
<script setup lang="ts">
import { PVCameraInfo } from "@/types/SettingTypes";
import { cameraInfoFor } from "@/lib/PhotonUtils";
function isEqual<T>(a: T, b: T): boolean {
if (a === b) {
@@ -25,19 +26,6 @@ const { saved, current } = defineProps({
required: true
}
});
const cameraInfoFor = (camera: PVCameraInfo): any => {
if (camera.PVUsbCameraInfo) {
return camera.PVUsbCameraInfo;
}
if (camera.PVCSICameraInfo) {
return camera.PVCSICameraInfo;
}
if (camera.PVFileCameraInfo) {
return camera.PVFileCameraInfo;
}
return {};
};
</script>
<template>

View File

@@ -25,7 +25,7 @@ const emit = defineEmits<{
(e: "onEscape"): void;
}>();
const handleKeydown = ({ key }) => {
const handleKeydown = ({ key }: KeyboardEvent) => {
switch (key) {
case "Enter":
// Explicitly check that all rule props return true

View File

@@ -1,13 +1,15 @@
<script setup lang="ts">
<script setup lang="ts" generic="T extends string | number">
import { computed } from "vue";
import TooltippedLabel from "@/components/common/pv-tooltipped-label.vue";
export interface SelectItem {
export interface SelectItem<TValue extends string | number> {
name: string | number;
value: string | number;
value: TValue;
disabled?: boolean;
}
const value = defineModel<string | number | undefined>({ required: true });
type SelectItems = SelectItem<T>[] | ReadonlyArray<T>;
const value = defineModel<T>({ required: true });
const props = withDefaults(
defineProps<{
@@ -15,7 +17,7 @@ const props = withDefaults(
tooltip?: string;
selectCols?: number;
disabled?: boolean;
items: string[] | number[] | SelectItem[];
items: SelectItems;
}>(),
{
selectCols: 9,
@@ -23,18 +25,20 @@ const props = withDefaults(
}
);
const areSelectItems = (items: SelectItems): items is SelectItem<T>[] => typeof items[0] === "object";
// Computed in case items changes
const items = computed<SelectItem[]>(() => {
const items = computed<SelectItem<T>[]>(() => {
// Trivial case for empty list; we have no data
if (!props.items.length) {
return [];
}
// Check if the prop exists on the object to infer object type
if ((props.items[0] as SelectItem).name) {
return props.items as SelectItem[];
if (areSelectItems(props.items)) {
return props.items;
}
return props.items.map((v, i) => ({ name: v, value: i }));
return props.items.map((item) => ({ name: item, value: item }));
});
</script>
@@ -49,7 +53,7 @@ const items = computed<SelectItem[]>(() => {
:items="items"
item-title="name"
item-value="value"
item-props.disabled="disabled"
item-props
:disabled="disabled"
hide-details="auto"
variant="underlined"

View File

@@ -18,11 +18,11 @@ const props = withDefaults(
const emit = defineEmits<{ (e: "update:modelValue", value: number): void }>();
// Debounce function
function debounce(func: (...args: any[]) => void, wait: number) {
function debounce(func: (...args: number[]) => void, wait: number) {
let timeout: ReturnType<typeof setTimeout>;
return function (...args: any[]) {
return function (...args: number[]) {
clearTimeout(timeout);
timeout = setTimeout(() => func.apply(this, args), wait);
timeout = setTimeout(() => func(...args), wait);
};
}

View File

@@ -13,28 +13,6 @@ import PvDeleteModal from "@/components/common/pv-delete-modal.vue";
const theme = useTheme();
const changeCurrentCameraUniqueName = (cameraUniqueName: string) => {
useCameraSettingsStore().setCurrentCameraUniqueName(cameraUniqueName, true);
switch (useCameraSettingsStore().cameras[cameraUniqueName].pipelineSettings.pipelineType) {
case PipelineType.Reflective:
pipelineType.value = WebsocketPipelineType.Reflective;
break;
case PipelineType.ColoredShape:
pipelineType.value = WebsocketPipelineType.ColoredShape;
break;
case PipelineType.AprilTag:
pipelineType.value = WebsocketPipelineType.AprilTag;
break;
case PipelineType.Aruco:
pipelineType.value = WebsocketPipelineType.Aruco;
break;
case PipelineType.ObjectDetection:
pipelineType.value = WebsocketPipelineType.ObjectDetection;
break;
}
};
// Common RegEx used for naming both pipelines and cameras
const nameChangeRegex = /^[A-Za-z0-9_ \-)(]*[A-Za-z0-9][A-Za-z0-9_ \-)(.]*$/;
@@ -87,17 +65,17 @@ const cancelCameraNameEdit = () => {
};
// Pipeline Name Edit
const pipelineNamesWrapper = computed<SelectItem[]>(() => {
const pipelineNamesWrapper = computed(() => {
const pipelineNames = useCameraSettingsStore().pipelineNames.map((name, index) => ({ name: name, value: index }));
if (useCameraSettingsStore().isDriverMode) {
pipelineNames.push({ name: "Driver Mode", value: WebsocketPipelineType.DriverMode });
pipelineNames.push({ name: "Driver Mode", value: WebsocketPipelineType.DriverMode.valueOf() });
}
if (useCameraSettingsStore().isFocusMode) {
pipelineNames.push({ name: "Focus Mode", value: WebsocketPipelineType.FocusCamera });
pipelineNames.push({ name: "Focus Mode", value: WebsocketPipelineType.FocusCamera.valueOf() });
}
if (useCameraSettingsStore().isCalibrationMode) {
pipelineNames.push({ name: "3D Calibration Mode", value: WebsocketPipelineType.Calib3d });
pipelineNames.push({ name: "3D Calibration Mode", value: WebsocketPipelineType.Calib3d.valueOf() });
}
return pipelineNames;
@@ -240,7 +218,7 @@ useCameraSettingsStore().$subscribe((mutation, state) => {
break;
}
});
const wrappedCameras = computed<SelectItem[]>(() =>
const wrappedCameras = computed<SelectItem<string>[]>(() =>
Object.keys(useCameraSettingsStore().cameras).map((cameraUniqueName) => ({
name: useCameraSettingsStore().cameras[cameraUniqueName].nickname,
value: cameraUniqueName
@@ -257,7 +235,7 @@ const wrappedCameras = computed<SelectItem[]>(() =>
v-model="useStateStore().currentCameraUniqueName"
label="Camera"
:items="wrappedCameras"
@update:modelValue="changeCurrentCameraUniqueName"
@update:modelValue="pipelineType = useCameraSettingsStore().currentWebsocketPipelineType"
/>
<pv-input
v-else

View File

@@ -13,9 +13,9 @@ import OutputTab from "@/components/dashboard/tabs/OutputTab.vue";
import TargetsTab from "@/components/dashboard/tabs/TargetsTab.vue";
import PnPTab from "@/components/dashboard/tabs/PnPTab.vue";
import Map3DTab from "@/components/dashboard/tabs/Map3DTab.vue";
import { PipelineType } from "@/types/PipelineTypes";
import { WebsocketPipelineType } from "@/types/WebsocketDataTypes";
import { useDisplay } from "vuetify/lib/composables/display";
import { useTheme } from "vuetify";
import { useDisplay, useTheme } from "vuetify";
const theme = useTheme();
@@ -106,6 +106,17 @@ const tabGroups = computed<ConfigOption[][]>(() => {
.filter((it) => it.length); // Remove empty tab groups
});
// This boolean is used to satisfy type-checking requirements.
const shouldUseWideSecondTabGroup = computed(() => {
const currentPipelineSettings = useCameraSettingsStore().currentPipelineSettings;
return (
(currentPipelineSettings.pipelineType === PipelineType.AprilTag ||
currentPipelineSettings.pipelineType === PipelineType.Aruco) &&
currentPipelineSettings.doMultiTarget
);
});
const onBeforeTabUpdate = () => {
// Force the current tab to the input tab on driver mode change
if (useCameraSettingsStore().isDriverMode) {
@@ -129,7 +140,7 @@ const onBeforeTabUpdate = () => {
<v-col
v-for="(tabGroupData, tabGroupIndex) in tabGroups"
:key="tabGroupIndex"
:cols="tabGroupIndex === 1 && useCameraSettingsStore().currentPipelineSettings.doMultiTarget ? 7 : ''"
:cols="tabGroupIndex === 1 && shouldUseWideSecondTabGroup ? 7 : ''"
:class="tabGroupIndex !== tabGroups.length - 1 && 'pr-3'"
@vue:before-update="onBeforeTabUpdate"
>

View File

@@ -1,18 +1,17 @@
<script setup lang="ts">
import { PipelineType } from "@/types/PipelineTypes";
import { PipelineType, type AprilTagPipelineSettings, AprilTagFamily } from "@/types/PipelineTypes";
import PvSelect from "@/components/common/pv-select.vue";
import PvSlider from "@/components/common/pv-slider.vue";
import PvSwitch from "@/components/common/pv-switch.vue";
import { computed } from "vue";
import { useStateStore } from "@/stores/StateStore";
import type { ActivePipelineSettings } from "@/types/PipelineTypes";
import { useCameraSettingsStore } from "@/stores/settings/CameraSettingsStore";
import { useDisplay } from "vuetify";
// TODO fix pipeline typing in order to fix this, the store settings call should be able to infer that only valid pipeline type settings are exposed based on pre-checks for the entire config section
// Defer reference to store access method
const currentPipelineSettings = computed<ActivePipelineSettings>(
() => useCameraSettingsStore().currentPipelineSettings
const currentPipelineSettings = computed<AprilTagPipelineSettings>(
() => useCameraSettingsStore().currentPipelineSettings as AprilTagPipelineSettings
);
const { mdAndDown } = useDisplay();
const interactiveCols = computed(() =>
@@ -25,7 +24,10 @@ const interactiveCols = computed(() =>
<pv-select
v-model="currentPipelineSettings.tagFamily"
label="Target family"
:items="['AprilTag 36h11 (6.5in)', 'AprilTag 16h5 (6in)']"
:items="[
{ value: AprilTagFamily.Family36h11, name: 'AprilTag 36h11 (6.5in)' },
{ value: AprilTagFamily.Family16h5, name: 'AprilTag 16h5 (6in)' }
]"
:select-cols="interactiveCols"
@update:modelValue="(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ tagFamily: value }, false)"
/>

View File

@@ -1,6 +1,6 @@
<script setup lang="ts">
import { useCameraSettingsStore } from "@/stores/settings/CameraSettingsStore";
import { PipelineType, type ActivePipelineSettings } from "@/types/PipelineTypes";
import { PipelineType, type ArucoPipelineSettings, AprilTagFamily } from "@/types/PipelineTypes";
import PvSlider from "@/components/common/pv-slider.vue";
import PvSwitch from "@/components/common/pv-switch.vue";
import PvRangeSlider from "@/components/common/pv-range-slider.vue";
@@ -11,8 +11,8 @@ import { useDisplay } from "vuetify";
// TODO fix pipeline typing in order to fix this, the store settings call should be able to infer that only valid pipeline type settings are exposed based on pre-checks for the entire config section
// Defer reference to store access method
const currentPipelineSettings = computed<ActivePipelineSettings>(
() => useCameraSettingsStore().currentPipelineSettings
const currentPipelineSettings = computed<ArucoPipelineSettings>(
() => useCameraSettingsStore().currentPipelineSettings as ArucoPipelineSettings
);
const { mdAndDown } = useDisplay();
const interactiveCols = computed(() =>
@@ -25,7 +25,10 @@ const interactiveCols = computed(() =>
<pv-select
v-model="currentPipelineSettings.tagFamily"
label="Target family"
:items="['AprilTag Family 36h11', 'AprilTag Family 16h5']"
:items="[
{ value: AprilTagFamily.Family36h11, name: 'AprilTag 36h11 (6.5in)' },
{ value: AprilTagFamily.Family16h5, name: 'AprilTag 16h5 (6in)' }
]"
:select-cols="interactiveCols"
@update:modelValue="(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ tagFamily: value }, false)"
/>

View File

@@ -1,6 +1,14 @@
<script setup lang="ts">
import { useCameraSettingsStore } from "@/stores/settings/CameraSettingsStore";
import { type ActivePipelineSettings, PipelineType } from "@/types/PipelineTypes";
import {
type ActivePipelineSettings,
PipelineType,
ContourSortMode,
ContourTargetOrientation,
ContourGroupingMode,
ContourIntersection,
ContourShape
} from "@/types/PipelineTypes";
import PvRangeSlider from "@/components/common/pv-range-slider.vue";
import PvSelect from "@/components/common/pv-select.vue";
import PvSlider from "@/components/common/pv-slider.vue";
@@ -61,7 +69,10 @@ const interactiveCols = computed(() =>
v-model="useCameraSettingsStore().currentPipelineSettings.contourTargetOrientation"
label="Target Orientation"
tooltip="Used to determine how to calculate target landmarks, as well as aspect ratio"
:items="['Portrait', 'Landscape']"
:items="[
{ value: ContourTargetOrientation.Portrait, name: 'Portrait' },
{ value: ContourTargetOrientation.Landscape, name: 'Landscape' }
]"
:select-cols="interactiveCols"
@update:modelValue="
(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ contourTargetOrientation: value }, false)
@@ -72,7 +83,15 @@ const interactiveCols = computed(() =>
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']"
:items="[
{ value: ContourSortMode.Largest, name: 'Largest' },
{ value: ContourSortMode.Smallest, name: 'Smallest' },
{ value: ContourSortMode.Highest, name: 'Highest' },
{ value: ContourSortMode.Lowest, name: 'Lowest' },
{ value: ContourSortMode.Rightmost, name: 'Rightmost' },
{ value: ContourSortMode.Leftmost, name: 'Leftmost' },
{ value: ContourSortMode.Centermost, name: 'Centermost' }
]"
@update:modelValue="
(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ contourSortMode: value }, false)
"
@@ -166,7 +185,11 @@ const interactiveCols = computed(() =>
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']"
:items="[
{ value: ContourGroupingMode.Single, name: 'Single' },
{ value: ContourGroupingMode.Dual, name: 'Dual' },
{ value: ContourGroupingMode.TwoOrMore, name: 'Two or More' }
]"
@update:modelValue="
(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ contourGroupingMode: value }, false)
"
@@ -176,7 +199,13 @@ const interactiveCols = computed(() =>
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="[
{ value: ContourIntersection.None, name: 'None' },
{ value: ContourIntersection.Up, name: 'Up' },
{ value: ContourIntersection.Down, name: 'Down' },
{ value: ContourIntersection.Left, name: 'Left' },
{ value: ContourIntersection.Right, name: 'Right' }
]"
:disabled="useCameraSettingsStore().currentPipelineSettings.contourGroupingMode === 0"
@update:modelValue="
(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ contourIntersection: value }, false)
@@ -189,7 +218,12 @@ const interactiveCols = computed(() =>
label="Target Shape"
tooltip="The shape of targets to look for"
:select-cols="interactiveCols"
:items="['Circle', 'Polygon', 'Triangle', 'Quadrilateral']"
:items="[
{ value: ContourShape.Circle, name: 'Circle' },
{ value: ContourShape.Polygon, name: 'Polygon' },
{ value: ContourShape.Triangle, name: 'Triangle' },
{ value: ContourShape.Quadrilateral, name: 'Quadrilateral' }
]"
@update:modelValue="
(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ contourShape: value }, false)
"

View File

@@ -30,11 +30,11 @@ const getFilteredStreamDivisors = (): number[] => {
};
const getNumberOfSkippedDivisors = () => streamDivisors.length - getFilteredStreamDivisors().length;
const cameraResolutions = computed(() =>
useCameraSettingsStore().currentCameraSettings.validVideoFormats.map(
(f) => `${getResolutionString(f.resolution)} at ${f.fps} FPS, ${f.pixelFormat}`
)
);
const cameraResolutions = (): { name: string; value: number }[] =>
useCameraSettingsStore().currentCameraSettings.validVideoFormats.map<{ name: string; value: number }>((f) => ({
name: `${getResolutionString(f.resolution)} at ${f.fps} FPS, ${f.pixelFormat}`,
value: f.index || 0 // Index won't ever be undefined
}));
const handleResolutionChange = (value: number) => {
useCameraSettingsStore().changeCurrentPipelineSetting({ cameraVideoModeIndex: value }, false);
@@ -49,20 +49,24 @@ const handleResolutionChange = (value: number) => {
const streamResolutions = computed(() => {
const streamDivisors = getFilteredStreamDivisors();
const currentResolution = useCameraSettingsStore().currentVideoFormat.resolution;
return streamDivisors.map(
(x) =>
`${getResolutionString({
width: Math.floor(currentResolution.width / x),
height: Math.floor(currentResolution.height / x)
})}`
);
return streamDivisors.map((x, i) => ({
name: `${Math.floor(currentResolution.width / x)}x${Math.floor(currentResolution.height / x)}`,
value: i
}));
});
const currentStreamResolutionIndex = computed<number>({
get: () => {
const stored = useCameraSettingsStore().currentPipelineSettings.streamingFrameDivisor;
const skipped = getNumberOfSkippedDivisors();
return stored - skipped;
},
set: (index) => {
useCameraSettingsStore().changeCurrentPipelineSetting(
{ streamingFrameDivisor: index + getNumberOfSkippedDivisors() },
false
);
}
});
const handleStreamResolutionChange = (value: number) => {
useCameraSettingsStore().changeCurrentPipelineSetting(
{ streamingFrameDivisor: value + getNumberOfSkippedDivisors() },
false
);
};
const { mdAndDown } = useDisplay();
const interactiveCols = computed(() =>
@@ -182,17 +186,16 @@ const interactiveCols = computed(() =>
v-model="useCameraSettingsStore().currentPipelineSettings.cameraVideoModeIndex"
label="Resolution"
tooltip="Resolution and FPS the camera should directly capture at"
:items="cameraResolutions"
:items="cameraResolutions()"
:select-cols="interactiveCols"
@update:modelValue="(args) => handleResolutionChange(args)"
/>
<pv-select
v-model="useCameraSettingsStore().currentPipelineSettings.streamingFrameDivisor"
v-model="currentStreamResolutionIndex"
label="Stream Resolution"
tooltip="Resolution to which camera frames are downscaled for streaming to the dashboard"
:items="streamResolutions"
:select-cols="interactiveCols"
@update:modelValue="(args) => handleStreamResolutionChange(args)"
/>
<pv-switch
v-if="useCameraSettingsStore().isDriverMode"

View File

@@ -1,8 +1,13 @@
<script setup lang="ts">
import { useCameraSettingsStore } from "@/stores/settings/CameraSettingsStore";
import { type ObjectDetectionPipelineSettings, PipelineType } from "@/types/PipelineTypes";
import {
type ObjectDetectionPipelineSettings,
PipelineType,
ContourSortMode,
ContourTargetOrientation
} from "@/types/PipelineTypes";
import PvSlider from "@/components/common/pv-slider.vue";
import PvSelect from "@/components/common/pv-select.vue";
import PvSelect, { type SelectItem } from "@/components/common/pv-select.vue";
import PvRangeSlider from "@/components/common/pv-range-slider.vue";
import { computed } from "vue";
import { useStateStore } from "@/stores/StateStore";
@@ -44,19 +49,19 @@ const supportedModels = computed<ObjectDetectionModelProperties[]>(() => {
return availableModels.filter(isSupported);
});
const selectedModel = computed({
get: () => {
const currentModel = currentPipelineSettings.value.model;
if (!currentModel) return undefined;
const modelWrapper = computed<SelectItem<string>[]>(() =>
supportedModels.value.map((model) => ({
name: model.nickname,
value: model.modelPath
}))
);
const index = supportedModels.value.findIndex((model) => model.modelPath === currentModel.modelPath);
return index === -1 ? undefined : index;
},
set: (v) => {
if (v !== undefined && v >= 0 && v < supportedModels.value.length) {
const newModel = supportedModels.value[v];
useCameraSettingsStore().changeCurrentPipelineSetting({ model: newModel }, true);
const selectedModel = computed<string>({
get: () => currentPipelineSettings.value.model?.modelPath ?? "",
set: (value) => {
const model = supportedModels.value.find((supportedModel) => supportedModel.modelPath === value);
if (model) {
useCameraSettingsStore().changeCurrentPipelineSetting({ model }, true);
}
}
});
@@ -69,7 +74,7 @@ const selectedModel = computed({
label="Model"
tooltip="The model used to detect objects in the camera feed"
:select-cols="interactiveCols"
:items="supportedModels.map((model) => model.nickname)"
:items="modelWrapper"
/>
<pv-slider
@@ -123,14 +128,13 @@ const selectedModel = computed({
v-model="useCameraSettingsStore().currentPipelineSettings.contourTargetOrientation"
label="Target Orientation"
tooltip="Used to determine how to calculate target landmarks, as well as aspect ratio"
:items="['Portrait', 'Landscape']"
:items="[
{ value: ContourTargetOrientation.Portrait, name: 'Portrait' },
{ value: ContourTargetOrientation.Landscape, name: 'Landscape' }
]"
:select-cols="interactiveCols"
@update:modelValue="
(value) =>
useCameraSettingsStore().changeCurrentPipelineSetting(
{ contourTargetOrientation: typeof value === 'string' ? Number(value) : value },
false
)
(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ contourTargetOrientation: value }, false)
"
/>
<pv-select
@@ -138,13 +142,17 @@ const selectedModel = computed({
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']"
:items="[
{ value: ContourSortMode.Largest, name: 'Largest' },
{ value: ContourSortMode.Smallest, name: 'Smallest' },
{ value: ContourSortMode.Highest, name: 'Highest' },
{ value: ContourSortMode.Lowest, name: 'Lowest' },
{ value: ContourSortMode.Rightmost, name: 'Rightmost' },
{ value: ContourSortMode.Leftmost, name: 'Leftmost' },
{ value: ContourSortMode.Centermost, name: 'Centermost' }
]"
@update:modelValue="
(value) =>
useCameraSettingsStore().changeCurrentPipelineSetting(
{ contourSortMode: typeof value === 'string' ? Number(value) : value },
false
)
(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ contourSortMode: value }, false)
"
/>
</div>

View File

@@ -1,7 +1,13 @@
<script setup lang="ts">
import PvSelect from "@/components/common/pv-select.vue";
import { useCameraSettingsStore } from "@/stores/settings/CameraSettingsStore";
import { type ActivePipelineSettings, PipelineType, RobotOffsetPointMode } from "@/types/PipelineTypes";
import {
type ActivePipelineSettings,
PipelineType,
RobotOffsetPointMode,
ContourTargetOrientation,
ContourTargetOffsetPointEdge
} from "@/types/PipelineTypes";
import PvSwitch from "@/components/common/pv-switch.vue";
import PvSlider from "@/components/common/pv-slider.vue";
import { computed } from "vue";
@@ -108,7 +114,13 @@ const interactiveCols = computed(() =>
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="[
{ value: ContourTargetOffsetPointEdge.Center, name: 'Center' },
{ value: ContourTargetOffsetPointEdge.Top, name: 'Top' },
{ value: ContourTargetOffsetPointEdge.Bottom, name: 'Bottom' },
{ value: ContourTargetOffsetPointEdge.Left, name: 'Left' },
{ value: ContourTargetOffsetPointEdge.Right, name: 'Right' }
]"
:select-cols="interactiveCols"
@update:modelValue="
(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ contourTargetOffsetPointEdge: value }, false)
@@ -119,7 +131,10 @@ const interactiveCols = computed(() =>
v-model="useCameraSettingsStore().currentPipelineSettings.contourTargetOrientation"
label="Target Orientation"
tooltip="Used to determine how to calculate target landmarks (e.g. the top, left, or bottom of the target)"
:items="['Portrait', 'Landscape']"
:items="[
{ value: ContourTargetOrientation.Portrait, name: 'Portrait' },
{ value: ContourTargetOrientation.Landscape, name: 'Landscape' }
]"
:select-cols="interactiveCols"
@update:modelValue="
(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ contourTargetOrientation: value }, false)
@@ -129,7 +144,11 @@ const interactiveCols = computed(() =>
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="[
{ value: RobotOffsetPointMode.None, name: 'None' },
{ value: RobotOffsetPointMode.Single, name: 'Single Point' },
{ value: RobotOffsetPointMode.Dual, name: 'Dual Point' }
]"
:select-cols="interactiveCols"
@update:modelValue="
(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ offsetRobotOffsetMode: value }, false)

View File

@@ -1,6 +1,5 @@
@ -0,0 +1,565 @@
<script setup lang="ts">
import { inject, computed, ref, watch } from "vue";
import { inject, computed, ref, watch, useTemplateRef } from "vue";
import { useStateStore } from "@/stores/StateStore";
import { useSettingsStore } from "@/stores/settings/GeneralSettingsStore";
import PvSelect from "@/components/common/pv-select.vue";
@@ -15,20 +14,20 @@ const theme = useTheme();
const restartProgram = async () => {
if (await axiosPost("/utils/restartProgram", "restart PhotonVision")) {
forceReloadPage();
await forceReloadPage();
}
};
const restartDevice = async () => {
if (await axiosPost("/utils/restartDevice", "restart the device")) {
forceReloadPage();
await forceReloadPage();
}
};
const address = inject<string>("backendHost");
const offlineUpdate = ref();
const offlineUpdate = useTemplateRef("offlineUpdate");
const openOfflineUpdatePrompt = () => {
offlineUpdate.value.click();
offlineUpdate.value?.click();
};
const offlineUpdateRegex = new RegExp("photonvision-((?:dev-)?v[\\w.-]+)-((?:linux|win|mac)\\w+)\\.jar");
@@ -37,8 +36,8 @@ const majorVersionRegex = new RegExp("(?:dev-)?(\\d+)\\.\\d+\\.\\d+");
const offlineUpdateDialog = ref({ show: false, confirmString: "" });
const handleOfflineUpdateRequest = async () => {
const files = offlineUpdate.value.files;
if (files.length === 0) return;
const files = offlineUpdate.value?.files;
if (!files?.length) return;
const match = files[0].name.match(offlineUpdateRegex);
if (!match) {
@@ -68,7 +67,7 @@ const handleOfflineUpdateRequest = async () => {
});
return;
} else if (versionMatch && !dev) {
handleOfflineUpdate(files[0]);
await handleOfflineUpdate(files[0]);
} else if (!versionMatch && !dev) {
offlineUpdateDialog.value = {
show: true,
@@ -99,7 +98,7 @@ const handleOfflineUpdate = async (file: File) => {
if (
await axiosPost("/utils/offlineUpdate", "upload new software", formData, {
headers: { "Content-Type": "multipart/form-data" },
onUploadProgress: ({ progress }) => {
onUploadProgress: ({ progress }: { progress?: number }) => {
const uploadPercentage = (progress || 0) * 100.0;
if (uploadPercentage < 99.5) {
useStateStore().showSnackbarMessage({
@@ -118,18 +117,18 @@ const handleOfflineUpdate = async (file: File) => {
color: "secondary",
timeout: -1
});
forceReloadPage();
await forceReloadPage();
}
};
const exportLogFile = ref();
const exportLogFile = useTemplateRef("exportLogFile");
const openExportLogsPrompt = () => {
exportLogFile.value.click();
exportLogFile.value?.click();
};
const exportSettings = ref();
const exportSettings = useTemplateRef("exportSettings");
const openExportSettingsPrompt = () => {
exportSettings.value.click();
exportSettings.value?.click();
};
enum ImportType {
@@ -141,10 +140,10 @@ enum ImportType {
}
const showImportDialog = ref(false);
const importType = ref<ImportType | undefined>(undefined);
const importType = ref<ImportType>(ImportType.AllSettings);
const importFile = ref<File | null>(null);
const handleSettingsImport = () => {
const handleSettingsImport = async () => {
if (importType.value === undefined || importFile.value === null) return;
const formData = new FormData();
formData.append("data", importFile.value);
@@ -167,18 +166,18 @@ const handleSettingsImport = () => {
settingsEndpoint = "";
break;
}
axiosPost(`/settings${settingsEndpoint}`, "import settings", formData, {
await axiosPost(`/settings${settingsEndpoint}`, "import settings", formData, {
headers: { "Content-Type": "multipart/form-data" }
});
showImportDialog.value = false;
importType.value = undefined;
importType.value = ImportType.AllSettings;
importFile.value = null;
};
const showFactoryReset = ref(false);
const nukePhotonConfigDirectory = async () => {
if (await axiosPost("/utils/nukeConfigDirectory", "delete the config directory")) {
forceReloadPage();
await forceReloadPage();
}
};
@@ -503,7 +502,7 @@ watch(metricsHistorySnapshot, () => {
width="600"
@update:modelValue="
() => {
importType = undefined;
importType = ImportType.AllSettings;
importFile = null;
}
"
@@ -517,7 +516,13 @@ watch(metricsHistorySnapshot, () => {
v-model="importType"
label="Type"
tooltip="Select the type of settings file you are trying to upload"
:items="['All Settings', 'Hardware Config', 'Hardware Settings', 'Network Config', 'Apriltag Layout']"
:items="[
{ value: ImportType.AllSettings, name: 'All Settings' },
{ value: ImportType.HardwareConfig, name: 'Hardware Config' },
{ value: ImportType.HardwareSettings, name: 'Hardware Settings' },
{ value: ImportType.NetworkConfig, name: 'Network Config' },
{ value: ImportType.ApriltagFieldLayout, name: 'AprilTag Field Layout' }
]"
:select-cols="10"
style="width: 100%"
/>
@@ -558,7 +563,9 @@ watch(metricsHistorySnapshot, () => {
:variant="theme.global.current.value.dark ? 'outlined' : 'elevated'"
@click="
offlineUpdateDialog.show = false;
handleOfflineUpdate(offlineUpdate.files[0]);
if (offlineUpdate?.files?.length) {
handleOfflineUpdate(offlineUpdate.files[0]);
}
"
>
<v-icon start class="open-icon" size="large"> mdi-upload </v-icon>

View File

@@ -106,6 +106,7 @@ const saveGeneralSettings = async () => {
// Update the local settings cause the backend checked their validity. Assign is to deref value
useSettingsStore().network = { ...useSettingsStore().network, ...Object.assign({}, tempSettingsStruct.value) };
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} catch (error: any) {
resetTempSettingsStruct();
if (error.response) {
@@ -150,14 +151,11 @@ const saveGeneralSettings = async () => {
}
};
const currentNetworkInterfaceIndex = computed<number | undefined>({
get: () => {
const index = useSettingsStore().networkInterfaceNames.indexOf(
useSettingsStore().network.networkManagerIface || ""
);
return index === -1 ? undefined : index;
},
set: (v) => v && (tempSettingsStruct.value.networkManagerIface = useSettingsStore().networkInterfaceNames[v])
const currentNetworkInterface = computed<string>({
get: () => useSettingsStore().network.networkManagerIface || "",
set: (v) => {
tempSettingsStruct.value.networkManagerIface = v;
}
});
watchEffect(() => {
@@ -256,7 +254,7 @@ watchEffect(() => {
/>
<pv-select
v-show="!useSettingsStore().network.networkingDisabled"
v-model="currentNetworkInterfaceIndex"
v-model="currentNetworkInterface"
label="NetworkManager interface"
:disabled="
!tempSettingsStruct.shouldManage ||

View File

@@ -1,5 +1,5 @@
<script setup lang="ts">
import { onMounted, ref, onBeforeUnmount, watch } from "vue";
import { onMounted, onBeforeUnmount, watch, useTemplateRef } from "vue";
import { useTheme } from "vuetify";
// Color - original (adjusted)
@@ -8,14 +8,14 @@ import { useTheme } from "vuetify";
// green - 65, 181, 127 (r: 75, g: 209, b: 147)
// red - 238, 102, 102 (r: 238, g: 102, b: 102)
const colors = {
"blue-LightTheme": { r: 255, g: 216, b: 67 },
"blue-DarkTheme": { r: 92, g: 154, b: 255 },
"purple-LightTheme": { r: 255, g: 216, b: 67 },
"purple-DarkTheme": { r: 167, g: 104, b: 196 },
"red-LightTheme": { r: 255, g: 216, b: 67 },
"red-DarkTheme": { r: 238, g: 102, b: 102 },
"green-LightTheme": { r: 255, g: 216, b: 67 },
"green-DarkTheme": { r: 75, g: 209, b: 147 }
"blue-light": { r: 255, g: 216, b: 67 },
"blue-dark": { r: 92, g: 154, b: 255 },
"purple-light": { r: 255, g: 216, b: 67 },
"purple-dark": { r: 167, g: 104, b: 196 },
"red-light": { r: 255, g: 216, b: 67 },
"red-dark": { r: 238, g: 102, b: 102 },
"green-light": { r: 255, g: 216, b: 67 },
"green-dark": { r: 75, g: 209, b: 147 }
};
const DEFAULT_COLOR = "blue";
@@ -26,9 +26,13 @@ const typeLabels = {
};
const theme = useTheme();
const chartRef = ref(null);
const chartRef = useTemplateRef("chartRef");
let chart: echarts.ECharts | null = null;
interface TooltipSeriesParam {
value: [number, number];
}
const getOptions = (data: ChartData[] = []) => {
const now = Date.now();
return {
@@ -37,7 +41,7 @@ const getOptions = (data: ChartData[] = []) => {
},
tooltip: {
trigger: "axis",
formatter: (params: any) => {
formatter: (params: TooltipSeriesParam[]) => {
const p = params[0];
const append = typeLabels[props.type];
const fmsLimitLabel = "FMS Limit - 7.000 Mb/s";
@@ -80,12 +84,12 @@ const getOptions = (data: ChartData[] = []) => {
min: now - 55 * 1000,
axisLine: {
lineStyle: {
color: theme.global.name.value === "LightTheme" ? "#aaa" : "#777"
color: theme.global.current.value.dark ? "#777" : "#aaa"
}
},
axisLabel: {
align: "left",
color: theme.global.name.value === "LightTheme" ? "#fff" : "#ddd",
color: theme.global.current.value.dark ? "#ddd" : "#fff",
formatter: (value: number) => {
const date = new Date(value);
return date.toLocaleTimeString([], {
@@ -102,12 +106,12 @@ const getOptions = (data: ChartData[] = []) => {
position: "right",
min:
props.min ??
function (value) {
function (value: { min: number; max: number }) {
return Math.max(0, (value.min - 10) | 0);
},
max:
props.max ??
function (value) {
function (value: { min: number; max: number }) {
return (value.max + 10) | 0;
},
splitNumber: 2,
@@ -118,7 +122,7 @@ const getOptions = (data: ChartData[] = []) => {
}
},
axisLabel: {
color: theme.global.name.value === "LightTheme" ? "#fff" : "#ddd"
color: theme.global.current.value.dark ? "#ddd" : "#fff"
}
},
series: getSeries(data),
@@ -127,7 +131,7 @@ const getOptions = (data: ChartData[] = []) => {
};
const getSeries = (data: ChartData[] = []) => {
const color = colors[`${props.color ?? DEFAULT_COLOR}-${theme.global.name.value}`];
const color = colors[`${props.color ?? DEFAULT_COLOR}-${theme.global.current.value.dark ? "dark" : "light"}`];
return [
{
type: "line",
@@ -188,10 +192,10 @@ interface ChartData {
// Type options: "percentage", "temperature", "mb"
const props = defineProps<{
data: ChartData[];
type: string;
type: keyof typeof typeLabels;
min?: number;
max?: number;
color?: string;
color?: "red" | "green" | "blue" | "purple";
}>();
onMounted(async () => {

View File

@@ -1,5 +1,5 @@
<script setup lang="ts">
import { ref, computed, inject } from "vue";
import { ref, computed, inject, useTemplateRef } from "vue";
import { useStateStore } from "@/stores/StateStore";
import { useSettingsStore } from "@/stores/settings/GeneralSettingsStore";
import { type ObjectDetectionModelProperties } from "@/types/SettingTypes";
@@ -46,7 +46,7 @@ const handleImport = async () => {
if (
await axiosPost("/objectdetection/import", "import an object detection model", formData, {
headers: { "Content-Type": "multipart/form-data" },
onUploadProgress: ({ progress }) => {
onUploadProgress: ({ progress }: { progress?: number }) => {
const uploadPercentage = (progress || 0) * 100.0;
if (uploadPercentage < 99.5) {
useStateStore().showSnackbarMessage({
@@ -74,20 +74,20 @@ const handleImport = async () => {
importVersion.value = null;
};
const deleteModel = (model: ObjectDetectionModelProperties) => {
axiosPost("/objectdetection/delete", "delete an object detection model", {
const deleteModel = async (model: ObjectDetectionModelProperties) => {
await axiosPost("/objectdetection/delete", "delete an object detection model", {
modelPath: model.modelPath
});
};
const renameModel = (model: ObjectDetectionModelProperties, newName: string) => {
const renameModel = async (model: ObjectDetectionModelProperties, newName: string) => {
useStateStore().showSnackbarMessage({
message: "Renaming Object Detection Model...",
color: "secondary",
timeout: -1
});
axiosPost("/objectdetection/rename", "rename an object detection model", {
await axiosPost("/objectdetection/rename", "rename an object detection model", {
modelPath: model.modelPath,
newName: newName
});
@@ -97,7 +97,7 @@ const renameModel = (model: ObjectDetectionModelProperties, newName: string) =>
// Filters out models that are not supported by the current backend, and returns a flattened list.
const supportedModels = computed(() => {
const { availableModels, supportedBackends } = useSettingsStore().general;
const isSupported = (model: any) => {
const isSupported = (model: ObjectDetectionModelProperties) => {
// Check if model's family is in the list of supported backends
return supportedBackends.some((backend: string) => backend.toLowerCase() === model.family.toLowerCase());
};
@@ -106,19 +106,19 @@ const supportedModels = computed(() => {
return availableModels.filter(isSupported);
});
const exportModels = ref();
const exportModels = useTemplateRef("exportModels");
const openExportPrompt = () => {
exportModels.value.click();
exportModels.value?.click();
};
const exportIndividualModel = ref();
const exportIndividualModel = useTemplateRef("exportIndividualModel");
const openExportIndividualModelPrompt = () => {
exportIndividualModel.value.click();
exportIndividualModel.value?.click();
};
const showNukeDialog = ref(false);
const nukeModels = () => {
axiosPost("/objectdetection/nuke", "clear and reset object detection models");
const nukeModels = async () => {
await axiosPost("/objectdetection/nuke", "clear and reset object detection models");
};
const showBulkImportDialog = ref(false);
@@ -132,7 +132,7 @@ const handleBulkImport = async () => {
if (
await axiosPost("/objectdetection/bulkimport", "import object detection models", formData, {
headers: { "Content-Type": "multipart/form-data" },
onUploadProgress: ({ progress }) => {
onUploadProgress: ({ progress }: { progress?: number }) => {
const uploadPercentage = (progress || 0) * 100.0;
if (uploadPercentage < 99.5) {
useStateStore().showSnackbarMessage({