- 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.
--- /dev/null
+/******************************************************************************\
+
+ This file is part of the Buildbotics firmware.
+
+ Copyright (c) 2015 - 2018, Buildbotics LLC
+ All rights reserved.
+
+ This file ("the software") is free software: you can redistribute it
+ and/or modify it under the terms of the GNU General Public License,
+ version 2 as published by the Free Software Foundation. You should
+ have received a copy of the GNU General Public License, version 2
+ along with the software. If not, see <http://www.gnu.org/licenses/>.
+
+ The software is distributed in the hope that it will be useful, but
+ WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ Lesser General Public License for more details.
+
+ You should have received a copy of the GNU Lesser General Public
+ License along with the software. If not, see
+ <http://www.gnu.org/licenses/>.
+
+ For information regarding this software email:
+ "Joseph Coffland" <joseph@buildbotics.com>
+
+\******************************************************************************/
+
+'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
+ }
+ }
+ }
+}
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) {
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) {
},
- 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;
},
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;
}
},
},
- 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);
},
data[axis + 'pl'] = x;
this.send(JSON.stringify(data));
}
- }
+ },
+
+
+ mixins: [require('./axis-vars')]
}
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
module.exports = {
template: '#path-viewer-template',
- props: ['toolpath', 'progress', 'state'],
+ props: ['toolpath', 'progress'],
data: function () {
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]}
},
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()}
},
},
- updateSurfaceMode: function (mode) {
+ update_surface_mode: function (mode) {
if (!this.enabled) return;
if (typeof this.surfaceMaterial != 'undefined') {
},
- getDims: function () {
+ get_dims: function () {
var t = $(this.target);
var width = t.innerWidth();
var height = t.innerHeight();
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();
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();
},
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);
},
- createSurfaceMaterial: function () {
+ create_surface_material: function () {
return new THREE.MeshPhongMaterial({
specular: 0x111111,
shininess: 10,
},
- drawWorkpiece: function (scene, material) {
+ draw_workpiece: function (scene, material) {
if (typeof this.workpiece == 'undefined') return;
var min = this.workpiece.min;
},
- drawSurface: function (scene, material) {
+ draw_surface: function (scene, material) {
if (typeof this.vertices == 'undefined') return;
var geometry = new THREE.BufferGeometry();
},
- 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;
},
- drawAxis: function (axis, up, length, radius) {
+ draw_axis: function (axis, up, length, radius) {
var color;
if (axis == 0) color = 0xff0000; // Red
},
- 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;
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);
},
- 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 = [];
},
- 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;
},
+ 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);
},
this.camera.position.copy(offset.multiplyScalar(dist * 1.2).add(center));
}
- }
+ },
+
+
+ mixins: [require('./axis-vars')]
}
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()") ∅
.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.`,
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}']`)
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(
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
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
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
from collections import deque
log = logging.getLogger('CmdQ')
-log.setLevel(logging.INFO)
+log.setLevel(logging.WARNING)
class CommandQueue():
import os
import bbctrl
import glob
+import logging
+
+
+log = logging.getLogger('FileHandler')
def safe_remove(path):
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:
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
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'])
import hashlib
import gzip
import glob
+import math
import threading
from concurrent.futures import Future, ThreadPoolExecutor, TimeoutError
from tornado import gen
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()
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)
# 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)
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')
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']
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:
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)
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)
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:
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
background-color #ccffcc
color #000
+ .warn
+ background-color #ffffcc
+
+ .error
+ background-color #ffcccc
+
.axis
.name
text-transform capitalize
.position
width 99%
+ td.state
+ text-align left
+
+ .fa
+ font-size 140%
+ margin-left 2px
+ margin-right 6px
+
.absolute, .offset
min-width 6em