/******************************************************************************\ 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' var api = require('./api'); var cookie = require('./cookie')('bbctrl-'); module.exports = { template: '#control-view-template', props: ['config', 'template', 'state'], data: function () { return { mach_units: 'METRIC', mdi: '', last_file: undefined, last_file_time: undefined, toolpath: {}, toolpath_progress: 0, axes: 'xyzabc', history: [], speed_override: 1, feed_override: 1, manual_home: { x: false, y: false, z: false, a: false, b: false, c: false }, position_msg: { x: false, y: false, z: false, a: false, b: false, c: false }, axis_position: 0, jog_step: cookie.get_bool('jog-step'), jog_adjust: parseInt(cookie.get('jog-adjust', 2)), deleteGCode: false, tab: 'auto', jog_incr: 1.0, tool_diameter: 6.35, tool_diameter_for_prompt: 6.35, show_probe_test_modal: false, show_tool_diameter_modal: false, toolpath_msg: { x: false, y: false, z: false, a: false, b: false, c: false }, ask_home: true, ask_home_msg: false, ask_zero_xy_msg: false, ask_zero_z_msg: false, showGcodeMessage: false } }, components: { 'axis-control': require('./axis-control'), 'path-viewer': require('./path-viewer'), 'gcode-viewer': require('./gcode-viewer') }, watch: { 'state.imperial': { handler: function (imperial) { this.mach_units = imperial ? 'IMPERIAL' : 'METRIC'; }, immediate: true }, 'state.bitDiameter': { handler: function (bitDiameter) { this.tool_diameter = bitDiameter; }, immediate: true }, mach_units: function (units) { if ((units == 'METRIC') != this.metric) this.send(units == 'METRIC' ? 'G21' : 'G20'); this.units_changed(); }, 'state.line': function () { if (this.mach_state != 'HOMING') this.$broadcast('gcode-line', this.state.line); }, 'state.selected_time': function () { this.load(); }, jog_step: function () { cookie.set_bool('jog-step', this.jog_step); }, jog_adjust: function () { cookie.set('jog-adjust', this.jog_adjust); } }, computed: { metric: function () { return !this.state.imperial; }, mach_state: function () { var cycle = this.state.cycle; var state = this.state.xx; if (typeof cycle != 'undefined' && state != 'ESTOPPED' && (cycle == 'jogging' || cycle == 'homing')) return cycle.toUpperCase(); return state || '' }, pause_reason: function () {return this.state.pr}, is_running: function () { return this.mach_state == 'RUNNING' || this.mach_state == 'HOMING'; }, is_stopping: function () {return this.mach_state == 'STOPPING'}, is_holding: function () {return this.mach_state == 'HOLDING'}, is_ready: function () {return this.mach_state == 'READY'}, is_idle: function () {return this.state.cycle == 'idle'}, is_paused: function () { return this.is_holding && (this.pause_reason == 'User pause' || this.pause_reason == 'Program pause') }, can_mdi: function () {return this.is_idle || this.state.cycle == 'mdi'}, can_set_axis: function () { return this.is_idle // TODO allow setting axis position during pause return this.is_idle || this.is_paused }, message: function () { if (this.mach_state == 'ESTOPPED') return this.state.er; if (this.mach_state == 'HOLDING') return this.state.pr; if (this.state.messages.length) return this.state.messages.slice(-1)[0].text; return ''; }, highlight_state: function () { return this.mach_state == 'ESTOPPED' || this.mach_state == 'HOLDING'; }, plan_time: function () {return this.state.plan_time}, plan_time_remaining: function () { if (!(this.is_stopping || this.is_running || this.is_holding)) return 0; return this.toolpath.time - this.plan_time }, eta: function () { if (this.mach_state != 'RUNNING') return ''; var remaining = this.plan_time_remaining; var d = new Date(); d.setSeconds(d.getSeconds() + remaining); return d.toLocaleString(); }, progress: function () { if (!this.toolpath.time || this.is_ready) return 0; var p = this.plan_time / this.toolpath.time; return p < 1 ? p : 1; } }, events: { jog: function (axis, power) { var data = {ts: new Date().getTime()}; data[axis] = power; api.put('jog', data); }, back2zero: function(axis0,axis1) { this.send("G0"+axis0+"0"+axis1+"0"); }, step: function (axis, value) { this.send('M70\nG91\nG0' + axis + value + '\nM72'); }, probing_complete: function() { if (this.config.settings['probing-prompts']) { Vue.set(this.state, "show_probe_complete_modal", true); } else { this.$emit("finalize_probe"); } }, finalize_probe: function() { Vue.set(this.state, "show_probe_complete_modal", false); if (this.state.goto_xy_zero_after_probe) { this.goto_zero(1, 1, 0, 0); } Vue.set(this.state, "goto_xy_zero_after_probe", false); } }, ready: function () { this.load() }, methods: { units_changed : function() { if(this.mach_units == 'METRIC') { document.getElementById("jog_button_fine").innerHTML = "0.1"; document.getElementById("jog_button_small").innerHTML = "1.0"; document.getElementById("jog_button_medium").innerHTML = "10"; document.getElementById("jog_button_large").innerHTML = "100"; } else { document.getElementById("jog_button_fine").innerHTML = "0.005"; document.getElementById("jog_button_small").innerHTML = "0.05"; document.getElementById("jog_button_medium").innerHTML = "0.5"; document.getElementById("jog_button_large").innerHTML = "5"; } this.set_jog_incr('small'); }, start_probe_test: function(on_finish) { if (!this.config.settings['probing-prompts']) { on_finish(); return; } this.show_probe_test_modal = true; Vue.set(this.state, "saw_probe_connected", false); Vue.set(this.state, "on_probe_finish", on_finish); }, finish_probe_test: function() { this.show_probe_test_modal = false; Vue.set(this.state, "saw_probe_connected", false); const on_finish = this.state.on_probe_finish; Vue.set(this.state, "on_probe_finish", undefined); on_finish(); }, prep_and_show_tool_diameter_modal() { this.tool_diameter_for_prompt = (this.mach_units == 'METRIC') ? this.tool_diameter : this.tool_diameter / 25.4; this.tool_diameter_for_prompt = this.tool_diameter_for_prompt.toFixed(3).replace(/0+$/, ""); this.show_tool_diameter_modal = true; }, set_tool_diameter() { this.tool_diameter = parseFloat(this.tool_diameter_for_prompt); if (!isFinite(this.tool_diameter)) { return; } this.show_tool_diameter_modal = false; if (this.mach_units !== "METRIC") { this.tool_diameter *= 25.4; } this.probe_xyz(); }, probe(zOnly = false) { const xdim = this.config.probe["probe-xdim"]; const ydim = this.config.probe["probe-ydim"]; const zdim = this.config.probe["probe-zdim"]; const slowSeek = this.config.probe["probe-slow-seek"]; const fastSeek = this.config.probe["probe-fast-seek"]; const zlift = 1; const xoffset = xdim + (this.tool_diameter / 2.0); const yoffset = ydim + (this.tool_diameter / 2.0); const zoffset = zdim; const metric = this.mach_units == "METRIC"; const mm = n => (metric ? n : n / 25.4).toFixed(5); const speed = s => `F${mm(s)}`; // After probing Z, we want to drop the bit down: // Ideally, 12.7mm/0.5in // And we don't want to be more than 75% down on the probe block // Also, add zlift to compensate for the fact that we lift after probing Z const plunge = Math.min(12.7, zoffset * 0.75) + zlift; Vue.set(this.state, "goto_xy_zero_after_probe", !zOnly); if (zOnly) { this.send(` ${metric ? "G21" : "G20"} G92 Z0 G38.2 Z ${mm(-25.4)} ${speed(fastSeek)} G91 G1 Z ${mm(1)} G38.2 Z ${mm(-2)} ${speed(slowSeek)} G92 Z ${mm(zoffset)} G91 G0 Z ${mm(3)} M2 `); } else { this.send(` ${metric ? "G21" : "G20"} G92 X0 Y0 Z0 G38.2 Z ${mm(-25.4)} ${speed(fastSeek)} G91 G1 Z ${mm(1)} G38.2 Z ${mm(-2)} ${speed(slowSeek)} G92 Z ${mm(zoffset)} G91 G0 Z ${mm(zlift)} G91 G0 X ${mm(20)} G91 G0 Z ${mm(-plunge)} G38.2 X ${mm(-20)} ${speed(fastSeek)} G91 G1 X ${mm(1)} G38.2 X ${mm(-2)} ${speed(slowSeek)} G92 X ${mm(xoffset)} G91 G0 X ${mm(1)} G91 G0 Y ${mm(20)} G91 G0 X ${mm(-20)} G38.2 Y ${mm(-20)} ${speed(fastSeek)} G91 G1 Y ${mm(1)} G38.2 Y ${mm(-2)} ${speed(slowSeek)} G92 Y ${mm(yoffset)} G91 G0 Y ${mm(3)} G91 G0 Z ${mm(25.4)} M2 `); } // Wait 1 second to let the probing sequence begin, // then wait for probing to be complete setTimeout(() => Vue.set(this.state, "wait_for_probing_complete", true), 1000); }, probe_xyz() { this.probe(false); }, probe_z() { this.probe(true); }, set_jog_incr: function(newValue) { document.getElementById("jog_button_fine").style.fontWeight = 'normal'; document.getElementById("jog_button_small").style.fontWeight = 'normal'; document.getElementById("jog_button_medium").style.fontWeight = 'normal'; document.getElementById("jog_button_large").style.fontWeight = 'normal'; if (newValue == 'fine') { document.getElementById("jog_button_fine").style.fontWeight = 'bold'; if(this.mach_units == 'METRIC') this.jog_incr = 0.1; else this.jog_incr = 0.005; } else if (newValue == 'small') { document.getElementById("jog_button_small").style.fontWeight = 'bold'; if(this.mach_units == 'METRIC') this.jog_incr = 1.0; else this.jog_incr = 0.05; } else if (newValue == 'medium') { document.getElementById("jog_button_medium").style.fontWeight = 'bold'; if(this.mach_units == 'METRIC') this.jog_incr = 10; else this.jog_incr = 0.5; } else if (newValue == 'large') { document.getElementById("jog_button_large").style.fontWeight = 'bold'; this.jog_incr = (this.mach_units == 'METRIC') ? 100 : 5; } }, goto_zero(zero_x,zero_y,zero_z,zero_a) { var xcmd = ""; var ycmd = ""; var zcmd = ""; var acmd = ""; if(zero_x) xcmd = "X0"; if(zero_y) ycmd = "Y0"; if(zero_z) zcmd = "Z0"; if(zero_a) acmd = "A0"; this.ask_zero_xy_msg = false; this.ask_zero_z_msg = false; this.send('G90\nG0' + xcmd + ycmd + zcmd + acmd + '\n'); }, jog_fn: function (x_jog,y_jog,z_jog,a_jog) { var xcmd = "X" + x_jog * this.jog_incr; var ycmd = "Y" + y_jog * this.jog_incr; var zcmd = "Z" + z_jog * this.jog_incr; var acmd = "A" + a_jog * this.jog_incr; this.send('G91\nG0' + xcmd + ycmd + zcmd + acmd + '\n'); }, send: function (msg) { this.$dispatch('send', msg) }, load: function () { var file_time = this.state.selected_time; var file = this.state.selected; if (this.last_file == file && this.last_file_time == file_time) return; this.last_file = file; this.last_file_time = file_time; this.$broadcast('gcode-load', file); this.$broadcast('gcode-line', this.state.line); this.toolpath_progress = 0; this.load_toolpath(file, file_time); }, load_toolpath: async function (file, file_time) { this.toolpath = {}; if (!file) return; if (this.last_file_time != file_time) return; this.showGcodeMessage = true; while (this.showGcodeMessage) { const toolpath = await api.get(`path/${file}`); this.toolpath_progress = toolpath.progress; if (toolpath.progress === 1 || typeof toolpath.progress == 'undefined') { this.showGcodeMessage = false if (toolpath.bounds) { toolpath.filename = file; this.toolpath_progress = 1; this.toolpath = toolpath; const state = this.$root.state; for (let axis of 'xyzabc') { Vue.set(state, 'path_min_' + axis, toolpath.bounds.min[axis]); Vue.set(state, 'path_max_' + axis, toolpath.bounds.max[axis]); } } } } }, submit_mdi: function () { this.send(this.mdi); if (!this.history.length || this.history[0] != this.mdi) this.history.unshift(this.mdi); this.mdi = ''; }, mdi_start_pause: function () { if (this.state.xx == 'RUNNING') this.pause(); else if (this.state.xx == 'STOPPING' || this.state.xx == 'HOLDING') this.unpause(); else this.submit_mdi(); }, load_history: function (index) { this.mdi = this.history[index]; }, open: function (e) { // If we don't reset the form the browser may cache file if name is same // even if contents have changed $('.gcode-file-input')[0].reset(); $('.gcode-file-input input').click(); }, upload: async function (e) { const files = e.target.files || e.dataTransfer.files; if (!files.length) { return; } const file = files[0]; const extension = file.name.split(".").pop(); switch (extension.toLowerCase()) { case "nc": case "ngc": case "gcode": case "gc": break; default: alert(`Unsupported file type: ${extension}`); return; } const fd = new FormData(); fd.append('gcode', file); try { await api.upload('file', fd); this.last_file_time = undefined; // Force reload this.$broadcast('gcode-reload', file.name); } catch (err) { api.alert('Upload failed', err) } }, delete_current: function () { if (this.state.selected) { api.delete('file/' + this.state.selected); } this.deleteGCode = false; }, delete_all: function () { api.delete('file'); this.deleteGCode = false; }, home: function (axis) { this.ask_home = false; this.ask_home_msg = false; if (typeof axis == 'undefined') api.put('home'); else { if (this[axis].homingMode != 'manual') api.put('home/' + axis); else this.manual_home[axis] = true; } }, set_home: function (axis, position) { this.manual_home[axis] = false; api.put('home/' + axis + '/set', {position: parseFloat(position)}); }, unhome: function (axis) { this.position_msg[axis] = false; api.put('home/' + axis + '/clear'); }, show_set_position: function (axis) { this.axis_position = 0; this.position_msg[axis] = true; }, show_toolpath_msg : function(axis) { this.toolpath_msg[axis] = true; }, set_position: function (axis, position) { this.position_msg[axis] = false; api.put('position/' + axis, {'position': parseFloat(position)}); }, zero_all: function () { for (var axis of 'xyzabc') if (this[axis].enabled) this.zero(axis); }, zero: function (axis) { if (typeof axis == 'undefined') this.zero_all(); else this.set_position(axis, 0); }, start_pause: function () { if (this.state.xx == 'RUNNING') this.pause(); else if (this.state.xx == 'STOPPING' || this.state.xx == 'HOLDING') this.unpause(); else this.start(); }, start: function () {api.put('start')}, pause: function () {api.put('pause')}, unpause: function () {api.put('unpause')}, optional_pause: function () {api.put('pause/optional')}, stop: function () {api.put('stop')}, step: function () {api.put('step')}, override_feed: function () {api.put('override/feed/' + this.feed_override)}, override_speed: function () { api.put('override/speed/' + this.speed_override) }, current: function (axis, value) { var x = value / 32.0; if (this.state[axis + 'pl'] == x) return; var data = {}; data[axis + 'pl'] = x; this.send(JSON.stringify(data)); } }, mixins: [require('./axis-vars')] }