Switch to native WebSocket in UI (#649)

Greatly improves Firefox performance
This commit is contained in:
Matt
2022-12-27 07:23:55 -08:00
committed by GitHub
parent 4382b8ea3f
commit 95c55f08cf
6 changed files with 242 additions and 300 deletions

View File

@@ -1,35 +1,17 @@
<template>
<v-app>
<!-- Although most of the app runs with the "light" theme, the navigation drawer needs to have white text and icons so it uses the dark theme-->
<v-navigation-drawer
dark
app
permanent
:mini-variant="compact"
color="primary"
>
<v-navigation-drawer dark app permanent :mini-variant="compact" color="primary">
<v-list>
<!-- List item for the heading; note that there are some tricks in setting padding and image width make things look right -->
<v-list-item :class="compact ? 'pr-0 pl-0' : ''">
<v-list-item-icon class="mr-0">
<img
v-if="!compact"
class="logo"
src="./assets/logoLarge.png"
>
<img
v-else
class="logo"
src="./assets/logoSmall.png"
>
<img v-if="!compact" class="logo" src="./assets/logoLarge.png">
<img v-else class="logo" src="./assets/logoSmall.png">
</v-list-item-icon>
</v-list-item>
<v-list-item
link
to="dashboard"
@click="rollbackPipelineIndex()"
>
<v-list-item link to="dashboard" @click="rollbackPipelineIndex()">
<v-list-item-icon>
<v-icon>mdi-view-dashboard</v-icon>
</v-list-item-icon>
@@ -37,12 +19,7 @@
<v-list-item-title>Dashboard</v-list-item-title>
</v-list-item-content>
</v-list-item>
<v-list-item
ref="camerasTabOpener"
link
to="cameras"
@click="switchToDriverMode()"
>
<v-list-item ref="camerasTabOpener" link to="cameras" @click="switchToDriverMode()">
<v-list-item-icon>
<v-icon>mdi-camera</v-icon>
</v-list-item-icon>
@@ -50,11 +27,7 @@
<v-list-item-title>Cameras</v-list-item-title>
</v-list-item-content>
</v-list-item>
<v-list-item
link
to="settings"
@click="switchToSettingsTab()"
>
<v-list-item link to="settings" @click="switchToSettingsTab()">
<v-list-item-icon>
<v-icon>mdi-settings</v-icon>
</v-list-item-icon>
@@ -62,10 +35,7 @@
<v-list-item-title>Settings</v-list-item-title>
</v-list-item-content>
</v-list-item>
<v-list-item
link
to="docs"
>
<v-list-item link to="docs">
<v-list-item-icon>
<v-icon>mdi-bookshelf</v-icon>
</v-list-item-icon>
@@ -73,11 +43,7 @@
<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
@click.stop="toggleCompactMode"
>
<v-list-item v-if="this.$vuetify.breakpoint.mdAndUp" link @click.stop="toggleCompactMode">
<v-list-item-icon>
<v-icon v-if="compact">
mdi-chevron-right
@@ -97,44 +63,24 @@
<v-icon v-if="$store.state.settings.networkSettings.runNTServer">
mdi-server
</v-icon>
<img
v-else-if="$store.state.ntConnectionInfo.connected"
src="@/assets/robot.svg"
alt=""
>
<img
v-else
class="pulse"
style="border-radius: 100%"
src="@/assets/robot-off.svg"
alt=""
>
<img v-else-if="$store.state.ntConnectionInfo.connected" src="@/assets/robot.svg" alt="">
<img v-else class="pulse" style="border-radius: 100%" src="@/assets/robot-off.svg" alt="">
</v-list-item-icon>
<v-list-item-content>
<v-list-item-title
v-if="$store.state.settings.networkSettings.runNTServer"
class="text-wrap"
>
NetworkTables server running for {{ $store.state.ntConnectionInfo.clients ? $store.state.ntConnectionInfo.clients : 'zero' }} clients!
<v-list-item-title v-if="$store.state.settings.networkSettings.runNTServer" class="text-wrap">
NetworkTables server running for {{ $store.state.ntConnectionInfo.clients ?
$store.state.ntConnectionInfo.clients : 'zero'
}} clients!
</v-list-item-title>
<v-list-item-title
v-else-if="$store.state.ntConnectionInfo.connected && $store.state.backendConnected"
class="text-wrap"
>
<v-list-item-title v-else-if="$store.state.ntConnectionInfo.connected && $store.state.backendConnected"
class="text-wrap">
Robot connected! {{ $store.state.ntConnectionInfo.address }}
</v-list-item-title>
<v-list-item-title
v-else
class="text-wrap"
>
<v-list-item-title v-else class="text-wrap">
Not connected to robot!
</v-list-item-title>
<router-link
v-if="!$store.state.settings.networkSettings.runNTServer"
to="settings"
class="accent--text"
@click="switchToSettingsTab"
>
<router-link v-if="!$store.state.settings.networkSettings.runNTServer" to="settings" class="accent--text"
@click="switchToSettingsTab">
Team number is {{ $store.state.settings.networkSettings.teamNumber }}
</router-link>
</v-list-item-content>
@@ -145,11 +91,7 @@
<v-icon v-if="$store.state.backendConnected">
mdi-wifi
</v-icon>
<v-icon
v-else
class="pulse"
style="border-radius: 100%;"
>
<v-icon v-else class="pulse" style="border-radius: 100%;">
mdi-wifi-off
</v-icon>
</v-list-item-icon>
@@ -163,10 +105,7 @@
</v-list>
</v-navigation-drawer>
<v-main>
<v-container
fluid
fill-height
>
<v-container fluid fill-height>
<v-layout>
<v-flex>
<router-view @switch-to-cameras="switchToDriverMode" />
@@ -175,32 +114,15 @@
</v-container>
</v-main>
<v-dialog
v-model="$store.state.logsOverlay"
width="1500"
dark
>
<v-dialog v-model="$store.state.logsOverlay" width="1500" dark>
<logs />
</v-dialog>
<v-dialog
v-model="needsTeamNumberSet"
width="500"
dark
persistent
>
<v-card
dark
color="primary"
flat
>
<v-dialog v-model="needsTeamNumberSet" width="500" dark persistent>
<v-card dark color="primary" flat>
<v-card-title>No team number set!</v-card-title>
<v-card-text>
PhotonVision cannot connect to your robot! Please
<router-link
to="settings"
class="accent--text"
@click="switchToSettingsTab"
>
<router-link to="settings" class="accent--text" @click="switchToSettingsTab">
visit the settings tab
</router-link>
and set your team number.
@@ -215,134 +137,152 @@ import Logs from "./views/LogsView"
// import {mapState} from "vuex";
export default {
name: 'App',
components: {
Logs
},
data: () => ({
// Used so that we can switch back to the previously selected pipeline after camera calibration
previouslySelectedIndices: [],
timer: undefined,
teamNumberDialog: true
}),
computed: {
needsTeamNumberSet: {
get() {
return this.$store.state.settings.networkSettings.teamNumber < 1
&& this.teamNumberDialog && this.$store.state.backendConnected
&& !this.$route.name.toLowerCase().includes("settings");
}
},
compact: {
get() {
if (this.$store.state.compactMode === undefined) {
return this.$vuetify.breakpoint.smAndDown;
} else {
return this.$store.state.compactMode || this.$vuetify.breakpoint.smAndDown;
}
},
set(value) {
// compactMode is the user's preference for compact mode; it overrides screen size
this.$store.commit("compactMode", value);
localStorage.setItem("compactMode", value);
},
},
},
created() {
document.addEventListener("keydown", e => {
switch (e.key) {
case "`":
this.$store.state.logsOverlay = !this.$store.state.logsOverlay;
break;
case "z":
if (e.ctrlKey && this.$store.getters.canUndo) {
this.$store.dispatch('undo', {vm: this});
}
break;
case "y":
if (e.ctrlKey && this.$store.getters.canRedo) {
this.$store.dispatch('redo', {vm: this});
}
break;
}
});
this.$options.sockets.onmessage = (data) => {
try {
let message = this.$msgPack.decode(data.data);
for (let prop in message) {
if (message.hasOwnProperty(prop)) {
this.handleMessage(prop, message[prop]);
}
}
} catch (error) {
console.error('error: ' + JSON.stringify(data.data) + " , " + error);
}
};
this.$options.sockets.onopen = () => {
this.$store.commit("backendConnected", true)
this.$store.state.connectedCallbacks.forEach(it => it())
};
let closed = () => {
this.$store.commit("backendConnected", false)
};
this.$options.sockets.onclose = closed;
this.$options.sockets.onerror = closed;
this.$connect();
},
methods: {
handleMessage(key, value) {
if (key === "logMessage") {
this.logMessage(value["logMessage"], value["logLevel"]);
} else if(key === "log"){
this.logMessage(value["logMessage"]["logMessage"], value["logMessage"]["logLevel"]);
} else if (key === "updatePipelineResult") {
this.$store.commit('mutatePipelineResults', value)
} else if (this.$store.state.hasOwnProperty(key)) {
this.$store.commit(key, value);
} else if (this.$store.getters.currentPipelineSettings.hasOwnProperty(key)) {
this.$store.commit('mutatePipeline', {[key]: value});
} else if (this.$store.state.settings.hasOwnProperty(key)) {
this.$store.commit('mutateSettings', {[key]: value});
} else {
console.error("Unknown message from backend: " + value);
}
},
toggleCompactMode() {
this.compact = !this.compact;
},
// eslint-disable-next-line no-unused-vars
logMessage(message, levelInt) {
this.$store.commit('logString', {
['level']: levelInt,
['message']: message
})
},
switchToDriverMode() {
if (!this.previouslySelectedIndices) this.previouslySelectedIndices = [];
for (const [i, cameraSettings] of this.$store.state.cameraSettings.entries()) {
this.previouslySelectedIndices[i] = cameraSettings.currentPipelineIndex;
this.handleInputWithIndex('currentPipeline', -1, i);
}
},
rollbackPipelineIndex()
{
if (this.previouslySelectedIndices !== null) {
for (const [i] of this.$store.state.cameraSettings.entries()) {
this.handleInputWithIndex('currentPipeline', this.previouslySelectedIndices[i] || 0, i);
}
}
this.previouslySelectedIndices = null;
},
switchToSettingsTab() {
this.axios.post('http://' + this.$address + '/api/sendMetrics', {})
}
name: 'App',
components: {
Logs
},
data: () => ({
// Used so that we can switch back to the previously selected pipeline after camera calibration
previouslySelectedIndices: [],
timer: undefined,
teamNumberDialog: true
}),
computed: {
needsTeamNumberSet: {
get() {
return this.$store.state.settings.networkSettings.teamNumber < 1
&& this.teamNumberDialog && this.$store.state.backendConnected
&& !this.$route.name.toLowerCase().includes("settings");
}
},
compact: {
get() {
if (this.$store.state.compactMode === undefined) {
return this.$vuetify.breakpoint.smAndDown;
} else {
return this.$store.state.compactMode || this.$vuetify.breakpoint.smAndDown;
}
};
},
set(value) {
// compactMode is the user's preference for compact mode; it overrides screen size
this.$store.commit("compactMode", value);
localStorage.setItem("compactMode", value);
},
},
},
created() {
document.addEventListener("keydown", e => {
switch (e.key) {
case "`":
this.$store.state.logsOverlay = !this.$store.state.logsOverlay;
break;
case "z":
if (e.ctrlKey && this.$store.getters.canUndo) {
this.$store.dispatch('undo', { vm: this });
}
break;
case "y":
if (e.ctrlKey && this.$store.getters.canRedo) {
this.$store.dispatch('redo', { vm: this });
}
break;
}
});
this.recreateWebsocket();
},
methods: {
recreateWebsocket() {
const wsDataURL = 'ws://' + this.$address + '/websocket_data';
let socket = new WebSocket(wsDataURL);
socket.binaryType = "arraybuffer";
socket.onmessage = (event) => {
try {
let message = this.$msgPack.decode(event.data);
for (let prop in message) {
if (message.hasOwnProperty(prop)) {
this.handleMessage(prop, message[prop]);
}
}
} catch (error) {
console.log(event)
console.error('error: ' + JSON.stringify(event.data) + " , " + error);
}
};
socket.onerror = () => {
socket.close();
this.$store.commit("backendConnected", false)
};
socket.onopen = () => {
clearInterval(this.timerId);
socket.onclose = () => {
this.$store.commit("backendConnected", false)
this.timerId = setInterval(() => {
this.recreateWebsocket();
}, 1000);
};
this.$store.commit("backendConnected", true)
this.$store.state.connectedCallbacks.forEach(it => it())
};
this.$store.commit("websocket", socket);
},
handleMessage(key, value) {
if (key === "logMessage") {
this.logMessage(value["logMessage"], value["logLevel"]);
} else if (key === "log") {
this.logMessage(value["logMessage"]["logMessage"], value["logMessage"]["logLevel"]);
} else if (key === "updatePipelineResult") {
this.$store.commit('mutatePipelineResults', value)
} else if (this.$store.state.hasOwnProperty(key)) {
this.$store.commit(key, value);
} else if (this.$store.getters.currentPipelineSettings.hasOwnProperty(key)) {
this.$store.commit('mutatePipeline', { [key]: value });
} else if (this.$store.state.settings.hasOwnProperty(key)) {
this.$store.commit('mutateSettings', { [key]: value });
} else {
console.error("Unknown message from backend: " + value);
}
},
toggleCompactMode() {
this.compact = !this.compact;
},
// eslint-disable-next-line no-unused-vars
logMessage(message, levelInt) {
this.$store.commit('logString', {
['level']: levelInt,
['message']: message
})
},
switchToDriverMode() {
if (!this.previouslySelectedIndices) this.previouslySelectedIndices = [];
for (const [i, cameraSettings] of this.$store.state.cameraSettings.entries()) {
this.previouslySelectedIndices[i] = cameraSettings.currentPipelineIndex;
this.handleInputWithIndex('currentPipeline', -1, i);
}
},
rollbackPipelineIndex() {
if (this.previouslySelectedIndices !== null) {
for (const [i] of this.$store.state.cameraSettings.entries()) {
this.handleInputWithIndex('currentPipeline', this.previouslySelectedIndices[i] || 0, i);
}
}
this.previouslySelectedIndices = null;
},
switchToSettingsTab() {
this.axios.post('http://' + this.$address + '/api/sendMetrics', {})
}
}
};
</script>
<style lang="sass">
@@ -350,76 +290,77 @@ export default {
</style>
<style>
.pulse {
animation: pulse-animation 2s infinite;
}
.pulse {
animation: pulse-animation 2s infinite;
}
@keyframes pulse-animation {
0% {
box-shadow: 0 0 0 0px rgba(0, 0, 0, 0.2);
background-color: rgba(0, 0, 0, 0.2);
}
100% {
box-shadow: 0 0 0 20px rgba(0, 0, 0, 0);
background-color: rgba(0, 0, 0, 0);
}
}
@keyframes pulse-animation {
0% {
box-shadow: 0 0 0 0px rgba(0, 0, 0, 0.2);
background-color: rgba(0, 0, 0, 0.2);
}
.logo {
width: 100%;
height: 70px;
object-fit: contain;
}
100% {
box-shadow: 0 0 0 20px rgba(0, 0, 0, 0);
background-color: rgba(0, 0, 0, 0);
}
}
::-webkit-scrollbar {
width: 0.5em;
border-radius: 5px;
}
.logo {
width: 100%;
height: 70px;
object-fit: contain;
}
::-webkit-scrollbar-track {
-webkit-box-shadow: inset 0 0 6px rgba(0, 0, 0, 0.3);
border-radius: 10px;
}
::-webkit-scrollbar {
width: 0.5em;
border-radius: 5px;
}
::-webkit-scrollbar-track {
-webkit-box-shadow: inset 0 0 6px rgba(0, 0, 0, 0.3);
border-radius: 10px;
}
::-webkit-scrollbar-thumb {
background-color: #ffd843;
border-radius: 10px;
}
::-webkit-scrollbar-thumb {
background-color: #ffd843;
border-radius: 10px;
}
.container {
background-color: #232c37;
padding: 0 !important;
}
.container {
background-color: #232c37;
padding: 0 !important;
}
#title {
color: #ffd843;
}
#title {
color: #ffd843;
}
</style>
<style>
/* Hacks */
/* Hacks */
.v-divider {
border-color: white !important;
}
.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;
}
/* 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">
@import '~vuetify/src/styles/settings/_variables';
@import '~vuetify/src/styles/settings/_variables';
@media #{map-get($display-breakpoints, 'md-and-down')} {
html {
font-size: 14px !important;
}
}
@media #{map-get($display-breakpoints, 'md-and-down')} {
html {
font-size: 14px !important;
}
}
</style>

View File

@@ -15,16 +15,15 @@ if (process.env.NODE_ENV === "production") {
Vue.prototype.$address = location.hostname + ":5800";
}
const wsDataURL = '//' + Vue.prototype.$address + '/websocket_data';
// const wsDataURL = '//' + Vue.prototype.$address + '/websocket_data';
// import VueNativeSock from 'vue-native-websocket';
// Vue.use(VueNativeSock, wsDataURL, {
// reconnection: true,
// reconnectionDelay: 100,
// connectManually: true,
// format: "arraybuffer",
// });
import VueNativeSock from 'vue-native-websocket';
Vue.use(VueNativeSock, wsDataURL, {
reconnection: true,
reconnectionDelay: 100,
connectManually: true,
format: "arraybuffer",
});
Vue.use(VueAxios, axios);
Vue.prototype.$msgPack = msgPack(true);

View File

@@ -2,14 +2,14 @@ export const dataHandleMixin = {
methods: {
handleInput(key, value) {
let msg = this.$msgPack.encode({[key]: value});
this.$socket.send(msg);
this.$store.state.websocket.send(msg);
},
handleInputWithIndex(key, value, cameraIndex = this.$store.getters.currentCameraIndex) {
let msg = this.$msgPack.encode({
[key]: value,
["cameraIndex"]: cameraIndex,
});
this.$socket.send(msg);
this.$store.state.websocket.send(msg);
},
handleData(val) {
this.handleInput(val, this[val]);
@@ -22,7 +22,7 @@ export const dataHandleMixin = {
["cameraIndex"]: this.$store.getters.currentCameraIndex
}
});
this.$socket.send(msg);
this.$store.state.websocket.send(msg);
this.$emit('update')
},
handlePipelineUpdate(key, val) {
@@ -32,7 +32,7 @@ export const dataHandleMixin = {
["cameraIndex"]: this.$store.getters.currentCameraIndex
}
});
this.$socket.send(msg);
this.$store.state.websocket.send(msg);
this.$emit('update')
},
handleTruthyPipelineData(val) {
@@ -42,7 +42,7 @@ export const dataHandleMixin = {
["cameraIndex"]: this.$store.getters.currentCameraIndex
}
});
this.$socket.send(msg);
this.$store.state.websocket.send(msg);
this.$emit('update')
},
rollback(val, e) {

View File

@@ -15,6 +15,7 @@ export default new Vuex.Store({
},
state: {
backendConnected: false,
websocket: null,
ntConnectionInfo: {
connected: false,
address: "",
@@ -169,6 +170,7 @@ export default new Vuex.Store({
},
mutations: {
compactMode: set('compactMode'),
websocket: set('websocket'),
cameraSettings: set('cameraSettings'),
currentCameraIndex: set('currentCameraIndex'),
selectedOutputs: set('selectedOutputs'),

View File

@@ -656,7 +656,7 @@ export default {
console.log("starting calibration with index " + calData.videoModeIndex);
}
this.$store.commit('currentPipelineIndex', -2);
this.$socket.send(this.$msgPack.encode(data));
this.$store.state.websocket.send(this.$msgPack.encode(data));
},
sendCalibrationFinish() {
console.log("finishing calibration for index " + this.$store.getters.currentCameraIndex);

View File

@@ -247,7 +247,7 @@ export default {
'cameraIndex': this.$store.state.currentCameraIndex
}
});
this.$socket.send(msg);
this.$store.state.websocket.send(msg);
this.$emit('update');
}
},