From: Joseph Coffland Date: Wed, 20 Feb 2019 23:58:50 +0000 (-0800) Subject: Demo mode, Right click to enable camera crosshair. X-Git-Url: https://git.buildbotics.com/?a=commitdiff_plain;h=038120ee4ca772735b93018ff04b677724e452f6;p=bbctrl-firmware Demo mode, Right click to enable camera crosshair. --- diff --git a/CHANGELOG.md b/CHANGELOG.md index c872b1f..8325c6e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,9 @@ Buildbotics CNC Controller Firmware Changelog ## v0.4.6 - Fixed a rare ``Negative s-curve time`` error. + - Don't allow manual axis homing when soft limits are not set. + - Right click to enable camera crosshair. + - Demo mode. ## v0.4.5 - Fix for random errors while running VFD. diff --git a/scripts/buildbotics.gc b/scripts/buildbotics.gc deleted file mode 100644 index 8c5ddb0..0000000 --- a/scripts/buildbotics.gc +++ /dev/null @@ -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 diff --git a/scripts/install.sh b/scripts/install.sh index 11f64d3..49965e8 100755 --- a/scripts/install.sh +++ b/scripts/install.sh @@ -118,12 +118,6 @@ cp scripts/xinitrc ~pi/.xinitrc chmod +x ~pi/.xinitrc chown pi:pi ~pi/.xinitrc -# Install default GCode -if [ ! -d /var/lib/bbctrl/upload -o -z "$(ls -A /var/lib/bbctrl/upload)" ]; then - mkdir -p /var/lib/bbctrl/upload/ - cp scripts/buildbotics.gc /var/lib/bbctrl/upload/ -fi - # Install rc.local cp scripts/rc.local /etc/ diff --git a/src/avr/emu/src/emu.c b/src/avr/emu/src/emu.c index c420758..a0b06a5 100644 --- a/src/avr/emu/src/emu.c +++ b/src/avr/emu/src/emu.c @@ -83,6 +83,9 @@ void emu_init() { // So usart_flush() returns SERIAL_PORT.STATUS = USART_DREIF_bm | USART_TXCIF_bm; + // Clear motor fault + PIN_PORT(MOTOR_FAULT_PIN)->IN |= PIN_BM(MOTOR_FAULT_PIN); + FD_ZERO(&readFDs); } diff --git a/src/js/admin-general-view.js b/src/js/admin-general-view.js index 2d04bbc..6c5d5fc 100644 --- a/src/js/admin-general-view.js +++ b/src/js/admin-general-view.js @@ -82,7 +82,7 @@ module.exports = { try { config = JSON.parse(e.target.result); } catch (ex) { - alert("Invalid config file"); + api.alert("Invalid config file"); return; } @@ -91,7 +91,7 @@ module.exports = { this.configRestored = true; }.bind(this)).fail(function (error) { - alert('Restore failed: ' + error); + api.alert('Restore failed', error); }) }.bind(this); @@ -106,7 +106,7 @@ module.exports = { this.configReset = true; }.bind(this)).fail(function (error) { - alert('Reset failed: ' + error); + api.alert('Reset failed', error); }); }, diff --git a/src/js/admin-network-view.js b/src/js/admin-network-view.js index 2572a15..4268d35 100644 --- a/src/js/admin-network-view.js +++ b/src/js/admin-network-view.js @@ -100,7 +100,7 @@ module.exports = { }.bind(this)); }.bind(this)).fail(function (error) { - alert('Set hostname failed: ' + JSON.stringify(error)); + api.alert('Set hostname failed', error); }) }, @@ -109,19 +109,19 @@ module.exports = { api.put('remote/username', {username: this.username}).done(function () { this.usernameSet = true; }.bind(this)).fail(function (error) { - alert('Set username failed: ' + JSON.stringify(error)); + api.alert('Set username failed', error); }) }, set_password: function () { if (this.password != this.password2) { - alert('Passwords to not match'); + api.alert('Passwords to not match'); return; } if (this.password.length < 6) { - alert('Password too short'); + api.alert('Password too short'); return; } @@ -131,13 +131,14 @@ module.exports = { }).done(function () { this.passwordSet = true; }.bind(this)).fail(function (error) { - alert('Set password failed: ' + JSON.stringify(error)); + api.alert('Set password failed', error); }) }, config_wifi: function () { this.wifiConfirm = false; + this.rebooting = true; var config = { @@ -148,8 +149,9 @@ module.exports = { } api.put('wifi', config).fail(function (error) { - alert('Failed to configure WiFi: ' + JSON.stringify(error)); - }) + api.alert('Failed to configure WiFi', error); + this.rebooting = false; + }.bind(this)) } } } diff --git a/src/js/api.js b/src/js/api.js index 251d546..8c07a02 100644 --- a/src/js/api.js +++ b/src/js/api.js @@ -89,5 +89,16 @@ module.exports = { 'delete': function (url, config) { return api_cb('DELETE', url, undefined, config); + }, + + + alert: function (msg, error) { + if (typeof error != 'undefined') { + if (typeof error.message != 'undefined') + msg += '\n' + error.message; + else msg += '\n' + JSON.stringify(error); + } + + alert(msg); } } diff --git a/src/js/app.js b/src/js/app.js index 8ba50dc..aeb185f 100644 --- a/src/js/app.js +++ b/src/js/app.js @@ -106,6 +106,7 @@ module.exports = new Vue({ state: {}, messages: [], video_size: cookie.get('video-size', 'small'), + crosshair: cookie.get('crosshair', false), errorTimeout: 30, errorTimeoutStart: 0, errorShow: false, @@ -218,13 +219,20 @@ module.exports = new Vue({ }, - toggle_video: function () { + toggle_video: function (e) { if (this.video_size == 'small') this.video_size = 'large'; else if (this.video_size == 'large') this.video_size = 'small'; cookie.set('video-size', this.video_size); }, + toggle_crosshair: function (e) { + e.preventDefault(); + this.crosshair = !this.crosshair; + cookie.set('crosshair', this.crosshair); + }, + + estop: function () { if (this.state.xx == 'ESTOPPED') api.put('clear'); else api.put('estop'); @@ -238,7 +246,7 @@ module.exports = new Vue({ this.firmwareUpgrading = true; }.bind(this)).fail(function () { - alert('Invalid password'); + api.alert('Invalid password'); }.bind(this)) }, @@ -262,7 +270,7 @@ module.exports = new Vue({ this.firmwareUpgrading = true; }.bind(this)).error(function () { - alert('Invalid password or bad firmware'); + api.alert('Invalid password or bad firmware'); }.bind(this)) }, @@ -356,7 +364,7 @@ module.exports = new Vue({ api.put('config/save', this.config).done(function (data) { this.modified = false; }.bind(this)).fail(function (error) { - alert('Save failed: ' + error); + api.alert('Save failed', error); }); }, diff --git a/src/js/control-view.js b/src/js/control-view.js index e756c91..46c199a 100644 --- a/src/js/control-view.js +++ b/src/js/control-view.js @@ -281,7 +281,7 @@ module.exports = { this.$broadcast('gcode-reload', file.name); }.bind(this)).fail(function (error) { - alert('Upload failed: ' + error) + api.alert('Upload failed', error) }.bind(this)); }, diff --git a/src/js/main.js b/src/js/main.js index 336a306..c3ebf6c 100644 --- a/src/js/main.js +++ b/src/js/main.js @@ -28,7 +28,46 @@ 'use strict'; +function cookie_get(name) { + var decodedCookie = decodeURIComponent(document.cookie); + var ca = decodedCookie.split(';'); + name = name + '='; + + for (var i = 0; i < ca.length; i++) { + var c = ca[i]; + while (c.charAt(0) == ' ') c = c.substring(1); + if (!c.indexOf(name)) return c.substring(name.length, c.length); + } +} + + +function cookie_set(name, value, days) { + var d = new Date(); + d.setTime(d.getTime() + days * 24 * 60 * 60 * 1000); + var expires = 'expires=' + d.toUTCString(); + document.cookie = name + '=' + value + ';' + expires + ';path=/'; +} + + +var uuid_chars = + 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_+'; + + +function uuid(length) { + if (typeof length == 'undefined') length = 52; + + var s = ''; + for (var i = 0; i < length; i++) + s += uuid_chars[Math.floor(Math.random() * uuid_chars.length)]; + + return s +} + + $(function() { + if (typeof cookie_get('client-id') == 'undefined') + cookie_set('client-id', uuid(), 10000); + // Vue debugging Vue.config.debug = true; //Vue.util.warn = function (msg) {console.debug('[Vue warn]: ' + msg)} diff --git a/src/pug/index.pug b/src/pug/index.pug index dfd0647..f960c0c 100644 --- a/src/pug/index.pug +++ b/src/pug/index.pug @@ -96,7 +96,8 @@ html(lang="en") span.left Build span.right botics .subtitle - | Machine Controller v{{config.version}} + | CNC Controller #[b {{state.demo ? 'Demo ' : ''}}] + | v{{config.version}} a.upgrade-version(v-if="show_upgrade()", href="#admin-general") | Upgrade to v{{latestVersion}} .fa.fa-check(v-if="!show_upgrade() && latestVersion", @@ -105,8 +106,15 @@ html(lang="en") .estop(:class="{active: state.es}") estop(@click="estop") - .video(title="Plug camera into USB.\nClick to change video size.") - img(src="/api/video", @click="toggle_video", :class="video_size") + .video(title="Plug camera into USB.\n" + + "Left click to toggle video size.\n" + + "Right click to toggle crosshair.", @click="toggle_video", + @contextmenu="toggle_crosshair", :class="video_size") + .crosshair(v-if="crosshair") + .vertical + .horizontal + .box + img(src="/api/video") .clear diff --git a/src/py/bbctrl/APIHandler.py b/src/py/bbctrl/APIHandler.py index 10bde7b..c21ca56 100644 --- a/src/py/bbctrl/APIHandler.py +++ b/src/py/bbctrl/APIHandler.py @@ -27,32 +27,13 @@ import json import traceback -import logging +import bbctrl -from tornado.web import RequestHandler, HTTPError +from tornado.web import HTTPError import tornado.httpclient -log = logging.getLogger('API') -log.setLevel(logging.DEBUG) - - -class APIHandler(RequestHandler): - def __init__(self, app, request, **kwargs): - super().__init__(app, request, **kwargs) - self.ctrl = app.ctrl - - - # Override exception logging - def log_exception(self, typ, value, tb): - if isinstance(value, HTTPError) and value.status_code in (404, 408): - return - - log.error(str(value)) - trace = ''.join(traceback.format_exception(typ, value, tb)) - log.debug(trace) - - +class APIHandler(bbctrl.RequestHandler): def delete(self, *args, **kwargs): self.delete_ok(*args, **kwargs) self.write_json('ok') @@ -76,7 +57,7 @@ class APIHandler(RequestHandler): try: self.json = tornado.escape.json_decode(self.request.body) except ValueError: - raise HTTPError(400, 'Unable to parse JSON.') + raise HTTPError(400, 'Unable to parse JSON') def set_default_headers(self): @@ -87,8 +68,13 @@ class APIHandler(RequestHandler): e = {} if 'message' in kwargs: e['message'] = kwargs['message'] + elif 'exc_info' in kwargs: - e['message'] = str(kwargs['exc_info'][1]) + typ, value, tb = kwargs['exc_info'] + if isinstance(value, HTTPError) and value.log_message: + e['message'] = value.log_message % value.args + else: e['message'] = str(kwargs['exc_info'][1]) + else: e['message'] = 'Unknown error' e['code'] = status_code diff --git a/src/py/bbctrl/AVR.py b/src/py/bbctrl/AVR.py index 528f00d..a54354c 100644 --- a/src/py/bbctrl/AVR.py +++ b/src/py/bbctrl/AVR.py @@ -26,25 +26,28 @@ ################################################################################ import serial -import logging import time import traceback import bbctrl import bbctrl.Cmd as Cmd -log = logging.getLogger('AVR') class AVR(object): def __init__(self, ctrl): self.ctrl = ctrl + self.log = ctrl.log.get('AVR') + self.sp = None self.i2c_addr = ctrl.args.avr_addr self.read_cb = None self.write_cb = None + def close(self): pass + + def _start(self): try: self.sp = serial.Serial(self.ctrl.args.serial, self.ctrl.args.baud, @@ -53,7 +56,7 @@ class AVR(object): except Exception as e: self.sp = None - log.warning('Failed to open serial port: %s', e) + self.log.warning('Failed to open serial port: %s', e) if self.sp is not None: self.ctrl.ioloop.add_handler(self.sp, self._serial_handler, @@ -62,7 +65,7 @@ class AVR(object): 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') + raise Exception('Handler already set') self.read_cb = read_cb self.write_cb = write_cb @@ -87,7 +90,7 @@ class AVR(object): self.read_cb(data) except Exception as e: - log.warning('%s: %s', e, data) + self.log.warning('%s: %s', e, data) def _serial_handler(self, fd, events): @@ -96,11 +99,11 @@ class AVR(object): if self.ctrl.ioloop.WRITE & events: self._serial_write() except Exception as e: - log.warning('Serial handler error: %s', traceback.format_exc()) + self.log.warning('Serial handler error: %s', traceback.format_exc()) def i2c_command(self, cmd, byte = None, word = None, block = None): - log.info('I2C: %s b=%s w=%s d=%s' % (cmd, byte, word, block)) + self.log.info('I2C: %s b=%s w=%s d=%s' % (cmd, byte, word, block)) retry = 5 cmd = ord(cmd[0]) @@ -113,10 +116,10 @@ class AVR(object): retry -= 1 if retry: - log.warning('AVR I2C failed, retrying: %s' % e) + self.log.warning('I2C failed, retrying: %s' % e) time.sleep(0.1) continue else: - log.error('AVR I2C failed: %s' % e) + self.log.error('I2C failed: %s' % e) raise diff --git a/src/py/bbctrl/AVREmu.py b/src/py/bbctrl/AVREmu.py index f06add3..02ee250 100644 --- a/src/py/bbctrl/AVREmu.py +++ b/src/py/bbctrl/AVREmu.py @@ -25,20 +25,20 @@ # # ################################################################################ -import logging import os import sys import traceback +import signal import bbctrl import bbctrl.Cmd as Cmd -log = logging.getLogger('AVREmu') - 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 @@ -47,6 +47,13 @@ class AVREmu(object): self.pid = None + def close(self): + if self.pid is None: return + os.kill(self.pid, signal.SIGINT) + os.waitpid(self.pid, 0) + self.pid = None + + def _start(self): try: if self.pid is not None: os.waitpid(self.pid, 0) @@ -104,7 +111,7 @@ class AVREmu(object): except Exception as e: self.pid = None self.avrOut, self.avrIn, self.i2cOut = None, None, None - log.exception('Failed to start bbemu') + self.log.exception('Failed to start bbemu') def set_handlers(self, read_cb, write_cb): @@ -150,7 +157,8 @@ class AVREmu(object): if not self.continue_write: break except Exception as e: - log.warning('AVR write handler error: %s', traceback.format_exc()) + self.log.warning('AVR write handler error: %s', + traceback.format_exc()) def _avr_read_handler(self, fd, events): @@ -165,7 +173,7 @@ class AVREmu(object): if data is not None: self.read_cb(data) except Exception as e: - log.warning('AVR read handler error: %s %s' % + self.log.warning('AVR read handler error: %s %s' % (data, traceback.format_exc())) diff --git a/src/py/bbctrl/Camera.py b/src/py/bbctrl/Camera.py index c0bfdde..f8ab73b 100755 --- a/src/py/bbctrl/Camera.py +++ b/src/py/bbctrl/Camera.py @@ -28,7 +28,6 @@ import os import fcntl -import logging import select import struct import mmap @@ -43,8 +42,6 @@ try: except: import bbctrl.v4l2 as v4l2 -log = logging.getLogger('Camera') - def array_to_string(a): return ''.join([chr(i) for i in a]) @@ -273,13 +270,14 @@ class VideoDevice(object): if self.fd is None: return try: os.close(self.fd) - except Exception as e: log.warning('While closing camera: %s', e) finally: self.fd = None class Camera(object): def __init__(self, ctrl): self.ctrl = ctrl + self.log = ctrl.log.get('Camera') + self.width = ctrl.args.width self.height = ctrl.args.height self.fps = ctrl.args.fps @@ -323,7 +321,7 @@ class Camera(object): try: self.clients[-1].write_frame(format_frame(frame)) except Exception as e: - log.warning('Failed to write frame to client: %s' % e) + self.log.warning('Failed to write frame to client: %s' % e) def _fd_handler(self, fd, events): @@ -337,7 +335,7 @@ class Camera(object): except Exception as e: if isinstance(e, BlockingIOError): return - log.warning('Failed to read from camera.') + self.log.warning('Failed to read from camera.') self.ctrl.ioloop.remove_handler(fd) self.close() return @@ -350,15 +348,15 @@ class Camera(object): self.dev = VideoDevice(path) caps = self.dev.get_info() - log.info('%s, %s, %s, %s', caps._driver, caps._card, caps._bus_info, - caps._caps) + self.log.info('%s, %s, %s, %s', caps._driver, caps._card, + caps._bus_info, caps._caps) if caps.capabilities & v4l2.V4L2_CAP_VIDEO_CAPTURE == 0: raise Exception('Video capture not supported.') - log.info('Formats: %s', self.dev.get_formats()) - log.info('Sizes: %s', self.dev.get_frame_sizes(self.fourcc)) - log.info('Audio: %s', self.dev.get_audio()) + self.log.info('Formats: %s', self.dev.get_formats()) + self.log.info('Sizes: %s', self.dev.get_frame_sizes(self.fourcc)) + self.log.info('Audio: %s', self.dev.get_audio()) self.dev.set_format(self.width, self.height, fourcc = self.fourcc) self.dev.set_fps(self.fps) @@ -368,14 +366,21 @@ class Camera(object): self.ctrl.ioloop.add_handler(self.dev, self._fd_handler, self.ctrl.ioloop.READ) - log.info('Opened camera ' + path) + self.log.info('Opened camera ' + path) except Exception as e: - log.warning('While loading camera: %s' % e) - if not self.dev is None: - self.dev.close() - self.dev = None + self.log.warning('While loading camera: %s' % e) + self._close_dev() + + + def _close_dev(self): + if self.dev is None: return + try: + self.dev.close() + except Exception as e: self.log.warning('While closing camera: %s', e) + + self.dev = None def close(self): @@ -385,19 +390,19 @@ class Camera(object): try: self.dev.stop() except: pass - self.dev.close() + self._close_dev() for client in self.clients: client.write_frame_twice(self.offline_jpg) - log.info('Closed camera %s' % self.path) + self.log.info('Closed camera %s' % self.path) - except: log.warning('Closing camera') + except: self.log.warning('Closing camera') finally: self.dev = None def add_client(self, client): - log.info('Adding camera client: %d' % len(self.clients)) + self.log.info('Adding camera client: %d' % len(self.clients)) if len(self.clients): self.clients[-1].write_frame_twice(self.in_use_jpg) @@ -409,7 +414,7 @@ class Camera(object): def remove_client(self, client): - log.info('Removing camera client') + self.log.info('Removing camera client') try: self.clients.remove(client) except: pass @@ -422,7 +427,7 @@ class VideoHandler(web.RequestHandler): def __init__(self, app, request, **kwargs): super().__init__(app, request, **kwargs) - self.camera = app.ctrl.camera + self.camera = app.get_ctrl(self.get_cookie('client-id')).camera @web.asynchronous @@ -470,9 +475,11 @@ class VideoHandler(web.RequestHandler): if __name__ == '__main__': class Ctrl(object): - def __init__(self): - from tornado import ioloop - self.ioloop = ioloop.IOLoop.current() + def __init__(self, args, ioloop): + self.args = args + self.ioloop = ioloop + self.log = bbctrl.log.Log(args, ioloop) + self.camera = Camera(self) class RootHandler(web.RequestHandler): @@ -482,8 +489,8 @@ if __name__ == '__main__': class Web(web.Application): - def __init__(self, ctrl): - self.ctrl = ctrl + def __init__(self, args, ioloop): + self.ctrl = Ctrl(args, ioloop) handlers = [ (r'/', RootHandler), @@ -493,6 +500,10 @@ if __name__ == '__main__': web.Application.__init__(self, handlers) self.listen(9000, address = '127.0.0.1') + + def get_ctrl(self, id = None): return self.ctrl + + import argparse parser = argparse.ArgumentParser(description = 'Camera Server Test') parser.add_argument('--width', default = 640, type = int) @@ -501,11 +512,8 @@ if __name__ == '__main__': parser.add_argument('--fourcc', default = 'MJPG') args = parser.parse_args() + from tornado import ioloop + ioloop = ioloop.IOLoop.current() - logging.basicConfig(level = logging.INFO) - - ctrl = Ctrl() - ctrl.args = args - ctrl.camera = Camera(ctrl) - server = Web(ctrl) - ctrl.ioloop.start() + server = Web(args, ioloop) + ioloop.start() diff --git a/src/py/bbctrl/Cmd.py b/src/py/bbctrl/Cmd.py index 3408fef..3b5c1af 100644 --- a/src/py/bbctrl/Cmd.py +++ b/src/py/bbctrl/Cmd.py @@ -30,9 +30,6 @@ import struct import base64 import json -import logging - -log = logging.getLogger('Cmd') # Keep this in sync with AVR code command.def SET = '$' @@ -261,12 +258,17 @@ def decode_command(cmd): elif cmd[0] == FLUSH: data['type'] = 'flush' elif cmd[0] == RESUME: data['type'] = 'resume' - print(json.dumps(data)) + return data def decode(cmd): for line in cmd.split('\n'): - decode_command(line.strip()) + yield decode_command(line.strip()) + + +def decode_and_print(cmd): + for data in decode(cmd): + print(json.dumps(data)) if __name__ == "__main__": @@ -274,8 +276,8 @@ if __name__ == "__main__": if 1 < len(sys.argv): for arg in sys.argv[1:]: - decode(arg) + decode_and_print(arg) else: for line in sys.stdin: - decode(line) + decode_and_print(line) diff --git a/src/py/bbctrl/Comm.py b/src/py/bbctrl/Comm.py index 8e7f3a6..742907c 100644 --- a/src/py/bbctrl/Comm.py +++ b/src/py/bbctrl/Comm.py @@ -26,7 +26,6 @@ ################################################################################ import serial -import logging import json import time import traceback @@ -35,13 +34,12 @@ from collections import deque import bbctrl import bbctrl.Cmd as Cmd -log = logging.getLogger('Comm') - class Comm(object): def __init__(self, ctrl, avr): self.ctrl = ctrl self.avr = avr + self.log = self.ctrl.log.get('Comm') self.queue = deque() self.in_buf = '' self.command = None @@ -57,7 +55,7 @@ class Comm(object): def i2c_command(self, cmd, byte = None, word = None, block = None): - log.info('I2C: %s b=%s w=%s d=%s' % (cmd, byte, word, block)) + self.log.info('I2C: %s b=%s w=%s d=%s' % (cmd, byte, word, block)) self.avr.i2c_command(cmd, byte, word, block) @@ -65,7 +63,7 @@ class Comm(object): def _load_next_command(self, cmd): - log.info('< ' + json.dumps(cmd).strip('"')) + self.log.info('< ' + json.dumps(cmd).strip('"')) self.command = bytes(cmd.strip() + '\n', 'utf-8') @@ -114,20 +112,19 @@ class Comm(object): self.queue_command(Cmd.set_axis(axis, position)) except Exception as e: - log.warning('AVR reload failed: %s', traceback.format_exc()) + self.log.warning('AVR reload failed: %s', traceback.format_exc()) self.ctrl.ioloop.call_later(1, self.connect) def _log_msg(self, msg): level = msg.get('level', 'info') - if 'where' in msg: extra = {'where': msg['where']} - else: extra = None + where = msg.get('where') msg = msg['msg'] - if level == 'info': log.info(msg, extra = extra) - elif level == 'debug': log.debug(msg, extra = extra) - elif level == 'warning': log.warning(msg, extra = extra) - elif level == 'error': log.error(msg, extra = extra) + if level == 'info': self.log.info(msg, where = where) + elif level == 'debug': self.log.debug(msg, where = where) + elif level == 'warning': self.log.warning(msg, where = where) + elif level == 'error': self.log.error(msg, where = where) if level == 'error': self.comm_error() @@ -147,20 +144,20 @@ class Comm(object): self.in_buf = self.in_buf[i + 1:] if line: - log.info('> ' + line) + self.log.info('> ' + line) try: msg = json.loads(line) except Exception as e: - log.warning('%s, data: %s', e, line) + self.log.warning('%s, data: %s', e, line) continue if 'variables' in msg: self._update_vars(msg) elif 'msg' in msg: self._log_msg(msg) elif 'firmware' in msg: - log.info('AVR firmware rebooted') + self.log.info('AVR firmware rebooted') self.connect() else: @@ -194,5 +191,5 @@ class Comm(object): self.queue_command(Cmd.HELP) # Load AVR commands and variables except Exception as e: - log.warning('Connect failed: %s', e) + self.log.warning('Connect failed: %s', e) self.ctrl.ioloop.call_later(1, self.connect) diff --git a/src/py/bbctrl/CommandQueue.py b/src/py/bbctrl/CommandQueue.py index ee20ee8..7f89a89 100644 --- a/src/py/bbctrl/CommandQueue.py +++ b/src/py/bbctrl/CommandQueue.py @@ -25,19 +25,19 @@ # # ################################################################################ -import logging +import bbctrl from collections import deque -log = logging.getLogger('CmdQ') -log.setLevel(logging.WARNING) - # 16-bit less with wrap around def id_less(a, b): return (1 << 15) < (a - b) & ((1 << 16) - 1) class CommandQueue(): - def __init__(self): + def __init__(self, ctrl): + self.log = ctrl.log.get('CmdQ') + self.log.set_level(bbctrl.log.WARNING) + self.lastEnqueueID = 0 self.releaseID = 0 self.q = deque() @@ -53,7 +53,7 @@ class CommandQueue(): def enqueue(self, id, cb, *args, **kwargs): - log.info('add(#%d) releaseID=%d', id, self.releaseID) + self.log.info('add(#%d) releaseID=%d', id, self.releaseID) self.lastEnqueueID = id self.q.append([id, cb, args, kwargs]) self._release() @@ -66,19 +66,19 @@ class CommandQueue(): # Execute commands <= releaseID if id_less(self.releaseID, id): return - log.info('releasing id=%d' % id) + self.log.info('releasing id=%d' % id) self.q.popleft() try: if cb is not None: cb(*args, **kwargs) except Exception as e: - log.exception('During command queue callback') + self.log.exception('During command queue callback') def release(self, id): if id and not id_less(self.releaseID, id): - log.debug('id out of order %d <= %d' % (id, self.releaseID)) + self.log.debug('id out of order %d <= %d' % (id, self.releaseID)) self.releaseID = id self._release() diff --git a/src/py/bbctrl/Config.py b/src/py/bbctrl/Config.py index 9d353ce..ad146c2 100644 --- a/src/py/bbctrl/Config.py +++ b/src/py/bbctrl/Config.py @@ -27,14 +27,11 @@ import os import json -import logging import pkg_resources import subprocess import copy from pkg_resources import Requirement, resource_filename -log = logging.getLogger('Config') - def get_resource(path): return resource_filename(Requirement.parse('bbctrl'), 'bbctrl/' + path) @@ -43,6 +40,8 @@ def get_resource(path): class Config(object): def __init__(self, ctrl): self.ctrl = ctrl + self.log = ctrl.log.get('Config') + self.values = {} try: @@ -53,7 +52,7 @@ class Config(object): encoding = 'utf-8') as f: self.template = json.load(f) - except Exception as e: log.exception(e) + except Exception as e: self.log.exception(e) def get(self, name, default = None): @@ -64,22 +63,20 @@ class Config(object): return self.values.get(name, {}).get(str(index), None) - def load_path(self, path): - with open(path, 'r') as f: - return json.load(f) + def load(self): + path = self.ctrl.get_path('config.json') - - def load(self, path = 'config.json'): try: - if os.path.exists(path): config = self.load_path(path) + if os.path.exists(path): + with open(path, 'r') as f: config = json.load(f) else: config = {'version': self.version} try: self.upgrade(config) - except Exception as e: log.exception(e) + except Exception as e: self.log.exception(e) except Exception as e: - log.warning('%s', e) + self.log.warning('%s', e) config = {'version': self.version} self._defaults(config) @@ -169,13 +166,13 @@ class Config(object): self.upgrade(config) self._update(config, False) - with open('config.json', 'w') as f: + with open(self.ctrl.get_path('config.json'), 'w') as f: json.dump(config, f) subprocess.check_call(['sync']) self.ctrl.preplanner.invalidate_all() - log.info('Saved') + self.log.info('Saved') def reset(self): @@ -235,37 +232,3 @@ class Config(object): def reload(self): self._update(self.load(), True) - - - -if __name__ == "__main__": - import sys - import argparse - - class State(object): - def config(self, name, value): print('config(%s, %s)' % (name, value)) - - - class Ctrl(object): - def __init__(self): - self.state = State() - - parser = argparse.ArgumentParser(description = 'Buildbotics Config Test') - - parser.add_argument('configs', metavar = 'CONFIG', nargs = '*', - help = 'Configuration file') - parser.add_argument('-u', '--update', action = 'store_true', - help = 'Update config') - args = parser.parse_args() - - config = Config(Ctrl()) - - def do_cfg(path): - cfg = config.load(path) - if args.update: config._update(cfg, True) - else: print(json.dumps(cfg, sort_keys = True, indent = 2)) - - if len(args.configs): - for path in args.configs: do_cfg(path) - - else: do_cfg('') diff --git a/src/py/bbctrl/Ctrl.py b/src/py/bbctrl/Ctrl.py index 4b447d0..5e5a786 100644 --- a/src/py/bbctrl/Ctrl.py +++ b/src/py/bbctrl/Ctrl.py @@ -25,31 +25,29 @@ # # ################################################################################ -import logging +import os import bbctrl -log = logging.getLogger('Ctrl') - - - class Ctrl(object): - def __init__(self, args, ioloop): + def __init__(self, args, ioloop, id): self.args = args - self.ioloop = ioloop + self.ioloop = bbctrl.IOLoop(ioloop) + self.id = id - self.msgs = bbctrl.Messages(self) + if id and not os.path.exists(id): os.mkdir(id) + + self.log = bbctrl.log.Log(self) self.state = bbctrl.State(self) self.config = bbctrl.Config(self) - self.web = bbctrl.Web(self) try: - if args.demo: avr = bbctrl.AVREmu(self) - else: avr = bbctrl.AVR(self) + if args.demo: self.avr = bbctrl.AVREmu(self) + else: self.avr = bbctrl.AVR(self) self.i2c = bbctrl.I2C(args.i2c_port, args.demo) self.lcd = bbctrl.LCD(self) - self.mach = bbctrl.Mach(self, avr) + self.mach = bbctrl.Mach(self, self.avr) self.preplanner = bbctrl.Preplanner(self) self.jog = bbctrl.Jog(self) self.pwr = bbctrl.Pwr(self) @@ -63,7 +61,21 @@ class Ctrl(object): if not args.disable_camera: self.camera = bbctrl.Camera(self) - except Exception as e: log.exception(e) + except Exception as e: self.log.get('Ctrl').exception(e) + + + def get_path(self, dir = None, filename = None): + path = './' + self.id if self.id else '.' + path = path if dir is None else (path + '/' + dir) + return path if filename is None else (path + '/' + filename) + + + def get_upload(self, filename = None): + return self.get_path('upload', filename) + + + def get_plan(self, filename = None): + return self.get_path('plans', filename) def configure(self): @@ -78,3 +90,5 @@ class Ctrl(object): def close(self): if not self.camera is None: self.camera.close() + self.ioloop.close() + self.avr.close() diff --git a/src/py/bbctrl/FileHandler.py b/src/py/bbctrl/FileHandler.py index 52cf476..0d8a706 100644 --- a/src/py/bbctrl/FileHandler.py +++ b/src/py/bbctrl/FileHandler.py @@ -29,14 +29,10 @@ import os import bbctrl import glob import html -import logging from tornado import gen from tornado.web import HTTPError -log = logging.getLogger('FileHandler') - - def safe_remove(path): try: os.unlink(path) @@ -50,30 +46,30 @@ class FileHandler(bbctrl.APIHandler): def delete_ok(self, filename): if not filename: # Delete everything - for path in glob.glob('upload/*'): safe_remove(path) - self.ctrl.preplanner.delete_all_plans() - self.ctrl.state.clear_files() + for path in glob.glob(self.get_upload('*')): safe_remove(path) + self.get_ctrl().preplanner.delete_all_plans() + self.get_ctrl().state.clear_files() else: # Delete a single file filename = os.path.basename(filename) - safe_remove('upload/' + filename) - self.ctrl.preplanner.delete_plans(filename) - self.ctrl.state.remove_file(filename) + safe_remove(self.get_upload(filename)) + 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']) - if not os.path.exists('upload'): os.mkdir('upload') + if not os.path.exists(self.get_upload()): os.mkdir(self.get_upload()) - with open('upload/' + filename, 'wb') as f: + with open(self.get_upload(filename), 'wb') as f: f.write(gcode['body']) - self.ctrl.preplanner.invalidate(filename) - self.ctrl.state.add_file(filename) - log.info('GCode received: ' + filename) + self.get_ctrl().preplanner.invalidate(filename) + self.get_ctrl().state.add_file(filename) + self.get_log('FileHandler').info('GCode received: ' + filename) @gen.coroutine @@ -81,7 +77,7 @@ class FileHandler(bbctrl.APIHandler): if not filename: raise HTTPError(400, 'Missing filename') filename = os.path.basename(filename) - with open('upload/' + filename, 'r') as f: + with open(self.get_upload(filename), 'r') as f: self.write(f.read()) - self.ctrl.state.select_file(filename) + self.get_ctrl().state.select_file(filename) diff --git a/src/py/bbctrl/GCodeStream.py b/src/py/bbctrl/GCodeStream.py deleted file mode 100644 index e09b2d0..0000000 --- a/src/py/bbctrl/GCodeStream.py +++ /dev/null @@ -1,86 +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 . # -# # -# 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 # -# . # -# # -# For information regarding this software email: # -# "Joseph Coffland" # -# # -################################################################################ - -import re -import logging - - -log = logging.getLogger('GCode') - - -class GCodeStream(): - comment1RE = re.compile(r';.*') - comment2RE = re.compile(r'\(([^\)]*)\)') - - - def __init__(self, path): - self.path = path - self.f = None - - self.open() - - - def close(self): - if self.f is not None: - self.f.close() - self.f = None - - - def open(self): - self.close() - - self.line = 0 - self.f = open('upload' + self.path, 'r') - - - def reset(self): self.open() - - - def comment(self, s): - log.debug('Comment: %s', s) - - - def next(self): - line = self.f.readline() - if line is None or line == '': return - - # Remove comments - line = self.comment1RE.sub('', line) - - for comment in self.comment2RE.findall(line): - self.comment(comment) - - line = self.comment2RE.sub(' ', line) - - # Remove space - line = line.strip() - - # Append line number - self.line += 1 - line += ' N%d' % self.line - - return line diff --git a/src/py/bbctrl/IOLoop.py b/src/py/bbctrl/IOLoop.py new file mode 100644 index 0000000..641ab45 --- /dev/null +++ b/src/py/bbctrl/IOLoop.py @@ -0,0 +1,72 @@ +################################################################################ +# # +# 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 . # +# # +# 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 # +# . # +# # +# For information regarding this software email: # +# "Joseph Coffland" # +# # +################################################################################ + +import tornado.ioloop +import bbctrl + + +class IOLoop(object): + READ = tornado.ioloop.IOLoop.READ + WRITE = tornado.ioloop.IOLoop.WRITE + ERROR = tornado.ioloop.IOLoop.ERROR + + + def __init__(self, ioloop): + self.ioloop = ioloop + self.fds = set() + self.handles = set() + + + def close(self): + for fd in self.fds: self.ioloop.remove_handler(fd) + for h in self.handles: self.ioloop.remove_timeout(h) + + + def add_handler(self, fd, handler, events): + self.ioloop.add_handler(fd, handler, events) + if hasattr(fd, 'fileno'): fd = fd.fileno() + self.fds.add(fd) + + + def remove_handler(self, fd): + self.ioloop.remove_handler(fd) + if hasattr(fd, 'fileno'): fd = fd.fileno() + self.fds.remove(fd) + + + def update_handler(self, fd, events): self.ioloop.update_handler(fd, events) + + + def call_later(self, delay, callback, *args, **kwargs): + h = self.ioloop.call_later(delay, callback, *args, **kwargs) + self.handles.add(h) + return h + + + def remove_timeout(self, h): + self.ioloop.remove_timeout(h) + self.handles.remove(h) diff --git a/src/py/bbctrl/Jog.py b/src/py/bbctrl/Jog.py index 04a5d08..c4544e2 100644 --- a/src/py/bbctrl/Jog.py +++ b/src/py/bbctrl/Jog.py @@ -27,15 +27,13 @@ import inevent from inevent.Constants import * -import logging - -log = logging.getLogger('Jog') # Listen for input events class Jog(inevent.JogHandler): def __init__(self, ctrl): self.ctrl = ctrl + self.log = ctrl.log.get('Jog') config = { "Logitech Logitech RumblePad 2 USB": { @@ -81,7 +79,7 @@ class Jog(inevent.JogHandler): self.ctrl.mach.jog(axes) except Exception as e: - log.warning('Jog: %s', e) + self.log.warning('Jog: %s', e) self.ctrl.ioloop.call_later(0.25, self.callback) diff --git a/src/py/bbctrl/LCD.py b/src/py/bbctrl/LCD.py index f954726..0d2cdcb 100644 --- a/src/py/bbctrl/LCD.py +++ b/src/py/bbctrl/LCD.py @@ -27,13 +27,9 @@ import lcd import atexit -import logging from tornado.ioloop import PeriodicCallback -log = logging.getLogger('LCD') - - class LCDPage: def __init__(self, lcd, text = None): self.lcd = lcd @@ -77,6 +73,7 @@ class LCDPage: class LCD: def __init__(self, ctrl): self.ctrl = ctrl + self.log = ctrl.log.get('LCD') self.addrs = self.ctrl.args.lcd_addr self.addr = self.addrs[0] @@ -102,7 +99,7 @@ class LCD: self.load_page(LCDPage(self, msg)) self._update() except IOError as e: - log.warning('LCD communication failed: %s' % e) + self.log.warning('LCD communication failed: %s' % e) def new_screen(self): @@ -189,7 +186,7 @@ class LCD: self.addr = self.addrs[self.addr_num] self.lcd = None - log.warning('LCD communication failed, ' + + self.log.warning('LCD communication failed, ' + 'retrying on address 0x%02x: %s' % (self.addr, e)) self.reset = True diff --git a/src/py/bbctrl/Log.py b/src/py/bbctrl/Log.py new file mode 100644 index 0000000..c1a5205 --- /dev/null +++ b/src/py/bbctrl/Log.py @@ -0,0 +1,138 @@ +################################################################################ +# # +# 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 . # +# # +# 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 # +# . # +# # +# For information regarding this software email: # +# "Joseph Coffland" # +# # +################################################################################ + +import os +import sys +import datetime +import traceback +import pkg_resources +from inspect import getframeinfo, stack +import bbctrl + + +DEBUG = 0 +INFO = 1 +WARNING = 2 +ERROR = 3 + + +def get_level_name(level): return 'debug info warning error'.split()[level] + + +class Logger(object): + def __init__(self, log, name, level): + self.log = log + self.name = name + self.level = level + + + def set_level(self, level): self.level = level + def _enabled(self, level): return self.level <= level and level <= ERROR + + + def _log(self, level, msg, *args, **kwargs): + if not self._enabled(level): return + + if not 'where' in kwargs: + caller = getframeinfo(stack()[2][0]) + kwargs['where'] = '%s:%d' % ( + os.path.basename(caller.filename), caller.lineno) + + if len(args): msg %= args + + self.log._log(msg, level = level, prefix = self.name, **kwargs) + + + def debug (self, *args, **kwargs): self._log(DEBUG, *args, **kwargs) + def info (self, *args, **kwargs): self._log(INFO, *args, **kwargs) + def warning(self, *args, **kwargs): self._log(WARNING, *args, **kwargs) + def error (self, *args, **kwargs): self._log(ERROR, *args, **kwargs) + + + def exception(self, *args, **kwargs): + msg = traceback.format_exc() + if len(args): msg = args[0] % args[1:] + '\n' + msg + self._log(ERROR, msg, **kwargs) + + +class Log(object): + def __init__(self, ctrl): + self.listeners = [] + self.loggers = {} + + self.level = DEBUG if ctrl.args.verbose else INFO + + if ctrl.args.demo: self.path = ctrl.get_path(filename = 'bbctrl.log') + else: self.path = ctrl.args.log + self.f = None if self.path is None else open(self.path, 'w') + + # Log header + version = pkg_resources.require('bbctrl')[0].version + self._log('Log started v%s' % version) + self._log_time(ctrl.ioloop) + + + def get_path(self): return self.path + + def add_listener(self, listener): self.listeners.append(listener) + def remove_listener(self, listener): self.listeners.remove(listener) + + + def get(self, name, level = None): + if not name in self.loggers: + self.loggers[name] = Logger(self, name, self.level) + return self.loggers[name] + + + def _log_time(self, ioloop): + self._log(datetime.datetime.now().strftime('%Y/%m/%d %H:%M:%S')) + ioloop.call_later(60 * 60, self._log_time, ioloop) + + + def broadcast(self, msg): + for listener in self.listeners: listener(msg) + + + def _log(self, msg, level = INFO, prefix = '', where = None): + if not msg: return + + hdr = '%s:%s:' % ('DIWE'[level], prefix) + s = hdr + ('\n' + hdr).join(msg.split('\n')) + + if self.f is not None: + self.f.write(s + '\n') + self.f.flush() + + print(s) + + # Broadcast to log listeners + if level == INFO: return + + msg = dict(level = get_level_name(level), source = prefix, msg = msg) + if where is not None: msg['where'] = where + + self.broadcast(dict(log = msg)) diff --git a/src/py/bbctrl/Mach.py b/src/py/bbctrl/Mach.py index 6bcbd13..50b172a 100644 --- a/src/py/bbctrl/Mach.py +++ b/src/py/bbctrl/Mach.py @@ -25,15 +25,11 @@ # # ################################################################################ -import logging - import bbctrl from bbctrl.Comm import Comm import bbctrl.Cmd as Cmd from tornado.ioloop import PeriodicCallback -log = logging.getLogger('Mach') - # Axis homing procedure: # @@ -65,7 +61,7 @@ for more information.\ def overrides(interface_class): def overrider(method): if not method.__name__ in dir(interface_class): - log.warning('%s does not override %s' % ( + raise Exception('%s does not override %s' % ( method.__name__, interface_class.__name__)) return method @@ -78,6 +74,8 @@ class Mach(Comm): super().__init__(ctrl, avr) self.ctrl = ctrl + self.mlog = self.ctrl.log.get('Mach') + self.planner = bbctrl.Planner(ctrl) self.unpausing = False self.last_cycle = 'idle' @@ -142,7 +140,7 @@ class Mach(Comm): for motor in range(4): key = '%ddf' % motor if key in update and update[key] & 0x1f: - log.error(motor_fault_error % motor) + self.mlog.error(motor_fault_error % motor) # Update cycle now, if it has changed self._update_cycle() @@ -172,7 +170,7 @@ class Mach(Comm): def _unpause(self): pause_reason = self._get_pause_reason() - log.info('Unpause: ' + pause_reason) + self.mlog.info('Unpause: ' + pause_reason) if pause_reason == 'User stop': self.planner.stop() @@ -214,7 +212,7 @@ class Mach(Comm): def _query_var(self, cmd): equal = cmd.find('=') if equal == -1: - log.info('%s=%s' % (cmd, self.ctrl.state.get(cmd[1:]))) + self.mlog.info('%s=%s' % (cmd, self.ctrl.state.get(cmd[1:]))) else: name, value = cmd[1:equal], cmd[equal + 1:] @@ -251,34 +249,36 @@ class Mach(Comm): def home(self, axis, position = None): state = self.ctrl.state - if position is not None: self.mdi('G28.3 %c%f' % (axis, position)) - - else: + if axis is None: axes = 'zxyabc' # TODO This should be configurable + else: axes = '%c' % axis + + for axis in axes: + enabled = state.is_axis_enabled(axis) + mode = state.axis_homing_mode(axis) + + # If this is not a request to home a specific axis and the + # axis is disabled or in manual homing mode, don't show any + # warnings + if 1 < len(axes) and (not enabled or mode == 'manual'): + continue + + # Error when axes cannot be homed + reason = state.axis_home_fail_reason(axis) + if reason is not None: + self.mlog.error('Cannot home %s axis: %s' % ( + axis.upper(), reason)) + continue + + if mode == 'manual': + if position is None: raise Exception('Position not set') + self.mdi('G28.3 %c%f' % (axis, position)) + continue + + # Home axis + self.mlog.info('Homing %s axis' % axis) self._begin_cycle('homing') - - if axis is None: axes = 'zxyabc' # TODO This should be configurable - else: axes = '%c' % axis - - for axis in axes: - # If this is not a request to home a specific axis and the - # axis is disabled or in manual homing mode, don't show any - # warnings - if 1 < len(axes) and ( - not state.is_axis_enabled(axis) or - state.axis_homing_mode(axis) == 'manual'): - continue - - # Error when axes cannot be homed - reason = state.axis_home_fail_reason(axis) - if reason is not None: - log.error('Cannot home %s axis: %s' % ( - axis.upper(), reason)) - continue - - # Home axis - log.info('Homing %s axis' % axis) - self.planner.mdi(axis_homing_procedure % {'axis': axis}, False) - super().resume() + self.planner.mdi(axis_homing_procedure % {'axis': axis}, False) + super().resume() def unhome(self, axis): self.mdi('G28.2 %c0' % axis) @@ -295,7 +295,7 @@ class Mach(Comm): filename = self.ctrl.state.get('selected', '') if not filename: return self._begin_cycle('running') - self.planner.load('upload/' + filename) + self.planner.load(filename) super().resume() diff --git a/src/py/bbctrl/Messages.py b/src/py/bbctrl/Messages.py deleted file mode 100644 index b6ae595..0000000 --- a/src/py/bbctrl/Messages.py +++ /dev/null @@ -1,69 +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 . # -# # -# 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 # -# . # -# # -# For information regarding this software email: # -# "Joseph Coffland" # -# # -################################################################################ - -import os -import logging -import bbctrl - - -log = logging.getLogger('Msgs') - - -class Messages(logging.Handler): - def __init__(self, ctrl): - logging.Handler.__init__(self) - - self.ctrl = ctrl - self.listeners = [] - - debug = os.path.exists('/etc/bbctrl-dev-mode') - self.setLevel(logging.DEBUG if debug else logging.WARNING) - - logging.getLogger().addHandler(self) - - - def add_listener(self, listener): self.listeners.append(listener) - def remove_listener(self, listener): self.listeners.remove(listener) - - - def broadcast(self, msg): - for listener in self.listeners: - listener(msg) - - - # From logging.Handler - def emit(self, record): - if record.levelno == logging.INFO: return - - msg = dict(level = record.levelname.lower(), - source = record.name, - msg = record.getMessage()) - - if hasattr(record, 'where'): msg['where'] = record.where - else: msg['where'] = '%s:%d' % (record.filename, record.lineno) - - self.broadcast({'log': msg}) diff --git a/src/py/bbctrl/Planner.py b/src/py/bbctrl/Planner.py index b4a101f..5d272b7 100644 --- a/src/py/bbctrl/Planner.py +++ b/src/py/bbctrl/Planner.py @@ -28,14 +28,12 @@ import json import math import re -import logging import time from collections import deque import camotics.gplan as gplan # pylint: disable=no-name-in-module,import-error import bbctrl.Cmd as Cmd from bbctrl.CommandQueue import CommandQueue -log = logging.getLogger('Planner') reLogLine = re.compile( r'^(?P[A-Z])[0-9 ]:' @@ -58,7 +56,8 @@ def log_json(o): return json.dumps(log_floats(o)) class Planner(): def __init__(self, ctrl): self.ctrl = ctrl - self.cmdq = CommandQueue() + self.log = ctrl.log.get('Planner') + self.cmdq = CommandQueue(ctrl) ctrl.state.add_listener(self._update) @@ -114,7 +113,7 @@ class Planner(): if overrides: cfg['overrides'] = overrides - log.info('Config:' + log_json(cfg)) + self.log.info('Config:' + log_json(cfg)) return cfg @@ -133,7 +132,7 @@ class Planner(): value = self.ctrl.state.get(name[1:], 0) if units == 'IMPERIAL': value /= 25.4 # Assume metric - log.info('Get: %s=%s (units=%s)' % (name, value, units)) + self.log.info('Get: %s=%s (units=%s)' % (name, value, units)) return value @@ -153,19 +152,16 @@ class Planner(): if line is not None: line = int(line) if column is not None: column = int(column) - if where: extra = dict(where = where) - else: extra = None - if level == 'I': log.info (msg, extra = extra) - elif level == 'D': log.debug (msg, extra = extra) - elif level == 'W': log.warning (msg, extra = extra) - elif level == 'E': log.error (msg, extra = extra) - elif level == 'C': log.critical(msg, extra = extra) - else: log.error('Could not parse planner log line: ' + line) + if level == 'I': self.log.info (msg, where = where) + elif level == 'D': self.log.debug (msg, where = where) + elif level == 'W': self.log.warning (msg, where = where) + elif level == 'E': self.log.error (msg, where = where) + else: self.log.error('Could not parse planner log line: ' + line) def _enqueue_set_cmd(self, id, name, value): - log.info('set(#%d, %s, %s)', id, name, value) + self.log.info('set(#%d, %s, %s)', id, name, value) self.cmdq.enqueue(id, self.ctrl.state.set, name, value) @@ -215,7 +211,7 @@ class Planner(): def __encode(self, block): type, id = block['type'], block['id'] - if type != 'set': log.info('Cmd:' + log_json(block)) + if type != 'set': self.log.info('Cmd:' + log_json(block)) if type == 'line': self._enqueue_line_time(block) @@ -228,7 +224,7 @@ class Planner(): if name == 'message': msg = dict(message = value) - self.cmdq.enqueue(id, self.ctrl.msgs.broadcast, msg) + self.cmdq.enqueue(id, self.ctrl.log.broadcast, msg) if name in ['line', 'tool']: self._enqueue_set_cmd(id, name, value) if name == 'speed': return Cmd.speed(value) @@ -300,13 +296,14 @@ class Planner(): def mdi(self, cmd, with_limits = True): - log.info('MDI:' + cmd) + self.log.info('MDI:' + cmd) self.planner.load_string(cmd, self.get_config(True, with_limits)) self.reset_times() def load(self, path): - log.info('GCode:' + path) + path = self.ctrl.get_path('upload', path) + self.log.info('GCode:' + path) self.planner.load(path, self.get_config(False, True)) self.reset_times() @@ -316,8 +313,8 @@ class Planner(): self.planner.stop() self.cmdq.clear() - except Exception as e: - log.exception(e) + except: + self.log.exception() self.reset() @@ -326,15 +323,15 @@ class Planner(): id = self.ctrl.state.get('id') position = self.ctrl.state.get_position() - log.info('Planner restart: %d %s' % (id, log_json(position))) + self.log.info('Planner restart: %d %s' % (id, log_json(position))) self.cmdq.clear() self.cmdq.release(id) self._plan_time_restart() self.planner.restart(id, position) - except Exception as e: - log.exception(e) + except: + self.log.exception() self.stop() @@ -345,6 +342,6 @@ class Planner(): cmd = self._encode(cmd) if cmd is not None: return cmd - except Exception as e: - log.exception(e) + except: + self.log.exception() self.stop() diff --git a/src/py/bbctrl/Preplanner.py b/src/py/bbctrl/Preplanner.py index b2aaafc..9a706e6 100644 --- a/src/py/bbctrl/Preplanner.py +++ b/src/py/bbctrl/Preplanner.py @@ -26,7 +26,6 @@ ################################################################################ import os -import logging import time import json import hashlib @@ -39,16 +38,12 @@ from tornado import gen import bbctrl -log = logging.getLogger('Preplanner') - - def hash_dump(o): s = json.dumps(o, separators = (',', ':'), sort_keys = True) return s.encode('utf8') def plan_hash(path, config): - path = 'upload/' + path h = hashlib.sha256() h.update('v4'.encode('utf8')) h.update(hash_dump(config)) @@ -68,10 +63,13 @@ class Preplanner(object): def __init__(self, ctrl, threads = 4, max_plan_time = 60 * 60 * 24, max_loop_time = 300): self.ctrl = ctrl + self.log = ctrl.log.get('Preplanner') + self.max_plan_time = max_plan_time self.max_loop_time = max_loop_time - if not os.path.exists('plans'): os.mkdir('plans') + path = self.ctrl.get_plan() + if not os.path.exists(path): os.mkdir(path) self.started = Future() @@ -82,7 +80,7 @@ class Preplanner(object): def start(self): if not self.started.done(): - log.info('Preplanner started') + self.log.info('Preplanner started') self.started.set_result(True) @@ -101,7 +99,7 @@ class Preplanner(object): def delete_all_plans(self): - files = glob.glob('plans/*') + files = glob.glob(self.ctrl.get_plan('*')) for path in files: try: @@ -112,7 +110,7 @@ class Preplanner(object): def delete_plans(self, filename): - files = glob.glob('plans/' + filename + '.*') + files = glob.glob(self.ctrl.get_plan(filename + '.*')) for path in files: try: @@ -158,7 +156,7 @@ class Preplanner(object): def _clean_plans(self, filename, max = 2): - plans = glob.glob('plans/' + filename + '.*') + plans = glob.glob(self.ctrl.get_plan(filename + '.*')) if len(plans) <= max: return # Delete oldest plans @@ -181,8 +179,8 @@ class Preplanner(object): try: os.nice(5) - hid = plan_hash(filename, config) - base = 'plans/' + filename + '.' + hid + hid = plan_hash(self.ctrl.get_upload(filename), config) + base = self.ctrl.get_plan(filename + '.' + hid) files = [ base + '.json', base + '.positions.gz', base + '.speeds.gz'] @@ -193,7 +191,7 @@ class Preplanner(object): if not found: self._clean_plans(filename) # Clean up old plans - path = os.path.abspath('upload/' + filename) + path = os.path.abspath(self.ctrl.get_upload(filename)) with tempfile.TemporaryDirectory() as tmpdir: cmd = ( '/usr/bin/env', 'python3', @@ -203,7 +201,7 @@ class Preplanner(object): '--max-loop=%s' % self.max_loop_time ) - log.info('Running: %s', cmd) + self.log.info('Running: %s', cmd) with subprocess.Popen(cmd, stdout = subprocess.PIPE, stderr = subprocess.PIPE, @@ -221,7 +219,8 @@ class Preplanner(object): if cancel.is_set(): return if proc.returncode: - log.error('Plan failed: ' + errs.decode('utf8')) + self.log.error('Plan failed: ' + + errs.decode('utf8')) return # Failed os.rename(tmpdir + '/meta.json', files[0]) @@ -234,4 +233,4 @@ class Preplanner(object): return meta, positions, speeds - except Exception as e: log.exception(e) + except Exception as e: self.log.exception(e) diff --git a/src/py/bbctrl/Pwr.py b/src/py/bbctrl/Pwr.py index e7f9d93..989a359 100644 --- a/src/py/bbctrl/Pwr.py +++ b/src/py/bbctrl/Pwr.py @@ -25,13 +25,10 @@ # # ################################################################################ -import logging from tornado.ioloop import PeriodicCallback import bbctrl -log = logging.getLogger('PWR') - # Must match regs in pwr firmware TEMP_REG = 0 @@ -66,6 +63,7 @@ reg_names = 'temp vin vout motor load1 load2 vdd pwr_flags pwr_version'.split() class Pwr(): def __init__(self, ctrl): self.ctrl = ctrl + self.log = ctrl.log.get('Pwr') self.i2c_addr = ctrl.args.pwr_addr self.regs = [-1] * 9 @@ -89,52 +87,52 @@ class Pwr(): flags = self.regs[FLAGS_REG] if self.check_fault('under_voltage', flags & UNDER_VOLTAGE_FLAG): - log.error('Device under voltage') + self.log.error('Device under voltage') if self.check_fault('over_voltage', flags & OVER_VOLTAGE_FLAG): - log.error('Device over voltage') + self.log.error('Device over voltage') if self.check_fault('over_current', flags & OVER_CURRENT_FLAG): - log.error('Device total current limit exceeded') + self.log.error('Device total current limit exceeded') if self.check_fault('sense_error', flags & SENSE_ERROR_FLAG): - log.error('Power sense error') + self.log.error('Power sense error') if self.check_fault('shunt_overload', flags & SHUNT_OVERLOAD_FLAG): - log.error('Power shunt overload') + self.log.error('Power shunt overload') if self.check_fault('motor_overload', flags & MOTOR_OVERLOAD_FLAG): - log.error('Motor power overload') + self.log.error('Motor power overload') if self.check_fault('load1_shutdown', flags & LOAD1_SHUTDOWN_FLAG): - log.error('Load 1 over temperature shutdown') + self.log.error('Load 1 over temperature shutdown') if self.check_fault('load2_shutdown', flags & LOAD2_SHUTDOWN_FLAG): - log.error('Load 2 over temperature shutdown') + self.log.error('Load 2 over temperature shutdown') if self.check_fault('motor_under_voltage', flags & MOTOR_UNDER_VOLTAGE_FLAG): - log.error('Motor under voltage') + self.log.error('Motor under voltage') if self.check_fault('motor_voltage_sense_error', flags & MOTOR_VOLTAGE_SENSE_ERROR_FLAG): - log.error('Motor voltage sense error') + self.log.error('Motor voltage sense error') if self.check_fault('motor_current_sense_error', flags & MOTOR_CURRENT_SENSE_ERROR_FLAG): - log.error('Motor current sense error') + self.log.error('Motor current sense error') if self.check_fault('load1_sense_error', flags & LOAD1_SENSE_ERROR_FLAG): - log.error('Load1 sense error') + self.log.error('Load1 sense error') if self.check_fault('load2_sense_error', flags & LOAD2_SENSE_ERROR_FLAG): - log.error('Load2 sense error') + self.log.error('Load2 sense error') if self.check_fault('vdd_current_sense_error', flags & VDD_CURRENT_SENSE_ERROR_FLAG): - log.error('Vdd current sense error') + self.log.error('Vdd current sense error') def _update_cb(self, now = True): @@ -167,9 +165,9 @@ class Pwr(): if i < 6: # Older pwr firmware does not have regs > 5 self.failures += 1 msg = 'Pwr communication failed at reg %d: %s' % (i, e) - if self.failures != 5: log.info(msg) + if self.failures != 5: self.log.info(msg) else: - log.warning(msg) + self.log.warning(msg) self.failures = 0 return diff --git a/src/py/bbctrl/RequestHandler.py b/src/py/bbctrl/RequestHandler.py new file mode 100644 index 0000000..050a425 --- /dev/null +++ b/src/py/bbctrl/RequestHandler.py @@ -0,0 +1,63 @@ +################################################################################ +# # +# 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 . # +# # +# 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 # +# . # +# # +# For information regarding this software email: # +# "Joseph Coffland" # +# # +################################################################################ + +import traceback +import bbctrl + +from tornado.web import HTTPError +import tornado.web + + +class RequestHandler(tornado.web.RequestHandler): + def __init__(self, app, request, **kwargs): + super().__init__(app, request, **kwargs) + self.app = app + + + def get_ctrl(self): return self.app.get_ctrl(self.get_cookie('client-id')) + def get_log(self, name = 'API'): return self.get_ctrl().log.get(name) + + + def get_path(self, path = None, filename = None): + return self.get_ctrl().get_path(path, filename) + + + def get_upload(self, filename = None): + return self.get_ctrl().get_upload(filename) + + + # Override exception logging + def log_exception(self, typ, value, tb): + if (isinstance(value, HTTPError) and + value.status_code in (400, 404, 408)): return + + log = self.get_log() + log.set_level(bbctrl.log.DEBUG) + + log.error(str(value)) + trace = ''.join(traceback.format_exception(typ, value, tb)) + log.debug(trace) diff --git a/src/py/bbctrl/State.py b/src/py/bbctrl/State.py index d00b324..bf1e0ab 100644 --- a/src/py/bbctrl/State.py +++ b/src/py/bbctrl/State.py @@ -25,7 +25,6 @@ # # ################################################################################ -import logging import traceback import copy import uuid @@ -33,12 +32,11 @@ import os import bbctrl -log = logging.getLogger('State') - - class State(object): def __init__(self, ctrl): self.ctrl = ctrl + self.log = ctrl.log.get('State') + self.callbacks = {} self.changes = {} self.listeners = [] @@ -51,6 +49,8 @@ class State(object): 'tool': 0, 'feed': 0, 'speed': 0, + 'sid': str(uuid.uuid4()), + 'demo': ctrl.args.demo, } # Add computed variable callbacks for each motor. @@ -74,8 +74,6 @@ class State(object): self.set_callback('metric', lambda name: 1 if self.is_metric() else 0) self.set_callback('imperial', lambda name: 0 if self.is_metric() else 1) - self.set('sid', str(uuid.uuid4())) - self.reset() self.load_files() @@ -96,10 +94,16 @@ class State(object): def load_files(self): self.files = [] - if os.path.exists('upload'): - for path in os.listdir('upload'): - if os.path.isfile('upload/' + path): - self.files.append(path) + upload = self.ctrl.get_upload() + + 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): + self.files.append(path) self.files.sort() self.set('files', self.files) @@ -144,7 +148,7 @@ class State(object): listener(self.changes) except Exception as e: - log.warning('Updating state listener: %s', + self.log.warning('Updating state listener: %s', traceback.format_exc()) self.changes = {} @@ -186,7 +190,9 @@ class State(object): if name in self.vars: return self.vars[name] if name in self.callbacks: return self.callbacks[name](name) - if default is None: log.warning('State variable "%s" not found' % name) + if default is None: + self.log.warning('State variable "%s" not found' % name) + return default @@ -318,13 +324,12 @@ class State(object): mode = self.motor_homing_mode(motor) - if mode == 'manual': return 'Configured for manual homing' - - if mode == 'switch-min' and not int(self.get(axis + '_ls', 0)): - return 'Configured for min switch but switch is disabled' + if mode != 'manual': + if mode == 'switch-min' and not int(self.get(axis + '_ls', 0)): + return 'Configured for min switch but switch is disabled' - if mode == 'switch-max' and not int(self.get(axis + '_xs', 0)): - return 'Configured for max switch but switch is disabled' + if mode == 'switch-max' and not int(self.get(axis + '_xs', 0)): + return 'Configured for max switch but switch is disabled' softMin = int(self.get(axis + '_tn', 0)) softMax = int(self.get(axis + '_tm', 0)) diff --git a/src/py/bbctrl/Web.py b/src/py/bbctrl/Web.py index 5e30ee8..199801e 100644 --- a/src/py/bbctrl/Web.py +++ b/src/py/bbctrl/Web.py @@ -30,7 +30,6 @@ import sys import json import tornado import sockjs.tornado -import logging import datetime import shutil import tarfile @@ -43,8 +42,6 @@ from tornado import web, gen import bbctrl -log = logging.getLogger('Web') - def call_get_output(cmd): p = subprocess.Popen(cmd, stdout = subprocess.PIPE) @@ -81,18 +78,13 @@ def check_password(password): class RebootHandler(bbctrl.APIHandler): def put_ok(self): - self.ctrl.lcd.goodbye('Rebooting...') + self.get_ctrl().lcd.goodbye('Rebooting...') subprocess.Popen('reboot') -class LogHandler(tornado.web.RequestHandler): - def __init__(self, app, request, **kwargs): - super().__init__(app, request, **kwargs) - self.filename = app.ctrl.args.log - - +class LogHandler(bbctrl.RequestHandler): def get(self): - with open(self.filename, 'r') as f: + with open(self.get_ctrl().log.get_path(), 'r') as f: self.write(f.read()) @@ -103,12 +95,7 @@ class LogHandler(tornado.web.RequestHandler): self.set_header('Content-Type', 'text/plain') -class BugReportHandler(tornado.web.RequestHandler): - def __init__(self, app, request, **kwargs): - super().__init__(app, request, **kwargs) - self.app = app - - +class BugReportHandler(bbctrl.RequestHandler): def get(self): import tarfile, io @@ -123,12 +110,14 @@ class BugReportHandler(tornado.web.RequestHandler): def check_add_basename(path): check_add(path, os.path.basename(path)) - check_add_basename(self.app.ctrl.args.log) - check_add_basename(self.app.ctrl.args.log + '.1') - check_add_basename(self.app.ctrl.args.log + '.2') - check_add_basename(self.app.ctrl.args.log + '.3') + ctrl = self.get_ctrl() + path = ctrl.log.get_path() + check_add_basename(path) + check_add_basename(path + '.1') + check_add_basename(path + '.2') + check_add_basename(path + '.3') check_add('config.json') - check_add('upload/' + self.app.ctrl.state.get('selected', '')) + check_add(ctrl.get_upload(ctrl.state.get('selected', ''))) tar.close() @@ -147,6 +136,9 @@ class HostnameHandler(bbctrl.APIHandler): def get(self): self.write_json(socket.gethostname()) def put(self): + if self.get_ctrl().args.demo: + raise HTTPError(400, 'Cannot set hostname in demo mode') + if 'hostname' in self.json: if subprocess.call(['/usr/local/bin/sethostname', self.json['hostname'].strip()]) == 0: @@ -164,7 +156,11 @@ class WifiHandler(bbctrl.APIHandler): except: pass self.write_json(data) + def put(self): + if self.get_ctrl().args.demo: + raise HTTPError(400, 'Cannot configure WiFi in demo mode') + if 'mode' in self.json: cmd = ['config-wifi', '-r'] mode = self.json['mode'] @@ -193,12 +189,18 @@ class UsernameHandler(bbctrl.APIHandler): def put_ok(self): + if self.get_ctrl().args.demo: + raise HTTPError(400, 'Cannot set username in demo mode') + if 'username' in self.json: set_username(self.json['username']) else: raise HTTPError(400, 'Missing "username"') class PasswordHandler(bbctrl.APIHandler): def put(self): + if self.get_ctrl().args.demo: + raise HTTPError(400, 'Cannot set password in demo mode') + if 'current' in self.json and 'password' in self.json: check_password(self.json['current']) @@ -218,7 +220,7 @@ class PasswordHandler(bbctrl.APIHandler): class ConfigLoadHandler(bbctrl.APIHandler): - def get(self): self.write_json(self.ctrl.config.load()) + def get(self): self.write_json(self.get_ctrl().config.load()) class ConfigDownloadHandler(bbctrl.APIHandler): @@ -230,15 +232,15 @@ class ConfigDownloadHandler(bbctrl.APIHandler): 'attachment; filename="%s"' % filename) def get(self): - self.write_json(self.ctrl.config.load(), pretty = True) + self.write_json(self.get_ctrl().config.load(), pretty = True) class ConfigSaveHandler(bbctrl.APIHandler): - def put_ok(self): self.ctrl.config.save(self.json) + def put_ok(self): self.get_ctrl().config.save(self.json) class ConfigResetHandler(bbctrl.APIHandler): - def put_ok(self): self.ctrl.config.reset() + def put_ok(self): self.get_ctrl().config.reset() class FirmwareUpdateHandler(bbctrl.APIHandler): @@ -261,31 +263,31 @@ class FirmwareUpdateHandler(bbctrl.APIHandler): with open('firmware/update.tar.bz2', 'wb') as f: f.write(firmware['body']) - self.ctrl.lcd.goodbye('Upgrading firmware') + self.get_ctrl().lcd.goodbye('Upgrading firmware') subprocess.Popen(['/usr/local/bin/update-bbctrl']) class UpgradeHandler(bbctrl.APIHandler): def put_ok(self): check_password(self.json['password']) - self.ctrl.lcd.goodbye('Upgrading firmware') + self.get_ctrl().lcd.goodbye('Upgrading firmware') subprocess.Popen(['/usr/local/bin/upgrade-bbctrl']) class PathHandler(bbctrl.APIHandler): @gen.coroutine def get(self, filename, dataType, *args): - if not os.path.exists('upload/' + filename): + if not os.path.exists(self.get_upload(filename)): raise HTTPError(404, 'File not found') - future = self.ctrl.preplanner.get_plan(filename) + future = self.get_ctrl().preplanner.get_plan(filename) try: delta = datetime.timedelta(seconds = 1) data = yield gen.with_timeout(delta, future) except gen.TimeoutError: - progress = self.ctrl.preplanner.get_plan_progress(filename) + progress = self.get_ctrl().preplanner.get_plan_progress(filename) self.write_json(dict(progress = progress)) return @@ -323,80 +325,81 @@ class HomeHandler(bbctrl.APIHandler): if not 'position' in self.json: raise HTTPError(400, 'Missing "position"') - self.ctrl.mach.home(axis, self.json['position']) + self.get_ctrl().mach.home(axis, self.json['position']) - elif action == '/clear': self.ctrl.mach.unhome(axis) - else: self.ctrl.mach.home(axis) + elif action == '/clear': self.get_ctrl().mach.unhome(axis) + else: self.get_ctrl().mach.home(axis) class StartHandler(bbctrl.APIHandler): - def put_ok(self): self.ctrl.mach.start() + def put_ok(self): self.get_ctrl().mach.start() class EStopHandler(bbctrl.APIHandler): - def put_ok(self): self.ctrl.mach.estop() + def put_ok(self): self.get_ctrl().mach.estop() class ClearHandler(bbctrl.APIHandler): - def put_ok(self): self.ctrl.mach.clear() + def put_ok(self): self.get_ctrl().mach.clear() class StopHandler(bbctrl.APIHandler): - def put_ok(self): self.ctrl.mach.stop() + def put_ok(self): self.get_ctrl().mach.stop() class PauseHandler(bbctrl.APIHandler): - def put_ok(self): self.ctrl.mach.pause() + def put_ok(self): self.get_ctrl().mach.pause() class UnpauseHandler(bbctrl.APIHandler): - def put_ok(self): self.ctrl.mach.unpause() + def put_ok(self): self.get_ctrl().mach.unpause() class OptionalPauseHandler(bbctrl.APIHandler): - def put_ok(self): self.ctrl.mach.optional_pause() + def put_ok(self): self.get_ctrl().mach.optional_pause() class StepHandler(bbctrl.APIHandler): - def put_ok(self): self.ctrl.mach.step() + def put_ok(self): self.get_ctrl().mach.step() class PositionHandler(bbctrl.APIHandler): def put_ok(self, axis): - self.ctrl.mach.set_position(axis, float(self.json['position'])) + self.get_ctrl().mach.set_position(axis, float(self.json['position'])) class OverrideFeedHandler(bbctrl.APIHandler): - def put_ok(self, value): self.ctrl.mach.override_feed(float(value)) + def put_ok(self, value): self.get_ctrl().mach.override_feed(float(value)) class OverrideSpeedHandler(bbctrl.APIHandler): - def put_ok(self, value): self.ctrl.mach.override_speed(float(value)) + def put_ok(self, value): self.get_ctrl().mach.override_speed(float(value)) class ModbusReadHandler(bbctrl.APIHandler): - def put_ok(self): self.ctrl.mach.modbus_read(int(self.json['address'])) + def put_ok(self): + self.get_ctrl().mach.modbus_read(int(self.json['address'])) class ModbusWriteHandler(bbctrl.APIHandler): def put_ok(self): - self.ctrl.mach.modbus_write(int(self.json['address']), + self.get_ctrl().mach.modbus_write(int(self.json['address']), int(self.json['value'])) class JogHandler(bbctrl.APIHandler): - def put_ok(self): self.ctrl.mach.jog(self.json) + def put_ok(self): self.get_ctrl().mach.jog(self.json) # Base class for Web Socket connections class ClientConnection(object): - def __init__(self, ctrl): - self.ctrl = ctrl + def __init__(self, app): + self.app = app self.count = 0 def heartbeat(self): - self.timer = self.ctrl.ioloop.call_later(3, self.heartbeat) + self.timer = self.app.ioloop.call_later(3, self.heartbeat) self.send({'heartbeat': self.count}) self.count += 1 @@ -404,17 +407,19 @@ class ClientConnection(object): def send(self, msg): raise HTTPError(400, 'Not implemented') - def on_open(self, *args, **kwargs): + def on_open(self, id = None): + self.ctrl = self.app.get_ctrl(id) + self.ctrl.state.add_listener(self.send) - self.ctrl.msgs.add_listener(self.send) + self.ctrl.log.add_listener(self.send) self.is_open = True self.heartbeat() def on_close(self): - self.ctrl.ioloop.remove_timeout(self.timer) + self.app.ioloop.remove_timeout(self.timer) self.ctrl.state.remove_listener(self.send) - self.ctrl.msgs.remove_listener(self.send) + self.ctrl.log.remove_listener(self.send) self.is_open = False @@ -424,7 +429,7 @@ class ClientConnection(object): # Used by CAMotics class WSConnection(ClientConnection, tornado.websocket.WebSocketHandler): def __init__(self, app, request, **kwargs): - ClientConnection.__init__(self, app.ctrl) + ClientConnection.__init__(self, app) tornado.websocket.WebSocketHandler.__init__( self, app, request, **kwargs) @@ -435,7 +440,7 @@ class WSConnection(ClientConnection, tornado.websocket.WebSocketHandler): # Used by Web frontend class SockJSConnection(ClientConnection, sockjs.tornado.SockJSConnection): def __init__(self, session): - ClientConnection.__init__(self, session.server.ctrl) + ClientConnection.__init__(self, session.server.app) sockjs.tornado.SockJSConnection.__init__(self, session) @@ -446,6 +451,18 @@ class SockJSConnection(ClientConnection, sockjs.tornado.SockJSConnection): self.close() + def on_open(self, info): + cookie = info.get_cookie('client-id') + if cookie is None: self.send(dict(sid = '')) # Trigger client reset + else: + id = cookie.value + + ip = info.ip + if 'X-Real-IP' in info.headers: ip = info.headers['X-Real-IP'] + self.app.get_ctrl(id).log.get('Web').info('Connection from %s' % ip) + super().on_open(id) + + class StaticFileHandler(tornado.web.StaticFileHandler): def set_extra_headers(self, path): self.set_header('Cache-Control', @@ -453,8 +470,13 @@ class StaticFileHandler(tornado.web.StaticFileHandler): class Web(tornado.web.Application): - def __init__(self, ctrl): - self.ctrl = ctrl + def __init__(self, args, ioloop): + self.args = args + self.ioloop = ioloop + self.ctrls = {} + + # Init controller + if not self.args.demo: self.get_ctrl() handlers = [ (r'/websocket', WSConnection), @@ -491,25 +513,55 @@ class Web(tornado.web.Application): (r'/api/video', bbctrl.VideoHandler), (r'/(.*)', StaticFileHandler, {'path': bbctrl.get_resource('http/'), - "default_filename": "index.html"}), + 'default_filename': 'index.html'}), ] router = sockjs.tornado.SockJSRouter(SockJSConnection, '/sockjs') - router.ctrl = ctrl + router.app = self tornado.web.Application.__init__(self, router.urls + handlers) try: - self.listen(ctrl.args.port, address = ctrl.args.addr) + self.listen(args.port, address = args.addr) except Exception as e: - log.error('Failed to bind %s:%d: %s', ctrl.args.addr, - ctrl.args.port, e) - sys.exit(1) + raise Exception('Failed to bind %s:%d: %s' % ( + args.addr, args.port, e)) + + print('Listening on http://%s:%d/' % (args.addr, args.port)) + + self._reap_ctrls() + + + def _reap_ctrls(self): + if not self.args.demo: return + now = time.time() + + for id in list(self.ctrls.keys()): + ctrl = self.ctrls[id] + + if ctrl.lastTS + self.args.client_timeout < now: + ctrl.close() + del self.ctrls[id] + + self.ioloop.call_later(60, self._reap_ctrls) + + + def get_ctrl(self, id = None): + if not id or not self.args.demo: id = '' + + if not id in self.ctrls: + ctrl = bbctrl.Ctrl(self.args, self.ioloop, id) + self.ctrls[id] = ctrl + + else: ctrl = self.ctrls[id] + + ctrl.lastTS = time.time() - log.info('Listening on http://%s:%d/', ctrl.args.addr, ctrl.args.port) + return ctrl # Override default logger def log_request(self, handler): + log = self.get_ctrl(handler.get_cookie('client-id')).log.get('Web') log.info("%d %s", handler.get_status(), handler._request_summary()) diff --git a/src/py/bbctrl/__init__.py b/src/py/bbctrl/__init__.py index 70f63be..70f8e07 100644 --- a/src/py/bbctrl/__init__.py +++ b/src/py/bbctrl/__init__.py @@ -32,15 +32,12 @@ import sys import signal import tornado import argparse -import logging -import datetime -import pkg_resources from pkg_resources import Requirement, resource_filename +from bbctrl.RequestHandler import RequestHandler from bbctrl.APIHandler import APIHandler from bbctrl.FileHandler import FileHandler -from bbctrl.GCodeStream import GCodeStream from bbctrl.Config import Config from bbctrl.LCD import LCD, LCDPage from bbctrl.Mach import Mach @@ -52,7 +49,6 @@ from bbctrl.I2C import I2C from bbctrl.Planner import Planner from bbctrl.Preplanner import Preplanner from bbctrl.State import State -from bbctrl.Messages import Messages from bbctrl.Comm import Comm from bbctrl.CommandQueue import CommandQueue from bbctrl.MainLCDPage import MainLCDPage @@ -60,8 +56,10 @@ from bbctrl.IPLCDPage import IPLCDPage from bbctrl.Camera import Camera, VideoHandler from bbctrl.AVR import AVR from bbctrl.AVREmu import AVREmu +from bbctrl.IOLoop import IOLoop import bbctrl.Cmd as Cmd import bbctrl.v4l2 as v4l2 +import bbctrl.Log as log ctrl = None @@ -74,7 +72,7 @@ def get_resource(path): def on_exit(sig = 0, func = None): global ctrl - logging.info('Exit handler triggered: signal = %d', sig) + print('Exit handler triggered: signal = %d', sig) if ctrl is not None: ctrl.close() @@ -83,11 +81,6 @@ def on_exit(sig = 0, func = None): sys.exit(1) -def log_time(log, ioloop): - log.info(datetime.datetime.now().strftime('%Y/%m/%d %H:%M:%S')) - ioloop.call_later(60 * 60, log_time, log, ioloop) - - def parse_args(): parser = argparse.ArgumentParser( description = 'Buildbotics Machine Controller') @@ -126,6 +119,8 @@ def parse_args(): help = 'Enter demo mode') 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() @@ -135,45 +130,19 @@ def run(): args = parse_args() - # Init logging - root = logging.getLogger() - level = logging.DEBUG if args.verbose else logging.INFO - root.setLevel(logging.NOTSET) - f = logging.Formatter('{levelname[0]}:{name}:{message}', style = '{') - h = logging.StreamHandler() - h.setLevel(level) - h.setFormatter(f) - root.addHandler(h) - - if args.log: - h = logging.handlers.RotatingFileHandler(args.log, maxBytes = 1000000, - backupCount = 5) - h.setLevel(level) - h.setFormatter(f) - root.addHandler(h) - - # Log header - version = pkg_resources.require('bbctrl')[0].version - root.info('Log started v%s' % version) - # Set signal handler signal.signal(signal.SIGTERM, on_exit) # Create ioloop ioloop = tornado.ioloop.IOLoop.current() - # Write time to log periodically - log_time(root, ioloop) - - # Start controller - ctrl = Ctrl(args, ioloop) + # Start server + web = Web(args, ioloop) try: ioloop.start() except KeyboardInterrupt: on_exit() - except SystemExit: raise - except: logging.getLogger().exception('') if __name__ == '__main__': run() diff --git a/src/resources/buildbotics.nc b/src/resources/buildbotics.nc new file mode 100644 index 0000000..8c5ddb0 --- /dev/null +++ b/src/resources/buildbotics.nc @@ -0,0 +1,403 @@ +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 diff --git a/src/resources/images/in-use.jpg b/src/resources/images/in-use.jpg index 17c43e6..c82090f 100644 Binary files a/src/resources/images/in-use.jpg and b/src/resources/images/in-use.jpg differ diff --git a/src/resources/images/offline.jpg b/src/resources/images/offline.jpg index 86137b5..1a4399b 100644 Binary files a/src/resources/images/offline.jpg and b/src/resources/images/offline.jpg differ diff --git a/src/stylus/style.styl b/src/stylus/style.styl index 6fd5abf..5320428 100644 --- a/src/stylus/style.styl +++ b/src/stylus/style.styl @@ -41,7 +41,8 @@ tt float right margin 5px - .video img + .video + position relative float right width 174px height 130px @@ -57,6 +58,35 @@ tt margin 5px 0 height inherit + .crosshair + > * + border 1px dashed #ccc + position absolute + + .vertical + height 100% + width 0 + left 50% + margin-left -1px + + .horizontal + height 0 + width 100% + top 50% + margin-top -1px + + .box + width 16px + height 16px + top 50% + left 50% + margin-top -9px + margin-left -9px + + img + width 100% + height 100% + .banner float left padding-top 40px @@ -661,10 +691,6 @@ tr.log-debug td background-color transparent color orange -.tab-content .video - text-align center - min-height 300px - tt.save display inline-block border-radius 2px