- 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.
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();
}
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
}
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 = [];
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);
# 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)
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')))
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
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):
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):
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',
# 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)
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)
try:
self.planner.stop()
self.cmdq.clear()
- self.update_position()
except Exception as e:
log.exception(e)
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')
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:
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()
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():
def delete_all_plans(self):
files = glob.glob('plans/*')
- files += glob.glob('meta/*')
for path in files:
try:
def delete_plans(self, filename):
files = glob.glob('plans/' + filename + '.*')
- files += glob.glob('meta/' + filename + '.*')
for path in files:
try:
# 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
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)
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
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):
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
--- /dev/null
+#!/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 <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> #
+# #
+################################################################################
+
+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<level>[A-Z])[0-9 ]:'
+ r'((?P<file>[^:]+):)?'
+ r'((?P<line>\d+):)?'
+ r'((?P<column>\d+):)?'
+ r'(?P<msg>.*)$')
+
+
+
+# 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()