Merge pull request #71 from dacarley/5
6 - Improved uploading, date/time/timezone setting, etc.
This commit is contained in:
@@ -4,7 +4,7 @@ module.exports = {
|
||||
attached: function () {
|
||||
this.svelteComponent = SvelteComponents.createComponent(
|
||||
"AdminNetworkView",
|
||||
document.getElementById("svelte-root")
|
||||
document.getElementById("admin-network")
|
||||
);
|
||||
},
|
||||
|
||||
|
||||
@@ -4,7 +4,6 @@ const api = require("./api");
|
||||
const cookie = require("./cookie")("bbctrl-");
|
||||
const Sock = require("./sock");
|
||||
|
||||
SvelteComponents.initNetworkInfo();
|
||||
SvelteComponents.createComponent("DialogHost",
|
||||
document.getElementById("svelte-dialog-host")
|
||||
);
|
||||
@@ -136,6 +135,7 @@ module.exports = new Vue({
|
||||
watch: {
|
||||
display_units: function (value) {
|
||||
localStorage.setItem("display_units", value);
|
||||
SvelteComponents.setDisplayUnits(value);
|
||||
},
|
||||
},
|
||||
|
||||
@@ -214,6 +214,10 @@ module.exports = new Vue({
|
||||
ready: function () {
|
||||
$(window).on("hashchange", this.parse_hash);
|
||||
this.connect();
|
||||
|
||||
SvelteComponents.registerControllerMethods({
|
||||
dispatch: (...args) => this.$dispatch(...args)
|
||||
});
|
||||
},
|
||||
|
||||
methods: {
|
||||
@@ -289,15 +293,6 @@ module.exports = new Vue({
|
||||
update_object(this.config, config, true);
|
||||
this.parse_hash();
|
||||
|
||||
if (!this.devModChecked) {
|
||||
this.devModChecked = true;
|
||||
if (this.config.devmode) {
|
||||
SvelteComponents.createComponent("Devmode",
|
||||
document.getElementById("svelte-devmode-host")
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (!this.checkedUpgrade) {
|
||||
this.checkedUpgrade = true;
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ module.exports = {
|
||||
|
||||
data: function () {
|
||||
return {
|
||||
current_time: "",
|
||||
mach_units: this.$root.state.metric ? "METRIC" : "IMPERIAL",
|
||||
mdi: '',
|
||||
last_file: undefined,
|
||||
@@ -19,22 +20,6 @@ module.exports = {
|
||||
history: [],
|
||||
speed_override: 1,
|
||||
feed_override: 1,
|
||||
manual_home: {
|
||||
x: false,
|
||||
y: false,
|
||||
z: false,
|
||||
a: false,
|
||||
b: false,
|
||||
c: false
|
||||
},
|
||||
position_msg: {
|
||||
x: false,
|
||||
y: false,
|
||||
z: false,
|
||||
a: false,
|
||||
b: false,
|
||||
c: false
|
||||
},
|
||||
jog_incr_amounts: {
|
||||
"METRIC": {
|
||||
fine: 0.1,
|
||||
@@ -49,7 +34,6 @@ module.exports = {
|
||||
large: 5,
|
||||
}
|
||||
},
|
||||
axis_position: 0,
|
||||
jog_incr: localStorage.getItem("jog_incr") || 'small',
|
||||
jog_step: cookie.get_bool('jog-step'),
|
||||
jog_adjust: parseInt(cookie.get('jog-adjust', 2)),
|
||||
@@ -253,10 +237,18 @@ module.exports = {
|
||||
ready: function () {
|
||||
this.load();
|
||||
|
||||
setInterval(() => {
|
||||
this.current_time = new Date().toLocaleTimeString();
|
||||
}, 1000);
|
||||
|
||||
SvelteComponents.registerControllerMethods({
|
||||
stop: (...args) => this.stop(...args),
|
||||
send: (...args) => this.send(...args),
|
||||
goto_zero: (...args) => this.goto_zero(...args)
|
||||
goto_zero: (...args) => this.goto_zero(...args),
|
||||
isAxisHomed: (axis) => this[axis].homed,
|
||||
unhome: (...args) => this.unhome(...args),
|
||||
set_position: (...args) => this.set_position(...args),
|
||||
set_home: (...args) => this.set_home(...args)
|
||||
});
|
||||
},
|
||||
|
||||
@@ -398,18 +390,13 @@ module.exports = {
|
||||
return;
|
||||
}
|
||||
|
||||
const fd = new FormData();
|
||||
|
||||
fd.append('gcode', file);
|
||||
|
||||
try {
|
||||
await api.upload('file', fd);
|
||||
|
||||
this.last_file_time = undefined; // Force reload
|
||||
this.$broadcast('gcode-reload', file.name);
|
||||
} catch (err) {
|
||||
api.alert('Upload failed', err)
|
||||
}
|
||||
SvelteComponents.showDialog("Upload", {
|
||||
file,
|
||||
onComplete: () => {
|
||||
this.last_file_time = undefined; // Force reload
|
||||
this.$broadcast('gcode-reload', file.name);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
delete_current: function () {
|
||||
@@ -433,23 +420,20 @@ module.exports = {
|
||||
} else if (this[axis].homingMode != 'manual') {
|
||||
api.put('home/' + axis);
|
||||
} else {
|
||||
this.manual_home[axis] = true;
|
||||
SvelteComponents.showDialog("ManualHomeAxis", { axis });
|
||||
}
|
||||
},
|
||||
|
||||
set_home: function (axis, position) {
|
||||
this.manual_home[axis] = false;
|
||||
api.put('home/' + axis + '/set', { position: parseFloat(position) });
|
||||
},
|
||||
|
||||
unhome: function (axis) {
|
||||
this.position_msg[axis] = false;
|
||||
api.put('home/' + axis + '/clear');
|
||||
},
|
||||
|
||||
show_set_position: function (axis) {
|
||||
this.axis_position = 0;
|
||||
this.position_msg[axis] = true;
|
||||
SvelteComponents.showDialog("SetAxisPosition", { axis });
|
||||
},
|
||||
|
||||
show_toolpath_msg: function (axis) {
|
||||
@@ -457,7 +441,6 @@ module.exports = {
|
||||
},
|
||||
|
||||
set_position: function (axis, position) {
|
||||
this.position_msg[axis] = false;
|
||||
api.put('position/' + axis, { 'position': parseFloat(position) });
|
||||
},
|
||||
|
||||
@@ -530,7 +513,7 @@ module.exports = {
|
||||
this.send(JSON.stringify(data));
|
||||
},
|
||||
|
||||
showProbeDialog: function(probeType) {
|
||||
showProbeDialog: function (probeType) {
|
||||
SvelteComponents.showDialog("Probe", { probeType });
|
||||
}
|
||||
},
|
||||
|
||||
@@ -1,31 +1,14 @@
|
||||
'use strict'
|
||||
|
||||
module.exports = {
|
||||
template: '#settings-view-template',
|
||||
props: ['config', 'template'],
|
||||
template: "#settings-view-template",
|
||||
|
||||
computed: {
|
||||
display_units: {
|
||||
cache: false,
|
||||
get: function () {
|
||||
return this.$root.display_units;
|
||||
},
|
||||
set: function (value) {
|
||||
this.$root.display_units = value;
|
||||
}
|
||||
},
|
||||
attached: function () {
|
||||
this.svelteComponent = SvelteComponents.createComponent(
|
||||
"SettingsView",
|
||||
document.getElementById("settings")
|
||||
);
|
||||
},
|
||||
|
||||
events: {
|
||||
'input-changed': function () {
|
||||
this.$dispatch('config-changed');
|
||||
return false;
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
showScreenRotationDialog: function () {
|
||||
SvelteComponents.showDialog("ScreenRotation");
|
||||
}
|
||||
detached: function() {
|
||||
this.svelteComponent.$destroy();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -19,7 +19,6 @@ html(lang="en")
|
||||
|
||||
body(v-cloak)
|
||||
#svelte-dialog-host
|
||||
#svelte-devmode-host
|
||||
|
||||
#overlay(v-if="status != 'connected'")
|
||||
span {{status}}
|
||||
|
||||
@@ -1,3 +1,2 @@
|
||||
script#admin-network-view-template(type="text/x-template")
|
||||
#admin-network
|
||||
#svelte-root
|
||||
#admin-network
|
||||
@@ -150,60 +150,18 @@ script#control-view-template(type="text/x-template")
|
||||
th.actions
|
||||
button.pure-button(:disabled="!can_set_axis",
|
||||
title=`Set {{'${axis}' | upper}} axis position.`,
|
||||
@click=`show_set_position('${axis}')`,style="height:60px;width:60px")
|
||||
@click=`show_set_position('${axis}')`, style="height:60px;width:60px")
|
||||
.fa.fa-cog
|
||||
|
||||
button.pure-button(:disabled="!can_set_axis",
|
||||
title=`Zero {{'${axis}' | upper}} axis offset.`,
|
||||
@click=`zero('${axis}')`,style="height:60px;width:60px")
|
||||
@click=`zero('${axis}')`, style="height:60px;width:60px")
|
||||
.fa.fa-map-marker
|
||||
|
||||
button.pure-button(:disabled="!is_idle", @click=`home('${axis}')`,
|
||||
title=`Home {{'${axis}' | upper}} axis.`,style="height:60px;width:60px")
|
||||
title=`Home {{'${axis}' | upper}} axis.`, style="height:60px;width:60px")
|
||||
.fa.fa-home
|
||||
|
||||
message(:show.sync=`position_msg['${axis}']`)
|
||||
h3(slot="header") Set {{'#{axis}' | upper}} axis position
|
||||
|
||||
div(slot="body")
|
||||
.pure-form
|
||||
.pure-control-group
|
||||
label Position
|
||||
input(v-model="axis_position",
|
||||
@keyup.enter=`set_position('${axis}', axis_position)`)
|
||||
p
|
||||
|
||||
div(slot="footer")
|
||||
button.pure-button(@click=`position_msg['${axis}'] = false`)
|
||||
| Cancel
|
||||
|
||||
button.pure-button(v-if=`${axis}.homed`,
|
||||
@click=`unhome('${axis}')`) Unhome
|
||||
|
||||
button.pure-button.button-success(
|
||||
@click=`set_position('${axis}', axis_position)`) Set
|
||||
|
||||
message(:show.sync=`manual_home['${axis}']`)
|
||||
h3(slot="header") Manually home {{'#{axis}' | upper}} axis
|
||||
|
||||
div(slot="body")
|
||||
p Set axis absolute position.
|
||||
|
||||
.pure-form
|
||||
.pure-control-group
|
||||
label Absolute
|
||||
input(v-model="axis_position",
|
||||
@keyup.enter=`set_home('${axis}', axis_position)`)
|
||||
|
||||
p
|
||||
|
||||
div(slot="footer")
|
||||
button.pure-button(@click=`manual_home['${axis}'] = false`)
|
||||
| Cancel
|
||||
|
||||
button.pure-button.button-success(
|
||||
title=`Home {{'${axis}' | upper}} axis.`,
|
||||
@click=`set_home('${axis}', axis_position)`) Set
|
||||
|
||||
tr(style="vertical-align: top;")
|
||||
td
|
||||
@@ -265,6 +223,11 @@ script#control-view-template(type="text/x-template")
|
||||
|
||||
td
|
||||
table.info
|
||||
tr
|
||||
th Current Time
|
||||
td
|
||||
span {{current_time}}
|
||||
|
||||
tr
|
||||
th Remaining
|
||||
td(title="Total run time (days:hours:mins:secs)").
|
||||
@@ -273,12 +236,7 @@ script#control-view-template(type="text/x-template")
|
||||
tr
|
||||
th ETA
|
||||
td.eta {{eta}}
|
||||
tr
|
||||
th Line
|
||||
td
|
||||
| {{0 <= state.line ? state.line : 0 | number}}
|
||||
span(v-if="toolpath.lines")
|
||||
| of {{toolpath.lines | number}}
|
||||
|
||||
tr
|
||||
th Progress
|
||||
td.progress
|
||||
|
||||
@@ -1,53 +1,2 @@
|
||||
script#settings-view-template(type="text/x-template")
|
||||
#settings
|
||||
h1 Settings
|
||||
|
||||
.pure-form.pure-form-aligned
|
||||
fieldset
|
||||
h2 Screen
|
||||
.pure-control-group
|
||||
label(for="screen-rotation")
|
||||
button.pure-button(name="screen-rotation", @click="showScreenRotationDialog") Change Screen Rotation
|
||||
|
||||
fieldset
|
||||
h2 Probe Dimensions
|
||||
templated-input(v-for="templ in template.probe", v-if="$key !== 'probe-diameter'", :name="$key"
|
||||
:model.sync="config.probe[$key]", :template="templ")
|
||||
|
||||
fieldset
|
||||
h2 GCode
|
||||
templated-input(v-for="templ in template.gcode", :name="$key",
|
||||
:model.sync="config.gcode[$key]", :template="templ")
|
||||
|
||||
fieldset
|
||||
h2 Path Accuracy
|
||||
templated-input(name="max-deviation",
|
||||
:model.sync="config.settings['max-deviation']",
|
||||
:template="template.settings['max-deviation']")
|
||||
|
||||
p.
|
||||
Lower #[tt max-deviation] to follow the programmed path more precisely
|
||||
but at a slower speed.
|
||||
|
||||
p.
|
||||
In order to improve traversal speed, the path planner may merge
|
||||
consecutive moves or round off sharp corners if doing so would deviate
|
||||
from the program path by less than #[tt max-deviation].
|
||||
|
||||
- var base = '//linuxcnc.org/docs/html/gcode/g-code.html'
|
||||
p.
|
||||
GCode commands
|
||||
#[a(href=base + "#gcode:g61", target="_blank") G61, G61.1] and
|
||||
#[a(href=base + "#gcode:g64", target="_blank") G64] also affect path
|
||||
planning accuracy.
|
||||
|
||||
h2 Cornering Speed (Advanced)
|
||||
templated-input(name="junction-accel",
|
||||
:model.sync="config.settings['junction-accel']",
|
||||
:template="template.settings['junction-accel']")
|
||||
|
||||
p.
|
||||
Junction acceleration limits the cornering speed the planner will
|
||||
allow. Increasing this value will allow for faster traversal of
|
||||
corners but may cause the planner to violate axis jerk limits and
|
||||
stall the motors. Use with caution.
|
||||
#settings
|
||||
@@ -1,207 +0,0 @@
|
||||
################################################################################
|
||||
# #
|
||||
# This file is part of the Buildbotics firmware. #
|
||||
# #
|
||||
# Copyright (c) 2015 - 2018, Buildbotics LLC #
|
||||
# All rights reserved. #
|
||||
# #
|
||||
# This file ("the software") is free software: you can redistribute it #
|
||||
# and/or modify it under the terms of the GNU General Public License, #
|
||||
# version 2 as published by the Free Software Foundation. You should #
|
||||
# have received a copy of the GNU General Public License, version 2 #
|
||||
# along with the software. If not, see <http://www.gnu.org/licenses/>. #
|
||||
# #
|
||||
# The software is distributed in the hope that it will be useful, but #
|
||||
# WITHOUT ANY WARRANTY; without even the implied warranty of #
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU #
|
||||
# Lesser General Public License for more details. #
|
||||
# #
|
||||
# You should have received a copy of the GNU Lesser General Public #
|
||||
# License along with the software. If not, see #
|
||||
# <http://www.gnu.org/licenses/>. #
|
||||
# #
|
||||
# For information regarding this software email: #
|
||||
# "Joseph Coffland" <joseph@buildbotics.com> #
|
||||
# #
|
||||
################################################################################
|
||||
|
||||
import os
|
||||
import sys
|
||||
import traceback
|
||||
import signal
|
||||
|
||||
import bbctrl
|
||||
import bbctrl.Cmd as Cmd
|
||||
|
||||
|
||||
class AVREmu(object):
|
||||
def __init__(self, ctrl):
|
||||
self.ctrl = ctrl
|
||||
self.log = ctrl.log.get('AVREmu')
|
||||
|
||||
self.avrOut = None
|
||||
self.avrIn = None
|
||||
self.i2cOut = None
|
||||
self.read_cb = None
|
||||
self.write_cb = None
|
||||
self.pid = None
|
||||
|
||||
|
||||
def close(self):
|
||||
# Close pipes
|
||||
def _close(fd, withHandle):
|
||||
if fd is None: return
|
||||
try:
|
||||
if withHandle: self.ctrl.ioloop.remove_handler(fd)
|
||||
except: pass
|
||||
try:
|
||||
os.close(fd)
|
||||
except: pass
|
||||
|
||||
_close(self.avrOut, True)
|
||||
_close(self.avrIn, True)
|
||||
_close(self.i2cOut, False)
|
||||
|
||||
self.avrOut, self.avrIn, self.i2cOut = None, None, None
|
||||
|
||||
# Kill process and wait for it
|
||||
if self.pid is not None:
|
||||
os.kill(self.pid, signal.SIGKILL)
|
||||
os.waitpid(self.pid, 0)
|
||||
self.pid = None
|
||||
|
||||
|
||||
def _start(self):
|
||||
try:
|
||||
self.close()
|
||||
|
||||
# Create pipes
|
||||
stdinFDs = os.pipe()
|
||||
stdoutFDs = os.pipe()
|
||||
i2cFDs = os.pipe()
|
||||
|
||||
self.pid = os.fork()
|
||||
|
||||
if not self.pid:
|
||||
# Dup child ends
|
||||
os.dup2(stdinFDs[0], 0)
|
||||
os.dup2(stdoutFDs[1], 1)
|
||||
os.dup2(i2cFDs[0], 3)
|
||||
|
||||
# Close orig fds
|
||||
os.close(stdinFDs[0])
|
||||
os.close(stdoutFDs[1])
|
||||
os.close(i2cFDs[0])
|
||||
|
||||
# Close parent ends
|
||||
os.close(stdinFDs[1])
|
||||
os.close(stdoutFDs[0])
|
||||
os.close(i2cFDs[1])
|
||||
|
||||
cmd = ['bbemu']
|
||||
if self.ctrl.args.fast_emu: cmd.append('--fast')
|
||||
|
||||
os.execvp(cmd[0], cmd)
|
||||
os._exit(1) # In case of failure
|
||||
|
||||
# Parent, close child ends
|
||||
os.close(stdinFDs[0])
|
||||
os.close(stdoutFDs[1])
|
||||
os.close(i2cFDs[0])
|
||||
|
||||
# Non-blocking IO
|
||||
os.set_blocking(stdinFDs[1], False)
|
||||
os.set_blocking(stdoutFDs[0], False)
|
||||
os.set_blocking(i2cFDs[1], False)
|
||||
|
||||
self.avrOut = stdinFDs[1]
|
||||
self.avrIn = stdoutFDs[0]
|
||||
self.i2cOut = i2cFDs[1]
|
||||
|
||||
ioloop = self.ctrl.ioloop
|
||||
ioloop.add_handler(self.avrOut, self._avr_write_handler,
|
||||
ioloop.WRITE | ioloop.ERROR)
|
||||
ioloop.add_handler(self.avrIn, self._avr_read_handler,
|
||||
ioloop.READ | ioloop.ERROR)
|
||||
|
||||
self.write_enabled = True
|
||||
|
||||
except Exception:
|
||||
self.close()
|
||||
self.log.exception('Internal error: Failed to start bbemu')
|
||||
|
||||
|
||||
def set_handlers(self, read_cb, write_cb):
|
||||
if self.read_cb is not None or self.write_cb is not None:
|
||||
raise Exception('AVR handler already set')
|
||||
|
||||
self.read_cb = read_cb
|
||||
self.write_cb = write_cb
|
||||
self._start()
|
||||
|
||||
|
||||
def enable_write(self, enable):
|
||||
if self.avrOut is None: return
|
||||
|
||||
flags = self.ctrl.ioloop.WRITE if enable else 0
|
||||
self.ctrl.ioloop.update_handler(self.avrOut, flags)
|
||||
self.write_enabled = enable
|
||||
|
||||
|
||||
def _avr_write(self, data):
|
||||
try:
|
||||
length = os.write(self.avrOut, data)
|
||||
self.continue_write = length and length == len(data)
|
||||
return length
|
||||
|
||||
except BlockingIOError: pass
|
||||
except BrokenPipeError: pass
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
def _avr_write_handler(self, fd, events):
|
||||
if self.avrOut is None: return
|
||||
|
||||
if events & self.ctrl.ioloop.ERROR:
|
||||
self._start()
|
||||
return
|
||||
|
||||
try:
|
||||
while True:
|
||||
self.continue_write = False
|
||||
self.write_cb(self._avr_write)
|
||||
if not self.continue_write: break
|
||||
|
||||
except Exception as e:
|
||||
self.log.warning('AVR write handler error: %s',
|
||||
traceback.format_exc())
|
||||
|
||||
|
||||
def _avr_read_handler(self, fd, events):
|
||||
if self.avrIn is None: return
|
||||
|
||||
if events & self.ctrl.ioloop.ERROR:
|
||||
self._start()
|
||||
return
|
||||
|
||||
try:
|
||||
data = os.read(self.avrIn, 4096)
|
||||
if data is not None: self.read_cb(data)
|
||||
|
||||
except Exception as e:
|
||||
self.log.warning('AVR read handler error: %s %s' %
|
||||
(data, traceback.format_exc()))
|
||||
|
||||
|
||||
def i2c_command(self, cmd, byte = None, word = None, block = None):
|
||||
if byte is not None: data = chr(byte)
|
||||
elif word is not None: data = word
|
||||
elif block is not None: data = block
|
||||
else: data = ''
|
||||
|
||||
try:
|
||||
if self.i2cOut is not None:
|
||||
os.write(self.i2cOut, bytes(cmd + data + '\n', 'utf-8'))
|
||||
|
||||
except BrokenPipeError: pass
|
||||
@@ -25,10 +25,7 @@ class Ctrl(object):
|
||||
self.log.get('Ctrl').info('Starting %s' % self.id)
|
||||
|
||||
try:
|
||||
if args.demo:
|
||||
self.avr = bbctrl.AVREmu(self)
|
||||
else:
|
||||
self.avr = bbctrl.AVR(self)
|
||||
self.avr = bbctrl.AVR(self)
|
||||
|
||||
self.i2c = bbctrl.I2C(args.i2c_port, args.demo)
|
||||
self.mach = bbctrl.Mach(self, self.avr)
|
||||
|
||||
@@ -1,34 +1,8 @@
|
||||
################################################################################
|
||||
# #
|
||||
# This file is part of the Buildbotics firmware. #
|
||||
# #
|
||||
# Copyright (c) 2015 - 2018, Buildbotics LLC #
|
||||
# All rights reserved. #
|
||||
# #
|
||||
# This file ("the software") is free software: you can redistribute it #
|
||||
# and/or modify it under the terms of the GNU General Public License, #
|
||||
# version 2 as published by the Free Software Foundation. You should #
|
||||
# have received a copy of the GNU General Public License, version 2 #
|
||||
# along with the software. If not, see <http://www.gnu.org/licenses/>. #
|
||||
# #
|
||||
# The software is distributed in the hope that it will be useful, but #
|
||||
# WITHOUT ANY WARRANTY; without even the implied warranty of #
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU #
|
||||
# Lesser General Public License for more details. #
|
||||
# #
|
||||
# You should have received a copy of the GNU Lesser General Public #
|
||||
# License along with the software. If not, see #
|
||||
# <http://www.gnu.org/licenses/>. #
|
||||
# #
|
||||
# For information regarding this software email: #
|
||||
# "Joseph Coffland" <joseph@buildbotics.com> #
|
||||
# #
|
||||
################################################################################
|
||||
|
||||
import os
|
||||
import tempfile
|
||||
import bbctrl
|
||||
import glob
|
||||
import html
|
||||
import tornado
|
||||
from tornado import gen
|
||||
from tornado.web import HTTPError
|
||||
|
||||
@@ -36,17 +10,32 @@ from tornado.web import HTTPError
|
||||
def safe_remove(path):
|
||||
try:
|
||||
os.unlink(path)
|
||||
except OSError: pass
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
|
||||
@tornado.web.stream_request_body
|
||||
class FileHandler(bbctrl.APIHandler):
|
||||
def prepare(self): pass
|
||||
def prepare(self):
|
||||
if self.request.method == 'PUT':
|
||||
self.request.connection.set_max_body_size(2 ** 30)
|
||||
|
||||
self.uploadFilename = self.request.path.split('/')[-1] \
|
||||
.replace('\\', '/') \
|
||||
.replace('#', '-') \
|
||||
.replace('?', '-')
|
||||
|
||||
self.uploadFile = tempfile.NamedTemporaryFile("wb")
|
||||
|
||||
def data_received(self, data):
|
||||
if self.request.method == 'PUT':
|
||||
self.uploadFile.write(data)
|
||||
|
||||
def delete_ok(self, filename):
|
||||
if not filename:
|
||||
# Delete everything
|
||||
for path in glob.glob(self.get_upload('*')): safe_remove(path)
|
||||
for path in glob.glob(self.get_upload('*')):
|
||||
safe_remove(path)
|
||||
self.get_ctrl().preplanner.delete_all_plans()
|
||||
self.get_ctrl().state.clear_files()
|
||||
|
||||
@@ -57,26 +46,29 @@ class FileHandler(bbctrl.APIHandler):
|
||||
self.get_ctrl().preplanner.delete_plans(filename)
|
||||
self.get_ctrl().state.remove_file(filename)
|
||||
|
||||
|
||||
def put_ok(self, *args):
|
||||
gcode = self.request.files['gcode'][0]
|
||||
filename = os.path.basename(gcode['filename'].replace('\\', '/'))
|
||||
filename = filename.replace('#', '-').replace('?', '-')
|
||||
if not os.path.exists(self.get_upload()):
|
||||
os.mkdir(self.get_upload())
|
||||
|
||||
if not os.path.exists(self.get_upload()): os.mkdir(self.get_upload())
|
||||
filename = self.get_upload(self.uploadFilename).encode('utf8')
|
||||
safe_remove(filename)
|
||||
os.link(self.uploadFile.name, filename)
|
||||
|
||||
with open(self.get_upload(filename).encode('utf8'), 'wb') as f:
|
||||
f.write(gcode['body'])
|
||||
os.sync()
|
||||
self.uploadFile.close()
|
||||
|
||||
self.get_ctrl().preplanner.invalidate(filename)
|
||||
self.get_ctrl().state.add_file(filename)
|
||||
self.get_log('FileHandler').info('GCode received: ' + filename)
|
||||
del(self.uploadFile)
|
||||
|
||||
self.get_ctrl().preplanner.invalidate(self.uploadFilename)
|
||||
self.get_ctrl().state.add_file(self.uploadFilename)
|
||||
self.get_log('FileHandler').info(
|
||||
'GCode received: ' + self.uploadFilename)
|
||||
|
||||
del(self.uploadFilename)
|
||||
|
||||
@gen.coroutine
|
||||
def get(self, filename):
|
||||
if not filename: raise HTTPError(400, 'Missing filename')
|
||||
if not filename:
|
||||
raise HTTPError(400, 'Missing filename')
|
||||
filename = os.path.basename(filename)
|
||||
|
||||
try:
|
||||
@@ -84,6 +76,7 @@ class FileHandler(bbctrl.APIHandler):
|
||||
self.write(f.read())
|
||||
except Exception:
|
||||
self.get_ctrl().state.select_file('')
|
||||
raise HTTPError(400, "Unable to read file - doesn't appear to be GCode.")
|
||||
raise HTTPError(
|
||||
400, "Unable to read file - doesn't appear to be GCode.")
|
||||
|
||||
self.get_ctrl().state.select_file(filename)
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,12 +1,24 @@
|
||||
import traceback
|
||||
import copy
|
||||
import json
|
||||
import uuid
|
||||
import os
|
||||
import socket
|
||||
import bbctrl
|
||||
import iw_parse
|
||||
from tornado import gen
|
||||
from watchdog.observers import Observer
|
||||
from watchdog.events import FileSystemEventHandler
|
||||
|
||||
|
||||
def call_get_output(cmd):
|
||||
p = subprocess.Popen(cmd, stdout=subprocess.PIPE)
|
||||
s = p.communicate()[0].decode('utf-8')
|
||||
if p.returncode:
|
||||
raise HTTPError(400, 'Command failed')
|
||||
return s
|
||||
|
||||
|
||||
class UploadChangeHandler(FileSystemEventHandler):
|
||||
def __init__(self, state):
|
||||
self.state = state
|
||||
@@ -64,6 +76,36 @@ class State(object):
|
||||
self), self.ctrl.get_upload(), recursive=True)
|
||||
observer.start()
|
||||
|
||||
self._updateNetworkInfo()
|
||||
|
||||
@gen.coroutine
|
||||
def _updateNetworkInfo(self):
|
||||
try:
|
||||
ipAddresses = call_get_output(['hostname', '-I']).split()
|
||||
except:
|
||||
ipAddresses = ""
|
||||
|
||||
hostname = socket.gethostname()
|
||||
|
||||
try:
|
||||
wifi = json.loads(call_get_output(['config-wifi', '-j']))
|
||||
except:
|
||||
wifi = {'enabled': False}
|
||||
|
||||
try:
|
||||
lines = iw_parse.call_iwlist().decode("utf-8").split("\n")
|
||||
wifi['networks'] = iw_parse.get_parsed_cells(lines)
|
||||
except:
|
||||
wifi['networks'] = []
|
||||
|
||||
self.set('networkInfo', {
|
||||
'ipAddresses': ipAddresses,
|
||||
'hostname': hostname,
|
||||
'wifi': wifi
|
||||
})
|
||||
|
||||
self.timeout = self.ctrl.ioloop.call_later(5, self._updateNetworkInfo)
|
||||
|
||||
def reset(self):
|
||||
# Unhome all motors
|
||||
for i in range(4):
|
||||
@@ -81,8 +123,6 @@ class State(object):
|
||||
|
||||
if not os.path.exists(upload):
|
||||
os.mkdir(upload)
|
||||
from shutil import copy
|
||||
copy(bbctrl.get_resource('http/buildbotics.nc'), upload)
|
||||
|
||||
for path in os.listdir(upload):
|
||||
if os.path.isfile(upload + '/' + path):
|
||||
@@ -170,8 +210,11 @@ class State(object):
|
||||
|
||||
return name
|
||||
|
||||
def has(self, name): return self.resolve(name) in self.vars
|
||||
def set_callback(self, name, cb): self.callbacks[self.resolve(name)] = cb
|
||||
def has(self, name):
|
||||
return self.resolve(name) in self.vars
|
||||
|
||||
def set_callback(self, name, cb):
|
||||
self.callbacks[self.resolve(name)] = cb
|
||||
|
||||
def set(self, name, value):
|
||||
name = self.resolve(name)
|
||||
@@ -233,7 +276,8 @@ class State(object):
|
||||
self.listeners.append(listener)
|
||||
listener(self.vars)
|
||||
|
||||
def remove_listener(self, listener): self.listeners.remove(listener)
|
||||
def remove_listener(self, listener):
|
||||
self.listeners.remove(listener)
|
||||
|
||||
def set_machine_vars(self, vars):
|
||||
# Record all machine vars, indexed or otherwise
|
||||
@@ -284,7 +328,8 @@ class State(object):
|
||||
if motor_axis == axis.lower() and self.vars.get('%dme' % motor, 0):
|
||||
return motor
|
||||
|
||||
def is_axis_homed(self, axis): return self.get('%s_homed' % axis, False)
|
||||
def is_axis_homed(self, axis):
|
||||
return self.get('%s_homed' % axis, False)
|
||||
|
||||
def is_axis_enabled(self, axis):
|
||||
motor = self.find_motor(axis)
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import os
|
||||
import re
|
||||
import json
|
||||
import tornado
|
||||
import sockjs.tornado
|
||||
import datetime
|
||||
@@ -10,7 +9,6 @@ from tornado.web import HTTPError
|
||||
from tornado import gen
|
||||
|
||||
import bbctrl
|
||||
import iw_parse
|
||||
|
||||
|
||||
def call_get_output(cmd):
|
||||
@@ -23,11 +21,17 @@ def call_get_output(cmd):
|
||||
|
||||
class RebootHandler(bbctrl.APIHandler):
|
||||
def put_ok(self):
|
||||
subprocess.Popen('reboot')
|
||||
subprocess.Popen(['plymouth', 'show-splash'])
|
||||
subprocess.Popen(['plymouth', 'change-mode', '--shutdown'])
|
||||
subprocess.Popen(['killall', 'xinit'])
|
||||
subprocess.Popen(['reboot'])
|
||||
|
||||
|
||||
class ShutdownHandler(bbctrl.APIHandler):
|
||||
def put_ok(self):
|
||||
subprocess.Popen(['plymouth', 'show-splash'])
|
||||
subprocess.Popen(['plymouth', 'change-mode', '--shutdown'])
|
||||
subprocess.Popen(['killall', 'xinit'])
|
||||
subprocess.Popen(['shutdown', '-h', 'now'])
|
||||
|
||||
|
||||
@@ -101,31 +105,6 @@ class HostnameHandler(bbctrl.APIHandler):
|
||||
|
||||
|
||||
class NetworkHandler(bbctrl.APIHandler):
|
||||
def get(self):
|
||||
try:
|
||||
ipAddresses = call_get_output(['hostname', '-I']).split()
|
||||
except:
|
||||
ipAddresses = ""
|
||||
|
||||
hostname = socket.gethostname()
|
||||
|
||||
try:
|
||||
wifi = json.loads(call_get_output(['config-wifi', '-j']))
|
||||
except:
|
||||
wifi = {'enabled': False}
|
||||
|
||||
try:
|
||||
lines = iw_parse.call_iwlist().decode("utf-8").split("\n")
|
||||
wifi['networks'] = iw_parse.get_parsed_cells(lines)
|
||||
except:
|
||||
wifi['networks'] = []
|
||||
|
||||
self.write_json({
|
||||
'ipAddresses': ipAddresses,
|
||||
'hostname': hostname,
|
||||
'wifi': wifi
|
||||
})
|
||||
|
||||
def put(self):
|
||||
if self.get_ctrl().args.demo:
|
||||
raise HTTPError(400, 'Cannot configure WiFi in demo mode')
|
||||
@@ -267,14 +246,6 @@ class HomeHandler(bbctrl.APIHandler):
|
||||
self.get_ctrl().mach.home(axis)
|
||||
|
||||
|
||||
class DevmodeHandler(bbctrl.APIHandler):
|
||||
def put_ok(self, command, *args):
|
||||
if command == "/probe":
|
||||
self.get_ctrl().mach.fake_probe_contact()
|
||||
else:
|
||||
raise HTTPError(400, 'Not implemented')
|
||||
|
||||
|
||||
class StartHandler(bbctrl.APIHandler):
|
||||
def put_ok(self): self.get_ctrl().mach.start()
|
||||
|
||||
@@ -383,13 +354,34 @@ class ScreenRotationHandler(bbctrl.APIHandler):
|
||||
text = config.read()
|
||||
text = transformationMatrixPattern.sub(r'\1\2\3\5', text)
|
||||
if rotated:
|
||||
text = matchIsTouchscreenPattern.sub(r'\1\2\3\2Option "TransformationMatrix" "-1 0 1 0 -1 1 0 0 1"\1\4', text)
|
||||
text = matchIsTouchscreenPattern.sub(
|
||||
r'\1\2\3\2Option "TransformationMatrix" "-1 0 1 0 -1 1 0 0 1"\1\4', text)
|
||||
with open("/usr/share/X11/xorg.conf.d/40-libinput.conf", 'wt') as config:
|
||||
config.write(text)
|
||||
|
||||
subprocess.run('reboot')
|
||||
|
||||
|
||||
class TimeHandler(bbctrl.APIHandler):
|
||||
def get(self):
|
||||
timeinfo = call_get_output(['timedatectl'])
|
||||
timezones = call_get_output(
|
||||
['timedatectl', 'list-timezones', '--no-pager'])
|
||||
self.get_log('TimeHandler').info(
|
||||
'Time stuff: {}, {}'.format(timeinfo, timezones))
|
||||
|
||||
self.write_json({
|
||||
'timeinfo': timeinfo,
|
||||
'timezones': timezones
|
||||
})
|
||||
|
||||
def put_ok(self):
|
||||
datetime = self.json['datetime']
|
||||
timezone = self.json['timezone']
|
||||
subprocess.Popen(['timedatectl', 'set-time', datetime])
|
||||
subprocess.Popen(['timedatectl', 'set-timezone', timezone])
|
||||
|
||||
|
||||
# Base class for Web Socket connections
|
||||
class ClientConnection(object):
|
||||
def __init__(self, app):
|
||||
@@ -509,7 +501,6 @@ class Web(tornado.web.Application):
|
||||
(r'/api/file(/[^/]+)?', bbctrl.FileHandler),
|
||||
(r'/api/path/([^/]+)((/positions)|(/speeds))?', PathHandler),
|
||||
(r'/api/home(/[xyzabcXYZABC]((/set)|(/clear))?)?', HomeHandler),
|
||||
(r'/api/devmode((/probe))?', DevmodeHandler),
|
||||
(r'/api/start', StartHandler),
|
||||
(r'/api/estop', EStopHandler),
|
||||
(r'/api/clear', ClearHandler),
|
||||
@@ -526,6 +517,7 @@ class Web(tornado.web.Application):
|
||||
(r'/api/jog', JogHandler),
|
||||
(r'/api/video', bbctrl.VideoHandler),
|
||||
(r'/api/screen-rotation', ScreenRotationHandler),
|
||||
(r'/api/time', TimeHandler),
|
||||
(r'/(.*)', StaticFileHandler,
|
||||
{'path': bbctrl.get_resource('http/'),
|
||||
'default_filename': 'index.html'}),
|
||||
@@ -545,7 +537,8 @@ class Web(tornado.web.Application):
|
||||
|
||||
print('Listening on http://%s:%d/' % (args.addr, args.port))
|
||||
|
||||
def opened(self, ctrl): ctrl.clear_timeout()
|
||||
def opened(self, ctrl):
|
||||
ctrl.clear_timeout()
|
||||
|
||||
def closed(self, ctrl):
|
||||
# Time out clients in demo mode
|
||||
|
||||
@@ -1,33 +1,5 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
################################################################################
|
||||
# #
|
||||
# This file is part of the Buildbotics firmware. #
|
||||
# #
|
||||
# Copyright (c) 2015 - 2018, Buildbotics LLC #
|
||||
# All rights reserved. #
|
||||
# #
|
||||
# This file ("the software") is free software: you can redistribute it #
|
||||
# and/or modify it under the terms of the GNU General Public License, #
|
||||
# version 2 as published by the Free Software Foundation. You should #
|
||||
# have received a copy of the GNU General Public License, version 2 #
|
||||
# along with the software. If not, see <http://www.gnu.org/licenses/>. #
|
||||
# #
|
||||
# The software is distributed in the hope that it will be useful, but #
|
||||
# WITHOUT ANY WARRANTY; without even the implied warranty of #
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU #
|
||||
# Lesser General Public License for more details. #
|
||||
# #
|
||||
# You should have received a copy of the GNU Lesser General Public #
|
||||
# License along with the software. If not, see #
|
||||
# <http://www.gnu.org/licenses/>. #
|
||||
# #
|
||||
# For information regarding this software email: #
|
||||
# "Joseph Coffland" <joseph@buildbotics.com> #
|
||||
# #
|
||||
################################################################################
|
||||
|
||||
import os
|
||||
import sys
|
||||
import signal
|
||||
import tornado
|
||||
@@ -53,13 +25,11 @@ from bbctrl.Comm import Comm
|
||||
from bbctrl.CommandQueue import CommandQueue
|
||||
from bbctrl.Camera import Camera, VideoHandler
|
||||
from bbctrl.AVR import AVR
|
||||
from bbctrl.AVREmu import AVREmu
|
||||
from bbctrl.IOLoop import IOLoop
|
||||
from bbctrl.MonitorTemp import MonitorTemp
|
||||
import bbctrl.Cmd as Cmd
|
||||
import bbctrl.v4l2 as v4l2
|
||||
import bbctrl.Log as log
|
||||
import bbctrl.ObjGraph as ObjGraph
|
||||
|
||||
|
||||
ctrl = None
|
||||
@@ -69,7 +39,7 @@ def get_resource(path):
|
||||
return resource_filename(Requirement.parse('bbctrl'), 'bbctrl/' + path)
|
||||
|
||||
|
||||
def on_exit(sig = 0, func = None):
|
||||
def on_exit(sig=0, func=None):
|
||||
global ctrl
|
||||
|
||||
print('Exit handler triggered: signal = %d', sig)
|
||||
@@ -85,76 +55,46 @@ def time_str():
|
||||
return datetime.datetime.now().strftime('%Y%m%d-%H:%M:%S')
|
||||
|
||||
|
||||
|
||||
class Debugger:
|
||||
def __init__(self, ioloop, freq = 60 * 15, depth = 100):
|
||||
self.ioloop = ioloop
|
||||
self.freq = freq
|
||||
self.depth = depth
|
||||
self._callback()
|
||||
|
||||
|
||||
def _callback(self):
|
||||
with open('bbctrl-debug-%s.log' % time_str(), 'w') as log:
|
||||
def line(name):
|
||||
log.write('==== ' + name + ' ' + '=' * (74 - len(name)) + '\n')
|
||||
|
||||
line('Common')
|
||||
ObjGraph.show_most_common_types(limit = self.depth, file = log)
|
||||
|
||||
log.write('\n')
|
||||
line('Growth')
|
||||
ObjGraph.show_growth(limit = self.depth, file = log)
|
||||
|
||||
log.write('\n')
|
||||
line('New IDs')
|
||||
ObjGraph.get_new_ids(limit = self.depth, file = log)
|
||||
|
||||
log.flush()
|
||||
self.ioloop.call_later(self.freq, self._callback)
|
||||
|
||||
|
||||
|
||||
def parse_args():
|
||||
parser = argparse.ArgumentParser(
|
||||
description = 'Buildbotics Machine Controller')
|
||||
description='Buildbotics Machine Controller')
|
||||
|
||||
parser.add_argument('-p', '--port', default = 80,
|
||||
type = int, help = 'HTTP port')
|
||||
parser.add_argument('-a', '--addr', metavar = 'IP', default = '0.0.0.0',
|
||||
help = 'HTTP address to bind')
|
||||
parser.add_argument('-s', '--serial', default = '/dev/ttyAMA0',
|
||||
help = 'Serial device')
|
||||
parser.add_argument('-b', '--baud', default = 230400, type = int,
|
||||
help = 'Serial baud rate')
|
||||
parser.add_argument('--i2c-port', default = 1, type = int,
|
||||
help = 'I2C port')
|
||||
parser.add_argument('--avr-addr', default = 0x2b, type = int,
|
||||
help = 'AVR I2C address')
|
||||
parser.add_argument('--pwr-addr', default = 0x60, type = int,
|
||||
help = 'Power AVR I2C address')
|
||||
parser.add_argument('-v', '--verbose', action = 'store_true',
|
||||
help = 'Verbose output')
|
||||
parser.add_argument('-l', '--log', metavar = "FILE",
|
||||
help = 'Set a log file')
|
||||
parser.add_argument('--disable-camera', action = 'store_true',
|
||||
help = 'Disable the camera')
|
||||
parser.add_argument('--width', default = 640, type = int,
|
||||
help = 'Camera width')
|
||||
parser.add_argument('--height', default = 480, type = int,
|
||||
help = 'Camera height')
|
||||
parser.add_argument('--fps', default = 15, type = int,
|
||||
help = 'Camera frames per second')
|
||||
parser.add_argument('--camera-clients', default = 4,
|
||||
help = 'Maximum simultaneous camera clients')
|
||||
parser.add_argument('--demo', action = 'store_true',
|
||||
help = 'Enter demo mode')
|
||||
parser.add_argument('--debug', default = 0, type = int,
|
||||
help = 'Enable debug mode and set frequency in seconds')
|
||||
parser.add_argument('--fast-emu', action = 'store_true',
|
||||
help = 'Enter demo mode')
|
||||
parser.add_argument('--client-timeout', default = 5 * 60, type = int,
|
||||
help = 'Demo client timeout in seconds')
|
||||
parser.add_argument('-p', '--port', default=80,
|
||||
type=int, help='HTTP port')
|
||||
parser.add_argument('-a', '--addr', metavar='IP', default='0.0.0.0',
|
||||
help='HTTP address to bind')
|
||||
parser.add_argument('-s', '--serial', default='/dev/ttyAMA0',
|
||||
help='Serial device')
|
||||
parser.add_argument('-b', '--baud', default=230400, type=int,
|
||||
help='Serial baud rate')
|
||||
parser.add_argument('--i2c-port', default=1, type=int,
|
||||
help='I2C port')
|
||||
parser.add_argument('--avr-addr', default=0x2b, type=int,
|
||||
help='AVR I2C address')
|
||||
parser.add_argument('--pwr-addr', default=0x60, type=int,
|
||||
help='Power AVR I2C address')
|
||||
parser.add_argument('-v', '--verbose', action='store_true',
|
||||
help='Verbose output')
|
||||
parser.add_argument('-l', '--log', metavar="FILE",
|
||||
help='Set a log file')
|
||||
parser.add_argument('--disable-camera', action='store_true',
|
||||
help='Disable the camera')
|
||||
parser.add_argument('--width', default=640, type=int,
|
||||
help='Camera width')
|
||||
parser.add_argument('--height', default=480, type=int,
|
||||
help='Camera height')
|
||||
parser.add_argument('--fps', default=15, type=int,
|
||||
help='Camera frames per second')
|
||||
parser.add_argument('--camera-clients', default=4,
|
||||
help='Maximum simultaneous camera clients')
|
||||
parser.add_argument('--demo', action='store_true',
|
||||
help='Enter demo mode')
|
||||
parser.add_argument('--debug', default=0, type=int,
|
||||
help='Enable debug mode and set frequency in seconds')
|
||||
parser.add_argument('--fast-emu', action='store_true',
|
||||
help='Enter demo mode')
|
||||
parser.add_argument('--client-timeout', default=5 * 60, type=int,
|
||||
help='Demo client timeout in seconds')
|
||||
|
||||
return parser.parse_args()
|
||||
|
||||
@@ -170,16 +110,15 @@ def run():
|
||||
# Create ioloop
|
||||
ioloop = tornado.ioloop.IOLoop.current()
|
||||
|
||||
# Set ObjGraph signal handler
|
||||
if args.debug: Debugger(ioloop, args.debug)
|
||||
|
||||
# Start server
|
||||
web = Web(args, ioloop)
|
||||
|
||||
try:
|
||||
ioloop.start()
|
||||
|
||||
except KeyboardInterrupt: on_exit()
|
||||
except KeyboardInterrupt:
|
||||
on_exit()
|
||||
|
||||
|
||||
if __name__ == '__main__': run()
|
||||
if __name__ == '__main__':
|
||||
run()
|
||||
|
||||
@@ -1,403 +0,0 @@
|
||||
G21
|
||||
(File: 'buildbotics_logo.tpl')
|
||||
G0 Z3
|
||||
F1600
|
||||
M3 S10000
|
||||
M6 T2
|
||||
G0 X59.25 Y5.85
|
||||
G1 Z-1.5
|
||||
G1 X61.68 Y6.7
|
||||
G1 X63.86 Y8.07
|
||||
G1 X65.68 Y9.89
|
||||
G1 X67.05 Y12.07
|
||||
G1 X67.9 Y14.5
|
||||
G1 X68.2 Y17.09
|
||||
G1 Y56.6
|
||||
G1 X67.73 Y59.04
|
||||
G1 X50.8
|
||||
G1 Y34.9
|
||||
G1 X50.65 Y34.55
|
||||
G1 X50.3 Y34.4
|
||||
G1 X23.46
|
||||
G1 X23.1 Y34.55
|
||||
G1 X22.96 Y34.9
|
||||
G1 X22.98 Y49.88
|
||||
G1 X22.96 Y59.05
|
||||
G1 X22.41
|
||||
G1 X19.26
|
||||
G1 X6.04
|
||||
G1 X5.56 Y56.53
|
||||
G1 Y17.09
|
||||
G1 X5.85 Y14.5
|
||||
G1 X6.7 Y12.07
|
||||
G1 X8.07 Y9.89
|
||||
G1 X9.89 Y8.07
|
||||
G1 X12.07 Y6.7
|
||||
G1 X14.5 Y5.85
|
||||
G1 X17.09 Y5.56
|
||||
G1 X56.67
|
||||
G1 X59.25 Y5.85
|
||||
G0 Z3
|
||||
G0 X64.26 Y64.72
|
||||
G1 Z-1.5
|
||||
G1 X61.78 Y66.52
|
||||
G1 X58.91 Y67.68
|
||||
G1 X56.54 Y68.08
|
||||
G1 X17.22
|
||||
G1 X14.84 Y67.68
|
||||
G1 X11.97 Y66.52
|
||||
G1 X9.49 Y64.72
|
||||
G1 X8.08 Y63.16
|
||||
G1 X27.35
|
||||
G1 X27.89 Y63.45
|
||||
G1 X27.96 Y63.48
|
||||
G1 X31.48 Y64.75
|
||||
G1 X31.52 Y64.76
|
||||
G1 X31.56 Y64.77
|
||||
G1 X35.19 Y65.41
|
||||
G1 X35.26
|
||||
G1 X35.97 Y65.44
|
||||
G1 X36.04 Y65.45
|
||||
G1 X36.07
|
||||
G1 X36.82 Y65.44
|
||||
G1 X36.83
|
||||
G1 X36.89
|
||||
G1 X36.95
|
||||
G1 X36.97
|
||||
G1 X37.72 Y65.43
|
||||
G1 X37.74
|
||||
G1 X37.8
|
||||
G1 X37.81
|
||||
G1 X37.88
|
||||
G1 X37.89
|
||||
G1 X38.65 Y65.38
|
||||
G1 X38.68
|
||||
G1 X38.75 Y65.37
|
||||
G1 X39.38 Y65.32
|
||||
G1 X39.44 Y65.31
|
||||
G1 X42.68 Y64.64
|
||||
G1 X42.76 Y64.62
|
||||
G1 X45.87 Y63.44
|
||||
G1 X45.93 Y63.41
|
||||
G1 X46.4 Y63.16
|
||||
G1 X65.67
|
||||
G1 X64.26 Y64.72
|
||||
G0 Z3
|
||||
G0 X36.88 Y9.4
|
||||
G1 Z-1.5
|
||||
G1 X37.31 Y9.64
|
||||
G1 X39.58 Y13.48
|
||||
G1 X39.63 Y13.6
|
||||
G1 X39.65 Y13.73
|
||||
G1 Y27.54
|
||||
G1 X41.67
|
||||
G1 Y25.39
|
||||
G1 X41.75 Y25.12
|
||||
G1 X41.97 Y24.93
|
||||
G1 X46.41 Y22.92
|
||||
G1 Y19.97
|
||||
G1 X45.44
|
||||
G1 X45.08 Y19.82
|
||||
G1 X44.94 Y19.47
|
||||
G1 Y13.73
|
||||
G1 X45.08 Y13.38
|
||||
G1 X45.44 Y13.23
|
||||
G1 X49.94 Y13.24
|
||||
G1 X50.29 Y13.39
|
||||
G1 X50.44 Y13.74
|
||||
G1 Y19.47
|
||||
G1 X50.29 Y19.83
|
||||
G1 X49.93 Y19.97
|
||||
G1 X48.92
|
||||
G1 Y23.61
|
||||
G1 X48.84 Y23.88
|
||||
G1 X48.63 Y24.06
|
||||
G1 X44.19 Y26.12
|
||||
G1 Y27.54
|
||||
G1 X49.22
|
||||
G1 X49.33 Y27.56
|
||||
G1 X49.44 Y27.6
|
||||
G1 X50.13 Y27.94
|
||||
G1 X50.25 Y28.02
|
||||
G1 X50.34 Y28.13
|
||||
G1 X50.73 Y28.77
|
||||
G1 X50.78 Y28.89
|
||||
G1 X50.8 Y29.03
|
||||
G1 Y33.05
|
||||
G1 Y34.25
|
||||
G1 Y34.65
|
||||
G1 X50.66 Y35.01
|
||||
G1 X50.3 Y35.15
|
||||
G1 X23.46
|
||||
G1 X23.1 Y35.01
|
||||
G1 X22.96 Y34.65
|
||||
G1 Y29.07
|
||||
G1 X22.97 Y28.94
|
||||
G1 X23.02 Y28.82
|
||||
G1 X23.4 Y28.17
|
||||
G1 X23.49 Y28.06
|
||||
G1 X23.6 Y27.98
|
||||
G1 X24.29 Y27.6
|
||||
G1 X24.4 Y27.56
|
||||
G1 X24.52 Y27.54
|
||||
G1 X25.55
|
||||
G1 Y26.4
|
||||
G1 X23.4 Y25.52
|
||||
G1 X23.17 Y25.33
|
||||
G1 X23.09 Y25.06
|
||||
G1 Y17.54
|
||||
G1 X23.23 Y17.19
|
||||
G1 X23.59 Y17.04
|
||||
G1 X24.6
|
||||
G1 Y10.36
|
||||
G1 X24.62 Y10.23
|
||||
G1 X24.66 Y10.11
|
||||
G1 X24.8 Y9.88
|
||||
G1 X24.88 Y9.77
|
||||
G1 X24.99 Y9.68
|
||||
G1 X25.25 Y9.54
|
||||
G1 X25.37 Y9.5
|
||||
G1 X25.49 Y9.48
|
||||
G1 X26.53
|
||||
G1 X26.65 Y9.49
|
||||
G1 X26.76 Y9.54
|
||||
G1 X27.01 Y9.66
|
||||
G1 X27.12 Y9.74
|
||||
G1 X27.21 Y9.85
|
||||
G1 X27.35 Y10.09
|
||||
G1 X27.41 Y10.22
|
||||
G1 X27.43 Y10.35
|
||||
G1 Y10.43
|
||||
G1 X27.47 Y17.04
|
||||
G1 X28.57
|
||||
G1 X28.92 Y17.19
|
||||
G1 X29.07 Y17.54
|
||||
G1 Y24.64
|
||||
G1 X30.72 Y25.3
|
||||
G1 X30.95 Y25.49
|
||||
G1 X31.03 Y25.77
|
||||
G1 X31.02 Y27.54
|
||||
G1 X34.03
|
||||
G1 Y13.73
|
||||
G1 X34.05 Y13.59
|
||||
G1 X34.1 Y13.47
|
||||
G1 X36.45 Y9.64
|
||||
G1 X36.88 Y9.4
|
||||
G0 Z3
|
||||
G0 X49.94 Y10.82
|
||||
G1 Z-1.5
|
||||
G1 X50.29 Y10.97
|
||||
G1 X50.44 Y11.32
|
||||
G1 Y13.34
|
||||
G1 X50.29 Y13.69
|
||||
G1 X49.94 Y13.84
|
||||
G1 X45.44
|
||||
G1 X45.08 Y13.69
|
||||
G1 X44.94 Y13.34
|
||||
G1 Y11.32
|
||||
G1 X45.08 Y10.97
|
||||
G1 X45.44 Y10.82
|
||||
G1 X49.94
|
||||
G0 Z3
|
||||
G0 X48.46 Y9.7
|
||||
G1 Z-1.5
|
||||
G1 X48.59 Y9.72
|
||||
G1 X48.71 Y9.77
|
||||
G1 X50.03 Y10.53
|
||||
G1 X50.21 Y10.71
|
||||
G1 X50.28 Y10.96
|
||||
G1 X50.14 Y11.31
|
||||
G1 X49.78 Y11.46
|
||||
G1 X45.62
|
||||
G1 X45.14 Y11.09
|
||||
G1 X45.37 Y10.53
|
||||
G1 X46.69 Y9.77
|
||||
G1 X46.81 Y9.72
|
||||
G1 X46.94 Y9.7
|
||||
G1 X48.46
|
||||
G0 Z3
|
||||
G0 X50.3 Y34.4
|
||||
G1 Z-1.5
|
||||
G1 X50.66 Y34.55
|
||||
G1 X50.8 Y34.9
|
||||
G1 Y49.88
|
||||
G1 X50.79 Y59.52
|
||||
G1 X50.76 Y59.69
|
||||
G1 X50.67 Y59.84
|
||||
G1 X50.09 Y60.52
|
||||
G1 X50.06 Y60.55
|
||||
G1 X50.02 Y60.58
|
||||
G1 X47.89 Y62.26
|
||||
G1 X47.85 Y62.28
|
||||
G1 X47.81 Y62.31
|
||||
G1 X44.46 Y64.03
|
||||
G1 X44.41 Y64.05
|
||||
G1 X44.37 Y64.06
|
||||
G1 X40.69 Y65.1
|
||||
G1 X40.62 Y65.12
|
||||
G1 X37.86 Y65.45
|
||||
G1 X37.8 Y65.46
|
||||
G1 X36.89 Y65.44
|
||||
G1 X36.83
|
||||
G1 X36.82
|
||||
G1 X36.07 Y65.45
|
||||
G1 X36.04 Y65.44
|
||||
G1 X35.97
|
||||
G1 X35.1 Y65.41
|
||||
G1 X35.04 Y65.4
|
||||
G1 X32.44 Y64.99
|
||||
G1 X32.37 Y64.97
|
||||
G1 X28.94 Y63.9
|
||||
G1 X28.89 Y63.88
|
||||
G1 X28.85 Y63.86
|
||||
G1 X25.74 Y62.2
|
||||
G1 X25.7 Y62.18
|
||||
G1 X25.66 Y62.15
|
||||
G1 X23.68 Y60.56
|
||||
G1 X23.65 Y60.53
|
||||
G1 X23.62 Y60.5
|
||||
G1 X23.08 Y59.87
|
||||
G1 X22.99 Y59.71
|
||||
G1 X22.96 Y59.54
|
||||
G1 X22.98 Y49.88
|
||||
G1 X22.96 Y34.9
|
||||
G1 X23.1 Y34.55
|
||||
G1 X23.46 Y34.4
|
||||
G1 X50.3
|
||||
G0 Z3
|
||||
G0 X55.2 Y43.67
|
||||
G1 Z-1.5
|
||||
G1 Y51.34
|
||||
G1 X55.11 Y51.94
|
||||
G1 X54.83 Y52.88
|
||||
G1 X54.39 Y53.88
|
||||
G1 X53.8 Y54.85
|
||||
G1 X53.09 Y55.74
|
||||
G1 X52.28 Y56.47
|
||||
G1 X51.41 Y56.98
|
||||
G1 X51.07 Y57.09
|
||||
G1 Y43.67
|
||||
G1 X55.2
|
||||
G0 Z3
|
||||
G0 X22.69 Y43.63
|
||||
G1 Z-1.5
|
||||
G1 Y57.09
|
||||
G1 X22.35 Y56.98
|
||||
G1 X21.47 Y56.47
|
||||
G1 X20.67 Y55.74
|
||||
G1 X19.95 Y54.85
|
||||
G1 X19.36 Y53.88
|
||||
G1 X18.92 Y52.88
|
||||
G1 X18.64 Y51.94
|
||||
G1 X18.55 Y51.34
|
||||
G1 Y43.63
|
||||
G1 X22.69
|
||||
G0 Z3
|
||||
G0 X28.55 Y35.84
|
||||
G1 Z-0.99
|
||||
G1 X30.11 Y36.15
|
||||
G1 X31.43 Y37.03
|
||||
G1 X32.32 Y38.35
|
||||
G1 X32.63 Y39.91
|
||||
G1 X32.32 Y41.47
|
||||
G1 X31.43 Y42.79
|
||||
G1 X30.11 Y43.68
|
||||
G1 X28.55 Y43.99
|
||||
G1 X26.99 Y43.68
|
||||
G1 X25.67 Y42.79
|
||||
G1 X24.79 Y41.47
|
||||
G1 X24.48 Y39.91
|
||||
G1 X24.79 Y38.35
|
||||
G1 X25.67 Y37.03
|
||||
G1 X26.99 Y36.15
|
||||
G1 X28.55 Y35.84
|
||||
G0 Z3
|
||||
G0 X45.33 Y35.93
|
||||
G1 Z-0.99
|
||||
G1 X46.88 Y36.24
|
||||
G1 X48.21 Y37.12
|
||||
G1 X49.09 Y38.45
|
||||
G1 X49.4 Y40
|
||||
G1 X49.09 Y41.56
|
||||
G1 X48.21 Y42.88
|
||||
G1 X46.88 Y43.77
|
||||
G1 X45.33 Y44.08
|
||||
G1 X43.77 Y43.77
|
||||
G1 X42.45 Y42.88
|
||||
G1 X41.56 Y41.56
|
||||
G1 X41.25 Y40
|
||||
G1 X41.56 Y38.45
|
||||
G1 X42.45 Y37.12
|
||||
G1 X43.77 Y36.24
|
||||
G1 X45.33 Y35.93
|
||||
G0 Z3
|
||||
G0 X45.2 Y39.12
|
||||
G1 Z-0.99
|
||||
G1 X45.7 Y39.19
|
||||
G1 X46.07 Y39.52
|
||||
G1 X46.22 Y40
|
||||
G1 X46.07 Y40.49
|
||||
G1 X45.7 Y40.81
|
||||
G1 X45.2 Y40.89
|
||||
G1 X44.74 Y40.68
|
||||
G1 X44.47 Y40.26
|
||||
G1 Y39.75
|
||||
G1 X44.74 Y39.33
|
||||
G1 X45.2 Y39.12
|
||||
G0 Z3
|
||||
G0 X28.43 Y39.03
|
||||
G1 Z-0.99
|
||||
G1 X28.92 Y39.1
|
||||
G1 X29.3 Y39.43
|
||||
G1 X29.44 Y39.91
|
||||
G1 X29.3 Y40.4
|
||||
G1 X28.92 Y40.72
|
||||
G1 X28.43 Y40.8
|
||||
G1 X27.97 Y40.59
|
||||
G1 X27.7 Y40.16
|
||||
G1 Y39.66
|
||||
G1 X27.97 Y39.24
|
||||
G1 X28.43 Y39.03
|
||||
G0 Z3
|
||||
G0 X55.76 Y0
|
||||
G1 Z-1.5
|
||||
G1 X59.27 Y0.35
|
||||
G1 X62.65 Y1.37
|
||||
G1 X65.76 Y3.03
|
||||
G1 X68.49 Y5.27
|
||||
G1 X70.73 Y8
|
||||
G1 X72.39 Y11.11
|
||||
G1 X73.42 Y14.49
|
||||
G1 X73.76 Y18
|
||||
G1 Y55.69
|
||||
G1 X73.42 Y59.2
|
||||
G1 X72.39 Y62.57
|
||||
G1 X70.73 Y65.69
|
||||
G1 X68.49 Y68.41
|
||||
G1 X65.76 Y70.65
|
||||
G1 X62.65 Y72.31
|
||||
G1 X59.27 Y73.34
|
||||
G1 X55.76 Y73.69
|
||||
G1 X18
|
||||
G1 X14.49 Y73.34
|
||||
G1 X11.11 Y72.31
|
||||
G1 X8 Y70.65
|
||||
G1 X5.27 Y68.41
|
||||
G1 X3.03 Y65.69
|
||||
G1 X1.37 Y62.57
|
||||
G1 X0.35 Y59.2
|
||||
G1 X0 Y55.69
|
||||
G1 Y18
|
||||
G1 X0.35 Y14.49
|
||||
G1 X1.37 Y11.11
|
||||
G1 X3.03 Y8
|
||||
G1 X5.27 Y5.27
|
||||
G1 X8 Y3.03
|
||||
G1 X11.11 Y1.37
|
||||
G1 X14.49 Y0.35
|
||||
G1 X18 Y0
|
||||
G1 X55.76
|
||||
G0 Z3
|
||||
M5
|
||||
G0 X40 Y75
|
||||
M2
|
||||
@@ -502,14 +502,14 @@
|
||||
},
|
||||
"probe-fast-seek": {
|
||||
"type": "float",
|
||||
"unit": "mm/m",
|
||||
"unit": "mm/min",
|
||||
"min": 0,
|
||||
"max": 1000,
|
||||
"default": 200
|
||||
},
|
||||
"probe-slow-seek": {
|
||||
"type": "float",
|
||||
"unit": "mm/m",
|
||||
"unit": "mm/min",
|
||||
"min": 0,
|
||||
"max": 1000,
|
||||
"default": 25
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
[Plymouth Theme]
|
||||
Name=buildbotics
|
||||
Description=Buildbotics boot splash
|
||||
ModuleName=script
|
||||
|
||||
[script]
|
||||
ImageDir=/usr/share/plymouth/themes/buildbotics
|
||||
ScriptFile=/usr/share/plymouth/themes/buildbotics/buildbotics.script
|
||||
@@ -1,32 +0,0 @@
|
||||
screenW = Window.GetWidth();
|
||||
screenH = Window.GetHeight();
|
||||
|
||||
image = Image("splash.png");
|
||||
imageW = image.GetWidth();
|
||||
imageH = image.GetHeight();
|
||||
|
||||
scaleX = imageW / screenW;
|
||||
scaleY = imageH / screenH;
|
||||
|
||||
flag = 1;
|
||||
|
||||
if (scaleX > 1 || scaleY > 1) {
|
||||
if (scaleX > scaleY) {
|
||||
resized = image.Scale(screenW, imageH / scaleX);
|
||||
imageX = 0;
|
||||
imageY = (screenH - ((imageH * screenW) / imageW)) / 2;
|
||||
|
||||
} else {
|
||||
resized = image.Scale(imageW / scaleY, screenH);
|
||||
imageX = (screenW - ((imageW * screenH) / imageH)) / 2;
|
||||
imageY = 0;
|
||||
}
|
||||
|
||||
} else {
|
||||
resized = image.Scale(imageW, imageH);
|
||||
imageX = (screenW - imageW) / 2;
|
||||
imageY = (screenH - imageH) / 2;
|
||||
}
|
||||
|
||||
sprite = Sprite(resized);
|
||||
sprite.SetPosition(imageX, imageY, -100);
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 41 KiB |
1394
src/svelte-components/package-lock.json
generated
1394
src/svelte-components/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -11,18 +11,20 @@
|
||||
"check": "svelte-check --tsconfig ./tsconfig.json"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@sveltejs/kit": "^1.0.0-next.357",
|
||||
"@sveltejs/vite-plugin-svelte": "^1.0.0-next.49",
|
||||
"@sveltejs/kit": "^1.0.0-next.392",
|
||||
"@sveltejs/vite-plugin-svelte": "^1.0.1",
|
||||
"@tsconfig/svelte": "^3.0.0",
|
||||
"node-sass": "^7.0.1",
|
||||
"polyfill-object.fromentries": "^1.0.1",
|
||||
"smui-theme": "^6.0.0-beta.16",
|
||||
"svelte": "^3.48.0",
|
||||
"string.prototype.matchall": "^4.0.7",
|
||||
"svelte": "^3.49.0",
|
||||
"svelte-check": "^2.8.0",
|
||||
"svelte-material-ui": "^6.0.0-beta.16",
|
||||
"svelte-preprocess": "^4.10.7",
|
||||
"svelte-tiny-virtual-list": "^2.0.5",
|
||||
"tslib": "^2.4.0",
|
||||
"typescript": "^4.7.4",
|
||||
"vite": "^2.9.13"
|
||||
"vite": "^3.0.2"
|
||||
}
|
||||
}
|
||||
|
||||
131
src/svelte-components/src/components/ConfigTemplatedInput.svelte
Normal file
131
src/svelte-components/src/components/ConfigTemplatedInput.svelte
Normal file
@@ -0,0 +1,131 @@
|
||||
<script lang="ts">
|
||||
import configTemplate from "../../../resources/config-template.json";
|
||||
import { Config, DisplayUnits } from "$lib/ConfigStore";
|
||||
import { ControllerMethods } from "$lib/RegisterControllerMethods";
|
||||
import { onMount } from "svelte";
|
||||
|
||||
type Template = {
|
||||
type?: string;
|
||||
values?: (string | number)[];
|
||||
unit?: "string";
|
||||
iunit?: "string";
|
||||
min?: number;
|
||||
max?: number;
|
||||
step?: number;
|
||||
help?: string;
|
||||
default?: string | number;
|
||||
scale?: number;
|
||||
};
|
||||
|
||||
export let key: string;
|
||||
let keyParts: string[];
|
||||
let template: Template;
|
||||
let name: string;
|
||||
let title: string;
|
||||
let units: string;
|
||||
let value;
|
||||
|
||||
onMount(() => {
|
||||
keyParts = (key || "").split(".");
|
||||
template = getTemplate();
|
||||
name = keyParts[keyParts.length - 1];
|
||||
title = getTitle();
|
||||
value = getValue();
|
||||
});
|
||||
|
||||
$: metric = $DisplayUnits === "METRIC";
|
||||
$: if (template) {
|
||||
units = metric || !template.iunit ? template.unit : template.iunit;
|
||||
}
|
||||
|
||||
function getTemplate(): Template {
|
||||
let template = configTemplate;
|
||||
for (const part of keyParts) {
|
||||
template = template[part];
|
||||
}
|
||||
|
||||
return template as Template;
|
||||
}
|
||||
|
||||
function getTitle(): string {
|
||||
const help = template.help ? `${template.help}\n` : "";
|
||||
return `${help}Default: ${template.default} ${template.unit || ""}`;
|
||||
}
|
||||
|
||||
function getValue(): string | number {
|
||||
let value: any = $Config;
|
||||
for (const part of keyParts) {
|
||||
value = value[part];
|
||||
}
|
||||
|
||||
if (template.scale) {
|
||||
if (metric) {
|
||||
return Number.parseFloat(value.toFixed(3));
|
||||
}
|
||||
|
||||
return Number.parseFloat((value / template.scale).toFixed(4));
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
function onChange() {
|
||||
Config.update((config) => {
|
||||
let target = config;
|
||||
for (const part of keyParts.slice(0, -1)) {
|
||||
target = target[part];
|
||||
}
|
||||
|
||||
target[keyParts[keyParts.length - 1]] = value;
|
||||
|
||||
return config;
|
||||
});
|
||||
|
||||
ControllerMethods.dispatch("config-changed");
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if template}
|
||||
<div class="pure-control-group" {title}>
|
||||
<label for={name}>{name}</label>
|
||||
|
||||
{#if template.values}
|
||||
<select {name} bind:value on:change={onChange}>
|
||||
{#each template.values as opt}
|
||||
<option value={opt} disabled={opt === "-----"}>
|
||||
{opt}
|
||||
</option>
|
||||
{/each}
|
||||
</select>
|
||||
{:else if template.type === "bool"}
|
||||
<input {name} type="checkbox" bind:value on:input={onChange} />
|
||||
{:else if template.type === "float"}
|
||||
<input
|
||||
{name}
|
||||
type="number"
|
||||
min={template.min}
|
||||
max={template.max}
|
||||
step={template.step || "any"}
|
||||
bind:value
|
||||
on:input={onChange}
|
||||
/>
|
||||
{:else if template.type === "int"}
|
||||
<input
|
||||
{name}
|
||||
type="number"
|
||||
min={template.min}
|
||||
max={template.max}
|
||||
bind:value
|
||||
on:input={onChange}
|
||||
/>
|
||||
{:else if template.type === "string"}
|
||||
<input {name} type="text" bind:value on:input={onChange} />
|
||||
{:else if template.type == "text"}
|
||||
<textarea {name} bind:value on:input={onChange} />
|
||||
{/if}
|
||||
|
||||
<label for="" class="units">{units || ""}</label>
|
||||
|
||||
<slot name="extra" />
|
||||
</div>
|
||||
{/if}
|
||||
@@ -1,33 +0,0 @@
|
||||
<script lang="ts">
|
||||
import * as api from "$lib/api";
|
||||
import { onMount } from "svelte";
|
||||
|
||||
onMount(() => {
|
||||
document.body.style.backgroundColor = "black";
|
||||
const layout = document.querySelector("#layout") as HTMLElement;
|
||||
layout.style.backgroundColor = "white";
|
||||
layout.style.width = "1280px";
|
||||
layout.style.height = "720px";
|
||||
layout.style.overflowY = "scroll";
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="devmode">
|
||||
<button on:click={() => api.PUT("devmode/probe")}> Probe Contact </button>
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
.devmode {
|
||||
background-color: greenyellow;
|
||||
z-index: 20000000;
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
left: 400px;
|
||||
width: 500px;
|
||||
height: 100px;
|
||||
padding: 6px;
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-items: space-between;
|
||||
}
|
||||
</style>
|
||||
97
src/svelte-components/src/components/SettingsView.svelte
Normal file
97
src/svelte-components/src/components/SettingsView.svelte
Normal file
@@ -0,0 +1,97 @@
|
||||
<script lang="ts">
|
||||
import configTemplate from "../../../resources/config-template.json";
|
||||
import ScreenRotationDialog from "$dialogs/ScreenRotationDialog.svelte";
|
||||
import ConfigTemplatedInput from "./ConfigTemplatedInput.svelte";
|
||||
import SetTimeDialog from "$dialogs/SetTimeDialog.svelte";
|
||||
import Button, { Label } from "@smui/button";
|
||||
|
||||
const gcodeURL = "https://linuxcnc.org/docs/html/gcode/g-code.html";
|
||||
|
||||
let showScreenRotationDialog = false;
|
||||
let showSetTimeDialog = false;
|
||||
</script>
|
||||
|
||||
<ScreenRotationDialog bind:open={showScreenRotationDialog} />
|
||||
<SetTimeDialog bind:open={showSetTimeDialog} />
|
||||
|
||||
<h1>Settings</h1>
|
||||
|
||||
<div class="pure-form pure-form-aligned">
|
||||
<h2>User Interface</h2>
|
||||
<fieldset>
|
||||
<div class="pure-control-group">
|
||||
<label for="screen-rotation" />
|
||||
<Button
|
||||
name="screen-rotation"
|
||||
touch
|
||||
variant="raised"
|
||||
on:click={() => (showScreenRotationDialog = true)}
|
||||
>
|
||||
<Label>Change Screen Rotation</Label>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div class="pure-control-group">
|
||||
<label for="set-time" />
|
||||
<Button
|
||||
name="set-time"
|
||||
touch
|
||||
variant="raised"
|
||||
on:click={() => (showSetTimeDialog = true)}
|
||||
>
|
||||
<Label>Change Time & Timezone</Label>
|
||||
</Button>
|
||||
</div>
|
||||
</fieldset>
|
||||
|
||||
<fieldset>
|
||||
<h2>Probe Dimensions</h2>
|
||||
{#each Object.keys(configTemplate.probe) as key}
|
||||
{#if key !== "probe-diameter"}
|
||||
<ConfigTemplatedInput key={`probe.${key}`} />
|
||||
{/if}
|
||||
{/each}
|
||||
</fieldset>
|
||||
|
||||
<fieldset>
|
||||
<h2>GCode</h2>
|
||||
{#each Object.keys(configTemplate.gcode) as key}
|
||||
<ConfigTemplatedInput key={`gcode.${key}`} />
|
||||
{/each}
|
||||
</fieldset>
|
||||
|
||||
<h2>Path Accuracy</h2>
|
||||
<fieldset>
|
||||
<ConfigTemplatedInput key={`settings.max-deviation`} />
|
||||
</fieldset>
|
||||
|
||||
<p>
|
||||
Lower <tt>max-deviation</tt> to follow the programmed path more precisely but
|
||||
at a slower speed.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
In order to improve traversal speed, the path planner may merge consecutive
|
||||
moves or round off sharp corners if doing so would deviate from the program
|
||||
path by less than <tt>max-deviation</tt>.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
GCode commands
|
||||
<a href={`${gcodeURL}#gcode:g61`} target="_blank">G61, G61.1</a>
|
||||
and <a href={`${gcodeURL}#gcode:g64`} target="_blank"> G64</a> also affect path
|
||||
planning accuracy.
|
||||
</p>
|
||||
|
||||
<h2>Cornering Speed (Advanced)</h2>
|
||||
<fieldset>
|
||||
<ConfigTemplatedInput key={`settings.junction-accel`} />
|
||||
</fieldset>
|
||||
|
||||
<p>
|
||||
Junction acceleration limits the cornering speed the planner will allow.
|
||||
Increasing this value will allow for faster traversal of corners but may
|
||||
cause the planner to violate axis jerk limits and stall the motors. Use with
|
||||
caution.
|
||||
</p>
|
||||
</div>
|
||||
@@ -3,6 +3,10 @@
|
||||
import HomeMachineDialog from "$dialogs/HomeMachineDialog.svelte";
|
||||
import ProbeDialog from "$dialogs/ProbeDialog.svelte";
|
||||
import ScreenRotationDialog from "$dialogs/ScreenRotationDialog.svelte";
|
||||
import UploadDialog from "$dialogs/UploadDialog.svelte";
|
||||
import SetTimeDialog from "./SetTimeDialog.svelte";
|
||||
import ManualHomeAxisDialog from "./ManualHomeAxisDialog.svelte";
|
||||
import SetAxisPositionDialog from "./SetAxisPositionDialog.svelte";
|
||||
|
||||
const HomeMachineDialogProps = writable<HomeMachineDialogPropsType>();
|
||||
type HomeMachineDialogPropsType = {
|
||||
@@ -16,11 +20,35 @@
|
||||
probeType: "xyz" | "z";
|
||||
};
|
||||
|
||||
const ScreenRotationDialogProps = writable<ProbeDialogPropsType>();
|
||||
const ScreenRotationDialogProps = writable<ScreenRotationDialogPropsType>();
|
||||
type ScreenRotationDialogPropsType = {
|
||||
open: boolean;
|
||||
};
|
||||
|
||||
const UploadDialogProps = writable<UploadDialogPropsType>();
|
||||
type UploadDialogPropsType = {
|
||||
open: boolean;
|
||||
file: File;
|
||||
onComplete: () => void;
|
||||
};
|
||||
|
||||
const SetTimeDialogProps = writable<SetTimeDialogPropsType>();
|
||||
type SetTimeDialogPropsType = {
|
||||
open: boolean;
|
||||
};
|
||||
|
||||
const ManualHomeAxisDialogProps = writable<ManualHomeAxisDialogPropsType>();
|
||||
type ManualHomeAxisDialogPropsType = {
|
||||
open: boolean;
|
||||
axis: string;
|
||||
};
|
||||
|
||||
const SetAxisPositionDialogProps = writable<SetAxisPositionDialogPropsType>();
|
||||
type SetAxisPositionDialogPropsType = {
|
||||
open: boolean;
|
||||
axis: string;
|
||||
};
|
||||
|
||||
export function showDialog(
|
||||
dialog: "HomeMachine",
|
||||
props: Omit<HomeMachineDialogPropsType, "open">
|
||||
@@ -31,6 +59,31 @@
|
||||
props: Omit<ProbeDialogPropsType, "open">
|
||||
);
|
||||
|
||||
export function showDialog(
|
||||
dialog: "ScreenRotation",
|
||||
props: Omit<ScreenRotationDialogPropsType, "open">
|
||||
);
|
||||
|
||||
export function showDialog(
|
||||
dialog: "Upload",
|
||||
props: Omit<UploadDialogPropsType, "open">
|
||||
);
|
||||
|
||||
export function showDialog(
|
||||
dialog: "SetTime",
|
||||
props: Omit<SetTimeDialogPropsType, "open">
|
||||
);
|
||||
|
||||
export function showDialog(
|
||||
dialog: "ManualHomeAxis",
|
||||
props: Omit<ManualHomeAxisDialogPropsType, "open">
|
||||
);
|
||||
|
||||
export function showDialog(
|
||||
dialog: "SetAxisPosition",
|
||||
props: Omit<SetAxisPositionDialogPropsType, "open">
|
||||
);
|
||||
|
||||
export function showDialog(dialog: string, props: any) {
|
||||
switch (dialog) {
|
||||
case "HomeMachine":
|
||||
@@ -45,6 +98,22 @@
|
||||
ScreenRotationDialogProps.set({ ...props, open: true });
|
||||
break;
|
||||
|
||||
case "Upload":
|
||||
UploadDialogProps.set({ ...props, open: true });
|
||||
break;
|
||||
|
||||
case "SetTime":
|
||||
SetTimeDialogProps.set({ ...props, open: true });
|
||||
break;
|
||||
|
||||
case "ManualHomeAxis":
|
||||
ManualHomeAxisDialogProps.set({ ...props, open: true });
|
||||
break;
|
||||
|
||||
case "SetAxisPosition":
|
||||
SetAxisPositionDialogProps.set({ ...props, open: true });
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new Error(`Unknown dialog '${dialog}`);
|
||||
}
|
||||
@@ -54,3 +123,7 @@
|
||||
<HomeMachineDialog {...$HomeMachineDialogProps} />
|
||||
<ProbeDialog {...$ProbeDialogProps} />
|
||||
<ScreenRotationDialog {...$ScreenRotationDialogProps} />
|
||||
<UploadDialog {...$UploadDialogProps} />
|
||||
<SetTimeDialog {...$SetTimeDialogProps} />
|
||||
<ManualHomeAxisDialog {...$ManualHomeAxisDialogProps} />
|
||||
<SetAxisPositionDialog {...$SetAxisPositionDialogProps} />
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
|
||||
<Dialog
|
||||
bind:open
|
||||
scrimClickAction=""
|
||||
aria-labelledby="home-machine-dialog-title"
|
||||
aria-describedby="home-machine-dialog-content"
|
||||
>
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
<script lang="ts">
|
||||
import Dialog, { Title, Content, Actions } from "@smui/dialog";
|
||||
import TextField from "@smui/textfield";
|
||||
import Button, { Label } from "@smui/button";
|
||||
import { ControllerMethods } from "$lib/RegisterControllerMethods";
|
||||
|
||||
export let open: boolean;
|
||||
export let axis = "";
|
||||
|
||||
let value = 0;
|
||||
|
||||
function onConfirm() {
|
||||
ControllerMethods.set_home(axis, value);
|
||||
}
|
||||
</script>
|
||||
|
||||
<Dialog
|
||||
bind:open
|
||||
scrimClickAction=""
|
||||
aria-labelledby="manual-home-axis-dialog-title"
|
||||
aria-describedby="manual-home-axis-dialog-content"
|
||||
>
|
||||
<Title id="manual-home-axis-dialog-title"
|
||||
>Manually Home {axis.toUpperCase()} Axis</Title
|
||||
>
|
||||
<Content id="manual-home-axis-dialog-content">
|
||||
<p>Set axis absolute position</p>
|
||||
|
||||
<TextField
|
||||
label="Absolute"
|
||||
type="number"
|
||||
bind:value
|
||||
variant="filled"
|
||||
style="width: 100%;"
|
||||
/>
|
||||
</Content>
|
||||
|
||||
<Actions>
|
||||
<Button>
|
||||
<Label>Cancel</Label>
|
||||
</Button>
|
||||
<Button defaultAction on:click={onConfirm}>
|
||||
<Label>Set</Label>
|
||||
</Button>
|
||||
</Actions>
|
||||
</Dialog>
|
||||
@@ -1,5 +1,18 @@
|
||||
<script type="ts" context="module">
|
||||
import { get, writable, type Writable } from "svelte/store";
|
||||
<script type="ts">
|
||||
import DimensionInput from "$components/DimensionInput.svelte";
|
||||
import Dialog, { Title, Content, Actions } from "@smui/dialog";
|
||||
import Button, { Label } from "@smui/button";
|
||||
import { waitForChange } from "$lib/StoreHelpers";
|
||||
import { ControllerMethods } from "$lib/RegisterControllerMethods";
|
||||
import { Config } from "$lib/ConfigStore";
|
||||
import { writable, type Writable } from "svelte/store";
|
||||
import {
|
||||
probingActive,
|
||||
probeContacted,
|
||||
probingComplete,
|
||||
probingFailed,
|
||||
probingStarted,
|
||||
} from "$lib/ControllerState";
|
||||
|
||||
type Step =
|
||||
| "None"
|
||||
@@ -19,49 +32,8 @@
|
||||
};
|
||||
|
||||
const cancelled = writable(false);
|
||||
const probingActive = writable(false);
|
||||
const probeContacted = writable(false);
|
||||
const probingStarted = writable(false);
|
||||
const probingFailed = writable(false);
|
||||
const probingComplete = writable(false);
|
||||
const userAcknowledged = writable(false);
|
||||
|
||||
export function handleControllerStateUpdate(state: Record<string, any>) {
|
||||
if (!get(probingActive)) {
|
||||
return;
|
||||
}
|
||||
|
||||
switch (true) {
|
||||
case state.pw === 0:
|
||||
probeContacted.set(true);
|
||||
break;
|
||||
|
||||
case state.log?.msg === "Switch not found":
|
||||
probingFailed.set(true);
|
||||
break;
|
||||
|
||||
case state.cycle !== "idle":
|
||||
probingStarted.set(true);
|
||||
break;
|
||||
|
||||
case state.cycle === "idle":
|
||||
if (get(probingStarted)) {
|
||||
probingStarted.set(false);
|
||||
probingComplete.set(true);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<script type="ts">
|
||||
import DimensionInput from "$components/DimensionInput.svelte";
|
||||
import Dialog, { Title, Content, Actions } from "@smui/dialog";
|
||||
import Button, { Label } from "@smui/button";
|
||||
import { waitForChange } from "$lib/StoreHelpers";
|
||||
import { ControllerMethods } from "$lib/RegisterControllerMethods";
|
||||
import { Config } from "$lib/ConfigStore";
|
||||
|
||||
const cutterDiameterOptions = [
|
||||
{ value: 0.5, label: '1/2 "', metric: false },
|
||||
{ value: 0.25, label: '1/4 "', metric: false },
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
<script lang="ts">
|
||||
import Dialog, { Title, Content, Actions } from "@smui/dialog";
|
||||
import TextField from "@smui/textfield";
|
||||
import Button, { Label } from "@smui/button";
|
||||
import { ControllerMethods } from "$lib/RegisterControllerMethods";
|
||||
|
||||
export let open: boolean;
|
||||
export let axis = "";
|
||||
|
||||
let value = 0;
|
||||
let homed = false;
|
||||
let wasOpen = false;
|
||||
|
||||
$: if (open != wasOpen) {
|
||||
if (open) {
|
||||
homed = ControllerMethods.isAxisHomed(axis);
|
||||
}
|
||||
|
||||
wasOpen = open;
|
||||
}
|
||||
|
||||
function onUnhome() {
|
||||
ControllerMethods.unhome(axis);
|
||||
}
|
||||
|
||||
function onConfirm() {
|
||||
ControllerMethods.set_position(axis, value);
|
||||
}
|
||||
</script>
|
||||
|
||||
<Dialog
|
||||
bind:open
|
||||
scrimClickAction=""
|
||||
aria-labelledby="set-axis-position-dialog-title"
|
||||
aria-describedby="set-axis-position-dialog-content"
|
||||
>
|
||||
<Title id="set-axis-position-dialog-title"
|
||||
>Set {axis.toUpperCase()} Axis Position</Title
|
||||
>
|
||||
<Content id="set-axis-position-dialog-content">
|
||||
<TextField
|
||||
label="Position"
|
||||
type="number"
|
||||
bind:value
|
||||
spellcheck="false"
|
||||
variant="filled"
|
||||
style="width: 100%;"
|
||||
/>
|
||||
</Content>
|
||||
|
||||
<Actions>
|
||||
<Button>
|
||||
<Label>Cancel</Label>
|
||||
</Button>
|
||||
{#if homed}
|
||||
<Button on:click={onUnhome}>
|
||||
<Label>Unhome</Label>
|
||||
</Button>
|
||||
{/if}
|
||||
<Button defaultAction on:click={onConfirm}>
|
||||
<Label>Set</Label>
|
||||
</Button>
|
||||
</Actions>
|
||||
</Dialog>
|
||||
240
src/svelte-components/src/dialogs/SetTimeDialog.svelte
Normal file
240
src/svelte-components/src/dialogs/SetTimeDialog.svelte
Normal file
@@ -0,0 +1,240 @@
|
||||
<script lang="ts">
|
||||
import Dialog, { Title, Content, Actions } from "@smui/dialog";
|
||||
import Button, { Label } from "@smui/button";
|
||||
import TextField from "@smui/textfield";
|
||||
import CircularProgress from "@smui/circular-progress";
|
||||
import VirtualList from "svelte-tiny-virtual-list";
|
||||
import * as api from "$lib/api";
|
||||
|
||||
const itemHeight = 35;
|
||||
|
||||
type Timezone = {
|
||||
label: string;
|
||||
value: string;
|
||||
};
|
||||
|
||||
export let open = false;
|
||||
let value = "";
|
||||
let wasOpen = false;
|
||||
let loading = true;
|
||||
let timezones: Timezone[] = [];
|
||||
let currentTimezoneIndex: number;
|
||||
let selectedTimezoneIndex: number;
|
||||
let networkTimeSynchronized: boolean;
|
||||
|
||||
$: if (open != wasOpen) {
|
||||
if (!wasOpen) {
|
||||
loadData();
|
||||
}
|
||||
|
||||
wasOpen = open;
|
||||
}
|
||||
|
||||
async function loadData() {
|
||||
loading = true;
|
||||
|
||||
const result = await api.GET("time");
|
||||
|
||||
parseTimezones(result.timezones);
|
||||
parseTimeinfo(result.timeinfo);
|
||||
value = getDateTimeValueString();
|
||||
|
||||
loading = false;
|
||||
}
|
||||
|
||||
function parseTimeinfo(str: string) {
|
||||
const matches = Array.from(str.matchAll(/\s*([^:]+):\s+(.+)/gm));
|
||||
|
||||
let currentTimezoneValue;
|
||||
for (const match of matches) {
|
||||
let [, label, value] = match;
|
||||
|
||||
switch (label) {
|
||||
case "Time zone":
|
||||
currentTimezoneValue = value.split(" ")[0];
|
||||
break;
|
||||
|
||||
case "NTP synchronized":
|
||||
networkTimeSynchronized = value === "yes";
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
currentTimezoneIndex = timezones.findIndex(
|
||||
(tz) => tz.value === currentTimezoneValue
|
||||
);
|
||||
selectedTimezoneIndex = currentTimezoneIndex;
|
||||
}
|
||||
|
||||
function parseTimezones(str: string) {
|
||||
const matches = Array.from(str.matchAll(/\s*(\S+)\s*/gm));
|
||||
|
||||
timezones = [];
|
||||
for (let [, value] of matches) {
|
||||
timezones.push({
|
||||
label: value.replace(/_/g, " "),
|
||||
value,
|
||||
});
|
||||
}
|
||||
|
||||
// Sort alphabetically, but with the current timezone at the top of the list
|
||||
timezones.sort((a, b) => {
|
||||
switch (true) {
|
||||
case a.value === "UTC":
|
||||
return -1;
|
||||
|
||||
case b.value === "UTC":
|
||||
return 1;
|
||||
|
||||
default:
|
||||
return a.value.localeCompare(b.value);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function getDateTimeValueString() {
|
||||
const date = new Date();
|
||||
|
||||
const year = date.getFullYear().toString().padStart(2, "0");
|
||||
const month = (date.getMonth() + 1).toString().padStart(2, "0");
|
||||
const day = date.getDate().toString().padStart(2, "0");
|
||||
const hour = date.getHours().toString().padStart(2, "0");
|
||||
const minute = date.getMinutes().toString().padStart(2, "0");
|
||||
|
||||
return `${year}-${month}-${day}T${hour}:${minute}:00`;
|
||||
}
|
||||
|
||||
async function onConfirm() {
|
||||
const date = new Date(value);
|
||||
const year = date.getFullYear().toString().padStart(2, "0");
|
||||
const month = (date.getMonth() + 1).toString().padStart(2, "0");
|
||||
const day = date.getDate().toString().padStart(2, "0");
|
||||
const hour = date.getHours().toString().padStart(2, "0");
|
||||
const minute = date.getMinutes().toString().padStart(2, "0");
|
||||
|
||||
await api.PUT("time", {
|
||||
datetime: `${year}-${month}-${day} ${hour}:${minute}:00`,
|
||||
timezone: timezones[selectedTimezoneIndex].value,
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<Dialog
|
||||
bind:open
|
||||
scrimClickAction=""
|
||||
aria-labelledby="set-time-dialog-title"
|
||||
aria-describedby="set-time-dialog-content"
|
||||
>
|
||||
<Title id="set-time-dialog-title">Adjust Clock & Timezone</Title>
|
||||
|
||||
<Content id="set-time-dialog-content">
|
||||
{#if loading}
|
||||
<div style="display: flex; justify-content: center">
|
||||
<CircularProgress style="height: 32px; width: 32px;" indeterminate />
|
||||
</div>
|
||||
{:else}
|
||||
{#if networkTimeSynchronized}
|
||||
<p>
|
||||
Because this controller is connected to the internet, the time is
|
||||
managed automatically, and cannot be manually set.
|
||||
</p>
|
||||
{:else}
|
||||
<p>
|
||||
Because this controller is not connected to the internet, you can
|
||||
manually set the time.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Note: any time the controller is turned off, the time will need to be
|
||||
reset. If you connect the controller to the internet, the time will be
|
||||
managed automatically.
|
||||
</p>
|
||||
|
||||
<Label>Date & Time</Label>
|
||||
<TextField
|
||||
bind:value
|
||||
label="Time"
|
||||
type="datetime-local"
|
||||
variant="filled"
|
||||
style="width: 100%;"
|
||||
/>
|
||||
{/if}
|
||||
|
||||
<p>
|
||||
To display your local time correctly, the controller must know what
|
||||
timezone it is in.
|
||||
</p>
|
||||
|
||||
<div class="timezones-container" style="margin-top: 20px;">
|
||||
<Label>Select your timezone</Label>
|
||||
<VirtualList
|
||||
width="100%"
|
||||
height={itemHeight * 6}
|
||||
itemCount={timezones.length}
|
||||
itemSize={itemHeight}
|
||||
scrollToIndex={currentTimezoneIndex}
|
||||
scrollToAlignment="center"
|
||||
>
|
||||
<div
|
||||
slot="item"
|
||||
let:index
|
||||
let:style
|
||||
{style}
|
||||
class="timezone"
|
||||
class:selected={index === selectedTimezoneIndex}
|
||||
on:click={() => (selectedTimezoneIndex = index)}
|
||||
>
|
||||
{timezones[index].label}
|
||||
</div>
|
||||
</VirtualList>
|
||||
</div>
|
||||
{/if}
|
||||
</Content>
|
||||
|
||||
<Actions>
|
||||
<Button>
|
||||
<Label>Cancel</Label>
|
||||
</Button>
|
||||
<Button
|
||||
defaultAction
|
||||
disabled={selectedTimezoneIndex === -1}
|
||||
on:click={onConfirm}
|
||||
>
|
||||
<Label>Confirm</Label>
|
||||
</Button>
|
||||
</Actions>
|
||||
</Dialog>
|
||||
|
||||
<style lang="scss">
|
||||
@use "sass:color";
|
||||
|
||||
$primary: #0078e7;
|
||||
$very-dark: #555;
|
||||
$text: #777;
|
||||
$grey: #bbb;
|
||||
$light: #ddd;
|
||||
|
||||
.timezones-container {
|
||||
:global {
|
||||
.virtual-list-wrapper {
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 3px;
|
||||
overflow-x: hidden;
|
||||
overflow-y: scroll;
|
||||
}
|
||||
}
|
||||
.timezone {
|
||||
font-size: 14px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin: 0;
|
||||
padding-left: 10px;
|
||||
|
||||
&.selected {
|
||||
color: $primary;
|
||||
background-color: color.adjust($primary, $lightness: 50%);
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
78
src/svelte-components/src/dialogs/UploadDialog.svelte
Normal file
78
src/svelte-components/src/dialogs/UploadDialog.svelte
Normal file
@@ -0,0 +1,78 @@
|
||||
<script lang="ts">
|
||||
import Dialog, { Title, Content, Actions } from "@smui/dialog";
|
||||
import Button, { Label } from "@smui/button";
|
||||
import LinearProgress from "@smui/linear-progress";
|
||||
|
||||
export let open = false;
|
||||
export let file: File;
|
||||
export let onComplete: () => void;
|
||||
|
||||
let wasOpen = false;
|
||||
let xhr;
|
||||
let progress;
|
||||
|
||||
$: if (open != wasOpen) {
|
||||
if (!wasOpen) {
|
||||
beginUpload();
|
||||
}
|
||||
|
||||
wasOpen = open;
|
||||
}
|
||||
|
||||
$: if (!open) {
|
||||
xhr = undefined;
|
||||
}
|
||||
|
||||
async function beginUpload() {
|
||||
progress = 0;
|
||||
|
||||
xhr = new XMLHttpRequest();
|
||||
xhr.upload.onload = () => {
|
||||
open = false;
|
||||
if (onComplete) {
|
||||
onComplete();
|
||||
}
|
||||
};
|
||||
|
||||
xhr.upload.onerror = () => {
|
||||
open = false;
|
||||
alert("Upload failed.");
|
||||
};
|
||||
|
||||
xhr.upload.onabort = () => {
|
||||
open = false;
|
||||
};
|
||||
|
||||
xhr.upload.onprogress = (event) => {
|
||||
progress = event.loaded / event.total;
|
||||
};
|
||||
|
||||
xhr.open("PUT", `/api/file/${encodeURIComponent(file.name)}`);
|
||||
xhr.send(file);
|
||||
}
|
||||
|
||||
function onCancel() {
|
||||
xhr.abort();
|
||||
}
|
||||
</script>
|
||||
|
||||
<Dialog
|
||||
bind:open
|
||||
scrimClickAction=""
|
||||
aria-labelledby="upload-dialog-title"
|
||||
aria-describedby="upload-dialog-content"
|
||||
>
|
||||
<Title id="upload-dialog-title">
|
||||
Uploading {#if file}{file.name}...{/if}
|
||||
</Title>
|
||||
|
||||
<Content id="upload-dialog-content">
|
||||
<LinearProgress {progress} />
|
||||
</Content>
|
||||
|
||||
<Actions>
|
||||
<Button on:click={onCancel}>
|
||||
<Label>Cancel</Label>
|
||||
</Button>
|
||||
</Actions>
|
||||
</Dialog>
|
||||
@@ -1,7 +1,14 @@
|
||||
import { writable } from "svelte/store";
|
||||
|
||||
type DisplayUnits = "METRIC" | "IMPERIAL";
|
||||
|
||||
export const Config = writable<Record<string, any>>({});
|
||||
export const DisplayUnits = writable<DisplayUnits>();
|
||||
|
||||
export function handleConfigUpdate(config: Record<string, any>) {
|
||||
Config.set(config);
|
||||
}
|
||||
|
||||
export function setDisplayUnits(value: DisplayUnits) {
|
||||
DisplayUnits.set(value);
|
||||
}
|
||||
36
src/svelte-components/src/lib/ControllerState.ts
Normal file
36
src/svelte-components/src/lib/ControllerState.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
import { get, writable } from "svelte/store";
|
||||
import { processNetworkInfo } from "./NetworkInfo";
|
||||
|
||||
export const networkInfo = writable({});
|
||||
|
||||
export const probingActive = writable(false);
|
||||
export const probeContacted = writable(false);
|
||||
export const probingStarted = writable(false);
|
||||
export const probingFailed = writable(false);
|
||||
export const probingComplete = writable(false);
|
||||
|
||||
export function handleControllerStateUpdate(state: Record<string, any>) {
|
||||
if (state.networkInfo) {
|
||||
processNetworkInfo(state.networkInfo);
|
||||
delete state.networkInfo;
|
||||
}
|
||||
|
||||
if (get(probingActive)) {
|
||||
if (state.pw === 0) {
|
||||
probeContacted.set(true);
|
||||
}
|
||||
|
||||
if (state.log?.msg === "Switch not found") {
|
||||
probingFailed.set(true);
|
||||
}
|
||||
|
||||
if (state.cycle !== "idle") {
|
||||
probingStarted.set(true);
|
||||
}
|
||||
|
||||
if (state.cycle === "idle" && get(probingStarted)) {
|
||||
probingStarted.set(false);
|
||||
probingComplete.set(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,4 @@
|
||||
import { readable } from "svelte/store";
|
||||
import * as api from "$lib/api";
|
||||
import { writable } from "svelte/store";
|
||||
|
||||
export type WifiNetwork = {
|
||||
Quality: string;
|
||||
@@ -34,60 +33,43 @@ const empty: NetworkInfo = {
|
||||
}
|
||||
}
|
||||
|
||||
export const networkInfo = readable<NetworkInfo>(empty, (set) => {
|
||||
getNetworkInfo();
|
||||
const networkInfoIntervalId = setInterval(getNetworkInfo, 5000);
|
||||
export const networkInfo = writable<NetworkInfo>(empty);
|
||||
|
||||
async function getNetworkInfo() {
|
||||
const networksByName: Record<string, WifiNetwork> = {}
|
||||
export function processNetworkInfo(rawNetworkInfo: NetworkInfo) {
|
||||
const now = Date.now();
|
||||
const networksByName: Record<string, WifiNetwork> = {}
|
||||
|
||||
try {
|
||||
const networkInfo: NetworkInfo = await api.GET("network");
|
||||
|
||||
const now = Date.now();
|
||||
for (let network of networkInfo.wifi.networks) {
|
||||
if (network.Name) {
|
||||
network.lastSeen = now;
|
||||
network.active = networkInfo.wifi.ssid === network.Name;
|
||||
networksByName[network.Name] = network;
|
||||
}
|
||||
}
|
||||
|
||||
for (let network of Object.values(networksByName)) {
|
||||
if (network.lastSeen - now > 30000) {
|
||||
delete networksByName[network.Name];
|
||||
}
|
||||
}
|
||||
|
||||
set({
|
||||
ipAddresses: networkInfo.ipAddresses,
|
||||
hostname: networkInfo.hostname,
|
||||
wifi: {
|
||||
ssid: networkInfo.wifi.ssid,
|
||||
networks: Object.values(networksByName).sort((a, b) => {
|
||||
switch (true) {
|
||||
case a.active:
|
||||
return -1;
|
||||
|
||||
case b.active:
|
||||
return 1;
|
||||
|
||||
default:
|
||||
return a.Name.localeCompare(b.Name);
|
||||
}
|
||||
})
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.debug("Failed to fetch network info", error);
|
||||
for (let network of rawNetworkInfo.wifi.networks) {
|
||||
if (network.Name) {
|
||||
network.lastSeen = now;
|
||||
network.active = rawNetworkInfo.wifi.ssid === network.Name;
|
||||
networksByName[network.Name] = network;
|
||||
}
|
||||
}
|
||||
|
||||
return () => {
|
||||
clearInterval(networkInfoIntervalId);
|
||||
for (let network of Object.values(networksByName)) {
|
||||
if (network.lastSeen - now > 30000) {
|
||||
delete networksByName[network.Name];
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
export function init() {
|
||||
return networkInfo.subscribe(() => ({}));
|
||||
networkInfo.set({
|
||||
ipAddresses: rawNetworkInfo.ipAddresses,
|
||||
hostname: rawNetworkInfo.hostname,
|
||||
wifi: {
|
||||
ssid: rawNetworkInfo.wifi.ssid,
|
||||
networks: Object.values(networksByName).sort((a, b) => {
|
||||
switch (true) {
|
||||
case a.active:
|
||||
return -1;
|
||||
|
||||
case b.active:
|
||||
return 1;
|
||||
|
||||
default:
|
||||
return a.Name.localeCompare(b.Name);
|
||||
}
|
||||
})
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -2,10 +2,18 @@ type ControllerMethods = {
|
||||
stop: () => void;
|
||||
send: (gcode: string) => void;
|
||||
goto_zero: (x: number, y: number, z: number, a: number) => void;
|
||||
dispatch: (event: string, ...args: any[]) => void;
|
||||
isAxisHomed: (axis: string) => boolean;
|
||||
unhome: (axis: string) => void;
|
||||
set_position: (axis: string, value: number) => void;
|
||||
set_home: (axis: string, value: number) => void;
|
||||
}
|
||||
|
||||
export let ControllerMethods: ControllerMethods;
|
||||
|
||||
export function registerControllerMethods(methods: ControllerMethods) {
|
||||
ControllerMethods = methods;
|
||||
export function registerControllerMethods(methods: Partial<ControllerMethods>) {
|
||||
ControllerMethods = {
|
||||
...ControllerMethods,
|
||||
...methods
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
import 'polyfill-object.fromentries';
|
||||
import matchAll from "string.prototype.matchall";
|
||||
|
||||
matchAll.shim();
|
||||
|
||||
import AdminNetworkView from '$components/AdminNetworkView.svelte';
|
||||
import SettingsView from '$components/SettingsView.svelte';
|
||||
import DialogHost, { showDialog } from "$dialogs/DialogHost.svelte";
|
||||
import Devmode from "$components/Devmode.svelte";
|
||||
import { handleConfigUpdate } from '$lib/ConfigStore';
|
||||
import { init as initNetworkInfo } from '$lib/NetworkInfo';
|
||||
import { handleControllerStateUpdate } from "$dialogs/ProbeDialog.svelte";
|
||||
import { handleConfigUpdate, setDisplayUnits } from '$lib/ConfigStore';
|
||||
import { handleControllerStateUpdate } from "$lib/ControllerState";
|
||||
import { registerControllerMethods } from "$lib/RegisterControllerMethods";
|
||||
|
||||
export function createComponent(component: string, target: HTMLElement, props: Record<string, any>) {
|
||||
@@ -13,21 +15,21 @@ export function createComponent(component: string, target: HTMLElement, props: R
|
||||
case "AdminNetworkView":
|
||||
return new AdminNetworkView({ target, props });
|
||||
|
||||
case "SettingsView":
|
||||
return new SettingsView({ target, props });
|
||||
|
||||
case "DialogHost":
|
||||
return new DialogHost({ target, props });
|
||||
|
||||
case "Devmode":
|
||||
return new Devmode({target, props});
|
||||
|
||||
default:
|
||||
throw new Error("Unknown component");
|
||||
}
|
||||
}
|
||||
|
||||
export {
|
||||
initNetworkInfo,
|
||||
showDialog,
|
||||
handleControllerStateUpdate,
|
||||
handleConfigUpdate,
|
||||
registerControllerMethods,
|
||||
setDisplayUnits
|
||||
};
|
||||
|
||||
@@ -15,6 +15,7 @@ export default defineConfig({
|
||||
}
|
||||
},
|
||||
build: {
|
||||
minify: false,
|
||||
target: "chrome60",
|
||||
lib: {
|
||||
entry: resolve(__dirname, 'src/main.ts'),
|
||||
|
||||
Reference in New Issue
Block a user