feature: add farm mode

Signed-off-by: Stefan Dej <meteyou@gmail.com>
This commit is contained in:
Stefan Dej 2021-01-10 02:53:17 +01:00
parent 21669f11f7
commit 5831e43fba
31 changed files with 1705 additions and 144 deletions

View File

@ -4,8 +4,9 @@
"private": true,
"scripts": {
"serve": "vue-cli-service serve",
"build": "vue-cli-service build",
"lint": "vue-cli-service lint"
"build": "vue-cli-service build && npm run build.zip",
"lint": "vue-cli-service lint",
"build.zip": "cd ./dist && zip -r mainsail.zip ./ && cd .."
},
"dependencies": {
"axios": "^0.21.1",

View File

@ -29,6 +29,7 @@
<v-toolbar-title>{{ printername !== "" ? printername : hostname }}</v-toolbar-title>
</div>
<ul class="navi" :expand="$vuetify.breakpoint.mdAndUp">
<printer-selecter></printer-selecter>
<li v-for="(category, index) in routes" :key="index" :prepend-icon="category.icon"
:class="[category.path !== '/' && currentPage.includes(category.path) ? 'active' : '', 'nav-item']"
:value="true"
@ -81,15 +82,8 @@
</v-scroll-y-transition>
</v-main>
<v-dialog v-model="overlayDisconnect" persistent width="300">
<v-card color="primary" dark >
<v-card-text class="pt-2">
Connecting...
<v-progress-linear indeterminate color="white" class="mb-0 mt-2"></v-progress-linear>
</v-card-text>
</v-card>
</v-dialog>
<select-printer-dialog v-if="remoteMode"></select-printer-dialog>
<connecting-dialog v-if="!remoteMode"></connecting-dialog>
<update-dialog></update-dialog>
</v-app>
</template>
@ -99,21 +93,25 @@
import { mapState, mapGetters } from 'vuex'
import TopCornerMenu from "@/components/TopCornerMenu"
import UpdateDialog from "@/components/UpdateDialog"
import ConnectingDialog from "@/components/ConnectingDialog";
import SelectPrinterDialog from "@/components/SelectPrinterDialog";
import PrinterSelecter from "@/components/PrinterSelecter"
export default {
props: {
source: String,
},
components: {
PrinterSelecter,
ConnectingDialog,
SelectPrinterDialog,
UpdateDialog,
TopCornerMenu,
},
data: () => ({
overlayDisconnect: true,
drawer: null,
activeClass: 'active',
routes: routes,
routes: routes.filter((element) => element.title !== "Printers"),
boolNaviHeightmap: false,
}),
created () {
@ -140,6 +138,7 @@ export default {
save_config_pending: state => state.printer.configfile.save_config_pending,
klipperVersion: state => state.printer.software_version,
remoteMode: state => state.socket.remoteMode,
}),
...mapGetters([
'getTitle',
@ -244,9 +243,6 @@ export default {
config() {
this.boolNaviHeightmap = (typeof(this.config.bed_mesh) !== "undefined");
},
isConnected(newVal) {
this.overlayDisconnect = !newVal;
},
customStylesheet(newVal) {
if (newVal !== null) {
let style = document.getElementById("customStylesheet")
@ -318,7 +314,7 @@ export default {
margin: 0;
}
nav ul.navi a.nav-link {
nav ul.navi .nav-link {
display: block;
color: white;
border-radius: .5em;
@ -332,25 +328,25 @@ export default {
margin: 0.5em 1em;
}
nav ul.navi a.nav-link:hover,
nav ul.navi li.active>a.nav-link,
nav ul.navi a.nav-link.router-link-active {
nav ul.navi .nav-link:hover,
nav ul.navi li.active>.nav-link,
nav ul.navi .nav-link.router-link-active {
background: rgba(255,255,255,.3);
opacity: 1;
}
nav ul.navi li.active>a.nav-link i.nav-arrow ,
nav ul.navi a.nav-link.router-link-active i.nav-arrow {
nav ul.navi li.active>.nav-link i.nav-arrow ,
nav ul.navi .nav-link.router-link-active i.nav-arrow {
transform: rotate(0);
}
nav ul.navi a.nav-link>i.v-icon {
nav ul.navi .nav-link>i.v-icon {
color: white;
font-size: 1.7em;
margin-right: .5em;
}
nav ul.navi a.nav-link>span.nav-title {
nav ul.navi .nav-link>span.nav-title {
line-height: 30px;
font-weight: 600;
text-transform: uppercase;
@ -358,12 +354,15 @@ export default {
letter-spacing: 1px;
}
nav ul.navi a.nav-link>i.nav-arrow {
nav ul.navi .nav-link>.nav-arrow {
float: right;
margin-top: 5px;
margin-right: 0;
transform: rotate(90deg);
}
nav ul.navi .nav-link>.nav-arrow.right {
transform: rotate(-90deg);
}
nav ul.navi>li>ul.child {
display: none;
@ -377,16 +376,16 @@ export default {
display: block;
}
nav ul.navi>li>ul.child a.nav-link {
nav ul.navi>li>ul.child .nav-link {
padding: 5px 15px 5px 15px;
}
nav ul.navi>li>ul.child a.nav-link:hover,
nav ul.navi>li>ul.child a.nav-link.router-link-active {
nav ul.navi>li>ul.child .nav-link:hover,
nav ul.navi>li>ul.child .nav-link.router-link-active {
background: rgba(255,255,255,.2);
}
nav ul.navi>li>ul.child a.nav-link>span.nav-title {
nav ul.navi>li>ul.child .nav-link>span.nav-title {
text-transform: capitalize;
font-weight: 400;
font-size: 14px;

View File

@ -0,0 +1,86 @@
<style scoped>
</style>
<template>
<v-dialog v-model="showDialog" persistent :width="400">
<v-card dark>
<v-toolbar flat dense color="primary">
<v-toolbar-title>
<span class="subheading">
<v-icon class="mdi mdi-connection" left></v-icon>Connecting<span v-if="connectingFailed"> failed</span><span v-if="isConnecting"> to {{ parseInt(port) !== 80 ? hostname+":"+port : hostname }}</span>
</span>
</v-toolbar-title>
</v-toolbar>
<v-card-text class="pt-5" v-if="isConnecting">
<v-progress-linear color="white" indeterminate></v-progress-linear>
</v-card-text>
<v-card-text class="pt-5" v-if="!isConnecting && connectingFailed">
<p>Cannot not connect to {{ parseInt(port) !== 80 ? hostname+":"+port : hostname }}.</p>
<div class="text-center">
<v-btn @click="reconnect" color="primary">try again</v-btn>
</div>
</v-card-text>
</v-card>
</v-dialog>
</template>
<script>
import { mapState } from "vuex";
import Vue from "vue";
export default {
data: function() {
return {
showDialog: true,
}
},
computed: {
...mapState({
isConnected: state => state.socket.isConnected,
isConnecting: state => state.socket.isConnecting,
connectingFailed: state => state.socket.connectingFailed,
}),
protocol: {
get() {
return this.$store.state.socket.protocol
}
},
hostname: {
get() {
return this.$store.state.socket.hostname
},
set(newName) {
return this.$store.dispatch('socket/setData', { hostname: newName });
}
},
port: {
get() {
return this.$store.state.socket.port
},
set(newName) {
return this.$store.dispatch('socket/setData', { port: newName });
}
}
},
methods: {
connect() {
window.console.log("save connection")
Vue.prototype.$socket.setUrl(this.protocol+"://"+this.hostname+":"+this.port+"/websocket")
Vue.prototype.$socket.connect()
},
reconnect() {
this.$store.dispatch('socket/setData', { connectingFailed: false })
Vue.prototype.$socket.connect()
}
},
mounted() {
this.tmpHostname = this.hostname+":"+this.port
},
watch: {
isConnected(newVal) {
this.showDialog = !newVal
}
}
}
</script>

View File

@ -0,0 +1,84 @@
<style scoped>
</style>
<template>
<div v-if="(remoteMode && countPrinters > 1) || (!remoteMode && countPrinters)">
<li :class="currentPage === '/allPrinters' ? 'nav-item active' : 'nav-item '">
<div
class="nav-link "
@click.prevent
@click="switchToPrinters"
role="button"
>
<v-icon>mdi-view-dashboard-outline</v-icon>
<span class="nav-title">Printers</span>
<v-menu bottom :offset-x="true">
<template v-slot:activator="{ on, attrs }">
<v-icon class="nav-arrow right" v-bind="attrs" v-on="on" >mdi-chevron-down</v-icon>
</template>
<v-list dense>
<v-list-item two-line v-for="printer in printers" v-bind:key="printer._namespace" @click="changePrinter(printer)" :disabled="!printer.socket.isConnected" link>
<v-list-item-content>
<v-list-item-title>{{ getPrinterName(printer._namespace) }}</v-list-item-title>
<v-list-item-subtitle>{{ getPrinterDescription(printer)}}</v-list-item-subtitle>
</v-list-item-content>
</v-list-item>
</v-list>
</v-menu>
</div>
<v-item-group class="v-btn-toggle mx-4 d-block row" name="printers" v-if="false">
<v-btn class="col" color="primary" @click="switchToPrinters">Printers</v-btn>
</v-item-group>
</li>
<v-divider class="my-4"></v-divider>
</div>
</template>
<script>
import { mapState } from 'vuex'
import router from "@/plugins/router";
export default {
name: "PrinterSelecter.vue",
computed: {
...mapState({
remoteMode: state => state.socket.remoteMode
}),
countPrinters: {
get() {
return this.$store.getters["farm/countPrinters"]
}
},
printers: {
get() {
return this.$store.getters["farm/getPrinters"]
}
},
currentPage: function() {
return this.$route.fullPath;
},
},
methods: {
switchToPrinters() {
router.push("/allPrinters");
},
getPrinterName(namespace) {
return this.$store.getters["farm/"+namespace+"/getPrinterName"]
},
getPrinterDescription(printer) {
return this.$store.getters["farm/"+printer._namespace+"/getStatus"]
},
changePrinter(printer) {
if (printer.socket.isConnected) {
if (this.remoteMode) this.$store.dispatch('changePrinter', { printer: printer._namespace })
else window.location.href = "//"+printer.socket.hostname+(parseInt(printer.socket.webPort) !== 80 ? ':'+printer.socket.webPort : '')
}
},
}
}
</script>

View File

@ -0,0 +1,248 @@
<style scoped>
</style>
<template>
<v-dialog v-model="showDialog" persistent :width="400">
<v-card dark>
<v-toolbar flat dense color="primary">
<v-toolbar-title>
<span class="subheading">
<v-icon class="mdi mdi-connection" left></v-icon>
<span v-if="isConnecting">Connection to {{ parseInt(port) !== 80 ? hostname+':'+port : hostname }}</span>
<span v-if="connectingFailed">Connection failed</span>
<span v-if="!isConnecting && !connectingFailed && !dialogAddPrinter.bool && !dialogEditPrinter.bool">Select Printer</span>
<span v-if="dialogAddPrinter.bool">Add Printer</span>
<span v-if="dialogEditPrinter.bool">Edit Printer</span>
</span>
</v-toolbar-title>
<v-spacer></v-spacer>
<v-btn v-if="!isConnecting && !connectingFailed && !dialogAddPrinter.bool && !dialogEditPrinter.bool" small class="minwidth-0" @click="checkPrinters"><v-icon small>mdi-sync</v-icon></v-btn>
</v-toolbar>
<v-card-text class="pt-5" v-if="isConnecting">
<v-progress-linear color="white" indeterminate></v-progress-linear>
</v-card-text>
<v-card-text class="pt-5" v-if="!isConnecting && connectingFailed">
<p>Cannot not connect to {{ parseInt(port) !== 80 ? hostname+":"+port : hostname }}.</p>
<div class="text-center">
<v-btn @click="switchToChangePrinter" color="white" outlined class="mr-3">change printer</v-btn>
<v-btn @click="reconnect" color="primary">try again</v-btn>
</div>
</v-card-text>
<v-card-text class="pt-3" v-if="!isConnecting && dialogAddPrinter.bool">
<v-container class="px-0 py-0">
<v-row>
<v-col class="col-8">
<v-text-field
v-model="dialogAddPrinter.hostname"
:rules="[v => !!v || 'Hostname is required']"
label="Hostname/IP"
required
></v-text-field>
</v-col>
<v-col class="col-4">
<v-text-field
v-model="dialogAddPrinter.port"
:rules="[v => !!v || 'Port is required']"
label="Port"
required
></v-text-field>
</v-col>
</v-row>
<v-row>
<v-col class="text-right">
<v-btn
color="white"
outlined
class="middle"
@click="addPrinter"
>
add printer
</v-btn>
</v-col>
</v-row>
</v-container>
</v-card-text>
<v-card-text class="pt-3" v-if="!isConnecting && dialogEditPrinter.bool">
<v-container class="px-0 py-0">
<v-row>
<v-col class="col-8">
<v-text-field
v-model="dialogEditPrinter.hostname"
:rules="[v => !!v || 'Hostname is required']"
label="Hostname/IP"
required
></v-text-field>
</v-col>
<v-col class="col-4">
<v-text-field
v-model="dialogEditPrinter.port"
:rules="[v => !!v || 'Port is required']"
label="Port"
required
></v-text-field>
</v-col>
</v-row>
<v-row>
<v-col class="">
<v-btn
color="red"
outlined
class="middle minwidth-0"
@click="delPrinter"
>
<v-icon small>mdi-delete</v-icon>
</v-btn>
</v-col>
<v-col class="text-right">
<v-btn
color="white"
outlined
class="middle"
@click="updatePrinter"
>
update printer
</v-btn>
</v-col>
</v-row>
</v-container>
</v-card-text>
<v-card-text class="pt-3" v-if="!isConnecting && !connectingFailed && !dialogAddPrinter.bool && !dialogEditPrinter.bool">
<v-container class="px-0 pb-0">
<v-row v-for="(printer, index) in this['farm/getPrinters']" v-bind:key="index">
<v-col class="rounded transition-swing secondary py-2 px-2 mb-6" style="cursor: pointer;" @click="connect(printer)">
<v-row align="center">
<v-col class="col-auto pr-0">
<v-progress-circular
indeterminate
color="primary"
v-if="printer.socket.isConnecting"
></v-progress-circular>
<v-icon
:color="printer.socket.isConnected ? 'green' : 'red'"
v-if="!printer.socket.isConnecting"
>mdi-{{ printer.socket.isConnected ? 'checkbox-marked-circle' : 'cancel' }}</v-icon>
</v-col>
<v-col>{{ printer.socket.hostname }}{{ parseInt(printer.socket.port) !== 80 ? ":"+printer.socket.port : "" }}</v-col>
<v-col class="col-auto"><v-btn small class="minwidth-0" v-on:click.stop.prevent="editPrinter(index)"><v-icon small>mdi-pencil</v-icon></v-btn></v-col>
</v-row>
</v-col>
</v-row>
<v-row>
<v-col class="text-center mt-0">
<v-btn @click="dialogAddPrinter.bool = true">add printer</v-btn>
</v-col>
</v-row>
</v-container>
</v-card-text>
</v-card>
</v-dialog>
</template>
<script>
import { mapState, mapGetters, mapActions } from "vuex";
import Vue from "vue";
export default {
data: function() {
return {
showDialog: true,
dialogAddPrinter: {
bool: false,
hostname: "",
port: 7125
},
dialogEditPrinter: {
bool: false,
index: 0,
hostname: "",
port: 0
}
}
},
computed: {
...mapState({
isConnected: state => state.socket.isConnected,
isConnecting: state => state.socket.isConnecting,
connectingFailed: state => state.socket.connectingFailed,
remoteMode: state => state.socket.remoteMode,
protocol: state => state.socket.protocol,
hostname: state => state.socket.hostname,
port: state => state.socket.port,
}),
...mapGetters([
"farm/countPrinters",
"farm/getPrinters"
]),
...mapActions({
readPrinters: "farm/readStoredPrinters"
})
},
methods: {
addPrinter() {
this.$store.commit('farm/addPrinter',{
hostname: this.dialogAddPrinter.hostname,
port: this.dialogAddPrinter.port,
protocol: this.protocol
})
this.dialogAddPrinter.hostname = ""
this.dialogAddPrinter.bool = false
this.$store.dispatch("farm/savePrinters")
},
editPrinter(index) {
this.dialogEditPrinter.hostname = this["farm/getPrinters"][index].socket.hostname
this.dialogEditPrinter.port = this["farm/getPrinters"][index].socket.port
this.dialogEditPrinter.index = index
this.dialogEditPrinter.bool = true
},
updatePrinter() {
this.$store.commit("farm/"+this.dialogEditPrinter.index+"/setSocketData", {
hostname: this.dialogEditPrinter.hostname,
port: this.dialogEditPrinter.port,
isConnecting: true,
})
this.$store.dispatch("farm/"+this.dialogEditPrinter.index+"/reconnect")
this.dialogEditPrinter.bool = false
this.checkPrinters()
},
delPrinter() {
this.$store.commit("farm/removePrinter", { name: this.dialogEditPrinter.index })
this.$store.dispatch("farm/savePrinters")
this.dialogEditPrinter.bool = false
},
connect(printer) {
this.$store.dispatch('socket/setData', {
hostname: printer.socket.hostname,
port: printer.socket.port
})
Vue.prototype.$socket.setUrl(this.protocol+"://"+printer.socket.hostname+":"+printer.socket.port+"/websocket")
Vue.prototype.$socket.connect()
},
reconnect() {
this.$store.dispatch('socket/setData', { connectingFailed: false })
Vue.prototype.$socket.connect()
},
switchToChangePrinter() {
this.$store.dispatch('socket/setData', { connectingFailed: false })
},
checkPrinters() {
Object.entries(this['farm/getPrinters']).forEach(([key, printer]) => {
if (!printer.socket.isConnected && !printer.socket.isConnecting) {
this.$store.dispatch('farm/'+key+'/connect')
}
})
}
},
mounted() {
this.$store.dispatch("farm/readStoredPrinters")
},
watch: {
isConnected(newVal) {
this.showDialog = !newVal
},
}
}
</script>

View File

@ -33,7 +33,7 @@
<v-subheader class="pt-2" style="height: auto;">Power Devices</v-subheader>
<v-list-item v-for="(device, index) in devices" v-bind:key="index" class="minheight30" @click="changeSwitch(device, device.status)" :disabled="(device.status === 'error')">
<v-list-item-title>
<v-icon class="mr-2">mdi-{{ device.status === 'on' ? 'toggle-switch' : 'toggle-switch-off' }}</v-icon>{{ device.device }}
<v-icon class="mr-2" :color="device.status === 'on' ? '' : 'grey darken-2'">mdi-{{ device.status === 'on' ? 'toggle-switch' : 'toggle-switch-off' }}</v-icon>{{ device.device }}
</v-list-item-title>
</v-list-item>
</div>

View File

@ -2,9 +2,15 @@
import panels from './panels'
import topCornerMenu from './TopCornerMenu'
import updateDialog from './UpdateDialog'
import connectingDialog from './ConnectingDialog'
import selectPrinterDialog from './SelectPrinterDialog'
import printerSelecter from './PrinterSelecter'
export default {
panels,
topCornerMenu,
updateDialog
updateDialog,
connectingDialog,
selectPrinterDialog,
printerSelecter,
}

View File

@ -0,0 +1,121 @@
<style>
.v-card.disabledPrinter {
opacity: 0.6;
filter: grayscale(70%);
}
</style>
<template>
<v-card
:class="(!printer.socket.isConnected && !printer.socket.isConnecting ? 'disabledPrinter' : '')"
:loading="printer.socket.isConnecting"
@click="clickPrinter"
>
<v-toolbar flat dense :color="isCurrentPrinter ? 'primary' : ''">
<v-toolbar-title>
<span class="subheading"><v-icon left>mdi-printer-3d</v-icon>{{ printer_name }}</span>
</v-toolbar-title>
</v-toolbar>
<v-img
height="200px"
:src="printer_image"
class="d-flex align-end"
>
<v-card-title class="white--text py-2" style="background-color: rgba(0,0,0,0.3); backdrop-filter: blur(3px);">
<v-row>
<v-col class="col-auto pr-0 d-flex align-center" style="width: 58px">
<img class="my-auto" :src="printer_logo" style="width: 100%;" />
</v-col>
<v-col class="col" style="width: 100px">
<h3 class="font-weight-regular">{{ printer_status }}</h3>
<span class="subtitle-2 text-truncate px-0 text--disabled d-block" v-if="printer_current_filename !== ''"><v-icon small class="mr-1">mdi-file-outline</v-icon>{{ printer_current_filename }}</span>
</v-col>
</v-row>
</v-card-title>
</v-img>
<v-card-text class="px-0 py-2" v-if="printer_preview.length">
<v-container class="py-0">
<v-row>
<v-col :class="object.name === 'ETA' ? 'col-auto' : 'col' + ' px-2'" v-for="object in printer_preview" v-bind:key="object.name">
<strong class="d-block text-center">{{ object.name }}</strong>
<span class="d-block text-center">{{ object.value }}</span>
</v-col>
</v-row>
</v-container>
</v-card-text>
</v-card>
</template>
<script>
import { mapState } from 'vuex'
export default {
components: {
},
props: {
printer: {
type: Object,
required: true,
},
},
computed: {
...mapState({
remoteMode: state => state.socket.remoteMode
}),
isCurrentPrinter: {
get() {
return this.$store.getters["farm/"+this.printer._namespace+"/isCurrentPrinter"]
}
},
printer_name: {
get() {
return this.$store.getters["farm/"+this.printer._namespace+"/getPrinterName"]
}
},
printer_status: {
get() {
return this.$store.getters["farm/"+this.printer._namespace+"/getStatus"]
}
},
printer_current_filename: {
get() {
return this.$store.getters["farm/"+this.printer._namespace+"/getCurrentFilename"]
}
},
printer_image: {
get() {
return this.$store.getters["farm/"+this.printer._namespace+"/getImage"]
}
},
printer_logo: {
get() {
return this.$store.getters["farm/"+this.printer._namespace+"/getLogo"]
}
},
printer_position: {
get() {
return this.$store.getters["farm/"+this.printer._namespace+"/getPosition"]
}
},
printer_preview: {
get() {
return this.$store.getters["farm/"+this.printer._namespace+"/getPrinterPreview"]
}
}
},
methods: {
clickPrinter() {
if (this.printer.socket.isConnected) this.changePrinter()
else this.reconnectPrinter()
},
changePrinter() {
if (this.remoteMode) this.$store.dispatch('changePrinter', { printer: this.printer._namespace })
else window.location.href = "//"+this.printer.socket.hostname+(parseInt(this.printer.socket.webPort) !== 80 ? ':'+this.printer.socket.webPort : '')
},
reconnectPrinter() {
this.$store.dispatch("farm/"+this.printer._namespace+"/reconnect")
}
}
}
</script>

View File

@ -11,7 +11,7 @@
<span class="subheading"><v-icon left>mdi-cog</v-icon>General</span>
</v-toolbar-title>
</v-toolbar>
<v-card-text class="py-0">
<v-card-text class="pt-2 pb-0">
<v-text-field
v-model="printerName"
label="Printer Name"

View File

@ -0,0 +1,323 @@
<template>
<div>
<v-card>
<v-toolbar flat dense >
<v-toolbar-title>
<span class="subheading"><v-icon left>mdi-printer-3d</v-icon>Remote Printers</span>
</v-toolbar-title>
</v-toolbar>
<v-card-text class="py-3">
<v-container>
<v-row v-for="(printer, index) in this['farm/getPrinters']" v-bind:key="index">
<v-col class="rounded transition-swing secondary py-2 px-2 mb-6" style="cursor: pointer;">
<v-row align="center">
<v-col class="col-auto pr-0">
<v-progress-circular
indeterminate
color="primary"
v-if="printer.socket.isConnecting"
></v-progress-circular>
<v-icon
:color="printer.socket.isConnected ? 'green' : 'red'"
v-if="!printer.socket.isConnecting"
>mdi-{{ printer.socket.isConnected ? 'checkbox-marked-circle' : 'cancel' }}</v-icon>
</v-col>
<v-col>{{ printer.socket.hostname }}{{ parseInt(printer.socket.port) !== 80 ? ":"+printer.socket.port : "" }}</v-col>
<v-col class="col-auto"><v-btn small class="minwidth-0" v-on:click.stop.prevent="editPrinter(index)"><v-icon small>mdi-pencil</v-icon></v-btn></v-col>
</v-row>
</v-col>
</v-row>
<v-row>
<v-col class="text-center mt-0">
<v-btn @click="dialogAddPrinter.bool = true">add printer</v-btn>
</v-col>
</v-row>
</v-container>
</v-card-text>
</v-card>
<v-dialog v-model="dialogAddPrinter.bool" persistent :width="400">
<v-card dark>
<v-toolbar flat dense color="primary">
<v-toolbar-title>
<span class="subheading">
<v-icon class="mdi mdi-connection" left></v-icon>
Add Printer
</span>
</v-toolbar-title>
<v-spacer></v-spacer>
<v-btn small class="minwidth-0" @click="dialogAddPrinter.bool = false"><v-icon small>mdi-close-thick</v-icon></v-btn>
</v-toolbar>
<v-card-text class="pt-3" v-if="remoteMode">
<v-container class="px-0 py-0">
<v-row>
<v-col class="col-8">
<v-text-field
v-model="dialogAddPrinter.hostname"
:rules="[v => !!v || 'Hostname is required']"
label="Hostname/IP"
required
></v-text-field>
</v-col>
<v-col class="col-4">
<v-text-field
v-model="dialogAddPrinter.port"
:rules="[v => !!v || 'Port is required']"
label="Port"
required
></v-text-field>
</v-col>
</v-row>
<v-row>
<v-col class="text-right">
<v-btn
color="white"
outlined
class="middle"
@click="addPrinter"
>
add printer
</v-btn>
</v-col>
</v-row>
</v-container>
</v-card-text>
<v-card-text class="pt-3" v-if="!remoteMode">
<v-container class="px-0 py-0">
<v-row>
<v-col class="col-12">
<v-text-field
v-model="dialogAddPrinter.hostname"
:rules="[v => !!v || 'Hostname is required']"
label="Hostname/IP"
required
></v-text-field>
</v-col>
</v-row>
<v-row>
<v-col class="col-6">
<v-text-field
v-model="dialogAddPrinter.webPort"
:rules="[v => !!v || 'Web-Port is required']"
label="Web-Port"
required
></v-text-field>
</v-col>
<v-col class="col-6">
<v-text-field
v-model="dialogAddPrinter.port"
:rules="[v => !!v || 'API-Port is required']"
label="API-Port"
required
></v-text-field>
</v-col>
</v-row>
<v-row>
<v-col class="text-right">
<v-btn
color="white"
outlined
class="middle"
@click="addPrinter"
>
add printer
</v-btn>
</v-col>
</v-row>
</v-container>
</v-card-text>
</v-card>
</v-dialog>
<v-dialog v-model="dialogEditPrinter.bool" persistent :width="400">
<v-card dark>
<v-toolbar flat dense color="primary">
<v-toolbar-title>
<span class="subheading">
<v-icon class="mdi mdi-connection" left></v-icon>
Edit Printer
</span>
</v-toolbar-title>
<v-spacer></v-spacer>
<v-btn small class="minwidth-0" @click="dialogEditPrinter.bool = false"><v-icon small>mdi-close-thick</v-icon></v-btn>
</v-toolbar>
<v-card-text class="pt-3" v-if="remoteMode">
<v-container class="px-0 py-0">
<v-row>
<v-col class="col-8">
<v-text-field
v-model="dialogEditPrinter.hostname"
:rules="[v => !!v || 'Hostname is required']"
label="Hostname/IP"
required
></v-text-field>
</v-col>
<v-col class="col-4">
<v-text-field
v-model="dialogEditPrinter.port"
:rules="[v => !!v || 'Port is required']"
label="Port"
required
></v-text-field>
</v-col>
</v-row>
<v-row>
<v-col class="">
<v-btn
color="red"
outlined
class="middle minwidth-0"
@click="delPrinter"
>
<v-icon small>mdi-delete</v-icon>
</v-btn>
</v-col>
<v-col class="text-right">
<v-btn
color="white"
outlined
class="middle"
@click="updatePrinter"
>
update printer
</v-btn>
</v-col>
</v-row>
</v-container>
</v-card-text>
<v-card-text class="pt-3" v-if="!remoteMode">
<v-container class="px-0 py-0">
<v-row>
<v-col class="col-12">
<v-text-field
v-model="dialogEditPrinter.hostname"
:rules="[v => !!v || 'Hostname is required']"
label="Hostname/IP"
required
></v-text-field>
</v-col>
</v-row>
<v-row>
<v-col class="col-6">
<v-text-field
v-model="dialogEditPrinter.webPort"
:rules="[v => !!v || 'Web-Port is required']"
label="Web-Port"
required
></v-text-field>
</v-col>
<v-col class="col-6">
<v-text-field
v-model="dialogEditPrinter.port"
:rules="[v => !!v || 'API-Port is required']"
label="API-Port"
required
></v-text-field>
</v-col>
</v-row>
<v-row>
<v-col class="">
<v-btn
color="red"
outlined
class="middle minwidth-0"
@click="delPrinter"
>
<v-icon small>mdi-delete</v-icon>
</v-btn>
</v-col>
<v-col class="text-right">
<v-btn
color="white"
outlined
class="middle"
@click="updatePrinter"
>
update printer
</v-btn>
</v-col>
</v-row>
</v-container>
</v-card-text>
</v-card>
</v-dialog>
</div>
</template>
<script>
import { mapState, mapGetters } from 'vuex';
export default {
components: {
},
data: function() {
return {
dialogAddPrinter: {
bool: false,
hostname: "",
port: "7125",
webPort: "80"
},
dialogEditPrinter: {
bool: false,
hostname: "",
port: "",
webPort: "80",
index: ""
}
}
},
computed: {
...mapState({
remoteMode: state => state.socket.remoteMode,
protocol: state => state.socket.protocol,
}),
...mapGetters([
'farm/getPrinters',
])
},
methods: {
addPrinter() {
this.$store.commit('farm/addPrinter',{
hostname: this.dialogAddPrinter.hostname,
port: this.dialogAddPrinter.port,
webPort: this.dialogAddPrinter.webPort,
protocol: this.protocol
})
this.dialogAddPrinter.hostname = ""
this.dialogAddPrinter.port = "7125"
this.dialogAddPrinter.webPort = "80"
this.dialogAddPrinter.bool = false
this.$store.dispatch("farm/savePrinters")
},
editPrinter(index) {
this.dialogEditPrinter.hostname = this["farm/getPrinters"][index].socket.hostname
this.dialogEditPrinter.port = this["farm/getPrinters"][index].socket.port
this.dialogEditPrinter.webPort = this["farm/getPrinters"][index].socket.webPort
this.dialogEditPrinter.index = index
this.dialogEditPrinter.bool = true
},
updatePrinter() {
this.$store.commit("farm/"+this.dialogEditPrinter.index+"/setSocketData", {
hostname: this.dialogEditPrinter.hostname,
port: this.dialogEditPrinter.port,
webPort: this.dialogEditPrinter.webPort,
isConnecting: true,
})
this.$store.dispatch("farm/"+this.dialogEditPrinter.index+"/reconnect")
this.dialogEditPrinter.bool = false
this.$store.dispatch("farm/savePrinters")
},
delPrinter() {
this.$store.commit("farm/removePrinter", { name: this.dialogEditPrinter.index })
this.$store.dispatch("farm/savePrinters")
this.dialogEditPrinter.bool = false
this.$store.dispatch("farm/savePrinters")
},
}
}
</script>

View File

@ -2,7 +2,7 @@
<v-card>
<v-toolbar flat dense >
<v-toolbar-title>
<span class="subheading"><v-icon left>mdi-update</v-icon>Update</span>
<span class="subheading"><v-icon left>mdi-update</v-icon>Update Manager</span>
</v-toolbar-title>
<v-spacer></v-spacer>
<v-tooltip top>
@ -12,82 +12,95 @@
<span>Check for updates</span>
</v-tooltip>
</v-toolbar>
<v-card-text class="px-0 pt-0 pb-0 content">
<v-row v-if="'version' in klipper">
<v-col class="pl-6 py-2 text-no-wrap">
<strong>Klipper</strong><br />
{{ klipper.version }}
</v-col>
<v-col class="pr-6 py-2 text-right">
<v-chip
small
label
outlined
:color="getColor(klipper)"
@click="updateKlipper"
:disabled="is_disabled(klipper)"
class="minwidth-0 mt-2 px-2 text-uppercase"
><v-icon small class="mr-1">mdi-{{ getIcon(klipper) }}</v-icon>{{ getText(klipper) }}</v-chip>
</v-col>
</v-row>
<div v-if="'version' in moonraker">
<v-divider class="mt-0 mb-0" ></v-divider>
<v-row>
<v-col class="pl-6 py-2 text-no-wrap">
<strong>Moonraker</strong><br />
{{ moonraker.version }}
<v-card-text class="px-0 py-0">
<v-container py-0 px-0>
<v-row v-if="'version' in klipper" class="py-2">
<v-col class="pl-6 text-no-wrap">
<strong>Klipper</strong><br />
<span v-if="'remote_version' in klipper && klipper.version !== klipper.remote_version">{{ klipper.version+' &gt; '+klipper.remote_version }}</span>
<span v-if="!('remote_version' in klipper && klipper.version !== klipper.remote_version)">{{ klipper.version }}</span>
</v-col>
<v-col class="pr-6 py-2 text-right">
<v-col class="pr-6 text-right">
<v-chip
small
label
outlined
:color="getColor(moonraker)"
@click="updateMoonraker"
:disabled="is_disabled(moonraker)"
class="minwidth-0 mt-2 px-2 text-uppercase"
><v-icon small class="mr-1">mdi-{{ getIcon(moonraker) }}</v-icon>{{ getText(moonraker) }}</v-chip>
:color="getColor(klipper)"
@click="updateKlipper"
:disabled="is_disabled(klipper)"
class="minwidth-0 mt-3 px-2 text-uppercase"
><v-icon small class="mr-1">mdi-{{ getIcon(klipper) }}</v-icon>{{ getText(klipper) }}</v-chip>
</v-col>
</v-row>
</div>
<div v-if="mainsail !== false && 'version' in mainsail">
<v-divider class="mt-0 mb-0" ></v-divider>
<v-row>
<v-col class="pl-6 py-2 text-no-wrap">
<strong>Mainsail</strong><br />
{{ 'v'+package_version }}
<span v-if="!is_disabled(mainsail)"> &gt; {{ mainsail.remote_version.replace('Version ', 'v') }}</span>
</v-col>
<v-col class="pr-6 py-2 text-right">
<v-chip
small
label
outlined
:color="getColor(mainsail)"
@click="updateMainsail"
:disabled="is_disabled(mainsail)"
class="minwidth-0 mt-2 px-2 text-uppercase"
><v-icon small class="mr-1">mdi-{{ getIcon(mainsail) }}</v-icon>{{ getText(mainsail) }}</v-chip>
</v-col>
</v-row>
</div>
<v-divider class="mt-0 mb-0 border-top-2" ></v-divider>
<v-row>
<v-col class="pl-6 py-2 text-no-wrap">
<strong>System</strong><br />
OS-Packages
</v-col>
<v-col class="pr-6 py-2 text-right">
<v-chip
small
label
outlined
color="gray"
@click="updateSystem"
class="minwidth-0 mt-2 px-2 text-uppercase"
><v-icon small class="mr-1">mdi-progress-upload</v-icon>upgrade</v-chip>
</v-col>
</v-row>
<div v-if="'version' in moonraker">
<v-divider class="my-0" ></v-divider>
<v-row class="py-2">
<v-col class="pl-6 text-no-wrap">
<strong>Moonraker</strong><br />
<span v-if="'remote_version' in moonraker && moonraker.version !== moonraker.remote_version">{{ moonraker.version+' &gt; '+moonraker.remote_version }}</span>
<span v-if="!('remote_version' in moonraker && moonraker.version !== moonraker.remote_version)">{{ moonraker.version }}</span>
</v-col>
<v-col class="pr-6 text-right">
<v-chip
small
label
outlined
:color="getColor(moonraker)"
@click="updateMoonraker"
:disabled="is_disabled(moonraker)"
class="minwidth-0 mt-3 px-2 text-uppercase"
><v-icon small class="mr-1">mdi-{{ getIcon(moonraker) }}</v-icon>{{ getText(moonraker) }}</v-chip>
</v-col>
</v-row>
</div>
<div v-if="mainsail !== false && 'version' in mainsail">
<v-divider class="mt-0 mb-0" ></v-divider>
<v-row class="py-2">
<v-col class="pl-6 text-no-wrap">
<strong>Mainsail</strong><br />
{{ 'v'+package_version }}
<span v-if="!is_disabled(mainsail)"> &gt; {{ mainsail.remote_version.replace('Version ', 'v') }}</span>
</v-col>
<v-col class="pr-6 text-right">
<v-chip
small
label
outlined
:color="getColor(mainsail)"
@click="updateMainsail"
:disabled="is_disabled(mainsail)"
class="minwidth-0 mt-3 px-2 text-uppercase"
><v-icon small class="mr-1">mdi-{{ getIcon(mainsail) }}</v-icon>{{ getText(mainsail) }}</v-chip>
</v-col>
</v-row>
</div>
<div v-if="system !== false && 'package_count' in system">
<v-divider class="my-0 border-top-2" ></v-divider>
<v-row class="pt-2">
<v-col class="pl-6 text-no-wrap">
<strong>System</strong><br />
<v-tooltip top v-if="system.package_count > 0">
<template v-slot:activator="{ on, attrs }">
<span v-bind="attrs" v-on="on">{{ system.package_count }} packages can be upgraded</span>
</template>
<span>{{ system.package_list.join(', ') }}</span>
</v-tooltip>
<span v-if="system.package_count === 0">OS-Packages</span>
</v-col>
<v-col class="pr-6 text-right">
<v-chip
small
label
outlined
:color="system.package_count ? 'primary' : 'green'"
:disabled="!(system.package_count)"
@click="updateSystem"
class="minwidth-0 mt-3 px-2 text-uppercase"
><v-icon small class="mr-1">mdi-{{ system.package_count ? 'progress-upload' : 'check' }}</v-icon>{{ system.package_count ? 'upgrade' : 'up-to-date' }}</v-chip>
</v-col>
</v-row>
</div>
</v-container>
</v-card-text>
</v-card>
</template>
@ -109,6 +122,7 @@
package_version: state => state.packageVersion,
klipper: state => state.server.updateManager.klipper,
moonraker: state => state.server.updateManager.moonraker,
system: state => state.server.updateManager.system,
loadings: state => state.socket.loadings,
}),
mainsail:{

View File

@ -12,7 +12,7 @@
<v-card-text>
<v-container px-0 py-0>
<v-row>
<v-col class="py-2">
<v-col class="pt-2 mb-1">
<v-text-field
v-model="webcamUrl"
hide-details

View File

@ -11,6 +11,7 @@ import ConfigFilesPanel from "./ConfigFilesPanel";
import RunoutPanel from "./RunoutPanel";
import LogfilesPanel from "./LogfilesPanel";
import UpdatePanel from "./UpdatePanel";
import RemotePrintersPanel from "./RemotePrintersPanel";
Vue.component('settings-general-panel', GeneralPanel);
Vue.component('settings-webcam-panel', WebcamPanel);
@ -23,6 +24,7 @@ Vue.component('settings-runout-panel', RunoutPanel);
Vue.component('settings-logfiles-panel', LogfilesPanel);
Vue.component('settings-config-files-panel', ConfigFilesPanel);
Vue.component('settings-update-panel', UpdatePanel);
Vue.component('settings-remote-printers-panel', RemotePrintersPanel);
export default {

View File

@ -24,13 +24,13 @@ fetch('/config.json')
.then(file => {
store.commit('socket/setData', file);
const websocketProtocol = document.location.protocol === 'https:' ? 'wss://' : 'ws://';
const socketClient = new WebSocketClient(websocketProtocol + store.state.socket.hostname + ':' + store.state.socket.port + '/websocket', {
const socketClient = new WebSocketClient(store.state.socket.protocol + '://' + store.state.socket.hostname + ':' + store.state.socket.port + '/websocket', {
store: store,
reconnectEnabled: true,
reconnectInterval: store.state.socket.reconnectInterval,
});
socketClient.connect();
if (!store.state.socket.remoteMode) socketClient.connect()
Vue.prototype.$socket = socketClient;
new Vue({

37
src/pages/Farm.vue Normal file
View File

@ -0,0 +1,37 @@
<style>
</style>
<template>
<v-row>
<v-col
v-for="(printer,key) in printers"
v-bind:key="key"
class="col-12 col-sm-6 col-md-4"
>
<farm-printer-panel v-bind:printer="printer"></farm-printer-panel>
</v-col>
</v-row>
</template>
<script>
import FarmPrinterPanel from "@/components/panels/FarmPrinterPanel";
export default {
components: { FarmPrinterPanel },
data () {
return {
}
},
computed: {
printers: {
get() {
return this.$store.getters["farm/getPrinters"]
}
}
},
methods: {
}
}
</script>

View File

@ -4,14 +4,15 @@
<v-col class="col-12 col-md-6 col-lg-4">
<settings-general-panel></settings-general-panel>
<settings-webcam-panel class="mt-6"></settings-webcam-panel>
</v-col>
<v-col class="col-12 col-md-6 col-lg-4">
<settings-dashboard-panel></settings-dashboard-panel>
<settings-dashboard-panel class="mt-6"></settings-dashboard-panel>
<settings-console-panel class="mt-6"></settings-console-panel>
</v-col>
<v-col class="col-12 col-md-6 col-lg-4">
<settings-macros-panel></settings-macros-panel>
</v-col>
<v-col class="col-12 col-md-6 col-lg-4">
<settings-remote-printers-panel></settings-remote-printers-panel>
</v-col>
</v-row>
</div>
</template>

View File

@ -1,26 +1,32 @@
export default class WebSocketClient {
constructor (url, options) {
this.instance = null;
this.url = url;
this.reconnectInterval = options.reconnectInterval;
this.store = options.store;
this.wsData = [];
this.timerId = 0;
this.keepAliveTimeout = 1000;
this.instance = null
this.url = url
this.reconnects = 0
this.maxReconnects = options.maxReconnects || 10
this.reconnectInterval = options.reconnectInterval || 1000
this.store = options.store
this.wsData = []
this.timerId = 0
this.keepAliveTimeout = 1000
this.onOpen = null;
this.onMessage = null;
this.onClose = null;
this.onError = null;
this.onOpen = null
this.onMessage = null
this.onClose = null
this.onError = null
this.blacklistMessages = [
"Metadata not available for",
"Klippy Request Timed Out",
"Klippy Host not connected",
];
]
this.blacklistFunctions = [
"getPowerDevices",
];
]
}
setUrl(url) {
this.url = url
}
createMessage (method, params, id) {
@ -42,26 +48,37 @@ export default class WebSocketClient {
}
connect () {
this.instance = new WebSocket(this.url);
this.store.dispatch("socket/setData", { isConnecting: true })
this.instance = new WebSocket(this.url)
this.instance.onopen = () => {
if (this.store) this.passToStore('socket/onOpen', event);
};
this.reconnects = 0
if (this.store) this.passToStore('socket/onOpen', event)
}
this.instance.onclose = (e) => {
this.passToStore('socket/onClose', e);
this.passToStore('socket/onClose', e)
window.console.log("reconnectInterval: "+this.reconnectInterval)
setTimeout(() => {
this.connect();
}, this.reconnectInterval);
};
if (!e.wasClean && this.reconnects < this.maxReconnects) {
this.reconnects++
setTimeout(() => {
this.connect()
}, this.reconnectInterval)
} else {
this.store.dispatch("socket/setData", {
isConnecting: false,
connectingFailed: true
})
}
}
this.instance.onerror = () => {
this.instance.close();
};
this.instance.close()
}
this.instance.onmessage = (msg) => {
let data = JSON.parse(msg.data);
let data = JSON.parse(msg.data)
if (this.store) {
if (this.wsData.filter(item => item.id === data.id).length > 0 &&
this.wsData.filter(item => item.id === data.id)[0].action !== "") {
@ -79,7 +96,7 @@ export default class WebSocketClient {
error: data.error,
requestParams: tmpWsData.params
})
);
)
}
} else {
let result = data.result
@ -95,7 +112,11 @@ export default class WebSocketClient {
}
} else this.passToStore('socket/onMessage', data)
}
};
}
}
close() {
if (this.instance) this.instance.close()
}
sendObj (method, params, action = '', actionPreload = null) {
@ -107,7 +128,7 @@ export default class WebSocketClient {
params: params,
actionPreload: actionPreload,
})
this.instance.send(this.createMessage(method, params, id));
this.instance.send(this.createMessage(method, params, id))
}
}
}

View File

@ -1,5 +1,6 @@
import Dashboard from '../pages/Dashboard.vue'
import Webcam from '../pages/Webcam.vue'
import Farm from '../pages/Farm.vue'
import Console from '../pages/Console.vue'
import Heightmap from '../pages/Heightmap.vue'
import Files from '../pages/Files.vue'
@ -11,10 +12,16 @@ const routes = [
{
title: "Dashboard",
path: '/',
icon: 'view-dashboard',
icon: 'monitor-dashboard',
component: Dashboard,
alwaysShow: true,
},
{
title: "Printers",
path: '/allPrinters',
component: Farm,
alwaysShow: false,
},
{
title: "Webcam",
path: '/cam',

View File

@ -1,3 +1,4 @@
//import Vue from 'vue'
import router from "../plugins/router";
@ -6,4 +7,18 @@ export default {
switchToDashboard() {
router.push("/");
},
changePrinter({ dispatch, getters }, payload) {
dispatch('files/reset')
dispatch('gui/reset')
dispatch('printer/reset')
dispatch('socket/reset')
const printerSocket = getters["farm/"+payload.printer+"/getSocketData"]
dispatch('socket/setSocket', {
hostname: printerSocket.hostname,
port: printerSocket.port
})
}
}

90
src/store/farm/index.js Normal file
View File

@ -0,0 +1,90 @@
import printer from './printer'
export default {
namespaced: true,
state: () => {
},
getters: {
countPrinters: state => {
return Object.keys(state).length
},
getPrinters: state => {
return state
},
getPrinterName: (getters) => (namespace) => {
return getters[namespace+"/getPrinterName"]
}
},
actions: {
readStoredPrinters({ rootState, commit }) {
if (rootState.socket.remoteMode) {
if (localStorage.getItem('printers')) {
try {
const printers = JSON.parse(localStorage.getItem('printers')) || []
printers.forEach((printer) => {
commit('addPrinter',{
hostname: printer.hostname,
port: printer.port,
protocol: rootState.socket.protocol
})
})
} catch(e) {
localStorage.removeItem('printers')
}
}
} else {
rootState.gui.remotePrinters.forEach((printer) => {
commit('addPrinter',{
hostname: printer.hostname,
port: printer.port,
webPort: printer.webPort,
protocol: rootState.socket.protocol
})
})
}
},
savePrinters({ rootState, state, dispatch }) {
const printers = []
if (rootState.socket.remoteMode) {
for (const key in state) {
printers.push({
hostname: state[key].socket.hostname,
port: state[key].socket.port,
})
}
localStorage.setItem('printers', JSON.stringify(printers))
} else {
for (const key in state) {
printers.push({
hostname: state[key].socket.hostname,
port: state[key].socket.port,
webPort: state[key].socket.webPort,
})
}
dispatch("gui/setSettings", { remotePrinters: printers }, { root: true })
}
}
},
mutations: {
addPrinter(state, payload) {
if ('hostname' in payload && payload.hostname !== "") {
const nextPrinterName = 'printer'+Object.entries(state).length
if (!this.hasModule(['farm', nextPrinterName])) {
this.registerModule(['farm', nextPrinterName], printer)
this.commit('farm/'+nextPrinterName+'/setSocketData', {...payload, _namespace: nextPrinterName })
this.dispatch('farm/'+nextPrinterName+'/connect', {}, { root: true })
}
}
},
removePrinter(state, payload) {
if (payload.name in state) {
if (state[payload.name].socket.instance) state[payload.name].socket.instance.close()
this.unregisterModule(['farm', payload.name])
}
}
}
}

View File

@ -0,0 +1,175 @@
export default {
reset({commit}) {
commit('reset')
},
connect({ state, commit, dispatch, getters }) {
commit("setSocketData", { isConnecting: true })
const socket = new WebSocket(getters.getSocketUrl)
socket.onopen = () => {
commit("setSocketData", {
instance: socket,
reconnects: 0,
isConnecting: false,
isConnected: true
})
dispatch("initPrinter")
}
socket.onclose = (e) => {
if (!e.wasClean && state.socket.reconnects < state.socket.maxReconnects) {
commit("setSocketData", { reconnects: state.socket.reconnects + 1 })
setTimeout(() => {
dispatch("connect")
}, state.socket.reconnectInterval)
} else {
commit("setSocketData", {
isConnecting: false,
isConnected: false,
reconnects: 0
})
}
}
socket.onerror = () => {
}
socket.onmessage = (msg) => {
let data = JSON.parse(msg.data)
if (data && data.method) {
switch (data.method) {
case "notify_status_update":
commit("setData", data.params[0])
break
case "notify_filelist_changed":
commit("notifyFilelistChanged", data.params[0])
break
case "notify_klippy_disconnected":
dispatch("disconnectKlippy")
break
case "notify_klippy_ready":
dispatch("initPrinter")
break
}
} else if ("result" in data) {
if (
state.socket.wsData.filter(item => item.id === data.id).length > 0 &&
state.socket.wsData.filter(item => item.id === data.id)[0].action !== undefined &&
state.socket.wsData.filter(item => item.id === data.id)[0].action !== ""
) {
let result = data.result
if (result === "ok") result = { result: result }
if (typeof(result) === "string") result = { result: result }
let preload = {}
let wsData = state.socket.wsData.filter(item => item.id === data.id)[0]
if (wsData.actionPreload) Object.assign(preload, wsData.actionPreload)
Object.assign(preload, { requestParams: wsData.params })
Object.assign(preload, result)
dispatch(wsData.action, preload)
}
}
}
},
reconnect({ state, dispatch }) {
if(state.socket.instance) state.socket.instance.close()
dispatch("connect")
},
sendObj ({ state, commit }, payload) {
if (state.socket.instance && state.socket.instance.readyState === WebSocket.OPEN) {
const id = Math.floor(Math.random() * 10000) + 1
commit("addWsData", {
id: id,
action: payload.action,
params: payload.params || {},
actionPreload: payload.actionPreload || null,
})
state.socket.instance.send(JSON.stringify({
jsonrpc: '2.0',
method: payload.method,
params: payload.params || {},
id: id
}))
}
},
disconnectKlippy({ commit }) {
commit("setData", { print_stats: { state: "error" }})
},
initPrinter({ commit, dispatch }) {
commit("resetData")
dispatch("sendObj", {
method: "printer.objects.list",
action: "getObjectsList",
})
dispatch("sendObj", {
method: "server.files.list",
action: "getConfigDir",
params: { root: "config"}
})
},
getObjectsList({ dispatch }, payload) {
const allowed = [
'webhooks',
'print_stats',
'virtual_sdcard',
'display_status',
'heaters',
'heater_bed',
'heater_fan',
'fan',
'temperature_fan',
'temperature_sensor',
'idle_timeout',
'toolhead'
]
let subscripts = {}
payload.objects.forEach((object) => {
const splits = object.split(" ")
const objectName = splits[0]
if (
allowed.includes(objectName) ||
objectName.startsWith("extruder")
) {
subscripts = {...subscripts, [object]: null }
}
})
if (subscripts !== {})
dispatch("sendObj", {
method: 'printer.objects.subscribe',
params: { objects: subscripts },
action: "getData"
});
},
getData({ commit }, payload) {
commit("setData", payload)
},
getMetadataCurrentFile({ commit }, payload) {
commit("setCurrentFile", payload)
},
getConfigDir({ commit }, payload) {
commit("setConfigDir", payload)
},
}

View File

@ -0,0 +1,174 @@
import {themeDir} from "@/store/variables";
export default {
getSocketUrl: (state) => {
return state.socket.protocol+"://"+state.socket.hostname+":"+state.socket.port+"/websocket"
},
getSocketData: (state) => {
return state.socket
},
isCurrentPrinter: (state, getters, rootState) => {
return (
rootState.socket.hostname === state.socket.hostname &&
rootState.socket.port === state.socket.port
)
},
getPrinterName: (state) => {
if (
'gui' in state.data &&
'general' in state.data.gui &&
'printername' in state.data.gui.general &&
state.data.gui.general.printername !== ""
) return state.data.gui.general.printername
return parseInt(state.socket.port) !== 80 ? state.socket.hostname+':'+state.socket.port : state.socket.hostname
},
getStatus: (state, getters) => {
if (!state.socket.isConnected) {
return state.socket.isConnecting ? "Connecting..." : "Disconnected"
} else if (state.data.print_stats && state.data.print_stats.state) {
if (state.data.print_stats.state === "printing") {
const percent = getters["getPrintPercent"]
return Math.round(percent*100)+"% Printing"
}
return state.data.print_stats.state.charAt(0).toUpperCase() + state.data.print_stats.state.slice(1)
}
return "Unknown"
},
getCurrentFilename: (state) => {
if (
'print_stats' in state.data &&
'filename' in state.data.print_stats
) {
return state.data.print_stats.filename
}
return ""
},
getPrintPercent: (state) => {
if (
'filename' in state.current_file &&
'gcode_start_byte' in state.current_file &&
'gcode_end_byte' in state.current_file &&
state.current_file.filename === state.data.print_stats.filename
) {
if (state.data.virtual_sdcard.file_position <= state.current_file.gcode_start_byte) return 0
if (state.data.virtual_sdcard.file_position >= state.current_file.gcode_end_byte) return 1
let currentPosition = state.data.virtual_sdcard.file_position - state.current_file.gcode_start_byte
let maxPosition = state.current_file.gcode_end_byte - state.current_file.gcode_start_byte
if (currentPosition > 0 && maxPosition > 0) return 1 / maxPosition * currentPosition
}
return state.data.virtual_sdcard.progress
},
getImage: state => {
if (
'gui' in state.data &&
'webcam' in state.data.gui.webcam &&
'url' in state.data.gui.webcam &&
state.data.gui.webcam.url !== "" &&
'bool' in state.data.gui.webcam &&
state.data.gui.webcam.bool &&
'dashbaord' in state.data.gui &&
'boolWebcam' in state.data.gui.dashboard &&
state.data.gui.dashboard.boolWebcam
) {
return state.data.gui.webcam.url
} else if (
state.current_file &&
"thumbnails" in state.current_file &&
state.current_file.thumbnails.find((element) => element.width === 400 && element.height === 300)
) {
return 'data:image/gif;base64,'+state.current_file.thumbnails.find((element) => element.width === 400 && element.height === 300).data
} else return "/img/sidebar-background.png"
},
getLogo: state => {
let file = state.theme_files.find(element =>
element === themeDir+'/sidebar-logo.svg' ||
element === themeDir+'/sidebar-logo.jpg' ||
element === themeDir+'/sidebar-logo.png' ||
element === themeDir+'/sidebar-logo.gif'
)
if (file) return "//"+state.socket.hostname+":"+state.socket.port+'/server/files/config/'+file
return '/img/logo.svg'
},
getPosition: state => {
if (
'toolhead' in state.data &&
'position' in state.data.toolhead
) return state.data.toolhead.position
return []
},
getPrinterPreview: (state, getters) => {
const output = []
Object.keys(state.data).filter((key) => key.startsWith("extruder")).forEach((key) => {
if (
'temperature' in state.data[key] &&
'target' in state.data[key]
) {
output.push({
name: key,
value: state.data[key].temperature.toFixed(0)+"° / "+state.data[key].target.toFixed(0)+"°",
})
}
})
if ('heater_bed' in state.data) {
output.push({
name: 'heater_bed',
value: state.data.heater_bed.temperature.toFixed(0)+"° / "+state.data.heater_bed.target.toFixed(0)+"°"
})
}
if ('temperature_fan chamber' in state.data) {
output.push({
name: 'chamber',
value: state.data['temperature_fan chamber'].temperature.toFixed(0)+"° / "+state.data['temperature_fan chamber'].target.toFixed(0)+"°"
})
}
if ('temperature_sensor chamber' in state.data) {
output.push({
name: 'chamber',
value: state.data['temperature_sensor chamber'].temperature.toFixed(0)+"°"
})
}
if (
'print_stats' in state.data &&
'state' in state.data.print_stats &&
'print_duration' in state.data.print_stats &&
state.data.print_stats.state === "printing" &&
state.data.print_stats.print_duration &&
getters["getPrintPercent"] > 0
) {
const eta = new Date(new Date().getTime() + (state.data.print_stats.print_duration / getters["getPrintPercent"] - state.data.print_stats.print_duration).toFixed() * 1000)
output.push({
name: "ETA",
value: (eta.getHours() > 9 ? eta.getHours() : '0'+eta.getHours())+":"+(eta.getMinutes() > 9 ? eta.getMinutes() : '0'+eta.getMinutes())
})
}
return output
},
}

View File

@ -0,0 +1,40 @@
import actions from './actions'
import mutations from './mutations'
import getters from './getters'
export function getDefaultState() {
return {
_namespace: "",
socket: {
instance: null,
hostname: "",
port: 7125,
webPort: 80,
protocol: 'ws',
isConnected: false,
isConnecting: false,
reconnects: 0,
maxReconnects: 2,
reconnectInterval: 1000,
wsData: [],
},
data: {
},
current_file: {},
theme_files: []
}
}
// initial state
const state = () => {
return getDefaultState()
}
export default {
namespaced: true,
state,
getters,
actions,
mutations
}

View File

@ -0,0 +1,88 @@
import Vue from 'vue'
import { getDefaultState } from './index'
export default {
reset(state) {
Object.assign(state, getDefaultState())
},
resetData(state) {
Object.assign(state.data, getDefaultState().data)
},
setSocketData (state, payload) {
if ("status" in payload) payload = payload.status;
if ("requestParams" in payload) delete payload.requestParams
if ("_namespace" in payload) {
Vue.set(state, "_namespace", payload._namespace)
delete payload._namespace
}
Object.entries(payload).forEach(([key, value]) => {
Vue.set(state.socket, key, value)
})
},
setData(state, payload) {
if ("status" in payload) payload = payload.status
if ("requestParams" in payload) delete payload.requestParams
Object.entries(payload).forEach(([key, value]) => {
if (key === "print_stats" && 'filename' in value) {
this.dispatch("farm/"+state._namespace+"/sendObj", {
method: "server.files.metadata",
params: {filename: value.filename},
action: "getMetadataCurrentFile"
});
}
if (typeof (value) === "object") {
Vue.set(state.data, key, {
...state.data[key],
...value
})
} else Vue.set(state.data, key, value)
})
},
addWsData(state, payload) {
state.socket.wsData.push(payload)
},
setCurrentFile(state, payload) {
if ("requestParams" in payload) delete payload.requestParams
Vue.set(state, 'current_file', payload)
},
setConfigDir(state, payload) {
for (const [, file] of Object.entries(payload)) {
if ("filename" in file) {
if (file.filename.startsWith(".theme/")) {
state.theme_files.push(file.filename)
} else if (file.filename === ".mainsail.json") {
fetch('//'+state.socket.hostname+':'+state.socket.port+'/server/files/config/.mainsail.json?time='+Date.now())
.then(res => res.json()).then(file => {
this.commit("farm/"+state._namespace+"/setMainsailJson", file)
})
}
}
}
},
setMainsailJson(state, payload) {
Vue.set(state.data, 'gui', payload.state)
},
notifyFilelistChanged( state, payload) {
if (
payload.action === "upload_file" &&
payload.item.root === "config" &&
payload.item.path === ".mainsail.json"
) {
fetch('//'+state.socket.hostname+':'+state.socket.port+'/server/files/config/.mainsail.json?time='+Date.now())
.then(res => res.json()).then(file => {
this.commit("farm/"+state._namespace+"/setMainsailJson", file)
})
}
}
}

View File

@ -61,6 +61,7 @@ export default {
fetch('//'+store.state.socket.hostname+':'+store.state.socket.port+'/server/files/config/.mainsail.json?time='+Date.now())
.then(res => res.json()).then(file => {
this.commit('gui/setData', file, { root: true })
if (!store.state.socket.remoteMode) this.dispatch('farm/readStoredPrinters', {}, { root: true })
})
}

View File

@ -37,7 +37,8 @@ export function getDefaultState() {
countPerPage: 10,
showHiddenFiles: false,
}
}
},
remotePrinters: []
}
}

View File

@ -11,6 +11,7 @@ import server from './server'
import printer from './printer'
import files from './files'
import gui from './gui'
import farm from './farm'
Vue.use(Vuex);
Vue.use(VueToast);
@ -25,6 +26,7 @@ export default new Vuex.Store({
printer,
files,
gui,
farm,
},
getters: getters,
mutations: mutations,

View File

@ -42,8 +42,8 @@ export default {
if (!blocklist.includes(nameSplit[0])) subscripts = {...subscripts, [key]: null }
}
if (subscripts !== {}) Vue.prototype.$socket.sendObj('printer.objects.subscribe', { objects: subscripts }, "printer/getData");
Vue.prototype.$socket.sendObj("server.temperature_store", {}, "printer/tempHistory/getHistory");
if (subscripts !== {}) Vue.prototype.$socket.sendObj('printer.objects.subscribe', { objects: subscripts }, "printer/getData")
Vue.prototype.$socket.sendObj("server.temperature_store", {}, "printer/tempHistory/getHistory")
commit('void', null, { root: true })
},

View File

@ -7,6 +7,10 @@ export function getDefaultState() {
moonraker: {},
klipper: {},
client: {},
system: {
package_count: 0,
package_list: []
},
updateResponse: {
application: "",
complete: true,

View File

@ -1,8 +1,24 @@
import Vue from 'vue'
export default {
reset({ commit }) {
commit('reset')
},
setData({ commit }, payload) {
commit('setData', payload)
},
setSocket({ commit, state }, payload) {
commit('setData', payload)
if ('$socket' in Vue.prototype) {
Vue.prototype.$socket.close()
Vue.prototype.$socket.setUrl(state.protocol+"://"+payload.hostname+":"+payload.port+"/websocket")
Vue.prototype.$socket.connect()
}
},
onOpen ({ commit, dispatch }) {
commit('setConnected')
dispatch('server/init', null, { root: true })
@ -10,6 +26,7 @@ export default {
onClose ({ commit }, event) {
commit('setDisconnected');
window.console.log(event)
if (event.wasClean) window.console.log('Socket closed clear')
},

View File

@ -4,10 +4,14 @@ import getters from './getters'
export function getDefaultState() {
return {
hostname: process.env.VUE_APP_HOSTNAME || window.location.hostname,
port: process.env.VUE_APP_PORT || window.location.port,
reconnectInterval: process.env.VUE_APP_RECONNECT_INTERVAL || 5000,
remoteMode: process.env.VUE_APP_REMOTE_MODE || (document.location.hostname === "my.mainsail.app"),
hostname: process.env.VUE_APP_HOSTNAME || (process.env.VUE_APP_REMOTE_MODE || document.location.hostname === "my.mainsail.app" ? "" : window.location.hostname),
port: process.env.VUE_APP_PORT || (process.env.VUE_APP_REMOTE_MODE || document.location.hostname === "my.mainsail.app" ? 7125 : window.location.port),
protocol: document.location.protocol === 'https:' ? 'wss' : 'ws',
reconnectInterval: process.env.VUE_APP_RECONNECT_INTERVAL || 2000,
isConnected: false,
isConnecting: false,
connectingFailed: false,
loadings: []
}