General UI Refinements (#1678)

Does the following:
- Adjusts the shade of red buttons and banners to increase readability
and reduce eye strain

![image](https://github.com/user-attachments/assets/7f741a9e-dc1e-4394-b87d-580e189245b1)

![image](https://github.com/user-attachments/assets/b23202f1-4cf6-46c1-aca5-2455a09259cd)

- Cleans up factory reset and camera deletion modals

![image](https://github.com/user-attachments/assets/e6564732-d578-43da-bc83-729ec6fdbc5e)

![image](https://github.com/user-attachments/assets/9c5a1cba-f4fd-47ea-811c-abbabe5fa3a4)

- Removes matchCamerasOnlyByPath as it is no longer used and throws
errors in the console

![image](https://github.com/user-attachments/assets/77043993-26a2-4de4-8e98-702e7f285dc6)

- Limits the criteria to flag a camera mismatch in Camera Matching to
only what is necessary based on camera type and highlights differences
in table properties (testing on this is appreciated)

![image](https://github.com/user-attachments/assets/cfbd96c1-09dd-414a-8177-693fc054b26f)

- Only displays both saved vs. current info in camera matching if there
is a difference between the two

![image](https://github.com/user-attachments/assets/6223ffc8-4cff-464f-8b54-720c3222a5d5)

- Some general code cleanup (reduced unnecessary padding/margin/row-col
statements, style="display:flex;" -> class="d-flex", etc.
- Moves Compact Mode button to the bottom away from all the menu items
(cleaner imo, open to thoughts)
- Establishes a general spacing format for cards and pages and applies
this to existing cards and pages to create a consistent look and feel to
the UI (e.g. keeping things in line and less erratic spacing/placement
of UI elements)

![image](https://github.com/user-attachments/assets/1ab0ca4b-303e-436d-97b3-da72d46c4fcb)

![image](https://github.com/user-attachments/assets/82ba9e53-f854-4309-bc00-7b5d0bad58b7)

![image](https://github.com/user-attachments/assets/18aa6ca4-e6fa-4125-8a0a-e6a007a0337d)

![image](https://github.com/user-attachments/assets/77043993-26a2-4de4-8e98-702e7f285dc6)


- Delete protection for camera matching modules
- Anti-backend-spam for activate/deactivate/delete modules to hopefully
prevent any odd behavior from button spamming
- Enforces a common camera stream size on camera matching view (NEEDS
MORE TESTING)

![image](https://github.com/user-attachments/assets/9032783d-1edf-4c6e-ba7b-00e5f20280df)

https://private-user-images.githubusercontent.com/29715865/400783758-dc99c151-b8a7-4367-a173-74c2fc5b2666.mp4?jwt=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3MzYyNTc3NzEsIm5iZiI6MTczNjI1NzQ3MSwicGF0aCI6Ii8yOTcxNTg2NS80MDA3ODM3NTgtZGM5OWMxNTEtYjhhNy00MzY3LWExNzMtNzRjMmZjNWIyNjY2Lm1wND9YLUFtei1BbGdvcml0aG09QVdTNC1ITUFDLVNIQTI1NiZYLUFtei1DcmVkZW50aWFsPUFLSUFWQ09EWUxTQTUzUFFLNFpBJTJGMjAyNTAxMDclMkZ1cy1lYXN0LTElMkZzMyUyRmF3czRfcmVxdWVzdCZYLUFtei1EYXRlPTIwMjUwMTA3VDEzNDQzMVomWC1BbXotRXhwaXJlcz0zMDAmWC1BbXotU2lnbmF0dXJlPWMwOWM1MDc2ZTVlOWZhM2MxYjAwZjAyZTc2MTYyZTk1ZTVmOGFhZmVkMzlmODRlZTk1ODVlOTk2ZGQzZmM0Y2EmWC1BbXotU2lnbmVkSGVhZGVycz1ob3N0In0.ovtRnObwbkEfljr9d5fqaory0nH91LWJSSkmrUUe_4Y
This commit is contained in:
Devon Doyle
2025-01-07 08:45:39 -05:00
committed by GitHub
parent fa2034d30b
commit 484e8d4298
34 changed files with 1098 additions and 965 deletions

View File

@@ -9,6 +9,7 @@ import type { UiCameraConfiguration } from "@/types/SettingTypes";
const props = defineProps<{
streamType: "Raw" | "Processed";
id: string;
outerId?: string;
cameraSettings: UiCameraConfiguration;
}>();
@@ -90,7 +91,7 @@ onBeforeUnmount(() => {
</script>
<template>
<div class="stream-container" :style="containerStyle">
<div :id="outerId" class="stream-container" :style="containerStyle">
<img :src="loadingImage" class="stream-loading" />
<img
:id="id"

View File

@@ -81,17 +81,17 @@ const needsCamerasConfigured = computed<boolean>(() => {
<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-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">
<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-list-item-icon>
<v-list-item-content>
<v-list-item-title>Compact Mode</v-list-item-title>
</v-list-item-content>
</v-list-item>
<v-list-item>
<v-list-item-icon>
<v-icon v-if="useSettingsStore().network.runNTServer"> mdi-server </v-icon>

View File

@@ -215,283 +215,254 @@ const setSelectedVideoFormat = (format: VideoFormat) => {
<template>
<div>
<v-card class="mb-3 pr-6 pb-3" color="primary" dark>
<v-card-title>Camera Calibration</v-card-title>
<div class="ml-5">
<v-row v-show="!isCalibrating" class="pb-12">
<v-card-subtitle class="pb-0 mb-0 pl-3">Complete Calibrations</v-card-subtitle>
<v-simple-table fixed-header height="100%" dense class="mt-2">
<thead>
<tr>
<th>Resolution</th>
<th>Mean Error</th>
<th>Horizontal FOV</th>
<th>Vertical FOV</th>
<th>Diagonal FOV</th>
<th>More Info</th>
</tr>
</thead>
<tbody style="cursor: pointer">
<tr v-for="(value, index) in getUniqueVideoFormatsByResolution()" :key="index">
<td>{{ getResolutionString(value.resolution) }}</td>
<td>
{{ value.mean !== undefined ? (isNaN(value.mean) ? "Unknown" : value.mean.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>
<v-tooltip bottom>
<template #activator="{ on, attrs }">
<td v-bind="attrs" v-on="on" @click="setSelectedVideoFormat(value)">
<v-icon small class="mr-2">mdi-information</v-icon>
</td>
</template>
<span>Click for more info on this calibration.</span>
</v-tooltip>
</tr>
</tbody>
</v-simple-table>
</v-row>
<v-divider />
<v-row v-if="useCameraSettingsStore().isConnected" style="display: flex; flex-direction: column" class="mt-4">
<v-card-subtitle v-show="!isCalibrating" class="pl-3 pa-0 ma-0"> Configure New Calibration</v-card-subtitle>
<v-form ref="form" v-model="settingsValid" class="pl-4 mb-10 pr-5">
<!-- TODO: the default videoFormatIndex is 0, but the list of unique video mode indexes might not include 0. getUniqueVideoResolutionStrings indexing is also different from the normal video mode indexing -->
<pv-select
v-model="useStateStore().calibrationData.videoFormatIndex"
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()"
/>
<pv-select
v-show="isCalibrating && boardType != CalibrationBoardTypes.Charuco"
v-model="useCameraSettingsStore().currentPipelineSettings.streamingFrameDivisor"
label="Decimation"
tooltip="Resolution to which camera frames are downscaled for detection. Calibration still uses full-res"
:items="calibrationDivisors"
:select-cols="8"
@input="(v) => useCameraSettingsStore().changeCurrentPipelineSetting({ streamingFrameDivisor: v }, false)"
/>
<pv-select
v-model="boardType"
label="Board Type"
tooltip="Calibration board pattern to use"
:select-cols="8"
:items="['Chessboard', 'Charuco']"
:disabled="isCalibrating"
/>
<pv-select
v-show="boardType == CalibrationBoardTypes.Charuco"
v-model="tagFamily"
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']"
:disabled="isCalibrating"
/>
<pv-number-input
v-model="squareSizeIn"
label="Pattern Spacing (in)"
tooltip="Spacing between pattern features in inches"
:disabled="isCalibrating"
:rules="[(v) => v > 0 || 'Size must be positive']"
:label-cols="4"
/>
<pv-number-input
v-show="boardType == CalibrationBoardTypes.Charuco"
v-model="markerSizeIn"
label="Marker Size (in)"
tooltip="Size of the tag markers in inches must be smaller than pattern spacing"
:disabled="isCalibrating"
:rules="[(v) => v > 0 || 'Size must be positive']"
:label-cols="4"
/>
<pv-number-input
v-model="patternWidth"
label="Board Width (squares)"
tooltip="Width of the board in dots or chessboard squares"
:disabled="isCalibrating"
:rules="[(v) => v >= 4 || 'Width must be at least 4']"
:label-cols="4"
/>
<pv-number-input
v-model="patternHeight"
label="Board Height (squares)"
tooltip="Height of the board in dots or chessboard squares"
:disabled="isCalibrating"
:rules="[(v) => v >= 4 || 'Height must be at least 4']"
:label-cols="4"
/>
<pv-switch
v-show="boardType == CalibrationBoardTypes.Charuco"
v-model="useOldPattern"
label="Old OpenCV Pattern"
:disabled="isCalibrating"
tooltip="If enabled, Photon will use the old OpenCV pattern for calibration."
:label-cols="4"
/>
<v-banner
v-show="useSettingsStore().general.mrCalWorking"
rounded
color="secondary"
text-color="white"
class="mt-3"
icon="mdi-alert-circle-outline"
>
Mrcal was successfully loaded, and will be used!
</v-banner>
<v-banner
v-show="!useSettingsStore().general.mrCalWorking"
rounded
color="red"
text-color="white"
class="mt-3"
icon="mdi-alert-circle-outline"
>
MrCal JNI could not be loaded! Consult journalctl logs for additional details.
</v-banner>
</v-form>
<v-row justify="center">
<v-chip
v-show="isCalibrating"
label
:color="useStateStore().calibrationData.hasEnoughImages ? 'secondary' : 'gray'"
class="mb-6"
>
Snapshots: {{ useStateStore().calibrationData.imageCount }} of at least
{{ useStateStore().calibrationData.minimumImageCount }}
</v-chip>
</v-row>
</v-row>
<v-row v-if="isCalibrating">
<v-col cols="12" class="pt-0">
<pv-slider
v-model="useCameraSettingsStore().currentPipelineSettings.cameraExposureRaw"
:disabled="useCameraSettingsStore().currentCameraSettings.pipelineSettings.cameraAutoExposure"
label="Exposure"
tooltip="Directly controls how long the camera shutter remains open. Units are dependant on the underlying driver."
:min="useCameraSettingsStore().minExposureRaw"
:max="useCameraSettingsStore().maxExposureRaw"
:slider-cols="8"
:step="1"
@input="
(args) => useCameraSettingsStore().changeCurrentPipelineSetting({ cameraExposureRaw: args }, false)
"
/>
<pv-slider
v-model="useCameraSettingsStore().currentPipelineSettings.cameraBrightness"
label="Brightness"
:min="0"
:max="100"
:slider-cols="8"
@input="
(args) => useCameraSettingsStore().changeCurrentPipelineSetting({ cameraBrightness: args }, false)
"
/>
<pv-switch
v-model="useCameraSettingsStore().currentPipelineSettings.cameraAutoExposure"
class="pt-2"
label="Auto Exposure"
:label-cols="4"
tooltip="Enables or Disables camera automatic adjustment for current lighting conditions"
@input="
(args) => useCameraSettingsStore().changeCurrentPipelineSetting({ cameraAutoExposure: args }, false)
"
/>
<pv-slider
v-if="useCameraSettingsStore().currentPipelineSettings.cameraGain >= 0"
v-model="useCameraSettingsStore().currentPipelineSettings.cameraGain"
label="Camera Gain"
tooltip="Controls camera gain, similar to brightness"
:min="0"
:max="100"
@input="(args) => useCameraSettingsStore().changeCurrentPipelineSetting({ cameraGain: args }, false)"
/>
<pv-slider
v-if="useCameraSettingsStore().currentPipelineSettings.cameraRedGain !== -1"
v-model="useCameraSettingsStore().currentPipelineSettings.cameraRedGain"
label="Red AWB Gain"
: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)"
/>
<pv-slider
v-if="useCameraSettingsStore().currentPipelineSettings.cameraBlueGain !== -1"
v-model="useCameraSettingsStore().currentPipelineSettings.cameraBlueGain"
label="Blue AWB Gain"
: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)"
/>
</v-col>
</v-row>
<v-row>
<v-col v-if="tooManyPoints" :cols="12">
<v-banner rounded color="red" text-color="white" class="mt-3" icon="mdi-alert-circle-outline">
Too many corners - finish calibration now!
</v-banner>
</v-col>
<v-col :cols="6">
<v-btn
small
color="secondary"
style="width: 100%"
:disabled="!settingsValid || tooManyPoints"
@click="isCalibrating ? useCameraSettingsStore().takeCalibrationSnapshot() : startCalibration()"
>
<v-icon left class="calib-btn-icon"> {{ isCalibrating ? "mdi-camera" : "mdi-flag-outline" }} </v-icon>
<span class="calib-btn-label">{{ isCalibrating ? "Take Snapshot" : "Start Calibration" }}</span>
</v-btn>
</v-col>
<v-col :cols="6">
<v-btn
small
:color="useStateStore().calibrationData.hasEnoughImages ? 'accent' : 'red'"
:class="useStateStore().calibrationData.hasEnoughImages ? 'black--text' : 'white---text'"
style="width: 100%"
:disabled="!isCalibrating || !settingsValid"
@click="endCalibration"
>
<v-icon left class="calib-btn-icon">
{{ useStateStore().calibrationData.hasEnoughImages ? "mdi-flag-checkered" : "mdi-flag-off-outline" }}
</v-icon>
<span class="calib-btn-label">{{
useStateStore().calibrationData.hasEnoughImages ? "Finish Calibration" : "Cancel Calibration"
}}</span>
</v-btn>
</v-col>
</v-row>
<v-row justify="center">
<v-col cols="12">
<v-btn
color="accent"
small
outlined
style="width: 100%"
:disabled="!settingsValid"
@click="downloadCalibBoard"
>
<v-icon left class="calib-btn-icon"> mdi-download </v-icon>
<span class="calib-btn-label">Generate Board</span>
</v-btn>
</v-col>
</v-row>
<v-row v-if="isCalibrating" style="display: flex; flex-direction: column">
<pv-switch
v-model="drawAllSnapshots"
class="pt-2"
label="Draw Collected Corners"
:switch-cols="8"
tooltip="Draw all snapshots"
@input="(args) => useCameraSettingsStore().changeCurrentPipelineSetting({ drawAllSnapshots: args }, false)"
<v-card class="mb-3" color="primary" dark>
<v-card-title class="pa-6 pb-3">Camera Calibration</v-card-title>
<v-card-text v-show="!isCalibrating">
<v-card-subtitle class="pt-3 pl-2 pb-3">Current Calibration</v-card-subtitle>
<v-simple-table fixed-header height="100%" dense>
<thead>
<tr>
<th>Resolution</th>
<th>Mean Error</th>
<th>Horizontal FOV</th>
<th>Vertical FOV</th>
<th>Diagonal FOV</th>
<th>Info</th>
</tr>
</thead>
<tbody style="cursor: pointer">
<tr v-for="(value, index) in getUniqueVideoFormatsByResolution()" :key="index">
<td>{{ getResolutionString(value.resolution) }}</td>
<td>
{{ value.mean !== undefined ? (isNaN(value.mean) ? "Unknown" : value.mean.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>
<v-tooltip bottom>
<template #activator="{ on, attrs }">
<td v-bind="attrs" v-on="on" @click="setSelectedVideoFormat(value)">
<v-icon small class="mr-2">mdi-information</v-icon>
</td>
</template>
<span>Click for more info on this calibration.</span>
</v-tooltip>
</tr>
</tbody>
</v-simple-table>
</v-card-text>
<v-card-text v-if="useCameraSettingsStore().isConnected" class="d-flex flex-column pa-6 pt-0">
<v-card-subtitle v-show="!isCalibrating" class="pl-0">Configure New Calibration</v-card-subtitle>
<v-form ref="form" v-model="settingsValid">
<!-- TODO: the default videoFormatIndex is 0, but the list of unique video mode indexes might not include 0. getUniqueVideoResolutionStrings indexing is also different from the normal video mode indexing -->
<pv-select
v-model="useStateStore().calibrationData.videoFormatIndex"
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()"
/>
</v-row>
</div>
<pv-select
v-show="isCalibrating && boardType != CalibrationBoardTypes.Charuco"
v-model="useCameraSettingsStore().currentPipelineSettings.streamingFrameDivisor"
label="Decimation"
tooltip="Resolution to which camera frames are downscaled for detection. Calibration still uses full-res"
:items="calibrationDivisors"
:select-cols="8"
@input="(v) => useCameraSettingsStore().changeCurrentPipelineSetting({ streamingFrameDivisor: +v }, false)"
/>
<pv-select
v-model="boardType"
label="Board Type"
tooltip="Calibration board pattern to use"
:select-cols="8"
:items="['Chessboard', 'Charuco']"
:disabled="isCalibrating"
/>
<pv-select
v-show="boardType == CalibrationBoardTypes.Charuco"
v-model="tagFamily"
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']"
:disabled="isCalibrating"
/>
<pv-number-input
v-model="squareSizeIn"
label="Pattern Spacing (in)"
tooltip="Spacing between pattern features in inches"
:disabled="isCalibrating"
:rules="[(v) => v > 0 || 'Size must be positive']"
:label-cols="4"
/>
<pv-number-input
v-show="boardType == CalibrationBoardTypes.Charuco"
v-model="markerSizeIn"
label="Marker Size (in)"
tooltip="Size of the tag markers in inches must be smaller than pattern spacing"
:disabled="isCalibrating"
:rules="[(v) => v > 0 || 'Size must be positive']"
:label-cols="4"
/>
<pv-number-input
v-model="patternWidth"
label="Board Width (squares)"
tooltip="Width of the board in dots or chessboard squares"
:disabled="isCalibrating"
:rules="[(v) => v >= 4 || 'Width must be at least 4']"
:label-cols="4"
/>
<pv-number-input
v-model="patternHeight"
label="Board Height (squares)"
tooltip="Height of the board in dots or chessboard squares"
:disabled="isCalibrating"
:rules="[(v) => v >= 4 || 'Height must be at least 4']"
:label-cols="4"
/>
<pv-switch
v-show="boardType == CalibrationBoardTypes.Charuco"
v-model="useOldPattern"
label="Old OpenCV Pattern"
:disabled="isCalibrating"
tooltip="If enabled, Photon will use the old OpenCV pattern for calibration."
:label-cols="4"
/>
<v-banner
v-if="useSettingsStore().general.mrCalWorking"
rounded
color="secondary"
text-color="white"
class="mt-3"
icon="mdi-alert-circle-outline"
>
Mrcal was successfully loaded and will be used!
</v-banner>
<v-banner v-else rounded color="error" text-color="white" class="mt-3" icon="mdi-alert-circle-outline">
MrCal JNI could not be loaded! Consult journalctl logs for additional details.
</v-banner>
</v-form>
</v-card-text>
<v-card-text v-if="isCalibrating" class="pa-6 pt-0">
<pv-switch
v-model="drawAllSnapshots"
label="Draw Collected Corners"
:switch-cols="8"
tooltip="Draw all snapshots"
@input="(args) => useCameraSettingsStore().changeCurrentPipelineSetting({ drawAllSnapshots: args }, false)"
/>
<pv-switch
v-model="useCameraSettingsStore().currentPipelineSettings.cameraAutoExposure"
label="Auto Exposure"
:label-cols="4"
tooltip="Enables or Disables camera automatic adjustment for current lighting conditions"
@input="(args) => useCameraSettingsStore().changeCurrentPipelineSetting({ cameraAutoExposure: args }, false)"
/>
<pv-slider
v-model="useCameraSettingsStore().currentPipelineSettings.cameraExposureRaw"
:disabled="useCameraSettingsStore().currentCameraSettings.pipelineSettings.cameraAutoExposure"
label="Exposure"
tooltip="Directly controls how long the camera shutter remains open. Units are dependant on the underlying driver."
:min="useCameraSettingsStore().minExposureRaw"
:max="useCameraSettingsStore().maxExposureRaw"
:slider-cols="7"
:step="1"
@input="(args) => useCameraSettingsStore().changeCurrentPipelineSetting({ cameraExposureRaw: args }, false)"
/>
<pv-slider
v-model="useCameraSettingsStore().currentPipelineSettings.cameraBrightness"
label="Brightness"
:min="0"
:max="100"
:slider-cols="7"
@input="(args) => useCameraSettingsStore().changeCurrentPipelineSetting({ cameraBrightness: args }, false)"
/>
<pv-slider
v-if="useCameraSettingsStore().currentPipelineSettings.cameraGain >= 0"
v-model="useCameraSettingsStore().currentPipelineSettings.cameraGain"
label="Camera Gain"
tooltip="Controls camera gain, similar to brightness"
:min="0"
:max="100"
:slider-cols="7"
@input="(args) => useCameraSettingsStore().changeCurrentPipelineSetting({ cameraGain: args }, false)"
/>
<pv-slider
v-if="useCameraSettingsStore().currentPipelineSettings.cameraRedGain !== -1"
v-model="useCameraSettingsStore().currentPipelineSettings.cameraRedGain"
label="Red AWB Gain"
:min="0"
:max="100"
:slider-cols="7"
tooltip="Controls red automatic white balance gain, which affects how the camera captures colors in different conditions"
@input="(args) => useCameraSettingsStore().changeCurrentPipelineSetting({ cameraRedGain: args }, false)"
/>
<pv-slider
v-if="useCameraSettingsStore().currentPipelineSettings.cameraBlueGain !== -1"
v-model="useCameraSettingsStore().currentPipelineSettings.cameraBlueGain"
label="Blue AWB Gain"
:min="0"
:max="100"
:slider-cols="7"
tooltip="Controls blue automatic white balance gain, which affects how the camera captures colors in different conditions"
@input="(args) => useCameraSettingsStore().changeCurrentPipelineSetting({ cameraBlueGain: args }, false)"
/>
<v-banner
v-if="tooManyPoints"
rounded
class="mt-3"
color="error"
text-color="white"
icon="mdi-alert-circle-outline"
>
Too many corners. Finish calibration now!
</v-banner>
</v-card-text>
<v-card-text v-if="isCalibrating" class="d-flex justify-center align-center pa-6 pt-0">
<v-chip label :color="useStateStore().calibrationData.hasEnoughImages ? 'secondary' : 'gray'">
Snapshots: {{ useStateStore().calibrationData.imageCount }} of at least
{{ useStateStore().calibrationData.minimumImageCount }}
</v-chip>
</v-card-text>
<v-card-text class="d-flex pa-6 pt-0">
<v-col cols="6" class="pa-0 pr-2">
<v-btn
small
block
color="secondary"
:disabled="!settingsValid || tooManyPoints"
@click="isCalibrating ? useCameraSettingsStore().takeCalibrationSnapshot() : startCalibration()"
>
<v-icon left class="calib-btn-icon"> {{ isCalibrating ? "mdi-camera" : "mdi-flag-outline" }} </v-icon>
<span class="calib-btn-label">{{ isCalibrating ? "Take Snapshot" : "Start Calibration" }}</span>
</v-btn>
</v-col>
<v-col cols="6" class="pa-0 pl-2">
<v-btn
small
block
:color="useStateStore().calibrationData.hasEnoughImages ? 'accent' : 'error'"
:class="useStateStore().calibrationData.hasEnoughImages ? 'black--text' : 'white---text'"
:disabled="!isCalibrating || !settingsValid"
@click="endCalibration"
>
<v-icon left class="calib-btn-icon">
{{ useStateStore().calibrationData.hasEnoughImages ? "mdi-flag-checkered" : "mdi-flag-off-outline" }}
</v-icon>
<span class="calib-btn-label">{{
useStateStore().calibrationData.hasEnoughImages ? "Finish Calibration" : "Cancel Calibration"
}}</span>
</v-btn>
</v-col>
</v-card-text>
<v-card-text class="pa-6 pt-0">
<v-btn color="accent" small block outlined :disabled="!settingsValid" @click="downloadCalibBoard">
<v-icon left class="calib-btn-icon"> mdi-download </v-icon>
<span class="calib-btn-label">Generate Board</span>
</v-btn>
</v-card-text>
</v-card>
<v-dialog v-model="showCalibEndDialog" width="500px" :persistent="true">
<v-card color="primary" dark>

View File

@@ -91,16 +91,14 @@ const expanded = ref([]);
</script>
<template>
<v-card dark class="pr-6 pb-3" style="background-color: #006492">
<v-card dark style="background-color: #006492">
<v-card-title>Camera Control</v-card-title>
<v-row class="pl-6">
<v-col>
<v-btn color="secondary" @click="fetchSnapshots">
<v-icon left class="open-icon"> mdi-folder </v-icon>
<span class="open-label">Show Saved Snapshots</span>
</v-btn>
</v-col>
</v-row>
<v-card-text>
<v-btn color="secondary" @click="fetchSnapshots">
<v-icon left class="open-icon"> mdi-folder </v-icon>
<span class="open-label">Show Saved Snapshots</span>
</v-btn>
</v-card-text>
<v-dialog v-model="showSnapshotViewerDialog">
<v-card dark class="pt-3 pl-5 pr-5" color="primary" flat>
<v-card-title> View Saved Frame Snapshots </v-card-title>

View File

@@ -119,7 +119,11 @@ const openExportSettingsPrompt = () => {
};
const yesDeleteMySettingsText = ref("");
const deletingCamera = ref(false);
const deleteThisCamera = () => {
if (deletingCamera.value) return;
deletingCamera.value = true;
const payload = {
cameraUniqueName: useStateStore().currentCameraUniqueName
};
@@ -149,8 +153,11 @@ const deleteThisCamera = () => {
color: "error"
});
}
})
.finally(() => {
deletingCamera.value = false;
showDeleteCamera.value = false;
});
showDeleteCamera.value = false;
};
const wrappedCameras = computed<SelectItem[]>(() =>
Object.keys(useCameraSettingsStore().cameras).map((cameraUniqueName) => ({
@@ -161,9 +168,9 @@ const wrappedCameras = computed<SelectItem[]>(() =>
</script>
<template>
<v-card class="mb-3 pr-6 pb-3" color="primary" dark>
<v-card-title>Camera Settings</v-card-title>
<div class="ml-5">
<v-card class="mb-3" color="primary" dark>
<v-card-title class="pa-6 pb-0">Camera Settings</v-card-title>
<v-card-text class="pa-6 pt-3">
<pv-select
v-model="useStateStore().currentCameraUniqueName"
label="Camera"
@@ -193,74 +200,67 @@ const wrappedCameras = computed<SelectItem[]>(() =>
]"
:select-cols="8"
/>
<br />
<v-row>
<v-col cols="6">
</v-card-text>
<v-card-text class="d-flex pa-6 pt-0">
<v-col cols="6" class="pa-0 pr-2">
<v-btn block small color="secondary" :disabled="!settingsHaveChanged()" @click="saveCameraSettings">
<v-icon left> mdi-content-save </v-icon>
Save Changes
</v-btn>
</v-col>
<v-col cols="6" class="pa-0 pl-2">
<v-btn block small color="error" @click="() => (showDeleteCamera = true)">
<v-icon left> mdi-trash-can-outline </v-icon>
Delete Camera
</v-btn>
</v-col>
</v-card-text>
<v-dialog v-model="showDeleteCamera" dark width="800">
<v-card dark class="dialog-container pa-3 pb-2" color="primary" flat>
<v-card-title> Delete {{ useCameraSettingsStore().currentCameraSettings.nickname }}? </v-card-title>
<v-card-text>
<v-row class="align-center pt-6">
<v-col cols="12" md="6">
<span class="white--text"> This will delete ALL OF YOUR SETTINGS and restart PhotonVision. </span>
</v-col>
<v-col cols="12" md="6">
<v-btn color="secondary" block @click="openExportSettingsPrompt">
<v-icon left class="open-icon"> mdi-export </v-icon>
<span class="open-label">Backup Settings</span>
<a
ref="exportSettings"
style="color: black; text-decoration: none; display: none"
:href="`http://${address}/api/settings/photonvision_config.zip`"
download="photonvision-settings.zip"
target="_blank"
/>
</v-btn>
</v-col>
</v-row>
</v-card-text>
<v-card-text>
<pv-input
v-model="yesDeleteMySettingsText"
:label="'Type &quot;' + useCameraSettingsStore().currentCameraName + '&quot;:'"
:label-cols="6"
:input-cols="6"
/>
</v-card-text>
<v-card-text>
<v-btn
class="mt-2 mb-3"
style="width: 100%"
small
color="secondary"
:disabled="!settingsHaveChanged()"
@click="saveCameraSettings"
>
<v-icon left> mdi-content-save </v-icon>
Save Changes
</v-btn>
</v-col>
<v-col cols="6">
<v-btn class="mt-2 mb-3" style="width: 100%" small color="red" @click="() => (showDeleteCamera = true)">
<v-icon left> mdi-bomb </v-icon>
Delete Camera
</v-btn>
</v-col>
</v-row>
</div>
<v-dialog v-model="showDeleteCamera" dark width="1500">
<v-card dark class="dialog-container pa-6" color="primary" flat>
<v-card-title>Delete camera "{{ useCameraSettingsStore().currentCameraName }}"</v-card-title>
<v-row class="pl-3 align-center pa-6">
<v-col cols="12" md="6">
<span class="mt-3"> This will delete ALL OF YOUR SETTINGS and restart PhotonVision. </span>
</v-col>
<v-col cols="12" md="6">
<v-btn color="secondary" style="float: right" @click="openExportSettingsPrompt">
<v-icon left class="open-icon"> mdi-export </v-icon>
<span class="open-label">Backup Settings</span>
<a
ref="exportSettings"
style="color: black; text-decoration: none; display: none"
:href="`http://${address}/api/settings/photonvision_config.zip`"
download="photonvision-settings.zip"
target="_blank"
/>
</v-btn>
</v-col>
</v-row>
<v-divider class="mt-4 mb-4" />
<v-row class="pl-3 align-center pa-6">
<v-col>
<pv-input
v-model="yesDeleteMySettingsText"
:label="'Type &quot;' + useCameraSettingsStore().currentCameraName + '&quot;:'"
:label-cols="12"
:input-cols="12"
/>
</v-col>
<v-btn
color="red"
block
color="error"
:disabled="
yesDeleteMySettingsText.toLowerCase() !== useCameraSettingsStore().currentCameraName.toLowerCase()
"
:loading="deletingCamera"
@click="deleteThisCamera"
>
<v-icon left class="open-icon"> mdi-skull </v-icon>
<v-icon left class="open-icon"> mdi-trash-can-outline </v-icon>
<span class="open-label">DELETE (UNRECOVERABLE)</span>
</v-btn>
</v-row>
</v-card-text>
</v-card>
</v-dialog>
</v-card>

View File

@@ -41,17 +41,9 @@ const fpsTooLow = computed<boolean>(() => {
</script>
<template>
<v-card
id="camera-settings-camera-view-card"
class="camera-settings-camera-view-card mb-3 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">
<v-card id="camera-settings-camera-view-card" class="camera-settings-camera-view-card" color="primary" dark>
<v-card-title class="justify-space-between align-content-center pa-0 pl-6 pr-6">
<div class="d-flex flex-wrap pt-4 pb-4">
<div>
<span class="mr-4" style="white-space: nowrap"> Cameras </span>
</div>
@@ -69,23 +61,23 @@ const fpsTooLow = computed<boolean>(() => {
</span>
</v-chip>
<v-chip v-else label color="transparent" text-color="red" style="font-size: 1rem; padding: 0; margin: 0">
<span class="pr-1"> Camera not connected </span>
<span class="pr-1">Camera not connected</span>
</v-chip>
</div>
</div>
<div>
<div class="d-flex align-center">
<v-switch
v-model="driverMode"
:disabled="useCameraSettingsStore().isCalibrationMode || useCameraSettingsStore().pipelineNames.length === 0"
label="Driver Mode"
style="margin-left: auto"
color="accent"
class="pt-2"
class="pt-2 pb-2"
hide-details="auto"
/>
</div>
</v-card-title>
<div class="stream-container pb-4">
<v-card-text class="stream-container">
<div class="stream">
<photon-camera-stream
v-if="value.includes(0)"
@@ -104,10 +96,8 @@ const fpsTooLow = computed<boolean>(() => {
style="max-width: 100%"
/>
</div>
</div>
<v-divider />
<div class="pt-4">
<p style="color: white">Stream Display</p>
</v-card-text>
<v-card-text class="pt-0">
<v-btn-toggle v-model="localValue" :multiple="true" mandatory dark class="fill" style="width: 100%">
<v-btn
color="secondary"
@@ -126,7 +116,7 @@ const fpsTooLow = computed<boolean>(() => {
<span class="mode-btn-label">Processed</span>
</v-btn>
</v-btn-toggle>
</div>
</v-card-text>
</v-card>
</template>

View File

@@ -1,15 +1,10 @@
<script setup lang="ts">
import { PVCameraInfo } from "@/types/SettingTypes";
const { camera, showTitle } = defineProps({
const { camera } = defineProps({
camera: {
type: PVCameraInfo,
required: true
},
showTitle: {
type: Boolean,
required: false,
default: true
}
});
@@ -29,12 +24,6 @@ const cameraInfoFor: any = (camera: PVCameraInfo) => {
<template>
<div>
<div v-if="showTitle === true">
<h3 v-if="camera.PVUsbCameraInfo" class="mb-3">USB Camera Info</h3>
<h3 v-if="camera.PVCSICameraInfo" class="mb-3">CSI Camera Info</h3>
<h3 v-if="camera.PVFileCameraInfo" class="mb-3">File Camera Info</h3>
</div>
<v-simple-table dense :style="{ backgroundColor: 'var(--v-primary-base)' }">
<tbody>
<tr v-if="cameraInfoFor(camera).dev !== undefined && cameraInfoFor(camera).dev !== null">
@@ -45,6 +34,13 @@ const cameraInfoFor: any = (camera: PVCameraInfo) => {
<td>Name:</td>
<td>{{ cameraInfoFor(camera).name }}</td>
</tr>
<tr>
<td>Type:</td>
<td v-if="camera.PVUsbCameraInfo" class="mb-3">USB Camera</td>
<td v-else-if="camera.PVCSICameraInfo" class="mb-3">CSI Camera</td>
<td v-else-if="camera.PVFileCameraInfo" class="mb-3">File Camera</td>
<td v-else>Unidentified Camera Type</td>
</tr>
<tr v-if="cameraInfoFor(camera).baseName !== undefined && cameraInfoFor(camera).baseName !== null">
<td>Base Name:</td>
<td>{{ cameraInfoFor(camera).baseName }}</td>

View File

@@ -1,12 +1,13 @@
<script setup lang="ts">
import { PVCameraInfo } from "@/types/SettingTypes";
import _ from "lodash";
const { saved, matched } = defineProps({
const { saved, current } = defineProps({
saved: {
type: PVCameraInfo,
required: true
},
matched: {
current: {
type: PVCameraInfo,
required: true
}
@@ -28,58 +29,95 @@ const cameraInfoFor = (camera: PVCameraInfo): any => {
<template>
<div>
<h3 v-if="saved.PVUsbCameraInfo" class="mb-3">USB Camera Info</h3>
<h3 v-if="saved.PVCSICameraInfo" class="mb-3">CSI Camera Info</h3>
<h3 v-if="saved.PVFileCameraInfo" class="mb-3">File Camera Info</h3>
<v-simple-table dense :style="{ backgroundColor: 'var(--v-primary-base)' }">
<tbody>
<tr>
<th></th>
<th>Saved</th>
<th>Matched</th>
<th>Current</th>
</tr>
<tr v-if="cameraInfoFor(saved).dev !== undefined && cameraInfoFor(saved).dev !== null">
<tr
v-if="cameraInfoFor(saved).dev !== undefined && cameraInfoFor(saved).dev !== null"
:class="cameraInfoFor(saved).dev !== cameraInfoFor(current).dev ? 'mismatch' : ''"
>
<td>Device Number:</td>
<td>{{ cameraInfoFor(saved).dev }}</td>
<td>{{ cameraInfoFor(matched).dev }}</td>
<td>{{ cameraInfoFor(current).dev }}</td>
</tr>
<tr v-if="cameraInfoFor(saved).name !== undefined && cameraInfoFor(saved).name !== null">
<tr
v-if="cameraInfoFor(saved).name !== undefined && cameraInfoFor(saved).name !== null"
:class="cameraInfoFor(saved).name !== cameraInfoFor(current).name ? 'mismatch' : ''"
>
<td>Name:</td>
<td>{{ cameraInfoFor(saved).name }}</td>
<td>{{ cameraInfoFor(matched).name }}</td>
<td>{{ cameraInfoFor(current).name }}</td>
</tr>
<tr v-if="cameraInfoFor(saved).baseName !== undefined && cameraInfoFor(saved).baseName !== null">
<tr
v-if="cameraInfoFor(saved).baseName !== undefined && cameraInfoFor(saved).baseName !== null"
:class="cameraInfoFor(saved).baseName !== cameraInfoFor(current).baseName ? 'mismatch' : ''"
>
<td>Base Name:</td>
<td>{{ cameraInfoFor(saved).baseName }}</td>
<td>{{ cameraInfoFor(matched).baseName }}</td>
<td>{{ cameraInfoFor(current).baseName }}</td>
</tr>
<tr v-if="cameraInfoFor(saved).vendorId !== undefined && cameraInfoFor(saved).vendorId !== null">
<tr>
<td>Type:</td>
<td v-if="saved.PVUsbCameraInfo" class="mb-3">USB Camera</td>
<td v-else-if="saved.PVCSICameraInfo" class="mb-3">CSI Camera</td>
<td v-else-if="saved.PVFileCameraInfo" class="mb-3">File Camera</td>
<td v-else>Unidentified Camera Type</td>
<td v-if="current.PVUsbCameraInfo" class="mb-3">USB Camera</td>
<td v-else-if="current.PVCSICameraInfo" class="mb-3">CSI Camera</td>
<td v-else-if="current.PVFileCameraInfo" class="mb-3">File Camera</td>
<td v-else>Unidentified Camera Type</td>
</tr>
<tr
v-if="cameraInfoFor(saved).vendorId !== undefined && cameraInfoFor(saved).vendorId !== null"
:class="cameraInfoFor(saved).vendorId !== cameraInfoFor(current).vendorId ? 'mismatch' : ''"
>
<td>Vendor ID:</td>
<td>{{ cameraInfoFor(saved).vendorId }}</td>
<td>{{ cameraInfoFor(matched).vendorId }}</td>
<td>{{ cameraInfoFor(current).vendorId }}</td>
</tr>
<tr v-if="cameraInfoFor(saved).productId !== undefined && cameraInfoFor(saved).productId !== null">
<tr
v-if="cameraInfoFor(saved).productId !== undefined && cameraInfoFor(saved).productId !== null"
:class="cameraInfoFor(saved).productId !== cameraInfoFor(current).productId ? 'mismatch' : ''"
>
<td>Product ID:</td>
<td>{{ cameraInfoFor(saved).productId }}</td>
<td>{{ cameraInfoFor(matched).productId }}</td>
<td>{{ cameraInfoFor(current).productId }}</td>
</tr>
<tr v-if="cameraInfoFor(saved).path !== undefined && cameraInfoFor(saved).path !== null">
<tr
v-if="cameraInfoFor(saved).path !== undefined && cameraInfoFor(saved).path !== null"
:class="cameraInfoFor(saved).path !== cameraInfoFor(current).path ? 'mismatch' : ''"
>
<td>Path:</td>
<td style="word-break: break-all">{{ cameraInfoFor(saved).path }}</td>
<td style="word-break: break-all">{{ cameraInfoFor(matched).path }}</td>
<td style="word-break: break-all">{{ cameraInfoFor(current).path }}</td>
</tr>
<tr v-if="cameraInfoFor(saved).otherPaths !== undefined && cameraInfoFor(saved).otherPaths !== null">
<tr
v-if="cameraInfoFor(saved).otherPaths !== undefined && cameraInfoFor(saved).otherPaths !== null"
:class="!_.isEqual(cameraInfoFor(saved).otherPaths, cameraInfoFor(current).otherPaths) ? 'mismatch' : ''"
>
<td>Other Paths:</td>
<td>{{ cameraInfoFor(saved).otherPaths }}</td>
<td>{{ cameraInfoFor(matched).otherPaths }}</td>
<td>{{ cameraInfoFor(current).otherPaths }}</td>
</tr>
<tr v-if="cameraInfoFor(saved).uniquePath !== undefined && cameraInfoFor(saved).uniquePath !== null">
<tr
v-if="cameraInfoFor(saved).uniquePath !== undefined && cameraInfoFor(saved).uniquePath !== null"
:class="cameraInfoFor(saved).uniquePath !== cameraInfoFor(current).uniquePath ? 'mismatch' : ''"
>
<td>Unique Path:</td>
<td style="word-break: break-all">{{ cameraInfoFor(saved).uniquePath }}</td>
<td style="word-break: break-all">{{ cameraInfoFor(matched).uniquePath }}</td>
<td style="word-break: break-all">{{ cameraInfoFor(current).uniquePath }}</td>
</tr>
</tbody>
</v-simple-table>
</div>
</template>
<style scoped>
.mismatch {
background: #39a4d546 !important;
}
</style>

View File

@@ -48,26 +48,24 @@ const handleKeydown = ({ key }) => {
</script>
<template>
<div>
<v-row dense align="center">
<v-col :cols="labelCols || 12 - inputCols">
<tooltipped-label :tooltip="tooltip" :label="label" />
</v-col>
<div class="d-flex">
<v-col :cols="labelCols || 12 - inputCols" class="d-flex align-center pl-0">
<tooltipped-label :tooltip="tooltip" :label="label" />
</v-col>
<v-col :cols="inputCols">
<v-text-field
v-model="localValue"
dark
dense
color="accent"
:placeholder="placeholder"
:disabled="disabled"
:error-messages="errorMessage"
:rules="rules"
class="mt-1 pt-2"
@keydown="handleKeydown"
/>
</v-col>
</v-row>
<v-col :cols="inputCols" class="d-flex align-center pr-0">
<v-text-field
v-model="localValue"
dark
dense
color="accent"
:placeholder="placeholder"
:disabled="disabled"
:error-messages="errorMessage"
:rules="rules"
hide-details="auto"
@keydown="handleKeydown"
/>
</v-col>
</div>
</template>

View File

@@ -31,26 +31,24 @@ const localValue = computed({
</script>
<template>
<div>
<v-row dense align="center">
<v-col :cols="labelCols">
<tooltipped-label :tooltip="tooltip" :label="label" />
</v-col>
<v-col>
<v-text-field
v-model="localValue"
dark
class="mt-0 pt-0"
hide-details
single-line
color="accent"
type="number"
style="width: 70px"
:step="step"
:disabled="disabled"
:rules="rules"
/>
</v-col>
</v-row>
<div class="d-flex">
<v-col :cols="labelCols" class="d-flex pl-0 align-center">
<tooltipped-label :tooltip="tooltip" :label="label" />
</v-col>
<v-col class="pr-0">
<v-text-field
v-model="localValue"
dark
class="mt-0 pt-0"
hide-details
single-line
color="accent"
type="number"
style="width: 70px"
:step="step"
:disabled="disabled"
:rules="rules"
/>
</v-col>
</div>
</template>

View File

@@ -29,23 +29,21 @@ const localValue = computed({
</script>
<template>
<div>
<v-row dense align="center">
<v-col :cols="12 - inputCols">
<tooltipped-label :tooltip="tooltip" :label="label" />
</v-col>
<v-col :cols="inputCols">
<v-radio-group v-model="localValue" row dark :mandatory="true">
<v-radio
v-for="(radioName, index) in list"
:key="index"
color="#ffd843"
:label="radioName"
:value="index"
:disabled="disabled"
/>
</v-radio-group>
</v-col>
</v-row>
<div class="d-flex">
<v-col :cols="12 - inputCols" class="d-flex align-center pl-0">
<tooltipped-label :tooltip="tooltip" :label="label" />
</v-col>
<v-col :cols="inputCols" class="d-flex align-center pr-0">
<v-radio-group v-model="localValue" row dark :mandatory="true" hide-details="auto">
<v-radio
v-for="(radioName, index) in list"
:key="index"
color="#ffd843"
:label="radioName"
:value="index"
:disabled="disabled"
/>
</v-radio-group>
</v-col>
</div>
</template>

View File

@@ -58,61 +58,59 @@ const checkNumberRange = (v: string): boolean => {
</script>
<template>
<div>
<v-row dense align="center">
<v-col :cols="12 - sliderCols">
<tooltipped-label :tooltip="tooltip" :label="label" />
</v-col>
<v-col :cols="sliderCols">
<v-range-slider
v-model="localValue"
:max="max"
:min="min"
:disabled="disabled"
hide-details
class="align-center"
dark
:color="inverted ? 'rgba(255, 255, 255, 0.2)' : 'accent'"
:track-color="inverted ? 'accent' : undefined"
thumb-color="accent"
:step="step"
>
<template #prepend>
<v-text-field
:value="localValue[0]"
dark
color="accent"
class="mt-0 pt-0"
hide-details
single-line
:max="max"
:min="min"
:step="step"
:rules="[checkNumberRange]"
type="number"
style="width: 60px"
@input="(v) => changeFromSlot(v, 0)"
/>
</template>
<template #append>
<v-text-field
:value="localValue[1]"
dark
color="accent"
class="mt-0 pt-0"
hide-details
single-line
:max="max"
:min="min"
:step="step"
:rules="[checkNumberRange]"
type="number"
style="width: 60px"
@input="(v) => changeFromSlot(v, 1)"
/>
</template>
</v-range-slider>
</v-col>
</v-row>
<div class="d-flex">
<v-col :cols="12 - sliderCols" class="d-flex align-center pl-0">
<tooltipped-label :tooltip="tooltip" :label="label" />
</v-col>
<v-col :cols="sliderCols" class="pr-0">
<v-range-slider
v-model="localValue"
:max="max"
:min="min"
:disabled="disabled"
hide-details
class="align-center"
dark
:color="inverted ? 'rgba(255, 255, 255, 0.2)' : 'accent'"
:track-color="inverted ? 'accent' : undefined"
thumb-color="accent"
:step="step"
>
<template #prepend>
<v-text-field
:value="localValue[0]"
dark
color="accent"
class="mt-0 pt-0"
hide-details
single-line
:max="max"
:min="min"
:step="step"
:rules="[checkNumberRange]"
type="number"
style="width: 60px"
@input="(v) => changeFromSlot(v, 0)"
/>
</template>
<template #append>
<v-text-field
:value="localValue[1]"
dark
color="accent"
class="mt-0 pt-0"
hide-details
single-line
:max="max"
:min="min"
:step="step"
:rules="[checkNumberRange]"
type="number"
style="width: 60px"
@input="(v) => changeFromSlot(v, 1)"
/>
</template>
</v-range-slider>
</v-col>
</div>
</template>

View File

@@ -49,24 +49,28 @@ const items = computed<SelectItem[]>(() => {
</script>
<template>
<div>
<v-row dense align="center">
<v-col :cols="12 - selectCols">
<tooltipped-label :tooltip="tooltip" :label="label" />
</v-col>
<v-col :cols="selectCols">
<v-select
v-model="localValue"
:items="items"
item-text="name"
item-value="value"
item-disabled="disabled"
dark
color="accent"
item-color="secondary"
:disabled="disabled"
/>
</v-col>
</v-row>
<div class="d-flex">
<v-col :cols="12 - selectCols" class="d-flex align-center pl-0">
<tooltipped-label :tooltip="tooltip" :label="label" />
</v-col>
<v-col :cols="selectCols" class="d-flex align-center pr-0">
<v-select
v-model="localValue"
:items="items"
item-text="name"
item-value="value"
item-disabled="disabled"
dark
color="accent"
item-color="secondary"
:disabled="disabled"
hide-details="auto"
/>
</v-col>
</div>
</template>
<style>
.v-select {
padding-top: 0px;
}
</style>

View File

@@ -45,47 +45,45 @@ const localValue = computed({
</script>
<template>
<div>
<v-row dense align="center">
<v-col :cols="12 - sliderCols - 1">
<tooltipped-label :tooltip="tooltip" :label="label" />
</v-col>
<v-col :cols="sliderCols">
<v-slider
v-model="localValue"
dark
class="align-center"
:max="max"
:min="min"
hide-details
color="accent"
:disabled="disabled"
:step="step"
append-icon="mdi-menu-right"
prepend-icon="mdi-menu-left"
@click:append="localValue += step"
@click:prepend="localValue -= step"
/>
</v-col>
<v-col :cols="1">
<v-text-field
:value="localValue"
dark
color="accent"
:max="max"
:min="min"
:disabled="disabled"
class="mt-0 pt-0"
hide-details
single-line
type="number"
style="width: 45px"
:step="step"
:hide-spin-buttons="true"
@keyup.enter="localValue = $event.target.value"
@blur="localValue = $event.target.value"
/>
</v-col>
</v-row>
<div class="d-flex">
<v-col :cols="12 - sliderCols - 1" class="pl-0 d-flex align-center">
<tooltipped-label :tooltip="tooltip" :label="label" />
</v-col>
<v-col :cols="sliderCols">
<v-slider
v-model="localValue"
dark
class="align-center"
:max="max"
:min="min"
hide-details
color="accent"
:disabled="disabled"
:step="step"
append-icon="mdi-menu-right"
prepend-icon="mdi-menu-left"
@click:append="localValue += step"
@click:prepend="localValue -= step"
/>
</v-col>
<v-col :cols="1" class="pr-0">
<v-text-field
:value="localValue"
dark
color="accent"
:max="max"
:min="min"
:disabled="disabled"
class="mt-0 pt-0"
hide-details
single-line
type="number"
style="width: 100%"
:step="step"
:hide-spin-buttons="true"
@keyup.enter="localValue = $event.target.value"
@blur="localValue = $event.target.value"
/>
</v-col>
</div>
</template>

View File

@@ -11,11 +11,13 @@ const props = withDefaults(
disabled?: boolean;
labelCols?: number;
switchCols?: number;
dense?: boolean;
}>(),
{
disabled: false,
labelCols: 2,
switchCols: 8
switchCols: 8,
dense: false
}
);
@@ -30,14 +32,17 @@ const localValue = computed({
</script>
<template>
<div>
<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>
</v-row>
<div class="d-flex">
<v-col :cols="12 - switchCols || labelCols" class="d-flex align-center pl-0">
<tooltipped-label :tooltip="tooltip" :label="label" />
</v-col>
<v-col :cols="switchCols || 12 - labelCols" class="d-flex align-center pr-0">
<v-switch v-model="localValue" dark :disabled="disabled" color="#ffd843" hide-details="auto" class="pb-1" />
</v-col>
</div>
</template>
<style>
.v-input--selection-controls {
margin-top: 0px;
}
</style>

View File

@@ -242,7 +242,7 @@ const wrappedCameras = computed<SelectItem[]>(() =>
<template>
<v-card color="primary">
<v-row style="padding: 12px 12px 0 24px">
<v-row style="padding: 20px 12px 0 30px">
<v-col cols="10" class="pa-0">
<pv-select
v-if="!isCameraNameEdit"
@@ -281,7 +281,7 @@ const wrappedCameras = computed<SelectItem[]>(() =>
/>
</v-col>
</v-row>
<v-row style="padding: 0 12px 0 24px">
<v-row style="padding: 0 12px 0 30px">
<v-col cols="10" class="pa-0">
<pv-select
v-if="!isPipelineNameEdit"
@@ -353,7 +353,7 @@ const wrappedCameras = computed<SelectItem[]>(() =>
/>
</v-col>
</v-row>
<v-row style="padding: 0 12px 12px 24px">
<v-row style="padding: 0 12px 24px 30px">
<v-col cols="10" class="pa-0">
<pv-select
v-model="currentPipelineType"
@@ -392,7 +392,12 @@ const wrappedCameras = computed<SelectItem[]>(() =>
<v-divider />
<v-card-actions>
<v-spacer />
<v-btn color="#ffd843" :disabled="checkPipelineName(newPipelineName) !== true" @click="createNewPipeline">
<v-btn
color="#ffd843"
class="black--text"
:disabled="checkPipelineName(newPipelineName) !== true"
@click="createNewPipeline"
>
Save
</v-btn>
<v-btn color="error" @click="cancelPipelineCreation"> Cancel </v-btn>
@@ -413,7 +418,9 @@ const wrappedCameras = computed<SelectItem[]>(() =>
<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="#ffd843" class="black--text" @click="showPipelineDeletionConfirmationDialog = false">
No, take me back
</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
@@ -429,7 +436,7 @@ const wrappedCameras = computed<SelectItem[]>(() =>
<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="#ffd843" class="black--text" @click="cancelChangePipelineType"> No, take me back </v-btn>
</v-card-actions>
</v-card>
</v-dialog>

View File

@@ -42,49 +42,33 @@ const performanceRecommendation = computed<string>(() => {
</script>
<template>
<v-card color="primary" height="100%" style="display: flex; flex-direction: column" dark>
<v-row>
<v-col class="align-self-center text-no-wrap">
<v-card-title>Cameras</v-card-title>
</v-col>
<v-col class="align-self-center" style="text-align: right; margin-right: 12px; padding-left: 24px">
<v-chip
v-if="useCameraSettingsStore().currentCameraSettings.isConnected"
label
:color="fpsTooLow ? 'error' : 'transparent'"
:text-color="fpsTooLow ? '#C7EA46' : '#ff4d00'"
style="font-size: 1rem; padding: 0; margin: 0"
>
<span class="pr-1"
>Processing @ {{ Math.round(useStateStore().currentPipelineResults?.fps || 0) }}&nbsp;FPS &ndash;</span
><span>{{ performanceRecommendation }}</span>
</v-chip>
<v-chip v-else label color="transparent" text-color="red" style="font-size: 1rem; padding: 0; margin: 0">
<span class="pr-1"> Camera not connected </span>
</v-chip>
</v-col>
<v-col
class="align-self-center"
style="
width: min-content;
flex-grow: 0;
display: flex;
justify-content: flex-end;
margin-right: 24px;
padding: 0;
"
<v-card color="primary" height="100%" class="d-flex flex-column" dark>
<v-card-title class="justify-space-between align-center pt-3 pb-3">
<span>Cameras</span>
<v-chip
v-if="useCameraSettingsStore().currentCameraSettings.isConnected"
label
:color="fpsTooLow ? 'error' : 'transparent'"
:text-color="fpsTooLow ? '#C7EA46' : '#ff4d00'"
style="font-size: 1rem; padding: 0; margin: 0"
>
<v-switch
v-model="driverMode"
:disabled="useCameraSettingsStore().isCalibrationMode || useCameraSettingsStore().pipelineNames.length === 0"
label="Driver Mode"
style="margin: 0; padding: 0; padding-left: 18px; margin-top: 14px"
color="accent"
/>
</v-col>
</v-row>
<v-divider style="border-color: white" />
<v-row class="stream-viewer-container pa-3">
<span class="pr-1"
>Processing @ {{ Math.round(useStateStore().currentPipelineResults?.fps || 0) }}&nbsp;FPS &ndash;</span
><span>{{ performanceRecommendation }}</span>
</v-chip>
<v-chip v-else label color="transparent" text-color="red" style="font-size: 1rem; padding: 0; margin: 0">
<span class="pr-1"> Camera not connected </span>
</v-chip>
<v-switch
v-model="driverMode"
:disabled="useCameraSettingsStore().isCalibrationMode || useCameraSettingsStore().pipelineNames.length === 0"
label="Driver Mode"
color="accent"
hide-details="auto"
/>
</v-card-title>
<v-divider class="ml-3 mr-3" />
<v-row class="stream-viewer-container pa-3 align-center">
<v-col v-if="value.includes(0)" class="stream-view">
<photon-camera-stream
id="input-camera-stream"

View File

@@ -146,7 +146,7 @@ onBeforeUpdate(() => {
<template>
<v-row no-gutters class="tabGroups">
<template v-if="!useCameraSettingsStore().hasConnected">
<v-col v-if="!useCameraSettingsStore().hasConnected" cols="12">
<v-col cols="12">
<v-card color="error">
<v-card-title class="white--text">
Camera has not connected. Please check your connection and try again.
@@ -173,7 +173,7 @@ onBeforeUpdate(() => {
{{ tabConfig.tabName }}
</v-tab>
</v-tabs>
<div class="pl-4 pr-4 pt-4 pb-2">
<div class="pl-2 pr-2 pt-3 pb-3">
<KeepAlive>
<Component :is="tabGroupData[selectedTabs[tabGroupIndex]].component" />
</KeepAlive>

View File

@@ -34,8 +34,8 @@ const processingMode = computed<number>({
color="primary"
style="height: 100%; display: flex; flex-direction: column"
>
<v-row align="center" class="pa-3 pb-0">
<v-col>
<v-row class="pa-3 pb-0 align-center">
<v-col class="pa-4">
<p style="color: white">Processing Mode</p>
<v-btn-toggle v-model="processingMode" mandatory dark class="fill">
<v-btn color="secondary" :disabled="!useCameraSettingsStore().hasConnected">
@@ -54,8 +54,8 @@ const processingMode = computed<number>({
</v-btn-toggle>
</v-col>
</v-row>
<v-row align="center" class="pa-3 pt-0">
<v-col>
<v-row class="pa-3 pt-0 align-center">
<v-col class="pa-4 pt-0">
<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">

View File

@@ -17,8 +17,8 @@ const currentPipelineSettings = computed<ActivePipelineSettings>(
const interactiveCols = computed(() =>
(getCurrentInstance()?.proxy.$vuetify.breakpoint.mdAndDown || false) &&
(!useStateStore().sidebarFolded || useCameraSettingsStore().isDriverMode)
? 9
: 8
? 8
: 7
);
</script>
@@ -33,7 +33,6 @@ const interactiveCols = computed(() =>
/>
<pv-slider
v-model="currentPipelineSettings.decimate"
class="pt-2"
:slider-cols="interactiveCols"
label="Decimate"
tooltip="Increases FPS at the expense of range by reducing image resolution initially"
@@ -43,7 +42,6 @@ const interactiveCols = computed(() =>
/>
<pv-slider
v-model="currentPipelineSettings.blur"
class="pt-2"
:slider-cols="interactiveCols"
label="Blur"
tooltip="Gaussian blur added to the image, high FPS cost for slightly decreased noise"
@@ -54,7 +52,6 @@ const interactiveCols = computed(() =>
/>
<pv-slider
v-model="currentPipelineSettings.threads"
class="pt-2"
:slider-cols="interactiveCols"
label="Threads"
tooltip="Number of threads spawned by the AprilTag detector"
@@ -62,16 +59,8 @@ const interactiveCols = computed(() =>
:max="8"
@input="(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ threads: value }, false)"
/>
<pv-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)"
/>
<pv-slider
v-model="currentPipelineSettings.decisionMargin"
class="pt-2 pb-4"
:slider-cols="interactiveCols"
label="Decision Margin Cutoff"
tooltip="Tags with a 'margin' (decoding quality score) less than this wil be rejected. Increase this to reduce the number of false positive detections"
@@ -81,7 +70,6 @@ const interactiveCols = computed(() =>
/>
<pv-slider
v-model="currentPipelineSettings.numIterations"
class="pt-2 pb-4"
:slider-cols="interactiveCols"
label="Pose Estimation Iterations"
tooltip="Number of iterations the pose estimation algorithm will run, 50-100 is a good starting point"
@@ -89,5 +77,12 @@ const interactiveCols = computed(() =>
:max="500"
@input="(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ numIterations: value }, false)"
/>
<pv-switch
v-model="currentPipelineSettings.refineEdges"
:switch-cols="interactiveCols"
label="Refine Edges"
tooltip="Further refines the AprilTag corner position initial estimate, suggested left on"
@input="(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ refineEdges: value }, false)"
/>
</div>
</template>

View File

@@ -17,8 +17,8 @@ const currentPipelineSettings = computed<ActivePipelineSettings>(
const interactiveCols = computed(() =>
(getCurrentInstance()?.proxy.$vuetify.breakpoint.mdAndDown || false) &&
(!useStateStore().sidebarFolded || useCameraSettingsStore().isDriverMode)
? 9
: 8
? 8
: 7
);
</script>
@@ -31,14 +31,6 @@ const interactiveCols = computed(() =>
:select-cols="interactiveCols"
@input="(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ tagFamily: value }, false)"
/>
<pv-switch
v-model="currentPipelineSettings.useCornerRefinement"
class="pt-2"
label="Refine Corners"
tooltip="Further refine the initial corners with subpixel accuracy."
:switch-cols="interactiveCols"
@input="(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ useCornerRefinement: value }, false)"
/>
<pv-range-slider
v-model="currentPipelineSettings.threshWinSizes"
label="Thresh Min/Max Size"
@@ -51,7 +43,6 @@ const interactiveCols = computed(() =>
/>
<pv-slider
v-model="currentPipelineSettings.threshStepSize"
class="pt-2"
:slider-cols="interactiveCols"
label="Thresh Step Size"
tooltip="Smaller values will cause more steps between the min/max sizes. More, varied steps can improve detection robustness to lighting, but may decrease performance."
@@ -62,7 +53,6 @@ const interactiveCols = computed(() =>
/>
<pv-slider
v-model="currentPipelineSettings.threshConstant"
class="pt-2"
:slider-cols="interactiveCols"
label="Thresh Constant"
tooltip="Affects the threshold window mean value cutoff for all steps. Higher values can improve performance, but may harm detection rate."
@@ -71,9 +61,15 @@ const interactiveCols = computed(() =>
:step="1"
@input="(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ threshConstant: value }, false)"
/>
<pv-switch
v-model="currentPipelineSettings.useCornerRefinement"
label="Refine Corners"
tooltip="Further refine the initial corners with subpixel accuracy."
:switch-cols="interactiveCols"
@input="(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ useCornerRefinement: value }, false)"
/>
<pv-switch
v-model="currentPipelineSettings.debugThreshold"
class="pt-2"
label="Debug Threshold"
tooltip="Display the first threshold step to the color stream."
:switch-cols="interactiveCols"

View File

@@ -52,13 +52,23 @@ const contourRadius = computed<[number, number]>({
const interactiveCols = computed(() =>
(getCurrentInstance()?.proxy.$vuetify.breakpoint.mdAndDown || false) &&
(!useStateStore().sidebarFolded || useCameraSettingsStore().isDriverMode)
? 9
: 8
? 8
: 7
);
</script>
<template>
<div>
<pv-select
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']"
:select-cols="interactiveCols"
@input="
(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ contourTargetOrientation: value }, false)
"
/>
<pv-range-slider
v-model="contourArea"
label="Area"
@@ -79,16 +89,6 @@ const interactiveCols = computed(() =>
:step="0.1"
@input="(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ contourRatio: value }, false)"
/>
<pv-select
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']"
:select-cols="interactiveCols"
@input="
(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ contourTargetOrientation: value }, false)
"
/>
<pv-range-slider
v-if="useCameraSettingsStore().currentPipelineType === PipelineType.ColoredShape"
v-model="contourFullness"
@@ -160,7 +160,6 @@ const interactiveCols = computed(() =>
/>
</template>
<template v-else-if="currentPipelineSettings.pipelineType === PipelineType.ColoredShape">
<v-divider class="mt-3" />
<pv-select
v-model="currentPipelineSettings.contourShape"
label="Target Shape"
@@ -191,15 +190,6 @@ const interactiveCols = computed(() =>
(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ circleDetectThreshold: value }, false)
"
/>
<pv-range-slider
v-model="contourRadius"
:disabled="currentPipelineSettings.contourShape !== 0"
label="Radius"
:min="0"
:max="100"
:slider-cols="interactiveCols"
@input="(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ contourRadius: value }, false)"
/>
<pv-slider
v-model="currentPipelineSettings.maxCannyThresh"
:disabled="currentPipelineSettings.contourShape !== 0"
@@ -218,7 +208,15 @@ const interactiveCols = computed(() =>
:slider-cols="interactiveCols"
@input="(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ circleAccuracy: value }, false)"
/>
<v-divider class="mt-3" />
<pv-range-slider
v-model="contourRadius"
:disabled="currentPipelineSettings.contourShape !== 0"
label="Radius"
:min="0"
:max="100"
:slider-cols="interactiveCols"
@input="(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ contourRadius: value }, false)"
/>
</template>
<pv-select
v-model="useCameraSettingsStore().currentPipelineSettings.contourSortMode"

View File

@@ -66,13 +66,21 @@ const handleStreamResolutionChange = (value: number) => {
const interactiveCols = computed(() =>
(getCurrentInstance()?.proxy.$vuetify.breakpoint.mdAndDown || false) &&
(!useStateStore().sidebarFolded || useCameraSettingsStore().isDriverMode)
? 9
: 8
? 8
: 7
);
</script>
<template>
<div>
<pv-switch
v-model="useCameraSettingsStore().currentPipelineSettings.cameraAutoExposure"
class="pt-2"
label="Auto Exposure"
:switch-cols="interactiveCols === 8 ? 9 : interactiveCols"
tooltip="Enables or Disables camera automatic adjustment for current lighting conditions"
@input="(args) => useCameraSettingsStore().changeCurrentPipelineSetting({ cameraAutoExposure: args }, false)"
/>
<pv-slider
v-model="useCameraSettingsStore().currentPipelineSettings.cameraExposureRaw"
:disabled="useCameraSettingsStore().currentCameraSettings.pipelineSettings.cameraAutoExposure"
@@ -92,14 +100,6 @@ const interactiveCols = computed(() =>
:slider-cols="interactiveCols"
@input="(args) => useCameraSettingsStore().changeCurrentPipelineSetting({ cameraBrightness: args }, false)"
/>
<pv-switch
v-model="useCameraSettingsStore().currentPipelineSettings.cameraAutoExposure"
class="pt-2"
label="Auto Exposure"
:switch-cols="interactiveCols"
tooltip="Enables or Disables camera automatic adjustment for current lighting conditions"
@input="(args) => useCameraSettingsStore().changeCurrentPipelineSetting({ cameraAutoExposure: args }, false)"
/>
<pv-slider
v-if="useCameraSettingsStore().currentPipelineSettings.cameraGain >= 0"
v-model="useCameraSettingsStore().currentPipelineSettings.cameraGain"
@@ -130,12 +130,11 @@ const interactiveCols = computed(() =>
tooltip="Controls blue automatic white balance gain, which affects how the camera captures colors in different conditions"
@input="(args) => useCameraSettingsStore().changeCurrentPipelineSetting({ cameraBlueGain: args }, false)"
/>
<pv-switch
v-model="useCameraSettingsStore().currentPipelineSettings.cameraAutoWhiteBalance"
class="pt-2"
label="Auto White Balance"
:switch-cols="interactiveCols"
:switch-cols="interactiveCols === 8 ? 9 : interactiveCols"
tooltip="Enables or Disables camera automatic adjustment for current lighting conditions"
@input="(args) => useCameraSettingsStore().changeCurrentPipelineSetting({ cameraAutoWhiteBalance: args }, false)"
/>
@@ -148,13 +147,12 @@ const interactiveCols = computed(() =>
:slider-cols="interactiveCols"
@input="(args) => useCameraSettingsStore().changeCurrentPipelineSetting({ cameraWhiteBalanceTemp: args }, false)"
/>
<pv-select
v-model="useCameraSettingsStore().currentPipelineSettings.inputImageRotationMode"
label="Orientation"
tooltip="Rotates the camera stream. Rotation not available when camera has been calibrated."
:items="cameraRotations"
:select-cols="interactiveCols"
:select-cols="interactiveCols === 8 ? 9 : interactiveCols"
@input="(args) => useCameraSettingsStore().changeCurrentPipelineSetting({ inputImageRotationMode: args }, false)"
/>
<pv-select
@@ -162,7 +160,7 @@ const interactiveCols = computed(() =>
label="Resolution"
tooltip="Resolution and FPS the camera should directly capture at"
:items="cameraResolutions"
:select-cols="interactiveCols"
:select-cols="interactiveCols === 8 ? 9 : interactiveCols"
@input="(args) => handleResolutionChange(args)"
/>
<pv-select
@@ -170,7 +168,7 @@ const interactiveCols = computed(() =>
label="Stream Resolution"
tooltip="Resolution to which camera frames are downscaled for streaming to the dashboard"
:items="streamResolutions"
:select-cols="interactiveCols"
:select-cols="interactiveCols === 8 ? 9 : interactiveCols"
@input="(args) => handleStreamResolutionChange(args)"
/>
</div>

View File

@@ -49,8 +49,8 @@ const currentPipelineSettings = computed<ActivePipelineSettings>(
const interactiveCols = computed(() =>
(getCurrentInstance()?.proxy.$vuetify.breakpoint.mdAndDown || false) &&
(!useStateStore().sidebarFolded || useCameraSettingsStore().isDriverMode)
? 9
: 8
? 8
: 7
);
</script>
@@ -115,7 +115,6 @@ const interactiveCols = computed(() =>
:disabled="!isTagPipeline || !currentPipelineSettings.doMultiTarget"
@input="(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ doSingleTargetAlways: value }, false)"
/>
<v-divider />
<table
v-if="useCameraSettingsStore().currentPipelineSettings.offsetRobotOffsetMode !== RobotOffsetPointMode.None"
class="metrics-table mt-3 mb-3"

View File

@@ -175,40 +175,47 @@ const interactiveCols = computed(() =>
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" />
<div>
<div class="pt-3 white--text">Color Picker</div>
<v-row justify="center" class="mt-3 mb-3">
<div class="white--text pt-3">Color Picker</div>
<div class="d-flex pt-3">
<template v-if="!useStateStore().colorPickingMode">
<v-btn
color="accent"
class="ma-2 black--text"
small
@click="enableColorPicking(useCameraSettingsStore().currentPipelineSettings.hueInverted ? 2 : 3)"
>
<v-icon left> mdi-minus </v-icon>
Shrink Range
</v-btn>
<v-btn color="accent" class="ma-2 black--text" small @click="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)"
>
<v-icon left> mdi-plus </v-icon>
Expand Range
</v-btn>
<v-col cols="4" class="pl-0 pr-2">
<v-btn
small
block
color="accent"
class="black--text"
@click="enableColorPicking(useCameraSettingsStore().currentPipelineSettings.hueInverted ? 2 : 3)"
>
<v-icon left> mdi-minus </v-icon>
Shrink Range
</v-btn>
</v-col>
<v-col cols="4" class="pl-0 pr-0">
<v-btn color="accent" class="black--text" small block @click="enableColorPicking(1)">
<v-icon left> mdi-plus-minus </v-icon>
{{ useCameraSettingsStore().currentPipelineSettings.hueInverted ? "Exclude" : "Set to" }} Average
</v-btn>
</v-col>
<v-col cols="4" class="pl-2 pr-0">
<v-btn
small
block
color="accent"
class="black--text"
@click="enableColorPicking(useCameraSettingsStore().currentPipelineSettings.hueInverted ? 3 : 2)"
>
<v-icon left> mdi-plus </v-icon>
Expand Range
</v-btn>
</v-col>
</template>
<template v-else>
<v-btn color="accent" class="ma-2 black--text" style="width: 30%" small @click="disableColorPicking">
Cancel
</v-btn>
</template>
</v-row>
</div>
</div>
</div>
</template>

View File

@@ -17,9 +17,9 @@ const quaternionToEuler = (rot_quat: Quaternion): { x: number; y: number; z: num
</script>
<template>
<v-card dark class="pr-6 pb-3" style="background-color: #006492">
<v-card-title>AprilTag Field Layout</v-card-title>
<div class="ml-5">
<v-card dark style="background-color: #006492">
<v-card-title class="pa-6">AprilTag Field Layout</v-card-title>
<v-card-text class="pa-6 pt-0">
<p>Field width: {{ useSettingsStore().currentFieldLayout.field.width.toFixed(2) }} meters</p>
<p>Field length: {{ useSettingsStore().currentFieldLayout.field.length.toFixed(2) }} meters</p>
@@ -48,7 +48,7 @@ const quaternionToEuler = (rot_quat: Quaternion): { x: number; y: number; z: num
</tbody>
</template>
</v-simple-table>
</div>
</v-card-text>
</v-card>
</template>

View File

@@ -237,18 +237,18 @@ const nukePhotonConfigDirectory = () => {
</script>
<template>
<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-card dark class="mb-3" style="background-color: #006492">
<v-card-title class="pa-6">Device Control</v-card-title>
<div class="pa-6 pt-0">
<v-row>
<v-col cols="12" lg="4" md="6">
<v-btn color="red" @click="restartProgram">
<v-btn color="error" @click="restartProgram">
<v-icon left class="open-icon"> mdi-restart </v-icon>
<span class="open-label">Restart PhotonVision</span>
</v-btn>
</v-col>
<v-col cols="12" lg="4" md="6">
<v-btn color="red" @click="restartDevice">
<v-btn color="error" @click="restartDevice">
<v-icon left class="open-icon"> mdi-restart-alert </v-icon>
<span class="open-label">Restart Device</span>
</v-btn>
@@ -261,7 +261,7 @@ const nukePhotonConfigDirectory = () => {
<input ref="offlineUpdate" type="file" accept=".jar" style="display: none" @change="handleOfflineUpdate" />
</v-col>
</v-row>
<v-divider style="margin: 12px 0" />
<v-divider class="mt-3 pb-3" />
<v-row>
<v-col cols="12" sm="6">
<v-btn color="secondary" @click="() => (showImportDialog = true)">
@@ -355,15 +355,15 @@ const nukePhotonConfigDirectory = () => {
</v-btn>
</v-col>
</v-row>
<v-divider style="margin: 12px 0" />
<v-divider class="mt-3 pb-3" />
<v-row>
<v-col cols="12">
<v-btn color="red" @click="() => (showFactoryReset = true)">
<v-btn color="error" @click="() => (showFactoryReset = true)">
<v-icon left class="open-icon"> mdi-skull-crossbones </v-icon>
<span class="open-icon">
{{
$vuetify.breakpoint.mdAndUp
? "Factory Reset PhotonVision and delete EVERYTHING (big scary button)"
? "Factory Reset PhotonVision and delete EVERYTHING"
: "Factory Reset PhotonVision"
}}
</span>
@@ -372,71 +372,63 @@ const nukePhotonConfigDirectory = () => {
</v-row>
</div>
<v-dialog v-model="showFactoryReset" width="1500" dark>
<v-card dark class="dialog-container pa-6" color="primary" flat>
<v-card-title>
<v-dialog v-model="showFactoryReset" width="800" dark>
<v-card dark color="primary" class="pa-3" flat>
<v-card-title style="justify-content: center" class="pb-6">
<span class="open-label">
<v-icon right color="red" class="open-icon">mdi-nuke</v-icon>
<v-icon right color="error" class="open-icon ma-1">mdi-nuke</v-icon>
Factory Reset PhotonVision
<v-icon right color="red" class="open-icon">mdi-nuke</v-icon>
<v-icon right color="error" class="open-icon ma-1">mdi-nuke</v-icon>
</span>
</v-card-title>
<v-row class="pl-3 align-center pa-6">
<v-col cols="12" md="6">
<span class="mt-3"> This will delete ALL OF YOUR SETTINGS and restart PhotonVision. </span>
</v-col>
<v-col cols="12" md="6">
<v-btn color="secondary" style="float: right" @click="openExportSettingsPrompt">
<v-icon left class="open-icon"> mdi-export </v-icon>
<span class="open-label">Backup Settings</span>
<a
ref="exportSettings"
style="color: black; text-decoration: none; display: none"
:href="`http://${address}/api/settings/photonvision_config.zip`"
download="photonvision-settings.zip"
target="_blank"
/>
</v-btn>
</v-col>
</v-row>
<v-divider class="mt-4 mb-4" />
<v-row class="pl-3 align-center pa-6">
<v-col>
<pv-input
v-model="yesDeleteMySettingsText"
:label="'Type &quot;' + expected + '&quot;:'"
:label-cols="2"
:input-cols="10"
/>
</v-col>
</v-row>
<v-btn
color="red"
:disabled="yesDeleteMySettingsText.toLowerCase() !== expected.toLowerCase()"
@click="nukePhotonConfigDirectory"
>
<v-icon left class="open-icon"> mdi-skull-crossbones </v-icon>
<span class="open-label">
{{ $vuetify.breakpoint.mdAndUp ? "Delete everything, I have backed up what I need" : "Delete Everything" }}
</span>
</v-btn>
<v-card-text class="pt-3">
<v-row class="align-center white--text">
<v-col cols="12" md="6">
<span class="mt-3"> This will delete ALL OF YOUR SETTINGS and restart PhotonVision. </span>
</v-col>
<v-col cols="12" md="6">
<v-btn color="secondary" style="float: right" @click="openExportSettingsPrompt">
<v-icon left class="open-icon"> mdi-export </v-icon>
<span class="open-label">Backup Settings</span>
<a
ref="exportSettings"
style="color: black; text-decoration: none; display: none"
:href="`http://${address}/api/settings/photonvision_config.zip`"
download="photonvision-settings.zip"
target="_blank"
/>
</v-btn>
</v-col>
</v-row>
</v-card-text>
<v-card-text>
<pv-input
v-model="yesDeleteMySettingsText"
:label="'Type &quot;' + expected + '&quot;:'"
:label-cols="6"
:input-cols="6"
/>
</v-card-text>
<v-card-text>
<v-btn
color="error"
:disabled="yesDeleteMySettingsText.toLowerCase() !== expected.toLowerCase()"
@click="nukePhotonConfigDirectory"
>
<v-icon left class="open-icon"> mdi-trash-can-outline </v-icon>
<span class="open-label">
{{
$vuetify.breakpoint.mdAndUp ? "Delete everything, I have backed up what I need" : "Delete Everything"
}}
</span>
</v-btn>
</v-card-text>
</v-card>
</v-dialog>
</v-card>
</template>
<style scoped>
.dialog-container {
min-height: 300px !important;
}
.v-divider {
border-color: white !important;
}
.v-btn {
width: 100%;
}

View File

@@ -2,7 +2,6 @@
import { useSettingsStore } from "@/stores/settings/GeneralSettingsStore";
import { computed, onBeforeMount, ref } from "vue";
import { useStateStore } from "@/stores/StateStore";
import PvIcon from "@/components/common/pv-icon.vue";
interface MetricItem {
header: string;
@@ -121,67 +120,134 @@ onBeforeMount(() => {
</script>
<template>
<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>
<pv-icon icon-name="mdi-reload" color="white" tooltip="Reload Metrics" hover @click="fetchMetrics" />
<v-card dark class="mb-3" style="background-color: #006492">
<v-card-title class="pl-6" style="display: flex; justify-content: space-between">
<span class="pt-2 pb-2">Stats</span>
<v-btn text @click="fetchMetrics">
<v-icon left class="open-icon">mdi-reload</v-icon>
Last Fetched: {{ metricsLastFetched }}
</v-btn>
</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-simple-table class="metrics-table">
<v-card-text class="pa-6 pt-0 pb-3">
<v-card-subtitle class="pa-0" style="font-size: 16px">General Metrics</v-card-subtitle>
<v-simple-table class="metrics-table mt-3">
<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"
:class="{
tl: itemIndex === 0,
tr: itemIndex === generalMetrics.length - 1,
t: 0 < itemIndex && itemIndex < generalMetrics.length - 1
}"
>
{{ 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"
:class="{
bl: itemIndex === 0,
br: itemIndex === generalMetrics.length - 1,
b: 0 < itemIndex && itemIndex < generalMetrics.length - 1
}"
>
{{ item.value }}
</td>
</tr>
</tbody>
</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-simple-table class="metrics-table">
</v-card-text>
<v-card-text class="pa-6 pt-4">
<v-card-subtitle class="pa-0 pb-1" style="font-size: 16px">Hardware Metrics</v-card-subtitle>
<v-simple-table class="metrics-table mt-3">
<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"
:class="{
tl: itemIndex === 0,
tr: itemIndex === platformMetrics.length - 1,
t: 0 < itemIndex && itemIndex < platformMetrics.length - 1
}"
>
{{ 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"
:class="{
bl: itemIndex === 0,
br: itemIndex === platformMetrics.length - 1,
b: 0 < itemIndex && itemIndex < platformMetrics.length - 1
}"
>
<span v-if="useSettingsStore().metrics.cpuUtil !== undefined">{{ item.value }}</span>
<span v-else>---</span>
</td>
</tr>
</tbody>
</v-simple-table>
</v-row>
<div style="text-align: right">
<span>Last Fetched: {{ metricsLastFetched }}</span>
</div>
</v-card-text>
</v-card>
</template>
<style scoped lang="scss">
.metrics-table {
border-collapse: separate;
border-spacing: 0;
border-radius: 5px;
margin-bottom: 10px;
border: 1px solid white;
width: 100%;
text-align: center;
}
.t {
border-top: 1px solid white;
border-right: 1px solid white;
}
.b {
border-bottom: 1px solid white;
border-right: 1px solid white;
}
.tl {
border-top: 1px solid white;
border-left: 1px solid white;
border-right: 1px solid white;
border-top-left-radius: 5px;
}
.tr {
border-top: 1px solid white;
border-right: 1px solid white;
border-top-right-radius: 5px;
}
.bl {
border-bottom: 1px solid white;
border-left: 1px solid white;
border-right: 1px solid white;
border-bottom-left-radius: 5px;
}
.br {
border-bottom: 1px solid white;
border-right: 1px solid white;
border-bottom-right-radius: 5px;
}
.metric-item {
font-size: 16px !important;
padding: 1px 15px 1px 10px;

View File

@@ -59,8 +59,7 @@ const settingsHaveChanged = (): boolean => {
a.shouldPublishProto !== b.shouldPublishProto ||
a.networkManagerIface !== b.networkManagerIface ||
a.setStaticCommand !== b.setStaticCommand ||
a.setDHCPcommand !== b.setDHCPcommand ||
a.matchCamerasOnlyByPath !== b.matchCamerasOnlyByPath
a.setDHCPcommand !== b.setDHCPcommand
);
};
@@ -78,7 +77,6 @@ const saveGeneralSettings = () => {
setStaticCommand: tempSettingsStruct.value.setStaticCommand || "",
shouldManage: tempSettingsStruct.value.shouldManage,
shouldPublishProto: tempSettingsStruct.value.shouldPublishProto,
matchCamerasOnlyByPath: tempSettingsStruct.value.matchCamerasOnlyByPath,
staticIp: tempSettingsStruct.value.staticIp
};
@@ -138,11 +136,11 @@ watchEffect(() => {
</script>
<template>
<v-card dark class="mb-3 pr-6 pb-3" style="background-color: #006492">
<v-card-title>Global Settings</v-card-title>
<v-divider />
<v-card-title>Networking</v-card-title>
<div class="ml-5">
<v-card dark class="mb-3" style="background-color: #006492">
<v-card-title class="pa-6">Global Settings</v-card-title>
<div class="pa-6 pt-0">
<v-divider class="pb-3" />
<v-card-title class="pl-0">Networking</v-card-title>
<v-form ref="form" v-model="settingsValid">
<pv-input
v-model="tempSettingsStruct.ntServerAddress"
@@ -157,9 +155,9 @@ watchEffect(() => {
]"
/>
<v-banner
v-show="!isValidNetworkTablesIP(tempSettingsStruct.ntServerAddress) && !tempSettingsStruct.runNTServer"
v-if="!isValidNetworkTablesIP(tempSettingsStruct.ntServerAddress) && !tempSettingsStruct.runNTServer"
rounded
color="red"
color="error"
text-color="white"
style="margin: 10px 0"
icon="mdi-alert-circle-outline"
@@ -204,8 +202,8 @@ watchEffect(() => {
useSettingsStore().network.networkingDisabled
"
/>
<v-divider class="pb-3" />
<span style="font-weight: 700">Advanced Networking</span>
<v-divider class="mt-3 pb-3" />
<v-card-title class="pl-0">Advanced Networking</v-card-title>
<pv-switch
v-show="!useSettingsStore().network.networkingDisabled"
v-model="tempSettingsStruct.shouldManage"
@@ -213,7 +211,6 @@ watchEffect(() => {
label="Manage Device Networking"
tooltip="If enabled, Photon will manage device hostname and network settings."
:label-cols="4"
class="pt-2"
/>
<pv-select
v-show="!useSettingsStore().network.networkingDisabled"
@@ -229,14 +226,14 @@ watchEffect(() => {
:items="useSettingsStore().networkInterfaceNames"
/>
<v-banner
v-show="
v-if="
!useSettingsStore().networkInterfaceNames.length &&
tempSettingsStruct.shouldManage &&
useSettingsStore().network.canManage &&
!useSettingsStore().network.networkingDisabled
"
rounded
color="red"
color="error"
text-color="white"
icon="mdi-information-outline"
>
@@ -246,65 +243,36 @@ watchEffect(() => {
v-model="tempSettingsStruct.runNTServer"
label="Run NetworkTables Server (Debugging Only)"
tooltip="If enabled, this device will create a NT server. This is useful for home debugging, but should be disabled on-robot."
class="mt-3 mb-2"
:label-cols="4"
/>
<v-banner
v-show="tempSettingsStruct.runNTServer"
v-if="tempSettingsStruct.runNTServer"
rounded
color="red"
color="error"
text-color="white"
icon="mdi-information-outline"
>
This mode is intended for debugging; it should be off for proper usage. PhotonLib will NOT work!
</v-banner>
<v-divider />
<v-card-title>Miscellaneous</v-card-title>
<v-divider class="mt-3 pb-3" />
<v-card-title class="pl-0">Miscellaneous</v-card-title>
<pv-switch
v-model="tempSettingsStruct.shouldPublishProto"
label="Also Publish Protobuf"
tooltip="If enabled, Photon will publish all pipeline results in both the Packet and Protobuf formats. This is useful for visualizing pipeline results from NT viewers such as glass and logging software such as AdvantageScope. Note: photon-lib will ignore this value and is not recommended on the field for performance."
class="mt-3 mb-2"
:label-cols="4"
/>
<v-banner
v-show="tempSettingsStruct.shouldPublishProto"
v-if="tempSettingsStruct.shouldPublishProto"
rounded
color="red"
class="mb-3"
color="error"
text-color="white"
icon="mdi-information-outline"
>
This mode is intended for debugging; it should be off for field use. You may notice a performance hit by using
this mode.
</v-banner>
<pv-switch
v-model="tempSettingsStruct.matchCamerasOnlyByPath"
label="Strictly match ONLY known cameras"
tooltip="ONLY match cameras by the USB port they're plugged into + (basename or USB VID/PID), and never only by the device product string. Also disables automatic detection of new cameras."
class="mt-3 mb-2"
:label-cols="4"
/>
<v-banner
v-show="tempSettingsStruct.matchCamerasOnlyByPath"
rounded
color="red"
class="mb-3"
text-color="white"
icon="mdi-information-outline"
>
Physical cameras will be strictly matched to camera configurations using physical USB port they are plugged
into, in addition to device name and other USB metadata. Additionally, no new cameras are allowed to be added.
This setting is useful for guaranteeing that an already known and configured camera can never be matched as an
"unknown"/"new" camera, which resets pipelines and calibration data.
<p />
Cameras will NOT be matched if they change USB ports, and new cameras plugged into this coprocessor will NOT
be automatically recognized or configured for vision processing.
<p />
To add a new camera to this coprocessor, disable this setting, connect the camera, and re-enable.
</v-banner>
<v-divider class="mb-3" />
<v-divider class="mt-3 mb-6" />
</v-form>
<v-btn
color="accent"

View File

@@ -11,7 +11,7 @@ const darkTheme: VuetifyThemeVariant = Object.freeze({
secondary: "#39A4D5",
accent: "#FFD843",
background: "#232C37",
error: "#FF5252",
error: "#b80000",
info: "#2196F3",
success: "#4CAF50",
warning: "#FFC107"
@@ -22,7 +22,7 @@ const lightTheme: VuetifyThemeVariant = Object.freeze({
secondary: "#39A4D5",
accent: "#FFD843",
background: "#232C37",
error: "#FF5252",
error: "#b80000",
info: "#2196F3",
success: "#4CAF50",
warning: "#FFC107"

View File

@@ -45,8 +45,7 @@ export const useSettingsStore = defineStore("settings", {
devName: "eth0"
}
],
networkingDisabled: false,
matchCamerasOnlyByPath: false
networkingDisabled: false
},
lighting: {
supported: true,

View File

@@ -50,7 +50,6 @@ export interface NetworkSettings {
setDHCPcommand?: string;
networkInterfaceNames: NetworkInterfaceType[];
networkingDisabled: boolean;
matchCamerasOnlyByPath: boolean;
}
export type ConfigurableNetworkSettings = Omit<

View File

@@ -1,49 +1,70 @@
<script setup lang="ts">
import { useCameraSettingsStore } from "@/stores/settings/CameraSettingsStore";
import { computed, inject, ref } from "vue";
import { computed, inject, onMounted, ref } from "vue";
import { useStateStore } from "@/stores/StateStore";
import {
PlaceholderCameraSettings,
PVCameraInfo,
type PVCSICameraInfo,
type PVFileCameraInfo,
type PVUsbCameraInfo
type PVUsbCameraInfo,
type UiCameraConfiguration
} from "@/types/SettingTypes";
import { getResolutionString } from "@/lib/PhotonUtils";
import PvCameraInfoCard from "@/components/common/pv-camera-info-card.vue";
import axios from "axios";
import _ from "lodash";
import PvCameraMatchCard from "@/components/common/pv-camera-match-card.vue";
import type { WebsocketCameraSettingsUpdate } from "@/types/WebsocketDataTypes";
const formatUrl = (port) => `http://${inject("backendHostname")}:${port}/stream.mjpg`;
const host = inject<string>("backendHost");
const activatingModule = ref(false);
const activateModule = (moduleUniqueName: string) => {
if (activatingModule.value) return;
activatingModule.value = true;
const url = new URL(`http://${host}/api/utils/activateMatchedCamera`);
url.searchParams.set("cameraUniqueName", moduleUniqueName);
fetch(url.toString(), {
method: "POST"
}).finally(() => {
activatingModule.value = false;
setTimeout(() => enforceStreamHeight(), 1000);
});
};
const activateCamera = (cameraInfo: PVCameraInfo) => {
const assigningCamera = ref(false);
const assignCamera = (cameraInfo: PVCameraInfo) => {
if (assigningCamera.value) return;
assigningCamera.value = true;
const url = new URL(`http://${host}/api/utils/assignUnmatchedCamera`);
url.searchParams.set("cameraInfo", JSON.stringify(cameraInfo));
fetch(url.toString(), {
method: "POST"
}).finally(() => {
assigningCamera.value = false;
setTimeout(() => enforceStreamHeight(), 1000);
});
};
const deactivateCamera = (cameraUniqueName: string) => {
console.log("Deactivating " + cameraUniqueName);
const deactivatingModule = ref(false);
const deactivateModule = (cameraUniqueName: string) => {
if (deactivatingModule.value) return;
deactivatingModule.value = true;
const url = new URL(`http://${host}/api/utils/unassignCamera`);
url.searchParams.set("cameraUniqueName", cameraUniqueName);
fetch(url.toString(), {
method: "POST"
});
}).finally(() => (deactivatingModule.value = false));
};
const deletingCamera = ref(false);
const deleteThisCamera = (cameraName: string) => {
if (deletingCamera.value) return;
deletingCamera.value = true;
const payload = {
cameraUniqueName: cameraName
};
@@ -52,7 +73,7 @@ const deleteThisCamera = (cameraName: string) => {
.post("/utils/nukeOneCamera", payload)
.then(() => {
useStateStore().showSnackbarMessage({
message: "Successfully deleted " + cameraName,
message: "Camera deleted successfully",
color: "success"
});
})
@@ -73,9 +94,34 @@ const deleteThisCamera = (cameraName: string) => {
color: "error"
});
}
})
.finally(() => {
setCameraDeleting(null);
deletingCamera.value = false;
});
};
const camerasMatch = (camera1: PVCameraInfo, camera2: PVCameraInfo) => {
if (camera1.PVUsbCameraInfo && camera2.PVUsbCameraInfo)
return (
camera1.PVUsbCameraInfo.name === camera2.PVUsbCameraInfo.name &&
camera1.PVUsbCameraInfo.vendorId === camera2.PVUsbCameraInfo.vendorId &&
camera1.PVUsbCameraInfo.productId === camera2.PVUsbCameraInfo.productId &&
camera1.PVUsbCameraInfo.uniquePath === camera2.PVUsbCameraInfo.uniquePath
);
else if (camera1.PVCSICameraInfo && camera2.PVCSICameraInfo)
return (
camera1.PVCSICameraInfo.uniquePath === camera2.PVCSICameraInfo.uniquePath &&
camera1.PVCSICameraInfo.baseName === camera2.PVCSICameraInfo.baseName
);
else if (camera1.PVFileCameraInfo && camera2.PVFileCameraInfo)
return (
camera1.PVFileCameraInfo.uniquePath === camera2.PVFileCameraInfo.uniquePath &&
camera1.PVFileCameraInfo.name === camera2.PVFileCameraInfo.name
);
else return false;
};
const cameraInfoFor = (camera: PVCameraInfo): PVUsbCameraInfo | PVCSICameraInfo | PVFileCameraInfo | any => {
if (camera.PVUsbCameraInfo) {
return camera.PVUsbCameraInfo;
@@ -155,38 +201,76 @@ const activeVisionModules = computed(() =>
const disabledVisionModules = computed(() => useStateStore().vsmState.disabledConfigs);
const viewingDetails = ref(false);
const showCurrentView = ref(false);
const viewingCamera = ref<PVCameraInfo | null>(null);
const setCameraView = (camera: PVCameraInfo | null, showCurrent: boolean = false) => {
const setCameraView = (camera: PVCameraInfo | null) => {
viewingDetails.value = camera !== null;
viewingCamera.value = camera;
showCurrentView.value = showCurrent;
};
const viewingDeleteCamera = ref(false);
const cameraToDelete = ref<UiCameraConfiguration | WebsocketCameraSettingsUpdate | null>(null);
const setCameraDeleting = (camera: UiCameraConfiguration | WebsocketCameraSettingsUpdate | null) => {
yesDeleteMySettingsText.value = "";
viewingDeleteCamera.value = camera !== null;
cameraToDelete.value = camera;
};
const yesDeleteMySettingsText = ref("");
const exportSettings = ref();
const openExportSettingsPrompt = () => {
exportSettings.value.click();
};
const enforceStreamHeight = () => {
const streamWidth = document.getElementById("stream-container-0")?.offsetWidth ?? 0;
if (streamWidth === 0) return;
Object.values(useCameraSettingsStore().cameras)
.filter((camera) => JSON.stringify(camera) !== JSON.stringify(PlaceholderCameraSettings))
.forEach((element, index) => {
let stream = document.getElementById(`outer-output-camera-stream-${index}`);
if (!stream) return;
stream?.classList.remove("tall-stream", "wide-stream", "d-none");
let streamRes = element.validVideoFormats[0].resolution.width / element.validVideoFormats[0].resolution.height;
let containerRes = streamWidth / 250.0;
if (element.pipelineSettings.inputImageRotationMode % 2 == 1) streamRes = 1 / streamRes;
if (streamRes > containerRes) stream?.classList.add("wide-stream");
else stream?.classList.add("tall-stream");
});
};
onMounted(() => {
setTimeout(() => enforceStreamHeight(), 1000);
window.addEventListener("resize", enforceStreamHeight);
});
</script>
<template>
<div class="pa-5">
<v-row>
<!-- Active modules -->
<v-col v-for="module in activeVisionModules" :key="`enabled-${module.uniqueName}`" cols="12" sm="6" lg="4">
<v-col
v-for="(module, index) in activeVisionModules"
:key="`enabled-${module.uniqueName}`"
cols="12"
sm="6"
lg="4"
>
<v-card dark color="primary">
<v-card-title>{{ module.nickname }}</v-card-title>
<v-card-subtitle v-if="_.isEqual(getMatchedDevice(module.matchedCameraInfo), module.matchedCameraInfo)"
<v-card-title>{{ cameraInfoFor(module.matchedCameraInfo).name }}</v-card-title>
<v-card-subtitle v-if="camerasMatch(getMatchedDevice(module.matchedCameraInfo), module.matchedCameraInfo)"
>Status: <span class="active-status">Active</span></v-card-subtitle
>
<v-card-subtitle v-else>Status: <span class="mismatch-status">Mismatch</span></v-card-subtitle>
<v-card-text>
<v-simple-table dark dense>
<v-simple-table dark dense class="mb-3">
<tbody>
<tr>
<td>Streams:</td>
<td>
<a :href="formatUrl(module.stream.inputPort)" target="_blank" class="active-status">
Input Stream
</a>
<a :href="formatUrl(module.stream.inputPort)" target="_blank" class="stream-link"> Input Stream </a>
/
<a :href="formatUrl(module.stream.outputPort)" target="_blank" class="active-status">
<a :href="formatUrl(module.stream.outputPort)" target="_blank" class="stream-link">
Output Stream
</a>
</td>
@@ -219,18 +303,24 @@ const setCameraView = (camera: PVCameraInfo | null, showCurrent: boolean = false
</tr>
</tbody>
</v-simple-table>
<photon-camera-stream
id="output-camera-stream"
class="mt-3"
:camera-settings="module"
stream-type="Processed"
style="width: 100%; height: auto"
/>
<div
:id="`stream-container-${index}`"
class="d-flex flex-column justify-center align-center"
style="height: 250px"
>
<photon-camera-stream
:id="`output-camera-stream-${index}`"
:camera-settings="module"
stream-type="Processed"
:outer-id="`outer-output-camera-stream-${index}`"
class="d-none"
/>
</div>
</v-card-text>
<v-card-text class="pt-0">
<v-row>
<v-col cols="12" md="4" class="pr-md-0 pb-0 pb-md-3">
<v-btn color="secondary" style="width: 100%" @click="setCameraView(module.matchedCameraInfo, true)">
<v-btn color="secondary" style="width: 100%" @click="setCameraView(module.matchedCameraInfo)">
<span>Details</span>
</v-btn>
</v-col>
@@ -239,18 +329,14 @@ const setCameraView = (camera: PVCameraInfo | null, showCurrent: boolean = false
class="black--text"
color="accent"
style="width: 100%"
@click="deactivateCamera(module.uniqueName)"
:loading="deactivatingModule"
@click="deactivateModule(module.uniqueName)"
>
Deactivate
</v-btn>
</v-col>
<v-col cols="6" md="3">
<v-btn
class="black--text pa-0"
color="red"
style="width: 100%"
@click="deleteThisCamera(module.uniqueName)"
>
<v-btn class="pa-0" color="error" style="width: 100%" @click="setCameraDeleting(module)">
<v-icon>mdi-trash-can-outline</v-icon>
</v-btn>
</v-col>
@@ -305,18 +391,14 @@ const setCameraView = (camera: PVCameraInfo | null, showCurrent: boolean = false
class="black--text"
color="accent"
style="width: 100%"
:loading="activatingModule"
@click="activateModule(module.uniqueName)"
>
Activate
</v-btn>
</v-col>
<v-col cols="6" md="3">
<v-btn
class="black--text pa-0"
color="red"
style="width: 100%"
@click="deleteThisCamera(module.uniqueName)"
>
<v-btn class="pa-0" color="error" style="width: 100%" @click="setCameraDeleting(module)">
<v-icon>mdi-trash-can-outline</v-icon>
</v-btn>
</v-col>
@@ -347,7 +429,13 @@ const setCameraView = (camera: PVCameraInfo | null, showCurrent: boolean = false
</v-btn>
</v-col>
<v-col cols="6">
<v-btn class="black--text" color="accent" style="width: 100%" @click="activateCamera(camera)">
<v-btn
class="black--text"
color="accent"
style="width: 100%"
:loading="assigningCamera"
@click="assignCamera(camera)"
>
Activate
</v-btn>
</v-col>
@@ -373,7 +461,7 @@ const setCameraView = (camera: PVCameraInfo | null, showCurrent: boolean = false
</v-row>
<!-- Camera details modal -->
<v-dialog v-model="viewingDetails">
<v-dialog v-model="viewingDetails" max-width="800">
<v-card v-if="viewingCamera !== null" dark flat color="primary">
<v-card-title class="d-flex justify-space-between">
<span>{{ cameraInfoFor(viewingCamera)?.name ?? cameraInfoFor(viewingCamera)?.baseName }}</span>
@@ -381,28 +469,62 @@ const setCameraView = (camera: PVCameraInfo | null, showCurrent: boolean = false
<v-icon>mdi-close-thick</v-icon>
</v-btn>
</v-card-title>
<v-card-text>
<v-banner
v-show="!_.isEqual(getMatchedDevice(viewingCamera), viewingCamera)"
rounded
color="red"
text-color="white"
icon="mdi-information-outline"
class="mb-3"
>
Camera Mismatched:<br />It looks like a different camera has been connected to this device! Compare the
below information carefully.
<v-card-text v-if="!camerasMatch(getMatchedDevice(viewingCamera), viewingCamera)">
<v-banner rounded color="error" text-color="white" icon="mdi-information-outline" class="mb-3">
It looks like a different camera may have been connected to this device! Compare the following information
carefully.
</v-banner>
<div v-if="showCurrentView">
<h3>Saved camera</h3>
<PvCameraInfoCard :camera="viewingCamera" :show-title="false" />
<br />
<h3>Current camera</h3>
<PvCameraInfoCard :camera="getMatchedDevice(viewingCamera)" :show-title="false" />
</div>
<div v-else>
<PvCameraInfoCard :camera="viewingCamera" />
</div>
<PvCameraMatchCard :saved="viewingCamera" :current="getMatchedDevice(viewingCamera)" />
</v-card-text>
<v-card-text v-else>
<PvCameraInfoCard :camera="getMatchedDevice(viewingCamera)" />
</v-card-text>
</v-card>
</v-dialog>
<!-- Camera delete modal -->
<v-dialog v-model="viewingDeleteCamera" dark width="800">
<v-card v-if="cameraToDelete !== null" dark class="dialog-container pa-3 pb-2" color="primary" flat>
<v-card-title> Delete {{ cameraToDelete.nickname }}? </v-card-title>
<v-card-text>
<v-row class="align-center pt-6">
<v-col cols="12" md="6">
<span class="white--text"> This will delete ALL OF YOUR SETTINGS and restart PhotonVision. </span>
</v-col>
<v-col cols="12" md="6">
<v-btn color="secondary" block @click="openExportSettingsPrompt">
<v-icon left class="open-icon"> mdi-export </v-icon>
<span class="open-label">Backup Settings</span>
<a
ref="exportSettings"
style="color: black; text-decoration: none; display: none"
:href="`http://${host}/api/settings/photonvision_config.zip`"
download="photonvision-settings.zip"
target="_blank"
/>
</v-btn>
</v-col>
</v-row>
</v-card-text>
<v-card-text>
<pv-input
v-model="yesDeleteMySettingsText"
:label="'Type &quot;' + cameraToDelete.nickname + '&quot;:'"
:label-cols="6"
:input-cols="6"
/>
</v-card-text>
<v-card-text>
<v-btn
block
color="error"
:disabled="yesDeleteMySettingsText.toLowerCase() !== cameraToDelete.nickname.toLowerCase()"
:loading="deletingCamera"
@click="deleteThisCamera(cameraToDelete.uniqueName)"
>
<v-icon left class="open-icon"> mdi-trash-can-outline </v-icon>
<span class="open-label">DELETE (UNRECOVERABLE)</span>
</v-btn>
</v-card-text>
</v-card>
</v-dialog>
@@ -414,7 +536,6 @@ const setCameraView = (camera: PVCameraInfo | null, showCurrent: boolean = false
background-color: #006492 !important;
}
a:link,
.active-status {
color: rgb(14, 240, 14);
background-color: transparent;
@@ -434,9 +555,20 @@ a:hover {
}
a:active,
.stream-link,
.mismatch-status {
color: yellow;
background-color: transparent;
text-decoration: none;
}
.wide-stream {
width: 100%;
height: auto;
}
.tall-stream {
height: 100%;
width: auto;
}
</style>

View File

@@ -66,7 +66,7 @@ const arducamWarningShown = computed<boolean>(() => {
v-if="arducamWarningShown"
v-model="arducamWarningShown"
rounded
color="red"
color="error"
dark
class="mb-3"
icon="mdi-alert-circle-outline"