From 312980dea169360d69865758f8bd13725cd1c6eb Mon Sep 17 00:00:00 2001 From: Joseph Coffland Date: Mon, 15 Oct 2018 01:38:18 -0700 Subject: [PATCH] Check axis fit and display errors/warnings. --- CHANGELOG.md | 2 + src/js/axis-vars.js | 184 ++++++++++++++++++++++++ src/js/control-view.js | 92 +++--------- src/js/path-viewer.js | 216 +++++++++++++++++------------ src/pug/templates/control-view.pug | 43 +++--- src/py/bbctrl/CommandQueue.py | 2 +- src/py/bbctrl/FileHandler.py | 6 + src/py/bbctrl/PlanTimer.py | 76 +++++----- src/py/bbctrl/Preplanner.py | 81 +++++++++-- src/py/bbctrl/State.py | 3 +- src/py/bbctrl/Web.py | 3 +- src/stylus/style.styl | 14 ++ 12 files changed, 489 insertions(+), 233 deletions(-) create mode 100644 src/js/axis-vars.js diff --git a/CHANGELOG.md b/CHANGELOG.md index b022887..03d705b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,8 @@ Buildbotics CNC Controller Firmware Changelog - Display accurate time remaining, ETA and progress during run. - Automatically collapase moves in planner which are too short in time. - Show IO status indicators on configuration pages. + - Check that axis dimensions fit path plan dimensions. + - Show machine working envelope in path plan viewer. ## v0.3.28 - Show step rate on motor configuration page. diff --git a/src/js/axis-vars.js b/src/js/axis-vars.js new file mode 100644 index 0000000..ffa0d4f --- /dev/null +++ b/src/js/axis-vars.js @@ -0,0 +1,184 @@ +/******************************************************************************\ + + 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" + +\******************************************************************************/ + +'use strict' + + +function is_defined(x) {return typeof x != 'undefined'} + + +module.exports = { + props: ['state', 'config'], + + + computed: { + x: function () {return this._compute_axis('x')}, + y: function () {return this._compute_axis('y')}, + z: function () {return this._compute_axis('z')}, + a: function () {return this._compute_axis('a')}, + b: function () {return this._compute_axis('b')}, + c: function () {return this._compute_axis('c')}, + axes: function () {return this._compute_axes()} + }, + + + methods: { + _convert_length: function (value) { + return this.state.imperial ? value / 25.4 : value; + }, + + + _length_str: function (value) { + return this._convert_length(value).toLocaleString() + + (this.state.imperial ? ' in' : ' mm'); + }, + + + _compute_axis: function (axis) { + var abs = this.state[axis + 'p'] || 0; + var off = this.state['offset_' + axis]; + var motor_id = this._get_motor_id(axis); + var motor = motor_id == -1 ? {} : this.config.motors[motor_id]; + var pm = motor['power-mode']; + var enabled = typeof pm != 'undefined' && pm != 'disabled'; + var homingMode = motor['homing-mode'] + var homed = this.state[motor_id + 'homed']; + var min = this.state[motor_id + 'tn']; + var max = this.state[motor_id + 'tm']; + var dim = max - min; + var pathMin = this.state['path_min_' + axis]; + var pathMax = this.state['path_max_' + axis]; + var pathDim = pathMax - pathMin; + var under = pathMin - off < min; + var over = max < pathMax - off; + var klass = (homed ? 'homed' : 'unhomed') + ' axis-' + axis; + var state = 'UNHOMED'; + var icon = 'question-circle'; + var title; + + if (0 < dim && dim < pathDim) { + state = 'NO FIT'; + klass += ' error'; + icon = 'ban'; + + } else if (homed) { + state = 'HOMED' + icon = 'check-circle'; + + if (over || under) { + state = over ? 'OVER' : 'UNDER'; + klass += ' warn'; + icon = 'exclamation-circle'; + } + } + + switch (state) { + case 'UNHOMED': title = 'Click the home button to home axis.'; break; + case 'HOMED': title = 'Axis successfuly homed.'; break; + + case 'OVER': + title = 'Tool path would move ' + + this._length_str(pathMax - off - max) + ' beyond axis bounds.'; + break; + + case 'UNDER': + title = 'Tool path would move ' + + this._length_str(min - pathMin + off) + ' below axis bounds.'; + break; + + case 'NO FIT': + title = 'Tool path dimensions exceed axis dimensions by ' + + this._length_str(pathDim - dim) + '.'; + break; + } + + return { + pos: abs + off, + abs: abs, + off: off, + min: min, + max: max, + dim: dim, + pathMin: pathMin, + pathMax: pathMax, + pathDim: pathDim, + motor: motor_id, + enabled: enabled, + homingMode: homingMode, + homed: homed, + klass: klass, + state: state, + icon: icon, + title: title + } + }, + + + _get_motor_id: function (axis) { + for (var i = 0; i < this.config.motors.length; i++) { + var motor = this.config.motors[i]; + if (motor.axis.toLowerCase() == axis) return i; + } + + return -1; + }, + + + _compute_axes: function () { + var homed = false; + + for (var name of 'xyzabc') { + var axis = this[name]; + + if (!axis.enabled) continue + if (!axis.homed) {homed = false; break} + homed = true; + } + + var error = false; + var warn = false; + + if (homed) + for (name of 'xyzabc') { + axis = this[name]; + + if (!axis.enabled) continue; + if (axis.klass.indexOf('error') != -1) error = true; + if (axis.klass.indexOf('warn') != -1) warn = true; + } + + var klass = homed ? 'homed' : 'unhomed'; + if (error) klass += ' error'; + else if (warn) klass += ' warn'; + + return { + homed: homed, + klass: klass + } + } + } +} diff --git a/src/js/control-view.js b/src/js/control-view.js index ea3daba..87065c4 100644 --- a/src/js/control-view.js +++ b/src/js/control-view.js @@ -181,57 +181,6 @@ module.exports = { send: function (msg) {this.$dispatch('send', msg)}, - get_axis_motor_id: function (axis) { - axis = axis.toLowerCase(); - - for (var i = 0; i < this.config.motors.length; i++) { - var motor = this.config.motors[i]; - if (motor.axis.toLowerCase() == axis) return i; - } - - return -1; - }, - - - get_axis_motor: function (axis) { - var motor = this.get_axis_motor_id(axis); - if (motor != -1) return this.config.motors[motor]; - }, - - - get_axis_motor_param: function (axis, name) { - var motor = this.get_axis_motor(axis); - if (typeof motor != 'undefined') return motor[name]; - }, - - - enabled: function (axis) { - var pm = this.get_axis_motor_param(axis, 'power-mode') - return typeof pm != 'undefined' && pm != 'disabled'; - }, - - - is_homed: function (axis) { - if (typeof axis == 'undefined') { - var enabled = false; - var axes = 'xyzabc'; - - for (var i in axes) { - if (this.enabled(axes.charAt(i))) { - if (!this.is_homed(axes.charAt(i))) return false; - else enabled = true; - } - } - - return enabled; - - } else { - var motor = this.get_axis_motor_id(axis); - return motor != -1 && this.state[motor + 'homed']; - } - }, - - update: function () { // Update file list api.get('file').done(function (files) { @@ -289,11 +238,7 @@ module.exports = { load_history: function (index) {this.mdi = this.history[index];}, - - - open: function (e) { - $('.gcode-file-input').click(); - }, + open: function (e) {$('.gcode-file-input').click()}, upload: function (e) { @@ -313,14 +258,14 @@ module.exports = { }, - deleteCurrent: function () { + delete_current: function () { if (this.state.selected) api.delete('file/' + this.state.selected).done(this.update); this.deleteGCode = false; }, - deleteAll: function () { + delete_all: function () { api.delete('file').done(this.update); this.deleteGCode = false; }, @@ -330,8 +275,7 @@ module.exports = { if (typeof axis == 'undefined') api.put('home'); else { - if (this.get_axis_motor_param(axis, 'homing-mode') != 'manual') - api.put('home/' + axis); + if (this[axis].homingMode != 'manual') api.put('home/' + axis); else this.manual_home[axis] = true; } }, @@ -355,28 +299,21 @@ module.exports = { }, - get_position: function (axis) { - return this.state[axis + 'p'] + this.get_offset(axis); - }, - - - get_offset: function (axis) {return this.state['offset_' + axis] || 0}, - - set_position: function (axis, position) { this.position_msg[axis] = false; api.put('position/' + axis, {'position': parseFloat(position)}); }, - zero: function (axis) { - if (typeof axis == 'undefined') { - var axes = 'xyzabc'; - for (var i in axes) - if (this.enabled(axes.charAt(i))) - this.zero(axes.charAt(i)); + zero_all: function () { + for (var axis of 'xyzabc') + if (this[axis].enabled) this.zero(axis); + }, + - } else this.set_position(axis, 0); + zero: function (axis) { + if (typeof axis == 'undefined') this.zero_all(); + else this.set_position(axis, 0); }, @@ -414,5 +351,8 @@ module.exports = { data[axis + 'pl'] = x; this.send(JSON.stringify(data)); } - } + }, + + + mixins: [require('./axis-vars')] } diff --git a/src/js/path-viewer.js b/src/js/path-viewer.js index 58df847..0ceed82 100644 --- a/src/js/path-viewer.js +++ b/src/js/path-viewer.js @@ -5,7 +5,7 @@ For information regarding this software email: Joseph Coffland - joseph@buildbotics.com + joseph@buildbotics.com This software is free software: you clan redistribute it and/or modify it under the terms of the GNU Lesser General Public License @@ -43,7 +43,7 @@ var surfaceModes = ['cut', 'wire', 'solid', 'off']; module.exports = { template: '#path-viewer-template', - props: ['toolpath', 'progress', 'state'], + props: ['toolpath', 'progress'], data: function () { @@ -63,26 +63,6 @@ module.exports = { computed: { - x: function () {return this.xAbs + this.xOff}, - y: function () {return this.yAbs + this.yOff}, - z: function () {return this.zAbs + this.zOff}, - - xAbs: function () {return this.state.xp || 0}, - yAbs: function () {return this.state.xy || 0}, - zAbs: function () {return this.state.xz || 0}, - - xOff: function () {return this.state.offset_x || 0}, - yOff: function () {return this.state.offset_y || 0}, - zOff: function () {return this.state.offset_z || 0}, - - xMin: function () {return this.state.xtn}, - yMin: function () {return this.state.ytn}, - zMin: function () {return this.state.ztn}, - - xMax: function () {return this.state.xtm}, - yMax: function () {return this.state.ytm}, - zMax: function () {return this.state.ztm}, - hasPath: function () {return typeof this.toolpath.path != 'undefined'}, target: function () {return $(this.$el).find('.path-viewer-content')[0]} }, @@ -90,15 +70,22 @@ module.exports = { watch: { toolpath: function () {Vue.nextTick(this.update)}, - surfaceMode: function (mode) {this.updateSurfaceMode(mode)}, + surfaceMode: function (mode) {this.update_surface_mode(mode)}, small: function () {Vue.nextTick(this.update_view)}, showPath: function (enable) {set_visible(this.path, enable)}, showTool: function (enable) {set_visible(this.tool, enable)}, - showBBox: function (enable) {set_visible(this.bbox, enable)}, showAxes: function (enable) {set_visible(this.axes, enable)}, - x: function () {this.update_tool()}, - y: function () {this.update_tool()}, - z: function () {this.update_tool()} + + + showBBox: function (enable) { + set_visible(this.bbox, enable); + set_visible(this.envelope, enable); + }, + + + x: function () {this.axis_changed()}, + y: function () {this.axis_changed()}, + z: function () {this.axis_changed()} }, @@ -128,7 +115,7 @@ module.exports = { }, - updateSurfaceMode: function (mode) { + update_surface_mode: function (mode) { if (!this.enabled) return; if (typeof this.surfaceMaterial != 'undefined') { @@ -159,7 +146,7 @@ module.exports = { }, - getDims: function () { + get_dims: function () { var t = $(this.target); var width = t.innerWidth(); var height = t.innerHeight(); @@ -169,7 +156,7 @@ module.exports = { update_view: function () { if (!this.enabled) return; - var dims = this.getDims(); + var dims = this.get_dims(); this.camera.aspect = dims.width / dims.height; this.camera.updateProjectionMatrix(); @@ -181,9 +168,34 @@ module.exports = { if (!this.enabled) return; if (typeof tool == 'undefined') tool = this.tool; if (typeof tool == 'undefined') return; - tool.position.x = this.x; - tool.position.y = this.y; - tool.position.z = this.z; + tool.position.x = this.x.pos; + tool.position.y = this.y.pos; + tool.position.z = this.z.pos; + }, + + + update_envelope: function (envelope) { + if (!this.enabled || !this.axes.homed) return; + if (typeof envelope == 'undefined') envelope = this.envelope; + if (typeof envelope == 'undefined') return; + + var min = new THREE.Vector3(); + var max = new THREE.Vector3(); + + for (var axis of 'xyz') { + min[axis] = this[axis].min + this[axis].off; + max[axis] = this[axis].max + this[axis].off; + } + + var bounds = new THREE.Box3(min, max); + if (bounds.isEmpty()) envelope.geometry = this.create_empty_geom(); + else envelope.geometry = this.create_bbox_geom(bounds); + }, + + + axis_changed: function () { + this.update_tool(); + this.update_envelope(); }, @@ -227,7 +239,7 @@ module.exports = { this.lights.add(backLight); // Surface material - this.surfaceMaterial = this.createSurfaceMaterial(); + this.surfaceMaterial = this.create_surface_material(); // Controls this.controls = new orbit(this.camera, this.renderer.domElement); @@ -257,7 +269,7 @@ module.exports = { }, - createSurfaceMaterial: function () { + create_surface_material: function () { return new THREE.MeshPhongMaterial({ specular: 0x111111, shininess: 10, @@ -267,7 +279,7 @@ module.exports = { }, - drawWorkpiece: function (scene, material) { + draw_workpiece: function (scene, material) { if (typeof this.workpiece == 'undefined') return; var min = this.workpiece.min; @@ -294,7 +306,7 @@ module.exports = { }, - drawSurface: function (scene, material) { + draw_surface: function (scene, material) { if (typeof this.vertices == 'undefined') return; var geometry = new THREE.BufferGeometry(); @@ -311,7 +323,7 @@ module.exports = { }, - drawTool: function (scene, bbox) { + draw_tool: function (scene, bbox) { // Tool size is relative to bounds var size = bbox.getSize(new THREE.Vector3()); var length = (size.x + size.y + size.z) / 24; @@ -335,7 +347,7 @@ module.exports = { }, - drawAxis: function (axis, up, length, radius) { + draw_axis: function (axis, up, length, radius) { var color; if (axis == 0) color = 0xff0000; // Red @@ -362,7 +374,7 @@ module.exports = { }, - drawAxes: function (scene, bbox) { + draw_axes: function (scene, bbox) { var size = bbox.getSize(new THREE.Vector3()); var length = (size.x + size.y + size.z) / 3; length /= 10; @@ -372,7 +384,7 @@ module.exports = { for (var axis = 0; axis < 3; axis++) for (var up = 0; up < 2; up++) - group.add(this.drawAxis(axis, up, length, radius)); + group.add(this.draw_axis(axis, up, length, radius)); group.visible = this.showAxes; scene.add(group); @@ -381,13 +393,13 @@ module.exports = { }, - drawPath: function (scene) { + draw_path: function (scene) { var cutting = [0, 1, 0]; var rapid = [1, 0, 0]; - var x = this.x; - var y = this.y; - var z = this.z; + var x = this.x.pos; + var y = this.y.pos; + var z = this.z.pos; var color = undefined; var positions = []; @@ -436,48 +448,62 @@ module.exports = { }, - drawBBox: function (scene, bbox) { - if (bbox.isEmpty()) return; + create_empty_geom: function () { + var geometry = new THREE.BufferGeometry(); + geometry.addAttribute('position', + new THREE.Float32BufferAttribute([], 3)); + return geometry; + }, + + create_bbox_geom: function (bbox) { var vertices = []; - // Top - vertices.push(bbox.min.x, bbox.min.y, bbox.min.z); - vertices.push(bbox.max.x, bbox.min.y, bbox.min.z); - vertices.push(bbox.max.x, bbox.min.y, bbox.min.z); - vertices.push(bbox.max.x, bbox.min.y, bbox.max.z); - vertices.push(bbox.max.x, bbox.min.y, bbox.max.z); - vertices.push(bbox.min.x, bbox.min.y, bbox.max.z); - vertices.push(bbox.min.x, bbox.min.y, bbox.max.z); - vertices.push(bbox.min.x, bbox.min.y, bbox.min.z); - - // Bottom - vertices.push(bbox.min.x, bbox.max.y, bbox.min.z); - vertices.push(bbox.max.x, bbox.max.y, bbox.min.z); - vertices.push(bbox.max.x, bbox.max.y, bbox.min.z); - vertices.push(bbox.max.x, bbox.max.y, bbox.max.z); - vertices.push(bbox.max.x, bbox.max.y, bbox.max.z); - vertices.push(bbox.min.x, bbox.max.y, bbox.max.z); - vertices.push(bbox.min.x, bbox.max.y, bbox.max.z); - vertices.push(bbox.min.x, bbox.max.y, bbox.min.z); - - // Sides - vertices.push(bbox.min.x, bbox.min.y, bbox.min.z); - vertices.push(bbox.min.x, bbox.max.y, bbox.min.z); - vertices.push(bbox.max.x, bbox.min.y, bbox.min.z); - vertices.push(bbox.max.x, bbox.max.y, bbox.min.z); - vertices.push(bbox.max.x, bbox.min.y, bbox.max.z); - vertices.push(bbox.max.x, bbox.max.y, bbox.max.z); - vertices.push(bbox.min.x, bbox.min.y, bbox.max.z); - vertices.push(bbox.min.x, bbox.max.y, bbox.max.z); + if (!bbox.isEmpty()) { + // Top + vertices.push(bbox.min.x, bbox.min.y, bbox.min.z); + vertices.push(bbox.max.x, bbox.min.y, bbox.min.z); + vertices.push(bbox.max.x, bbox.min.y, bbox.min.z); + vertices.push(bbox.max.x, bbox.min.y, bbox.max.z); + vertices.push(bbox.max.x, bbox.min.y, bbox.max.z); + vertices.push(bbox.min.x, bbox.min.y, bbox.max.z); + vertices.push(bbox.min.x, bbox.min.y, bbox.max.z); + vertices.push(bbox.min.x, bbox.min.y, bbox.min.z); + + // Bottom + vertices.push(bbox.min.x, bbox.max.y, bbox.min.z); + vertices.push(bbox.max.x, bbox.max.y, bbox.min.z); + vertices.push(bbox.max.x, bbox.max.y, bbox.min.z); + vertices.push(bbox.max.x, bbox.max.y, bbox.max.z); + vertices.push(bbox.max.x, bbox.max.y, bbox.max.z); + vertices.push(bbox.min.x, bbox.max.y, bbox.max.z); + vertices.push(bbox.min.x, bbox.max.y, bbox.max.z); + vertices.push(bbox.min.x, bbox.max.y, bbox.min.z); + + // Sides + vertices.push(bbox.min.x, bbox.min.y, bbox.min.z); + vertices.push(bbox.min.x, bbox.max.y, bbox.min.z); + vertices.push(bbox.max.x, bbox.min.y, bbox.min.z); + vertices.push(bbox.max.x, bbox.max.y, bbox.min.z); + vertices.push(bbox.max.x, bbox.min.y, bbox.max.z); + vertices.push(bbox.max.x, bbox.max.y, bbox.max.z); + vertices.push(bbox.min.x, bbox.min.y, bbox.max.z); + vertices.push(bbox.min.x, bbox.max.y, bbox.max.z); + } var geometry = new THREE.BufferGeometry(); - var material = new THREE.LineBasicMaterial({color: 0xffffff}); geometry.addAttribute('position', new THREE.Float32BufferAttribute(vertices, 3)); - var line = new THREE.LineSegments(geometry, material) + return geometry; + }, + + + draw_bbox: function (scene, bbox) { + var geometry = this.create_bbox_geom(bbox); + var material = new THREE.LineBasicMaterial({color: 0xffffff}); + var line = new THREE.LineSegments(geometry, material); line.visible = this.showBBox; @@ -487,24 +513,39 @@ module.exports = { }, + draw_envelope: function (scene) { + var geometry = this.create_empty_geom(); + var material = new THREE.LineBasicMaterial({color: 0x00f7ff}); + var line = new THREE.LineSegments(geometry, material); + + line.visible = this.showBBox; + + scene.add(line); + this.update_envelope(line); + + return line; + }, + + draw: function (scene) { // Lights scene.add(this.ambient); scene.add(this.lights); // Model - this.path = this.drawPath(scene); - this.surfaceMesh = this.drawSurface(scene, this.surfaceMaterial); - this.workpieceMesh = this.drawWorkpiece(scene, this.surfaceMaterial); - this.updateSurfaceMode(this.surfaceMode); + this.path = this.draw_path(scene); + this.surfaceMesh = this.draw_surface(scene, this.surfaceMaterial); + this.workpieceMesh = this.draw_workpiece(scene, this.surfaceMaterial); + this.update_surface_mode(this.surfaceMode); // Compute bounding box var bbox = this.get_model_bounds(); // Tool, axes & bounds - this.tool = this.drawTool(scene, bbox); - this.axes = this.drawAxes(scene, bbox); - this.bbox = this.drawBBox(scene, bbox); + this.tool = this.draw_tool(scene, bbox); + this.axes = this.draw_axes(scene, bbox); + this.bbox = this.draw_bbox(scene, bbox); + this.envelope = this.draw_envelope(scene); }, @@ -614,5 +655,8 @@ module.exports = { this.camera.position.copy(offset.multiplyScalar(dist * 1.2).add(center)); } - } + }, + + + mixins: [require('./axis-vars')] } diff --git a/src/pug/templates/control-view.pug b/src/pug/templates/control-view.pug index d635264..5d9f928 100644 --- a/src/pug/templates/control-view.pug +++ b/src/pug/templates/control-view.pug @@ -28,11 +28,12 @@ script#control-view-template(type="text/x-template") #control table.axes - tr(:class="{'homed': is_homed()}") + tr(:class="axes.klass") th.name Axis th.position Position th.absolute Absolute th.offset Offset + th.state State th.actions button.pure-button(:disabled="!can_mdi", title="Zero all axis offsets.", @click="zero()") ∅ @@ -42,13 +43,16 @@ script#control-view-template(type="text/x-template") .fa.fa-home each axis in 'xyzabc' - tr.axis(:class=`{'homed': is_homed('${axis}'), 'axis-${axis}': true}`, - v-if=`enabled('${axis}')`) + tr.axis(:class=`${axis}.klass`, v-if=`${axis}.enabled`, + :title=`${axis}.title`) th.name= axis - td.position - unit-value(:value=`get_position('${axis}')`, precision="4") - td.absolute: unit-value(:value="state." + axis + "p", precision="3") - td.offset: unit-value(:value=`get_offset('${axis}')`, precision="3") + td.position: unit-value(:value=`${axis}.pos`, precision=4) + td.absolute: unit-value(:value=`${axis}.abs`, precision=3) + td.offset: unit-value(:value=`${axis}.off`, precision=3) + td.state + .fa(:class=`'fa-' + ${axis}.icon`) + | {{#{axis}.state}} + th.actions button.pure-button(:disabled="!can_mdi", title=`Set {{'${axis}' | upper}} axis position.`, @@ -59,9 +63,8 @@ script#control-view-template(type="text/x-template") title=`Zero {{'${axis}' | upper}} axis offset.`, @click=`zero('${axis}')`) ∅ - button.pure-button(:disabled="!can_mdi", - title=`Home {{'${axis}' | upper}} axis.`, - @click=`home('${axis}')`) + button.pure-button(:disabled="!can_mdi", @click=`home('${axis}')`, + title=`Home {{'${axis}' | upper}} axis.`) .fa.fa-home message(:show.sync=`position_msg['${axis}']`) @@ -79,7 +82,7 @@ script#control-view-template(type="text/x-template") button.pure-button(@click=`position_msg['${axis}'] = false`) | Cancel - button.pure-button(v-if="is_homed('" + axis + "')", + button.pure-button(v-if=`${axis}.homed`, @click=`unhome('${axis}')`) Unhome button.pure-button.button-success( @@ -239,10 +242,10 @@ script#control-view-template(type="text/x-template") p(slot="body") div(slot="footer") button.pure-button(@click="deleteGCode = false") Cancel - button.pure-button.button-error(@click="deleteAll") + button.pure-button.button-error(@click="delete_all") .fa.fa-trash |  all - button.pure-button.button-success(@click="deleteCurrent") + button.pure-button.button-success(@click="delete_current") .fa.fa-trash |  selected @@ -251,7 +254,7 @@ script#control-view-template(type="text/x-template") option(v-for="file in files", :value="file") {{file}} path-viewer(:toolpath="toolpath", :progress="toolpath_progress", - :state="state") + :state="state", :config="config") gcode-viewer section#content2.tab-content @@ -276,16 +279,16 @@ script#control-view-template(type="text/x-template") section#content3.tab-content .jog axis-control(axes="XY", :colors="['red', 'green']", - :enabled="[enabled('x'), enabled('y')]", - v-if="enabled('x') || enabled('y')", :adjust="jog_adjust") + :enabled="[x.enabled, y.enabled]", + v-if="x.enabled || y.enabled", :adjust="jog_adjust") axis-control(axes="AZ", :colors="['orange', 'blue']", - :enabled="[enabled('a'), enabled('z')]", - v-if="enabled('a') || enabled('z')", :adjust="jog_adjust") + :enabled="[a.enabled, z.enabled]", + v-if="a.enabled || z.enabled", :adjust="jog_adjust") axis-control(axes="BC", :colors="['cyan', 'purple']", - :enabled="[enabled('b'), enabled('c')]", - v-if="enabled('b') || enabled('c')", :adjust="jog_adjust") + :enabled="[b.enabled, c.enabled]", + v-if="b.enabled || c.enabled", :adjust="jog_adjust") .jog-adjust | Fine adjust diff --git a/src/py/bbctrl/CommandQueue.py b/src/py/bbctrl/CommandQueue.py index 59fecfa..b04ad63 100644 --- a/src/py/bbctrl/CommandQueue.py +++ b/src/py/bbctrl/CommandQueue.py @@ -29,7 +29,7 @@ import logging from collections import deque log = logging.getLogger('CmdQ') -log.setLevel(logging.INFO) +log.setLevel(logging.WARNING) class CommandQueue(): diff --git a/src/py/bbctrl/FileHandler.py b/src/py/bbctrl/FileHandler.py index 4b63c13..6d03fa5 100644 --- a/src/py/bbctrl/FileHandler.py +++ b/src/py/bbctrl/FileHandler.py @@ -28,6 +28,10 @@ import os import bbctrl import glob +import logging + + +log = logging.getLogger('FileHandler') def safe_remove(path): @@ -69,6 +73,8 @@ class FileHandler(bbctrl.APIHandler): self.ctrl.preplanner.invalidate(gcode['filename']) self.ctrl.state.set('selected', gcode['filename']) + log.info('GCode updated: ' + gcode['filename']) + def get(self, path): if path: diff --git a/src/py/bbctrl/PlanTimer.py b/src/py/bbctrl/PlanTimer.py index 4968fc4..05b9d60 100644 --- a/src/py/bbctrl/PlanTimer.py +++ b/src/py/bbctrl/PlanTimer.py @@ -35,19 +35,18 @@ log = logging.getLogger('PlanTimer') class PlanTimer(object): def __init__(self, ctrl): self.ctrl = ctrl + self.plan_times = None self.reset() - self._report() - self.ctrl.state.set('plan_time', 0) ctrl.state.add_listener(self._update) + self._report() def reset(self): self.plan_time = 0 self.move_start = None self.hold_start = None - self.plan_times = None self.plan_index = 0 @@ -66,42 +65,53 @@ class PlanTimer(object): self.ctrl.state.set('plan_time', round(t)) - self.timer = self.ctrl.ioloop.call_later(1, self._report) + self.ctrl.ioloop.call_later(1, self._report) - def _update(self, update): - # Check state - if 'xx' in update: - state = update['xx'] + def _update_state(self, state): + if state in ['READY', 'ESTOPPED']: + self.ctrl.state.set('plan_time', 0) + self.reset() - if state in ['READY', 'ESTOPPED']: - self.ctrl.state.set('plan_time', 0) - self.reset() + elif state == 'HOLDING': self.hold_start = time.time() + elif (state == 'RUNNING' and self.hold_start is not None and + self.move_start is not None): + self.move_start += time.time() - self.hold_start + self.hold_start = None - elif state == 'HOLDING': self.hold_start = time.time() - elif (state == 'RUNNING' and self.hold_start is not None and - self.move_start is not None): - self.move_start += time.time() - self.hold_start - self.hold_start = None - # Get plan times - if self.plan_times is None or 'selected' in update: - active_plan = self.ctrl.state.get('selected', '') + def _update_times(self, filename): + if not filename: return + future = self.ctrl.preplanner.get_plan(filename) + + def cb(future): + if (filename != self.ctrl.state.get('selected') or + future.cancelled()): return - if active_plan: - plan = self.ctrl.preplanner.get_plan(active_plan) + self.reset() + path, meta = future.result() + self.plan_times = meta['times'] - if plan is not None and plan.done(): - self.reset() - self.plan_times = plan.result()[1] + self.ctrl.ioloop.add_future(future, cb) + + + def _update_time(self, currentID): + if self.plan_times is None: return + + while self.plan_index < len(self.plan_times): + id, t = self.plan_times[self.plan_index] + if id <= currentID: self.move_start = time.time() + if currentID <= id: break + self.plan_time = t + self.plan_index += 1 + + + def _update(self, update): + # Check state + if 'xx' in update: self._update_state(update['xx']) + + # Get plan times + if 'selected' in update: self._update_times(update['selected']) # Get plan time for current id - if self.plan_times is not None and 'id' in update: - currentID = update['id'] - - while self.plan_index < len(self.plan_times): - id, t = self.plan_times[self.plan_index] - if id <= currentID: self.move_start = time.time() - if currentID <= id: break - self.plan_time = t - self.plan_index += 1 + if 'id' in update: self._update_time(update['id']) diff --git a/src/py/bbctrl/Preplanner.py b/src/py/bbctrl/Preplanner.py index 92e62a2..f506afc 100644 --- a/src/py/bbctrl/Preplanner.py +++ b/src/py/bbctrl/Preplanner.py @@ -32,6 +32,7 @@ import json import hashlib import gzip import glob +import math import threading from concurrent.futures import Future, ThreadPoolExecutor, TimeoutError from tornado import gen @@ -103,7 +104,9 @@ class Preplanner(object): self.max_plan_time = max_plan_time self.max_loop_time = max_loop_time - for dir in ['plans', 'times']: + ctrl.state.add_listener(self._update) + + for dir in ['plans', 'meta']: if not os.path.exists(dir): os.mkdir(dir) self.started = Future() @@ -113,6 +116,30 @@ class Preplanner(object): self.lock = threading.Lock() + def _update(self, update): + if not 'selected' in update: return + filename = update['selected'] + future = self.get_plan(filename) + + def set_bounds(type, bounds): + for axis in 'xyzabc': + if axis in bounds[type]: + self.ctrl.state.set('path_%s_%s' % (type, axis), + bounds[type][axis]) + + def cb(future): + if (filename != self.ctrl.state.get('selected') or + future.cancelled()): return + + path, meta = future.result() + bounds = meta['bounds'] + + set_bounds('min', bounds) + set_bounds('max', bounds) + + self.ctrl.ioloop.add_future(future, cb) + + def start(self): log.info('Preplanner started') self.started.set_result(True) @@ -202,13 +229,14 @@ class Preplanner(object): # Check if this plan was already run hid = plan_hash(filename, config) plan_path = 'plans/' + filename + '.' + hid + '.gz' - times_path = 'times/' + filename + '.' + hid + '.gz' + meta_path = 'meta/' + filename + '.' + hid + '.gz' try: - if os.path.exists(plan_path) and os.path.exists(times_path): + if os.path.exists(plan_path) and os.path.exists(meta_path): with open(plan_path, 'rb') as f: data = f.read() - with open(times_path, 'rb') as f: times = f.read() - return (data, json.loads(gzip.decompress(times).decode('utf8'))) + with open(meta_path, 'rb') as f: meta = f.read() + meta = json.loads(gzip.decompress(meta).decode('utf8')) + return (data, meta) except Exception as e: log.error(e) @@ -234,12 +262,24 @@ class Preplanner(object): maxLine = 0 maxLineTime = time.time() totalTime = 0 - position = dict(x = 0, y = 0, z = 0) + position = {} rapid = False moves = [] times = [] + bounds = dict(min = {}, max = {}) messages = [] count = 0 + cancelled = False + + for axis in 'xyzabc': + position[axis] = 0 + bounds['min'][axis] = math.inf + bounds['max'][axis] = -math.inf + + def add_to_bounds(axis, value): + if value < bounds['min'][axis]: bounds['min'][axis] = value + if bounds['max'][axis] < value: bounds['max'][axis] = value + levels = dict(I = 'info', D = 'debug', W = 'warning', E = 'error', C = 'critical') @@ -264,17 +304,18 @@ class Preplanner(object): if planner.is_synchronizing(): planner.synchronize(0) if cmd['type'] == 'line': - if not 'first' in cmd: - totalTime += sum(cmd['times']) / 1000 - times.append((cmd['id'], totalTime)) + if 'first' in cmd: continue + totalTime += sum(cmd['times']) / 1000 + times.append((cmd['id'], totalTime)) target = cmd['target'] move = {} - for axis in 'xyz': + for axis in 'xyzabc': if axis in target: position[axis] = target[axis] move[axis] = target[axis] + add_to_bounds(axis, move[axis]) if 'rapid' in cmd: move['rapid'] = cmd['rapid'] @@ -291,6 +332,7 @@ class Preplanner(object): times.append((cmd['id'], totalTime)) if not self._progress(filename, maxLine / totalLines): + cancelled = True raise Exception('Plan canceled.') if self.max_plan_time < time.time() - start: @@ -309,14 +351,23 @@ class Preplanner(object): self._progress(filename, 1) + # Remove infinity from bounds + for axis in 'xyzabc': + if bounds['min'][axis] == math.inf: del bounds['min'][axis] + if bounds['max'][axis] == -math.inf: del bounds['max'][axis] + # Encode data as string data = dict(time = totalTime, lines = totalLines, path = moves, messages = messages) data = gzip.compress(dump_json(data).encode('utf8')) - # Save plan & times - with open(plan_path, 'wb') as f: f.write(data) - with open(times_path, 'wb') as f: - f.write(gzip.compress(dump_json(times).encode('utf8'))) + # Meta data + meta = dict(times = times, bounds = bounds) + meta_comp = gzip.compress(dump_json(meta).encode('utf8')) + + # Save plan & meta data + if not cancelled: + with open(plan_path, 'wb') as f: f.write(data) + with open(meta_path, 'wb') as f: f.write(meta_comp) - return (data, times) + return (data, meta) diff --git a/src/py/bbctrl/State.py b/src/py/bbctrl/State.py index 3b34660..b724c2a 100644 --- a/src/py/bbctrl/State.py +++ b/src/py/bbctrl/State.py @@ -186,8 +186,9 @@ class State(object): def add_listener(self, listener): + log.info(self.vars) self.listeners.append(listener) - if self.vars: listener(self.vars) + listener(self.vars) def remove_listener(self, listener): self.listeners.remove(listener) diff --git a/src/py/bbctrl/Web.py b/src/py/bbctrl/Web.py index f63d4b3..9b7c8b5 100644 --- a/src/py/bbctrl/Web.py +++ b/src/py/bbctrl/Web.py @@ -237,6 +237,7 @@ class PathHandler(bbctrl.APIHandler): def get(self, filename): if not os.path.exists('upload/' + filename): raise HTTPError(404, 'File not found') + future = self.ctrl.preplanner.get_plan(filename) try: @@ -249,7 +250,7 @@ class PathHandler(bbctrl.APIHandler): return if data is not None: - data = data[0] + data = data[0] # We only want the compressed path self.set_header('Content-Encoding', 'gzip') # Respond with chunks to avoid long delays diff --git a/src/stylus/style.styl b/src/stylus/style.styl index df18854..3daa697 100644 --- a/src/stylus/style.styl +++ b/src/stylus/style.styl @@ -247,6 +247,12 @@ span.unit background-color #ccffcc color #000 + .warn + background-color #ffffcc + + .error + background-color #ffcccc + .axis .name text-transform capitalize @@ -258,6 +264,14 @@ span.unit .position width 99% + td.state + text-align left + + .fa + font-size 140% + margin-left 2px + margin-right 6px + .absolute, .offset min-width 6em -- 2.27.0