From 5f0d0754dbccb4ad7ef05df4f3bff0da77c01383 Mon Sep 17 00:00:00 2001 From: Joseph Coffland Date: Tue, 20 Nov 2018 04:32:33 -0800 Subject: [PATCH] Fixed Web disconnect during simulation of large GCode. Disabled jog during pause until zeroing during pause can also be implemented. Fixed messsage repeat count. Eliminated separate meta data file for GCode path simulations. Path viewer no longer displays move from current tool position to start. Path preplan now occurs in separate process. --- CHANGELOG.md | 2 +- src/js/console.js | 1 + src/js/control-view.js | 13 +- src/js/path-viewer.js | 12 +- src/py/bbctrl/Comm.py | 2 +- src/py/bbctrl/Mach.py | 3 +- src/py/bbctrl/Planner.py | 14 +- src/py/bbctrl/Preplanner.py | 311 ++++---------------------------- src/py/bbctrl/State.py | 56 +++--- src/py/bbctrl/Web.py | 1 - src/py/bbctrl/plan.py | 343 ++++++++++++++++++++++++++++++++++++ 11 files changed, 430 insertions(+), 328 deletions(-) create mode 100755 src/py/bbctrl/plan.py diff --git a/CHANGELOG.md b/CHANGELOG.md index c90fb20..f89c8d3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,7 @@ Buildbotics CNC Controller Firmware Changelog - Suppress ``Auto-creating missing tool`` warning. - Prevent ``Stream is closed`` error. - Suppress ``WebGL not supported`` warning. - - Allow jogging during program or user pause. + - Fixed Web disconnect during simulation of large GCode. ## v0.4.1 - Fix toolpath view axes bug. diff --git a/src/js/console.js b/src/js/console.js index 52e0cd3..5607bba 100644 --- a/src/js/console.js +++ b/src/js/console.js @@ -61,6 +61,7 @@ module.exports = { var repeat = messages.length && _msg_equal(msg, messages[0]); if (repeat) messages[0].repeat++; else { + msg.repeat = msg.repeat || 1; messages.unshift(msg); while (256 < messages.length) messages.pop(); } diff --git a/src/js/control-view.js b/src/js/control-view.js index 25e910c..8aa64b5 100644 --- a/src/js/control-view.js +++ b/src/js/control-view.js @@ -217,8 +217,17 @@ module.exports = { api.get('path/' + file).done(function (toolpath) { if (this.last_file != file) return; - if (typeof toolpath.progress == 'undefined') this.toolpath = toolpath; - else { + if (typeof toolpath.progress == 'undefined') { + this.toolpath = toolpath; + + var state = this.$root.state; + var bounds = toolpath.bounds; + for (var axis of 'xyzabc') { + Vue.set(state, 'path_min_' + axis, bounds.min[axis]); + Vue.set(state, 'path_max_' + axis, bounds.max[axis]); + } + + } else { this.toolpath_progress = toolpath.progress; this.load_toolpath(file); // Try again } diff --git a/src/js/path-viewer.js b/src/js/path-viewer.js index 7f07777..38eca27 100644 --- a/src/js/path-viewer.js +++ b/src/js/path-viewer.js @@ -433,9 +433,9 @@ module.exports = { draw_path: function (scene) { var s = undefined; - var x = this.x.pos; - var y = this.y.pos; - var z = this.z.pos; + var x = 0; + var y = 0; + var z = 0; var color = undefined; var positions = []; @@ -448,11 +448,11 @@ module.exports = { var newColor = this.get_color(step.rapid, s); // Handle color change - if (!i || newColor != color) { - color = newColor; + if (i && newColor != color) { positions.push(x, y, z); - colors.push.apply(colors, color); + colors.push.apply(colors, newColor); } + color = newColor; // Draw to move target x = get(step, 'x', x); diff --git a/src/py/bbctrl/Comm.py b/src/py/bbctrl/Comm.py index 4515576..f8468ba 100644 --- a/src/py/bbctrl/Comm.py +++ b/src/py/bbctrl/Comm.py @@ -132,7 +132,7 @@ class Comm(object): # Load next command from callback else: - cmd = self.comm_next() + cmd = self.comm_next() # pylint: disable=assignment-from-no-return if cmd is None: self._set_write(False) # Stop writing else: self._load_next_command(cmd) diff --git a/src/py/bbctrl/Mach.py b/src/py/bbctrl/Mach.py index fdb07ca..434945d 100644 --- a/src/py/bbctrl/Mach.py +++ b/src/py/bbctrl/Mach.py @@ -97,6 +97,8 @@ class Mach(Comm): def _can_jog(self): + return self._get_cycle() == 'idle' + # TODO handle jog during pause for manual tool changes return (self._get_cycle() == 'idle' or (self._is_holding() and self._get_pause_reason() in ('User pause', 'Program pause'))) @@ -107,7 +109,6 @@ class Mach(Comm): if current == cycle: return # No change if (current == 'idle' or (cycle == 'jogging' and self._can_jog())): - self.planner.update_position() self.ctrl.state.set('cycle', cycle) self.last_cycle = current diff --git a/src/py/bbctrl/Planner.py b/src/py/bbctrl/Planner.py index fa69e14..06df2e6 100644 --- a/src/py/bbctrl/Planner.py +++ b/src/py/bbctrl/Planner.py @@ -62,7 +62,6 @@ class Planner(): def is_busy(self): return self.is_running() or self.cmdq.is_active() def is_running(self): return self.planner.is_running() def is_synchronizing(self): return self.planner.is_synchronizing() - def set_position(self, position): self.planner.set_position(position) def get_position(self): @@ -76,8 +75,10 @@ class Planner(): return position - def update_position(self): - self.set_position(self.get_position()) + def set_position(self, position): + for axis in 'xyzabc': + if not self.ctrl.state.is_axis_enabled(axis): continue + self.ctrl.state.set(axis + 'p', position[axis]) def _get_config_vector(self, name, scale): @@ -104,7 +105,7 @@ class Planner(): return limit - def get_config(self, mdi, with_limits): + def get_config(self, mdi, with_limits, with_position = True): metric = self.ctrl.state.get('metric', True) config = { 'default-units': 'METRIC' if metric else 'IMPERIAL', @@ -115,6 +116,8 @@ class Planner(): # TODO junction deviation & accel } + if with_position: config['position'] = self.get_position() + if with_limits: minLimit = self._get_soft_limit('tn', -math.inf) maxLimit = self._get_soft_limit('tm', math.inf) @@ -275,7 +278,7 @@ class Planner(): if name == 'speed': return Cmd.speed(value) if len(name) and name[0] == '_': - # Don't queue axis positions, can be triggered by set_position() + # Don't queue axis positions, can be triggered by new position if len(name) != 2 or name[1] not in 'xyzabc': self._enqueue_set_cmd(id, name[1:], value) @@ -352,7 +355,6 @@ class Planner(): try: self.planner.stop() self.cmdq.clear() - self.update_position() except Exception as e: log.exception(e) diff --git a/src/py/bbctrl/Preplanner.py b/src/py/bbctrl/Preplanner.py index 7b31727..7b8d6e8 100644 --- a/src/py/bbctrl/Preplanner.py +++ b/src/py/bbctrl/Preplanner.py @@ -30,61 +30,18 @@ import logging import time import json import hashlib -import gzip import glob -import math import threading +import subprocess +import tempfile from concurrent.futures import Future, ThreadPoolExecutor, TimeoutError from tornado import gen -import camotics.gplan as gplan # pylint: disable=no-name-in-module,import-error import bbctrl log = logging.getLogger('Preplaner') -# Formats floats with no more than two decimal places -def _dump_json(o): - if isinstance(o, str): yield json.dumps(o) - elif o is None: yield 'null' - elif o is True: yield 'true' - elif o is False: yield 'false' - elif isinstance(o, int): yield str(o) - - elif isinstance(o, float): - if o != o: yield '"NaN"' - elif o == float('inf'): yield '"Infinity"' - elif o == float('-inf'): yield '"-Infinity"' - else: yield format(o, '.2f') - - elif isinstance(o, (list, tuple)): - yield '[' - first = True - - for item in o: - if first: first = False - else: yield ',' - yield from _dump_json(item) - - yield ']' - - elif isinstance(o, dict): - yield '{' - first = True - - for key, value in o.items(): - if first: first = False - else: yield ',' - yield from _dump_json(key) - yield ':' - yield from _dump_json(value) - - yield '}' - - -def dump_json(o): return ''.join(_dump_json(o)) - - def hash_dump(o): s = json.dumps(o, separators = (',', ':'), sort_keys = True) return s.encode('utf8') @@ -93,7 +50,7 @@ def hash_dump(o): def plan_hash(path, config): path = 'upload/' + path h = hashlib.sha256() - h.update('v2'.encode('utf8')) + h.update('v3'.encode('utf8')) h.update(hash_dump(config)) with open(path, 'rb') as f: @@ -101,47 +58,20 @@ def plan_hash(path, config): buf = f.read(1024 * 1024) if not buf: break h.update(buf) - time.sleep(0.001) # Yield some time + time.sleep(0.0001) # Yield some time return h.hexdigest() -def compute_unit(a, b): - unit = dict() - length = 0 - - for axis in 'xyzabc': - if axis in a and axis in b: - unit[axis] = b[axis] - a[axis] - length += unit[axis] * unit[axis] - - length = math.sqrt(length) - - for axis in 'xyzabc': - if axis in unit: unit[axis] /= length - - return unit - - -def compute_move(start, unit, dist): - move = dict() - - for axis in 'xyzabc': - if axis in unit and axis in start: - move[axis] = start[axis] + unit[axis] * dist - - return move - class Preplanner(object): - def __init__(self, ctrl, threads = 4, max_preplan_time = 600, + def __init__(self, ctrl, threads = 4, max_plan_time = 600, max_loop_time = 30): self.ctrl = ctrl - self.max_preplan_time = max_preplan_time + self.max_plan_time = max_plan_time self.max_loop_time = max_loop_time - for dir in ['plans', 'meta']: - if not os.path.exists(dir): os.mkdir(dir) + if not os.path.exists('plans'): os.mkdir('plans') self.started = Future() @@ -149,34 +79,6 @@ class Preplanner(object): self.pool = ThreadPoolExecutor(threads) self.lock = threading.Lock() - # Must be last - ctrl.state.add_listener(self._update) - - - def _update(self, update): - if not 'selected' in update: return - filename = update['selected'] - if filename is None: return - 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): if not self.started.done(): @@ -200,7 +102,6 @@ class Preplanner(object): def delete_all_plans(self): files = glob.glob('plans/*') - files += glob.glob('meta/*') for path in files: try: @@ -212,7 +113,6 @@ class Preplanner(object): def delete_plans(self, filename): files = glob.glob('plans/' + filename + '.*') - files += glob.glob('meta/' + filename + '.*') for path in files: try: @@ -247,7 +147,7 @@ class Preplanner(object): # Copy state for thread state = self.ctrl.state.snapshot() - config = self.ctrl.mach.planner.get_config(False, False) + config = self.ctrl.mach.planner.get_config(False, False, False) del config['default-units'] # Start planner thread @@ -277,183 +177,44 @@ class Preplanner(object): def _exec_plan(self, filename, state, config): - # Check if this plan was already run hid = plan_hash(filename, config) plan_path = 'plans/' + filename + '.' + hid + '.gz' - meta_path = 'meta/' + filename + '.' + hid + '.gz' - - try: - if os.path.exists(plan_path) and os.path.exists(meta_path): - with open(plan_path, 'rb') as f: data = f.read() - 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) - - - # Clean up old plans - self._clean_plans(filename) + if not os.path.exists(plan_path): + self._clean_plans(filename) # Clean up old plans - def get_var_cb(name, units): - value = 0 + path = os.path.abspath('upload/' + filename) + with tempfile.TemporaryDirectory() as tmpdir: + cmd = ( + '/usr/bin/env', 'python3', + bbctrl.get_resource('plan.py'), + path, json.dumps(state), json.dumps(config), + '--max-time=%s' % self.max_plan_time, + '--max-loop=%s' % self.max_loop_time + ) - if len(name) and name[0] == '_': - value = state.get(name[1:], 0) - if units == 'IMPERIAL': value /= 25.4 + log.info('Running: %s', cmd) - return value + with subprocess.Popen(cmd, stdout = subprocess.PIPE, + stderr = subprocess.PIPE, + cwd = tmpdir) as proc: + for line in proc.stdout: + if not self._progress(filename, float(line)): + proc.terminate() + return # Cancelled - start = time.time() - moves = [] - line = 0 - totalLines = sum(1 for line in open('upload/' + filename)) - maxLine = 0 - maxLineTime = time.time() - totalTime = 0 - position = {} - maxSpeed = 0 - currentSpeed = None - rapid = False - moves = [] - bounds = dict(min = {}, max = {}) - messages = [] - count = 0 + out, errs = proc.communicate() - # Initialized axis states and bounds - for axis in 'xyzabc': - position[axis] = 0 - bounds['min'][axis] = math.inf - bounds['max'][axis] = -math.inf + if not self._progress(filename, 1): return # Cancelled + if proc.returncode: + log.error('Plan failed: ' + errs) + return # Failed - def add_to_bounds(axis, value): - if value < bounds['min'][axis]: bounds['min'][axis] = value - if bounds['max'][axis] < value: bounds['max'][axis] = value + os.rename(tmpdir + '/plan.json.gz', plan_path) - - def update_speed(s): - nonlocal currentSpeed, maxSpeed - if currentSpeed == s: return False - currentSpeed = s - if maxSpeed < s: maxSpeed = s - return True - - - # Capture planner log messages - levels = dict(I = 'info', D = 'debug', W = 'warning', E = 'error', - C = 'critical') - - def log_cb(level, msg, filename, line, column): - if level in levels: level = levels[level] - - # Ignore missing tool warning - if (level == 'warning' and - msg.startswith('Auto-creating missing tool')): - return - - messages.append(dict(level = level, msg = msg, filename = filename, - line = line, column = column)) - - - # Initialize planner - self.ctrl.mach.planner.log_intercept(log_cb) - planner = gplan.Planner() - planner.set_resolver(get_var_cb) - planner.load('upload/' + filename, config) - - # Execute plan try: - while planner.has_more(): - cmd = planner.next() - planner.set_active(cmd['id']) # Release plan - - # Cannot synchronize with actual machine so fake it - if planner.is_synchronizing(): planner.synchronize(0) - - if cmd['type'] == 'line': - if not (cmd.get('first', False) or - cmd.get('seeking', False)): - totalTime += sum(cmd['times']) / 1000 - - target = cmd['target'] - move = {} - startPos = dict() - - for axis in 'xyzabc': - if axis in target: - startPos[axis] = position[axis] - position[axis] = target[axis] - move[axis] = target[axis] - add_to_bounds(axis, move[axis]) - - if 'rapid' in cmd: move['rapid'] = cmd['rapid'] - - if 'speeds' in cmd: - unit = compute_unit(startPos, target) - - for d, s in cmd['speeds']: - cur = currentSpeed - - if update_speed(s): - m = compute_move(startPos, unit, d) - if cur is not None: - m['s'] = cur - moves.append(m) - move['s'] = s - - moves.append(move) - - elif cmd['type'] == 'set': - if cmd['name'] == 'line': - line = cmd['value'] - if maxLine < line: - maxLine = line - maxLineTime = time.time() - - elif cmd['name'] == 'speed': - s = cmd['value'] - if update_speed(s): moves.append({'s': s}) - - elif cmd['type'] == 'dwell': totalTime += cmd['seconds'] - - if not self._progress(filename, maxLine / totalLines): - return # Plan cancelled - - if self.max_preplan_time < time.time() - start: - raise Exception('Max planning time (%d sec) exceeded.' % - self.max_preplan_time) - - if self.max_loop_time < time.time() - maxLineTime: - raise Exception('Max loop time (%d sec) exceeded.' % - self.max_loop_time) - - count += 1 - if count % 50 == 0: time.sleep(0.001) # Yield some time - - except Exception as e: - log_cb('error', str(e), filename, line, 0) - - if not self._progress(filename, 1): return # Cancelled - - # 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, - maxSpeed = maxSpeed, messages = messages) - data = gzip.compress(dump_json(data).encode('utf8')) - - # Meta data - meta = dict(bounds = bounds) - meta_comp = gzip.compress(dump_json(meta).encode('utf8')) - - # Save plan & meta data - with open(plan_path, 'wb') as f: f.write(data) - with open(meta_path, 'wb') as f: f.write(meta_comp) - - return (data, meta) + with open(plan_path, 'rb') as f: + return f.read() + except Exception as e: log.error(e) diff --git a/src/py/bbctrl/State.py b/src/py/bbctrl/State.py index 648f9b0..f1be5b6 100644 --- a/src/py/bbctrl/State.py +++ b/src/py/bbctrl/State.py @@ -35,40 +35,6 @@ import bbctrl log = logging.getLogger('State') -class StateSnapshot: - def __init__(self, state): - self.vars = copy.deepcopy(state.vars) - - for name in state.callbacks: - if not name in self.vars: - self.vars[name] = state.callbacks[name](name) - - self.motors = {} - for axis in 'xyzabc': - self.motors[axis] = state.find_motor(axis) - - - def json(self): return dict(vars = self.vars, motors = self.motors) - - - def resolve(self, name): - # Resolve axis prefixes to motor numbers - if 2 < len(name) and name[1] == '_' and name[0] in 'xyzabc': - motor = self.motors[name[0]] - if motor is not None: return str(motor) + name[2:] - - return name - - - def get(self, name, default = None): - name = self.resolve(name) - - if name in self.vars: return self.vars[name] - if default is None: log.warning('State variable "%s" not found' % name) - return default - - - class State(object): def __init__(self, ctrl): self.ctrl = ctrl @@ -179,7 +145,27 @@ class State(object): return default - def snapshot(self): return StateSnapshot(self) + def snapshot(self): + vars = copy.deepcopy(self.vars) + + for name in self.callbacks: + if not name in vars: + vars[name] = self.callbacks[name](name) + + axis_motors = {axis: self.find_motor(axis) for axis in 'xyzabc'} + axis_vars = {} + + for name, value in vars.items(): + if name[0] in '0123': + motor = int(name[0]) + + for axis in 'xyzabc': + if motor == axis_motors[axis]: + axis_vars[axis + '_' + name[1:]] = value + + vars.update(axis_vars) + + return vars def config(self, code, value): diff --git a/src/py/bbctrl/Web.py b/src/py/bbctrl/Web.py index 1062b9c..0e2f0f1 100644 --- a/src/py/bbctrl/Web.py +++ b/src/py/bbctrl/Web.py @@ -251,7 +251,6 @@ class PathHandler(bbctrl.APIHandler): try: if data is not None: - 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/py/bbctrl/plan.py b/src/py/bbctrl/plan.py new file mode 100755 index 0000000..dffaf82 --- /dev/null +++ b/src/py/bbctrl/plan.py @@ -0,0 +1,343 @@ +#!/usr/bin/env python3 + +################################################################################ +# # +# This file is part of the Buildbotics firmware. # +# # +# Copyright (c) 2015 - 2018, Buildbotics LLC # +# All rights reserved. # +# # +# This file ("the software") is free software: you can redistribute it # +# and/or modify it under the terms of the GNU General Public License, # +# version 2 as published by the Free Software Foundation. You should # +# have received a copy of the GNU General Public License, version 2 # +# along with the software. If not, see . # +# # +# 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 sys +import argparse +import json +import time +import math +import os +import re +import gzip +import camotics.gplan as gplan # pylint: disable=no-name-in-module,import-error + + +reLogLine = re.compile( + r'^(?P[A-Z])[0-9 ]:' + r'((?P[^:]+):)?' + r'((?P\d+):)?' + r'((?P\d+):)?' + r'(?P.*)$') + + + +# Formats floats with no more than two decimal places +def _dump_json(o): + if isinstance(o, str): yield json.dumps(o) + elif o is None: yield 'null' + elif o is True: yield 'true' + elif o is False: yield 'false' + elif isinstance(o, int): yield str(o) + + elif isinstance(o, float): + if o != o: yield '"NaN"' + elif o == float('inf'): yield '"Infinity"' + elif o == float('-inf'): yield '"-Infinity"' + else: yield format(o, '.2f') + + elif isinstance(o, (list, tuple)): + yield '[' + first = True + + for item in o: + if first: first = False + else: yield ',' + yield from _dump_json(item) + + yield ']' + + elif isinstance(o, dict): + yield '{' + first = True + + for key, value in o.items(): + if first: first = False + else: yield ',' + yield from _dump_json(key) + yield ':' + yield from _dump_json(value) + + yield '}' + + +def dump_json(o): return ''.join(_dump_json(o)) + + +def compute_unit(a, b): + unit = dict() + length = 0 + + for axis in 'xyzabc': + if axis in a and axis in b: + unit[axis] = b[axis] - a[axis] + length += unit[axis] * unit[axis] + + length = math.sqrt(length) + + for axis in 'xyzabc': + if axis in unit: unit[axis] /= length + + return unit + + +def compute_move(start, unit, dist): + move = dict() + + for axis in 'xyzabc': + if axis in unit and axis in start: + move[axis] = start[axis] + unit[axis] * dist + + return move + + +class Plan(object): + def __init__(self, path, state, config): + self.path = path + self.state = state + self.config = config + + self.lines = sum(1 for line in open(path)) + + self.planner = gplan.Planner() + self.planner.set_resolver(self.get_var_cb) + self.planner.set_logger(self._log_cb, 1, 'LinePlanner:3') + self.planner.load(self.path, config) + + self.messages = [] + self.levels = dict(I = 'info', D = 'debug', W = 'warning', E = 'error', + C = 'critical') + + # Initialized axis states and bounds + self.bounds = dict(min = {}, max = {}) + for axis in 'xyzabc': + self.bounds['min'][axis] = math.inf + self.bounds['max'][axis] = -math.inf + + self.maxSpeed = 0 + self.currentSpeed = None + self.lastProgress = None + self.lastProgressTime = 0 + self.time = 0 + + + def add_to_bounds(self, axis, value): + if value < self.bounds['min'][axis]: self.bounds['min'][axis] = value + if self.bounds['max'][axis] < value: self.bounds['max'][axis] = value + + + def get_bounds(self): + # Remove infinity from bounds + for axis in 'xyzabc': + if self.bounds['min'][axis] == math.inf: + del self.bounds['min'][axis] + if self.bounds['max'][axis] == -math.inf: + del self.bounds['max'][axis] + + return self.bounds + + + def update_speed(self, s): + if self.currentSpeed == s: return False + self.currentSpeed = s + if self.maxSpeed < s: self.maxSpeed = s + + return True + + + def get_var_cb(self, name, units): + value = 0 + + if len(name) and name[0] == '_': + value = self.state.get(name[1:], 0) + if units == 'IMPERIAL': value /= 25.4 + + return value + + + def log_cb(self, level, msg, filename, line, column): + if level in self.levels: level = self.levels[level] + + # Ignore missing tool warning + if (level == 'warning' and + msg.startswith('Auto-creating missing tool')): + return + + self.messages.append( + dict(level = level, msg = msg, filename = filename, line = line, + column = column)) + + + def _log_cb(self, line): + line = line.strip() + m = reLogLine.match(line) + if not m: return + + level = m.group('level') + msg = m.group('msg') + filename = m.group('file') + line = m.group('line') + column = m.group('column') + + where = ':'.join(filter(None.__ne__, [filename, line, column])) + + if line is not None: line = int(line) + if column is not None: column = int(column) + + self.log_cb(level, msg, filename, line, column) + + + def progress(self, x): + if time.time() - self.lastProgressTime < 1 and x != 1: return + self.lastProgressTime = time.time() + + p = '%.4f\n' % x + + if self.lastProgress == p: return + self.lastProgress = p + + sys.stdout.write(p) + sys.stdout.flush() + + + def _run(self): + start = time.time() + line = 0 + maxLine = 0 + maxLineTime = time.time() + position = {axis: 0 for axis in 'xyzabc'} + rapid = False + + # Execute plan + try: + while self.planner.has_more(): + cmd = self.planner.next() + self.planner.set_active(cmd['id']) # Release plan + + # Cannot synchronize with actual machine so fake it + if self.planner.is_synchronizing(): self.planner.synchronize(0) + + if cmd['type'] == 'line': + if not (cmd.get('first', False) or + cmd.get('seeking', False)): + self.time += sum(cmd['times']) / 1000 + + target = cmd['target'] + move = {} + startPos = dict() + + for axis in 'xyzabc': + if axis in target: + startPos[axis] = position[axis] + position[axis] = target[axis] + move[axis] = target[axis] + self.add_to_bounds(axis, move[axis]) + + if 'rapid' in cmd: move['rapid'] = cmd['rapid'] + + if 'speeds' in cmd: + unit = compute_unit(startPos, target) + + for d, s in cmd['speeds']: + cur = self.currentSpeed + + if self.update_speed(s): + m = compute_move(startPos, unit, d) + + if cur is not None: + m['s'] = cur + yield m + + move['s'] = s + + yield move + + elif cmd['type'] == 'set': + if cmd['name'] == 'line': + line = cmd['value'] + if maxLine < line: + maxLine = line + maxLineTime = time.time() + + elif cmd['name'] == 'speed': + s = cmd['value'] + if self.update_speed(s): yield {'s': s} + + elif cmd['type'] == 'dwell': self.time += cmd['seconds'] + + if args.max_time < time.time() - start: + raise Exception('Max planning time (%d sec) exceeded.' % + args.max_time) + + if args.max_loop < time.time() - maxLineTime: + raise Exception('Max loop time (%d sec) exceeded.' % + args.max_loop) + + self.progress(maxLine / self.lines) + + except Exception as e: + self.log_cb('error', str(e), os.path.basename(self.path), line, 0) + + + def run(self): + with gzip.open('plan.json.gz', 'wb') as f: + def write(s): f.write(s.encode('utf8')) + + write('{"path":[') + + first = True + for move in self._run(): + if first: first = False + else: write(',') + write(dump_json(move)) + + write('],') + write('"time":%.2f,' % self.time) + write('"lines":%s,' % self.lines) + write('"maxSpeed":%s,' % self.maxSpeed) + write('"bounds":%s,' % dump_json(self.get_bounds())) + write('"messages":%s}' % dump_json(self.messages)) + + +parser = argparse.ArgumentParser(description = 'Buildbotics GCode Planner') +parser.add_argument('gcode', help = 'The GCode file to plan') +parser.add_argument('state', help = 'GCode state variables') +parser.add_argument('config', help = 'Planner config') + +parser.add_argument('--max-time', default = 600, + type = int, help = 'Maximum planning time in seconds') +parser.add_argument('--max-loop', default = 30, + type = int, help = 'Maximum time in loop in seconds') + +args = parser.parse_args() + +state = json.loads(args.state) +config = json.loads(args.config) + +plan = Plan(args.gcode, state, config) +plan.run() -- 2.27.0