mirror of
https://github.com/PhotonVision/photonvision
synced 2026-06-29 02:21:41 +00:00
Update log viewer, add uncalibrated modal (#108)
Adds a prettier log viewer, with the ability to filter logs. Also warns user and prevents switching into 3d mode if the resolution is uncalibrated.
This commit is contained in:
978
photon-client/package-lock.json
generated
978
photon-client/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -8,33 +8,33 @@
|
||||
"lint": "vue-cli-service lint"
|
||||
},
|
||||
"dependencies": {
|
||||
"@femessage/log-viewer": "^1.4.1",
|
||||
"@femessage/log-viewer": "^1.4.2",
|
||||
"@vue/eslint-config-prettier": "^6.0.0",
|
||||
"axios": "^0.19.2",
|
||||
"core-js": "^2.6.11",
|
||||
"downloadjs": "^1.4.7",
|
||||
"material-design-icons-iconfont": "^5.0.1",
|
||||
"msgpack5": "^4.2.1",
|
||||
"vue": "^2.6.11",
|
||||
"vue": "^2.6.12",
|
||||
"vue-axios": "^2.1.5",
|
||||
"vue-native-websocket": "git+https://github.com/PhotonVision/vue-native-websocket.git#7a32791",
|
||||
"vue-router": "^3.3.2",
|
||||
"vuetify": "^2.2.34",
|
||||
"vuex": "^3.4.0"
|
||||
"vue-router": "^3.4.3",
|
||||
"vuetify": "^2.3.10",
|
||||
"vuex": "^3.5.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@mdi/font": "^4.9.95",
|
||||
"@vue/cli-plugin-babel": "^3.12.1",
|
||||
"@vue/cli-plugin-eslint": "^4.4.1",
|
||||
"@vue/cli-service": "^4.4.1",
|
||||
"@vue/cli-plugin-eslint": "^4.5.4",
|
||||
"@vue/cli-service": "^4.5.4",
|
||||
"babel-eslint": "^10.1.0",
|
||||
"eslint": "^5.16.0",
|
||||
"eslint-plugin-vue": "^5.0.0",
|
||||
"papaparse": "^5.2.0",
|
||||
"sass": "^1.26.5",
|
||||
"papaparse": "^5.3.0",
|
||||
"sass": "^1.26.10",
|
||||
"sass-loader": "^7.1.0",
|
||||
"vue-cli-plugin-vuetify": "^0.6.3",
|
||||
"vue-template-compiler": "^2.6.11",
|
||||
"vuetify-loader": "^1.4.4"
|
||||
"vue-template-compiler": "^2.6.12",
|
||||
"vuetify-loader": "^1.6.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,6 +40,7 @@
|
||||
<v-list-item
|
||||
link
|
||||
to="cameras"
|
||||
ref="camerasTabOpener"
|
||||
@click="switchToDriverMode()"
|
||||
>
|
||||
<v-list-item-icon>
|
||||
@@ -71,7 +72,6 @@
|
||||
<v-list-item-title>Documentation</v-list-item-title>
|
||||
</v-list-item-content>
|
||||
</v-list-item>
|
||||
|
||||
<v-list-item
|
||||
v-if="this.$vuetify.breakpoint.mdAndUp"
|
||||
link
|
||||
@@ -111,63 +111,43 @@
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</v-navigation-drawer>
|
||||
<v-content>
|
||||
<v-main>
|
||||
<v-container
|
||||
fluid
|
||||
fill-height
|
||||
>
|
||||
<v-layout>
|
||||
<v-flex>
|
||||
<router-view @save="startTimer" />
|
||||
<v-snackbar
|
||||
v-model="saveSnackbar"
|
||||
:timeout="1000"
|
||||
top
|
||||
color="accent"
|
||||
>
|
||||
<div style="text-align: center;width: 100%;">
|
||||
<h4>Saved All changes</h4>
|
||||
</div>
|
||||
</v-snackbar>
|
||||
<div v-if="isLogger">
|
||||
<keep-alive>
|
||||
<log-view
|
||||
class="loggerClass"
|
||||
:log="log"
|
||||
/>
|
||||
</keep-alive>
|
||||
</div>
|
||||
<router-view v-on:switch-to-cameras="switchToDriverMode" />
|
||||
</v-flex>
|
||||
</v-layout>
|
||||
</v-container>
|
||||
</v-content>
|
||||
</v-main>
|
||||
|
||||
<v-dialog
|
||||
v-model="$store.state.logsOverlay"
|
||||
width="1500"
|
||||
dark
|
||||
>
|
||||
<logs />
|
||||
</v-dialog>
|
||||
</v-app>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import logView from '@femessage/log-viewer';
|
||||
import Logs from "./views/LogsView"
|
||||
|
||||
export default {
|
||||
name: 'App',
|
||||
components: {
|
||||
logView
|
||||
Logs
|
||||
},
|
||||
data: () => ({
|
||||
// Used so that we can switch back to the previously selected pipeline after camera calibration
|
||||
previouslySelectedIndex: undefined,
|
||||
timer: undefined,
|
||||
isLogger: false,
|
||||
log: "",
|
||||
}),
|
||||
computed: {
|
||||
saveSnackbar: {
|
||||
get() {
|
||||
return this.$store.state.saveBar;
|
||||
},
|
||||
set(value) {
|
||||
this.$store.commit("saveBar", value);
|
||||
}
|
||||
},
|
||||
compact: {
|
||||
get() {
|
||||
if (this.$store.state.compactMode === undefined) {
|
||||
@@ -186,8 +166,8 @@
|
||||
created() {
|
||||
document.addEventListener("keydown", e => {
|
||||
switch (e.key) {
|
||||
case '`' :
|
||||
this.isLogger = !this.isLogger;
|
||||
case "`":
|
||||
this.$store.state.logsOverlay = !this.$store.state.logsOverlay;
|
||||
break;
|
||||
case "z":
|
||||
if (e.ctrlKey && this.$store.getters.canUndo) {
|
||||
@@ -250,22 +230,12 @@
|
||||
toggleCompactMode() {
|
||||
this.compact = !this.compact;
|
||||
},
|
||||
saveSettings() {
|
||||
clearInterval(this.timer);
|
||||
this.saveSnackbar = true;
|
||||
this.handleInput("command", "save");
|
||||
},
|
||||
startTimer() {
|
||||
if (this.timer !== undefined) {
|
||||
clearInterval(this.timer);
|
||||
}
|
||||
this.timer = setInterval(this.saveSettings, 4000);
|
||||
},
|
||||
logMessage(message, level) {
|
||||
const colors = ["\u001B[30m", "\u001B[31m", "\u001B[33m", "\u001B[32m", "\u001B[37m", "\u001B[36m"]
|
||||
const reset = "\u001b[0m"
|
||||
this.log += `${colors[level]}${message}${reset}\n`
|
||||
console.log(message)
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
logMessage(message, levelInt) {
|
||||
this.$store.commit('logString', {
|
||||
['level']: levelInt,
|
||||
['message']: message
|
||||
})
|
||||
},
|
||||
switchToDriverMode() {
|
||||
this.previouslySelectedIndex = this.$store.getters.currentPipelineIndex;
|
||||
@@ -307,16 +277,6 @@
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.loggerClass {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
height: 25% !important;
|
||||
left: 0;
|
||||
right: 0;
|
||||
box-shadow: #282828 0 0 5px 1px;
|
||||
background-color: #2b2b2b;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar {
|
||||
width: 0.5em;
|
||||
border-radius: 5px;
|
||||
@@ -341,21 +301,23 @@
|
||||
#title {
|
||||
color: #ffd843;
|
||||
}
|
||||
|
||||
span {
|
||||
color: white;
|
||||
}
|
||||
</style>
|
||||
|
||||
<style>
|
||||
/* Hack */
|
||||
/* Hacks */
|
||||
|
||||
.v-divider {
|
||||
border-color: white !important;
|
||||
}
|
||||
|
||||
.v-input {
|
||||
font-size: 1rem !important;
|
||||
}
|
||||
.v-input {
|
||||
font-size: 1rem !important;
|
||||
}
|
||||
|
||||
/* This is unfortunately the only way to override table background color */
|
||||
.theme--dark.v-data-table > .v-data-table__wrapper > table > tbody > tr:hover:not(.v-data-table__expanded__content):not(.v-data-table__empty-wrapper) {
|
||||
background: #005281 !important;
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="scss">
|
||||
|
||||
@@ -29,7 +29,6 @@
|
||||
height: `${this.scale}%`,
|
||||
cursor: (this.colorPicking ? `url(${require("../../assets/eyedropper.svg")}),` : "") + "default",
|
||||
};
|
||||
console.log(ret);
|
||||
|
||||
if (this.$vuetify.breakpoint.xl) {
|
||||
ret["max-height"] = this.maxHeightXl;
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
<template v-slot:activator="{ on, attrs }">
|
||||
<span
|
||||
style="cursor: text !important;"
|
||||
class="white--text"
|
||||
v-bind="attrs"
|
||||
v-on="on"
|
||||
>{{ text }}</span>
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
align="center"
|
||||
cols="12"
|
||||
>
|
||||
<span class="text--white">Target Location</span>
|
||||
<span class="white--text">Target Location</span>
|
||||
<canvas
|
||||
id="canvasId"
|
||||
class="mt-2"
|
||||
|
||||
@@ -78,6 +78,7 @@
|
||||
<v-menu
|
||||
offset-y
|
||||
auto
|
||||
v-if="!$store.getters.isDriverMode"
|
||||
>
|
||||
<template v-slot:activator="{ on }">
|
||||
<v-icon
|
||||
|
||||
@@ -4,6 +4,7 @@ import Dashboard from "./views/PipelineView";
|
||||
import Cameras from "./views/CamerasView";
|
||||
import Settings from "./views/SettingsView";
|
||||
import Docs from "./views/DocsView";
|
||||
|
||||
Vue.use(Router);
|
||||
|
||||
export default new Router({
|
||||
|
||||
@@ -11,18 +11,14 @@ const set = key => (state, val) => {
|
||||
|
||||
export default new Vuex.Store({
|
||||
modules: {
|
||||
reflectivePipelineSettings: {
|
||||
state: {
|
||||
currentResolutionIndex: 0,
|
||||
},
|
||||
},
|
||||
undoRedo: undoRedo
|
||||
},
|
||||
state: {
|
||||
backendConnected: false,
|
||||
colorPicking: false,
|
||||
saveBar: false,
|
||||
logsOverlay: false,
|
||||
compactMode: localStorage.getItem("compactMode") === undefined ? undefined : localStorage.getItem("compactMode") === "true", // Compact mode is initially unset on purpose
|
||||
logMessages: [],
|
||||
currentCameraIndex: 0,
|
||||
selectedOutputs: [0, 1], // 0 indicates normal, 1 indicates threshold
|
||||
cameraSettings: [ // This is a list of objects representing the settings of all cameras
|
||||
@@ -132,13 +128,17 @@ export default new Vuex.Store({
|
||||
},
|
||||
},
|
||||
mutations: {
|
||||
saveBar: set('saveBar'),
|
||||
compactMode: set('compactMode'),
|
||||
cameraSettings: set('cameraSettings'),
|
||||
currentCameraIndex: set('currentCameraIndex'),
|
||||
selectedOutputs: set('selectedOutputs'),
|
||||
settings: set('settings'),
|
||||
calibrationData: set('calibrationData'),
|
||||
logString: (state, newStr) => {
|
||||
const str = state.logMessages;
|
||||
str.push(newStr)
|
||||
Vue.set(state, 'logString', str)
|
||||
},
|
||||
|
||||
solvePNPEnabled: (state, val) => {
|
||||
state.cameraSettings[state.currentCameraIndex].currentPipelineSettings.solvePNPEnabled = val;
|
||||
@@ -205,6 +205,11 @@ export default new Vuex.Store({
|
||||
currentPipelineResults: state => {
|
||||
return state.pipelineResults;
|
||||
},
|
||||
isCalibrated: state => {
|
||||
let resolution = state.cameraSettings[state.currentCameraIndex].videoFormatList[state.cameraSettings[state.currentCameraIndex].currentPipelineSettings.cameraVideoModeIndex];
|
||||
return state.cameraSettings[state.currentCameraIndex].calibrations
|
||||
.some(e => e.width === resolution.width && e.height === resolution.height);
|
||||
},
|
||||
cameraList: state => state.cameraSettings.map(it => it.nickname),
|
||||
currentCameraSettings: state => state.cameraSettings[state.currentCameraIndex],
|
||||
currentCameraIndex: state => state.currentCameraIndex,
|
||||
|
||||
@@ -232,7 +232,7 @@
|
||||
<v-icon left>
|
||||
mdi-download
|
||||
</v-icon>
|
||||
Download Checkerboard
|
||||
Download Chessboard
|
||||
</v-btn>
|
||||
<a
|
||||
ref="calibrationFile"
|
||||
@@ -415,8 +415,8 @@ export default {
|
||||
return this.filteredVideomodeIndex
|
||||
},
|
||||
set(i) {
|
||||
console.log(`Setting filtered index to ${i}`)
|
||||
this.filteredVideomodeIndex = i
|
||||
console.log(`Setting filtered index to ${i}`);
|
||||
this.filteredVideomodeIndex = i;
|
||||
this.$store.commit('mutateCalibrationState', {['videoModeIndex']: this.filteredResolutionList[i].index});
|
||||
}
|
||||
},
|
||||
@@ -430,13 +430,13 @@ export default {
|
||||
if(cal.width === resolution.width && cal.height === resolution.height) {
|
||||
ret = cal
|
||||
}
|
||||
})
|
||||
});
|
||||
return ret;
|
||||
},
|
||||
downloadBoard() {
|
||||
this.axios.get("http://" + this.$address + require('../assets/chessboard.png'), {responseType: 'blob'}).then((response) => {
|
||||
require('downloadjs')(response.data, "Calibration Board", "image/png")
|
||||
})
|
||||
require('downloadjs')(response.data, "Calibration Board", "image/png");
|
||||
});
|
||||
},
|
||||
sendCameraSettings() {
|
||||
this.axios.post("http://" + this.$address + "/api/settings/camera", {
|
||||
@@ -453,7 +453,7 @@ export default {
|
||||
|
||||
isCalibrated(resolution) {
|
||||
return this.$store.getters.currentCameraSettings.calibrations
|
||||
.some(e => e.width === resolution.width && e.height === resolution.height)
|
||||
.some(e => e.width === resolution.width && e.height === resolution.height);
|
||||
},
|
||||
|
||||
sendCalibrationMode() {
|
||||
@@ -464,20 +464,20 @@ export default {
|
||||
if (this.isCalibrating === true) {
|
||||
data['takeCalibrationSnapshot'] = true
|
||||
} else {
|
||||
const calData = this.calibrationData
|
||||
calData.isCalibrating = true
|
||||
data['startPnpCalibration'] = calData
|
||||
const calData = this.calibrationData;
|
||||
calData.isCalibrating = true;
|
||||
data['startPnpCalibration'] = calData;
|
||||
|
||||
console.log("starting calibration with index " + calData.videoModeIndex)
|
||||
console.log("starting calibration with index " + calData.videoModeIndex);
|
||||
}
|
||||
|
||||
this.$socket.send(this.$msgPack.encode(data));
|
||||
},
|
||||
sendCalibrationFinish() {
|
||||
console.log("finishing calibration for index " + this.$store.getters.currentCameraIndex)
|
||||
console.log("finishing calibration for index " + this.$store.getters.currentCameraIndex);
|
||||
|
||||
this.snackbar.text = "Calibrating...";
|
||||
this.snackbar.color = "secondary"
|
||||
this.snackbar.color = "secondary";
|
||||
this.snack = true;
|
||||
|
||||
this.axios.post("http://" + this.$address + "/api/settings/endCalibration", this.$store.getters.currentCameraIndex)
|
||||
@@ -525,9 +525,4 @@ export default {
|
||||
.v-data-table th,td {
|
||||
font-size: 1rem !important;
|
||||
}
|
||||
|
||||
/** This is unfortunately the only way to override table background color **/
|
||||
.theme--dark.v-data-table tbody tr:hover:not(.v-data-table__expanded__content):not(.v-data-table__empty-wrapper) {
|
||||
background: #005281;
|
||||
}
|
||||
</style>
|
||||
130
photon-client/src/views/LogsView.vue
Normal file
130
photon-client/src/views/LogsView.vue
Normal file
@@ -0,0 +1,130 @@
|
||||
<template>
|
||||
<v-card
|
||||
dark
|
||||
class="pt-3"
|
||||
color="primary"
|
||||
flat
|
||||
>
|
||||
<v-card-title>
|
||||
View Program Logs
|
||||
|
||||
<v-btn
|
||||
color="secondary"
|
||||
style="margin-left: auto;"
|
||||
depressed
|
||||
@click="download('photonlog.log', rawLogs.map(it => it.message).join('\n'))"
|
||||
>
|
||||
<v-icon left>
|
||||
mdi-download
|
||||
</v-icon>
|
||||
Download Log
|
||||
</v-btn>
|
||||
</v-card-title>
|
||||
<div class="pr-6 pl-6">
|
||||
<v-btn-toggle
|
||||
v-model="logLevel"
|
||||
dark
|
||||
multiple
|
||||
class="fill mb-4"
|
||||
>
|
||||
<v-btn
|
||||
v-for="(level) in possibleLevelArray"
|
||||
:key="level"
|
||||
color="secondary"
|
||||
class="fill"
|
||||
>
|
||||
{{ level }}
|
||||
</v-btn>
|
||||
</v-btn-toggle>
|
||||
<!-- Logs -->
|
||||
|
||||
<v-virtual-scroll
|
||||
:items="logMessageArray"
|
||||
item-height="50"
|
||||
height="600"
|
||||
>
|
||||
<template v-slot="{ item }">
|
||||
<div :class="[getColor(item) + '--text', 'log-item']">{{ item.message }}</div>
|
||||
</template>
|
||||
</v-virtual-scroll>
|
||||
</div>
|
||||
|
||||
<v-divider />
|
||||
|
||||
<v-card-actions>
|
||||
<v-spacer />
|
||||
<v-btn
|
||||
color="white"
|
||||
text
|
||||
@click="$store.state.logsOverlay = false"
|
||||
>
|
||||
Close
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
export default {
|
||||
name: "Logs",
|
||||
components: {
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
selectedLevel: [0, 1, 2],
|
||||
possibleLevelArray: ['ERROR', 'WARN', 'INFO', 'DEBUG'],
|
||||
colorArray: ['red', 'yellow','green', 'white'],
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
rawLogs() {
|
||||
return this.$store.state.logMessages;
|
||||
},
|
||||
logMessageArray() {
|
||||
const logArray = this.$store.state.logMessages;
|
||||
return logArray.filter(it => this.selectedLevel.includes(it.level));
|
||||
},
|
||||
logLevel: {
|
||||
get() {
|
||||
return this.selectedLevel
|
||||
},
|
||||
set(value) {
|
||||
this.selectedLevel = value;
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
getColor(message) {
|
||||
return this.colorArray[message.level];
|
||||
},
|
||||
|
||||
download(filename, text) {
|
||||
const element = document.createElement('a');
|
||||
element.setAttribute('href', 'data:text/plain;charset=utf-8,' + encodeURIComponent(text));
|
||||
element.setAttribute('download', filename);
|
||||
|
||||
element.style.display = 'none';
|
||||
document.body.appendChild(element);
|
||||
|
||||
element.click();
|
||||
|
||||
document.body.removeChild(element);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.v-btn-toggle.fill {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.v-btn-toggle.fill > .v-btn {
|
||||
width: 25%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
</style>
|
||||
@@ -82,6 +82,7 @@
|
||||
align="center"
|
||||
class="pl-3 pr-3"
|
||||
>
|
||||
<!-- -->
|
||||
<v-col lg="12">
|
||||
<p style="color: white;">
|
||||
Processing mode:
|
||||
@@ -100,6 +101,7 @@
|
||||
</v-btn>
|
||||
<v-btn
|
||||
color="secondary"
|
||||
@click="on3DClick"
|
||||
>
|
||||
<v-icon>mdi-cube-outline</v-icon>
|
||||
<span>3D</span>
|
||||
@@ -180,7 +182,7 @@
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-container>
|
||||
<!-- snack bar -->
|
||||
<!-- snack bar and modal -->
|
||||
<v-snackbar
|
||||
v-model="snackbar"
|
||||
:timeout="3000"
|
||||
@@ -196,6 +198,44 @@
|
||||
Close
|
||||
</v-btn>
|
||||
</v-snackbar>
|
||||
|
||||
<v-dialog
|
||||
v-model="dialog"
|
||||
width="500"
|
||||
>
|
||||
<v-card
|
||||
color="primary"
|
||||
dark
|
||||
>
|
||||
<v-card-title>
|
||||
Current resolution not calibrated
|
||||
</v-card-title>
|
||||
|
||||
<v-card-text>
|
||||
Because the current resolution {{ this.$store.getters.videoFormatList[this.$store.getters.currentPipelineSettings.cameraVideoModeIndex].width }}
|
||||
x {{ this.$store.getters.videoFormatList[this.$store.getters.currentPipelineSettings.cameraVideoModeIndex].height }}
|
||||
is not yet calibrated, 3D mode cannot be enabled. Please
|
||||
<a
|
||||
href="/#/cameras"
|
||||
class="white--text"
|
||||
@click="$emit('switch-to-cameras')"
|
||||
> visit the Cameras tab</a> to calibrate this resolution. For now, SolvePNP will do nothing.
|
||||
</v-card-text>
|
||||
|
||||
<v-divider />
|
||||
|
||||
<v-card-actions>
|
||||
<v-spacer />
|
||||
<v-btn
|
||||
color="white"
|
||||
text
|
||||
@click="closeUncalibratedDialog"
|
||||
>
|
||||
OK
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -226,6 +266,8 @@
|
||||
selectedTabsData: [0, 0, 0, 0],
|
||||
snackbar: false,
|
||||
counterData: 0,
|
||||
dialog: false,
|
||||
processingModeOverride: false
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
@@ -293,11 +335,13 @@
|
||||
},
|
||||
processingMode: {
|
||||
get() {
|
||||
return this.$store.getters.currentPipelineSettings.solvePNPEnabled ? 1 : 0;
|
||||
return (this.$store.getters.currentPipelineSettings.solvePNPEnabled || this.processingModeOverride) ? 1 : 0;
|
||||
},
|
||||
set(value) {
|
||||
this.$store.getters.currentPipelineSettings.solvePNPEnabled = value === 1;
|
||||
this.handlePipelineUpdate("solvePNPEnabled", value === 1);
|
||||
if (this.$store.getters.isCalibrated) {
|
||||
this.$store.getters.currentPipelineSettings.solvePNPEnabled = value === 1;
|
||||
this.handlePipelineUpdate("solvePNPEnabled", value === 1);
|
||||
}
|
||||
}
|
||||
},
|
||||
driverMode: {
|
||||
@@ -350,6 +394,11 @@
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
isCalibrated() {
|
||||
const resolution = this.$store.getters.videoFormatList[this.$store.getters.currentPipelineSettings.cameraVideoModeIndex];
|
||||
return this.$store.getters.currentCameraSettings.calibrations
|
||||
.some(e => e.width === resolution.width && e.height === resolution.height)
|
||||
},
|
||||
onImageClick(event) {
|
||||
// Only run on the input stream
|
||||
if (event.target.alt !== "Stream0") return;
|
||||
@@ -358,6 +407,18 @@
|
||||
if (ref && ref[0])
|
||||
ref[0].onClick(event)
|
||||
},
|
||||
on3DClick() {
|
||||
if (!this.$store.getters.isCalibrated) {
|
||||
this.dialog = true;
|
||||
this.processingModeOverride = true;
|
||||
}
|
||||
},
|
||||
closeUncalibratedDialog() {
|
||||
this.dialog = false;
|
||||
this.processingModeOverride = false;
|
||||
// this.$store.getters.currentPipelineSettings.solvePNPEnabled = false;
|
||||
this.handlePipelineUpdate("solvePNPEnabled", false);
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div>
|
||||
<span>Target Manipulation</span>
|
||||
<span class="white--text">Target Manipulation</span>
|
||||
<v-divider class="mt-2" />
|
||||
|
||||
<CVselect
|
||||
@@ -31,7 +31,7 @@
|
||||
|
||||
@rollback="e=> rollback('outputShowMultipleTargets', e)"
|
||||
/>
|
||||
<span>Robot Offset</span>
|
||||
<span class="white--text">Robot Offset</span>
|
||||
<v-divider class="mt-2" />
|
||||
<CVselect
|
||||
v-model="offsetRobotOffsetMode"
|
||||
|
||||
@@ -96,11 +96,6 @@
|
||||
return Math.atan(Math.tan(diagonalView / 2) * (resolution.width / diagonalAspect)) * 2 * (180 / Math.PI)
|
||||
}
|
||||
},
|
||||
allow3D: {
|
||||
get() {
|
||||
return this.$store.getters.currentCameraSettings.calibrated;
|
||||
}
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
let tmp = [];
|
||||
|
||||
@@ -97,9 +97,4 @@
|
||||
.v-data-table td {
|
||||
font-family: monospace !important;
|
||||
}
|
||||
|
||||
/** This is unfortunately the only way to override table background color **/
|
||||
.theme--dark.v-data-table tbody tr:hover:not(.v-data-table__expanded__content):not(.v-data-table__empty-wrapper) {
|
||||
background: #005281;
|
||||
}
|
||||
</style>
|
||||
@@ -67,6 +67,7 @@
|
||||
v-model="snack"
|
||||
top
|
||||
:color="snackbar.color"
|
||||
timeout="0"
|
||||
>
|
||||
<span>{{ snackbar.text }}</span>
|
||||
</v-snackbar>
|
||||
@@ -115,15 +116,16 @@ export default {
|
||||
{headers: {"Content-Type": "multipart/form-data"}}).then(() => {
|
||||
this.snackbar = {
|
||||
color: "success",
|
||||
text: "Settings imported successfully",
|
||||
text: "Settings imported successfully! Program will now exit...",
|
||||
};
|
||||
this.snack = true;
|
||||
}).catch(() => {
|
||||
this.snackbar = {
|
||||
color: "error",
|
||||
text: "Couldn't import settings",
|
||||
}
|
||||
color: "success",
|
||||
text: "Settings imported successfully! Program will now exit...",
|
||||
};
|
||||
this.snack = true;
|
||||
});
|
||||
this.snack = true;
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -57,17 +57,17 @@ public class ConfigManager {
|
||||
}
|
||||
|
||||
public static void saveUploadedSettingsZip(File uploadPath) {
|
||||
logger.info(uploadPath.getAbsolutePath());
|
||||
var folderPath = Path.of(System.getProperty("java.io.tmpdir"), "photonvision").toFile();
|
||||
folderPath.mkdirs();
|
||||
ZipUtil.unpack(uploadPath, folderPath);
|
||||
FileUtils.deleteDirectory(getRootFolder());
|
||||
try {
|
||||
org.apache.commons.io.FileUtils.copyDirectory(folderPath, getRootFolder().toFile());
|
||||
logger.info("Copied settings successfully!");
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
logger.error("Exception copying uploaded settings!", e);
|
||||
return;
|
||||
}
|
||||
System.exit(666);
|
||||
}
|
||||
|
||||
public PhotonConfiguration getConfig() {
|
||||
@@ -159,8 +159,6 @@ public class ConfigManager {
|
||||
}
|
||||
|
||||
public void saveToDisk() {
|
||||
logger.info("Saving settings...");
|
||||
|
||||
// Delete old configs
|
||||
FileUtils.deleteDirectory(camerasFolder.toPath());
|
||||
|
||||
@@ -214,6 +212,7 @@ public class ConfigManager {
|
||||
}
|
||||
}
|
||||
}
|
||||
logger.info("Settings saved!");
|
||||
}
|
||||
|
||||
private HashMap<String, CameraConfiguration> loadCameraConfigs() {
|
||||
@@ -341,7 +340,7 @@ public class ConfigManager {
|
||||
}
|
||||
|
||||
public void requestSave() {
|
||||
logger.debug("Requesting save...");
|
||||
logger.trace("Requesting save...");
|
||||
saveRequestTimestamp = System.currentTimeMillis();
|
||||
}
|
||||
|
||||
|
||||
@@ -18,12 +18,11 @@
|
||||
package org.photonvision.common.logging;
|
||||
|
||||
public enum LogLevel {
|
||||
OFF(0, Logger.ANSI_BLACK),
|
||||
ERROR(1, Logger.ANSI_RED),
|
||||
WARN(2, Logger.ANSI_YELLOW),
|
||||
INFO(3, Logger.ANSI_GREEN),
|
||||
DEBUG(4, Logger.ANSI_WHITE),
|
||||
TRACE(5, Logger.ANSI_CYAN);
|
||||
ERROR(0, Logger.ANSI_RED),
|
||||
WARN(1, Logger.ANSI_YELLOW),
|
||||
INFO(2, Logger.ANSI_GREEN),
|
||||
DEBUG(3, Logger.ANSI_WHITE),
|
||||
TRACE(4, Logger.ANSI_CYAN);
|
||||
|
||||
public final String colorCode;
|
||||
public final int code;
|
||||
|
||||
@@ -25,6 +25,7 @@ import java.util.Date;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.function.Supplier;
|
||||
import org.apache.commons.lang3.tuple.Pair;
|
||||
import org.photonvision.common.configuration.ConfigManager;
|
||||
import org.photonvision.common.dataflow.DataChangeService;
|
||||
import org.photonvision.common.dataflow.events.OutgoingUIEvent;
|
||||
@@ -47,6 +48,11 @@ public class Logger {
|
||||
private static final SimpleDateFormat simpleDateFormat =
|
||||
new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
|
||||
|
||||
private static final List<Pair<String, LogLevel>> uiBacklog = new ArrayList<>();
|
||||
private static boolean connected = false;
|
||||
|
||||
private static UILogAppender uiLogAppender = new UILogAppender();
|
||||
|
||||
private final String className;
|
||||
private final LogGroup group;
|
||||
|
||||
@@ -97,7 +103,7 @@ public class Logger {
|
||||
|
||||
static {
|
||||
currentAppenders.add(new ConsoleLogAppender());
|
||||
currentAppenders.add(new UILogAppender());
|
||||
currentAppenders.add(uiLogAppender);
|
||||
addFileAppender(ConfigManager.getInstance().getLogPath());
|
||||
}
|
||||
|
||||
@@ -126,6 +132,15 @@ public class Logger {
|
||||
var formattedMessage = format(message, level, group, clazz, shouldColor);
|
||||
a.log(formattedMessage, level);
|
||||
}
|
||||
if (!connected) uiBacklog.add(Pair.of(format(message, level, group, clazz, false), level));
|
||||
}
|
||||
|
||||
public static void sendConnectedBacklog() {
|
||||
for (var message : uiBacklog) {
|
||||
uiLogAppender.log(message.getLeft(), message.getRight());
|
||||
}
|
||||
connected = true;
|
||||
uiBacklog.clear();
|
||||
}
|
||||
|
||||
public boolean shouldLog(LogLevel logLevel) {
|
||||
@@ -244,6 +259,7 @@ public class Logger {
|
||||
|
||||
private static class FileLogAppender implements LogAppender {
|
||||
private OutputStream out;
|
||||
private boolean wantsFlush;
|
||||
|
||||
public FileLogAppender(Path logFilePath) {
|
||||
try {
|
||||
@@ -253,7 +269,10 @@ public class Logger {
|
||||
"FileLogAppender",
|
||||
() -> {
|
||||
try {
|
||||
out.flush();
|
||||
if (wantsFlush) {
|
||||
out.flush();
|
||||
wantsFlush = false;
|
||||
}
|
||||
} catch (IOException ignored) {
|
||||
}
|
||||
},
|
||||
@@ -269,6 +288,7 @@ public class Logger {
|
||||
message += "\n";
|
||||
try {
|
||||
out.write(message.getBytes());
|
||||
wantsFlush = true;
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
||||
@@ -32,6 +32,7 @@ import org.photonvision.common.configuration.ConfigManager;
|
||||
import org.photonvision.common.configuration.NetworkConfig;
|
||||
import org.photonvision.common.dataflow.networktables.NetworkTablesManager;
|
||||
import org.photonvision.common.hardware.HardwareManager;
|
||||
import org.photonvision.common.hardware.Platform;
|
||||
import org.photonvision.common.logging.LogGroup;
|
||||
import org.photonvision.common.logging.Logger;
|
||||
import org.photonvision.common.networking.NetworkManager;
|
||||
@@ -56,9 +57,17 @@ public class RequestHandler {
|
||||
e.printStackTrace();
|
||||
}
|
||||
ConfigManager.saveUploadedSettingsZip(tempZipPath);
|
||||
// restartDevice();
|
||||
ctx.status(200);
|
||||
logger.info("Settings uploaded, going down for restart.");
|
||||
|
||||
if (!Platform.isRaspberryPi()) {
|
||||
logger.info("(On non-PI platforms, the program may not restart manually...)");
|
||||
}
|
||||
|
||||
System.exit(0);
|
||||
} else {
|
||||
logger.error("Couldn't read uploaded settings ZIP! Ignoring.");
|
||||
ctx.status(500);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -53,7 +53,7 @@ public class Server {
|
||||
ws ->
|
||||
ws.onBinaryMessage(
|
||||
ctx ->
|
||||
logger.debug(
|
||||
logger.trace(
|
||||
() -> {
|
||||
var insa = ctx.session.getRemote().getInetSocketAddress();
|
||||
var host = insa.getAddress().toString() + ":" + insa.getPort();
|
||||
|
||||
@@ -105,14 +105,15 @@ public class SocketHandler {
|
||||
var entryValue = entry.getValue();
|
||||
var socketMessageType = SocketMessageType.fromEntryKey(entryKey);
|
||||
|
||||
logger.debug(
|
||||
"Got WS message: ["
|
||||
+ socketMessageType
|
||||
+ "] ==> ["
|
||||
+ entryKey
|
||||
+ "], ["
|
||||
+ entryValue
|
||||
+ "]");
|
||||
logger.trace(
|
||||
() ->
|
||||
"Got WS message: ["
|
||||
+ socketMessageType
|
||||
+ "] ==> ["
|
||||
+ entryKey
|
||||
+ "], ["
|
||||
+ entryValue
|
||||
+ "]");
|
||||
|
||||
if (socketMessageType == null) {
|
||||
logger.warn("Got unknown socket message type: " + entryKey);
|
||||
|
||||
@@ -26,6 +26,7 @@ import org.photonvision.common.dataflow.DataChangeSubscriber;
|
||||
import org.photonvision.common.dataflow.events.DataChangeEvent;
|
||||
import org.photonvision.common.dataflow.events.IncomingWebSocketEvent;
|
||||
import org.photonvision.common.dataflow.events.OutgoingUIEvent;
|
||||
import org.photonvision.common.logging.Logger;
|
||||
|
||||
public class UIInboundSubscriber extends DataChangeSubscriber {
|
||||
|
||||
@@ -46,6 +47,7 @@ public class UIInboundSubscriber extends DataChangeSubscriber {
|
||||
var message =
|
||||
new OutgoingUIEvent<>("fullsettings", settings, incomingWSEvent.originContext);
|
||||
DataChangeService.getInstance().publishEvent(message);
|
||||
Logger.sendConnectedBacklog();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -41,8 +41,21 @@ public class SolvePNPPipe
|
||||
|
||||
private final MatOfPoint2f imagePoints = new MatOfPoint2f();
|
||||
|
||||
private boolean hasWarned = false;
|
||||
|
||||
@Override
|
||||
protected List<TrackedTarget> process(List<TrackedTarget> targetList) {
|
||||
if (params.cameraCoefficients == null
|
||||
|| params.cameraCoefficients.getCameraIntrinsicsMat() == null
|
||||
|| params.cameraCoefficients.getCameraExtrinsicsMat() == null) {
|
||||
if (!hasWarned) {
|
||||
logger.warn(
|
||||
"Cannot perform solvePNP an uncalibrated camera! Please calibrate this resolution...");
|
||||
hasWarned = true;
|
||||
}
|
||||
return targetList;
|
||||
}
|
||||
|
||||
for (var target : targetList) {
|
||||
calculateTargetPose(target);
|
||||
}
|
||||
@@ -55,6 +68,7 @@ public class SolvePNPPipe
|
||||
var corners = target.getTargetCorners();
|
||||
if (corners == null
|
||||
|| corners.isEmpty()
|
||||
|| params.cameraCoefficients == null
|
||||
|| params.cameraCoefficients.getCameraIntrinsicsMat() == null
|
||||
|| params.cameraCoefficients.getCameraExtrinsicsMat() == null) {
|
||||
return;
|
||||
|
||||
@@ -40,6 +40,7 @@ public class VisionSourceManager {
|
||||
|
||||
final List<UsbCameraInfo> knownUsbCameras = new CopyOnWriteArrayList<>();
|
||||
final List<CameraConfiguration> unmatchedLoadedConfigs = new CopyOnWriteArrayList<>();
|
||||
private boolean hasWarned;
|
||||
|
||||
private static class SingletonHolder {
|
||||
private static final VisionSourceManager INSTANCE = new VisionSourceManager();
|
||||
@@ -132,12 +133,14 @@ public class VisionSourceManager {
|
||||
// Match camera configs to physical cameras
|
||||
var matchedCameras = matchUSBCameras(notYetLoadedCams, unmatchedLoadedConfigs);
|
||||
unmatchedLoadedConfigs.removeAll(matchedCameras);
|
||||
if (!unmatchedLoadedConfigs.isEmpty())
|
||||
if (!unmatchedLoadedConfigs.isEmpty() && !hasWarned) {
|
||||
logger.warn(
|
||||
() ->
|
||||
"After matching, "
|
||||
+ unmatchedLoadedConfigs.size()
|
||||
+ " configs remained unmatched. Is your camera disconnected?");
|
||||
hasWarned = true;
|
||||
}
|
||||
|
||||
// We add the matched cameras to the known camera list
|
||||
for (var cam : notYetLoadedCams) {
|
||||
|
||||
Reference in New Issue
Block a user