From 78a01501806b3b033a478e5807abfcf765804076 Mon Sep 17 00:00:00 2001 From: Joseph Coffland Date: Sun, 2 Dec 2018 00:43:23 -0800 Subject: [PATCH] Use cgroups to restrict Chromium memory, Send GCode as text not JSON string --- CHANGELOG.md | 2 + MANIFEST.in | 1 + scripts/browser | 36 ++++------- scripts/install.sh | 23 ++++--- scripts/rc.local | 26 ++++++++ src/js/api.js | 4 +- src/js/control-view.js | 4 ++ src/js/gcode-viewer.js | 32 +++++----- src/js/orbit.js | 5 +- src/js/path-viewer.js | 100 +++++++++++++++--------------- src/js/sock.js | 2 +- src/py/bbctrl/APIHandler.py | 3 +- src/py/bbctrl/CommandQueue.py | 2 +- src/py/bbctrl/FileHandler.py | 4 +- src/py/bbctrl/Preplanner.py | 54 +++++++++++------ src/py/bbctrl/Web.py | 32 +++++++--- src/py/bbctrl/plan.py | 111 ++++++++++++++-------------------- src/stylus/style.styl | 6 +- 18 files changed, 245 insertions(+), 202 deletions(-) create mode 100755 scripts/rc.local diff --git a/CHANGELOG.md b/CHANGELOG.md index 630943e..0549a7f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,8 @@ Buildbotics CNC Controller Firmware Changelog - Added ``Bug Report`` button to ``Admin`` -> ``General``. - Only render 3D view as needed to save CPU. - Prevent lockup due to browser causing out of memory condition. + - Show error message when too large GCode upload fails. + - Much faster 3D view loading. ## v0.4.1 - Fix toolpath view axes bug. diff --git a/MANIFEST.in b/MANIFEST.in index 24da8e0..9dbe3de 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -4,5 +4,6 @@ include src/avr/bbctrl-avr-firmware.hex include scripts/avr109-flash.py include scripts/buildbotics.gc include scripts/xinitrc +include scripts/rc.local recursive-include src/py/camotics * global-exclude .gitignore diff --git a/scripts/browser b/scripts/browser index 5bb9edf..9dfc709 100755 --- a/scripts/browser +++ b/scripts/browser @@ -1,28 +1,16 @@ -#!/usr/bin/env python3 - -import os -import sys -import resource -import subprocess - -# Limit memory usage -limit = 1.5e9 -resource.setrlimit(resource.RLIMIT_DATA, (limit, limit)) +#!/bin/bash # Clear browser errors -prefs = '/home/pi/.config/chromium/Default/Preferences' -subprocess.run( - ('sed', '-i', 's/"exited_cleanly":false/"exited_cleanly":true/', prefs)) -subprocess.run( - ('sed', '-i', 's/"exit_type":"Crashed"/"exit_type":"Normal"/', prefs)) +PREFS='/home/pi/.config/chromium/Default/Preferences' +sed -i 's/"exited_cleanly":false/"exited_cleanly":true/' $PREFS +sed -i 's/"exit_type":"Crashed"/"exit_type":"Normal"/' $PREFS # Start browser -cmd = '/usr/lib/chromium-browser/chromium-browser' -args = ( - '--no-first-run', - '--disable-infobars', - '--noerrdialogs', - '--single-process', - 'http://localhost/' -) -os.execvp(cmd, args) +/usr/lib/chromium-browser/chromium-browser --no-first-run \ + --disable-infobars --noerrdialogs --disable-3d-apis --single-process \ + http://localhost/ & + +# Enter cgroup +echo $! >> /sys/fs/cgroup/memory/chrome/tasks + +wait diff --git a/scripts/install.sh b/scripts/install.sh index 8748351..11f64d3 100755 --- a/scripts/install.sh +++ b/scripts/install.sh @@ -43,10 +43,7 @@ fi # ) >> /boot/config.txt # mount -o remount,ro /boot #fi -#grep "plymouth quit" /etc/rc.local -#if [ $? -ne 0 ]; then -# sed -i 's/cd \/home\/pi/cd \/home\/pi; plymouth quit/' /etc/rc.local -#fi +#chmod ug+s /usr/lib/xorg/Xorg # Fix camera grep dwc_otg.fiq_fsm_mask /boot/cmdline.txt >/dev/null @@ -57,6 +54,15 @@ if [ $? -ne 0 ]; then REBOOT=true fi +# Enable memory cgroups +grep cgroup_memory /boot/cmdline.txt >/dev/null +if [ $? -ne 0 ]; then + mount -o remount,rw /boot && + sed -i 's/\(.*\)/\1 cgroup_memory=1/' /boot/cmdline.txt + mount -o remount,ro /boot + REBOOT=true +fi + # Remove Hawkeye if [ -e /etc/init.d/hawkeye ]; then apt-get remove --purge -y hawkeye @@ -96,11 +102,6 @@ if [ ! -e /etc/udev/rules.d/11-automount.rules ]; then echo 'LABEL="automount_end"' ) > /etc/udev/rules.d/11-automount.rules - grep "/etc/init.d/udev restart" /etc/rc.local >/dev/null - if [ $? -ne 0 ]; then - echo "/etc/init.d/udev restart" >> /etc/rc.local - fi - sed -i 's/^\(MountFlags=slave\)/#\1/' /lib/systemd/system/systemd-udevd.service REBOOT=true fi @@ -123,6 +124,10 @@ if [ ! -d /var/lib/bbctrl/upload -o -z "$(ls -A /var/lib/bbctrl/upload)" ]; then cp scripts/buildbotics.gc /var/lib/bbctrl/upload/ fi +# Install rc.local +cp scripts/rc.local /etc/ + +# Install bbctrl if $UPDATE_PY; then rm -rf /usr/local/lib/python*/dist-packages/bbctrl-* ./setup.py install --force diff --git a/scripts/rc.local b/scripts/rc.local new file mode 100755 index 0000000..f3dc808 --- /dev/null +++ b/scripts/rc.local @@ -0,0 +1,26 @@ +#!/bin/bash + +# Mount /boot read only +mount -o remount,ro /boot + +# Set SPI GPIO mode +gpio mode 27 alt3 + +# Create browser memory limited cgroup +if [ -d sys/fs/cgroup/memory ]; then + CGROUP=/sys/fs/cgroup/memory/chrome + mkdir $CGROUP + chown -R pi:pi $CGROUP + echo 700000000 > $CGROUP/memory.limit_in_bytes +fi + +# Reload udev +/etc/init.d/udev restart + +# Stop boot splash so it does not interfere with X if GPU enabled +grep ^dtoverlay=vc4-kms-v3d /boot/config.txt >/dev/null +if [ $? -eq 0 ]; then plymouth quit; fi + +# Start X in /home/pi +cd /home/pi +sudo -u pi startx diff --git a/src/js/api.js b/src/js/api.js index e8186b2..251d546 100644 --- a/src/js/api.js +++ b/src/js/api.js @@ -49,8 +49,10 @@ function api_cb(method, url, data, config) { }).error(function (xhr, status, error) { var text = xhr.responseText; try {text = $.parseJSON(xhr.responseText)} catch(e) {} + if (!text) text = error; + d.reject(text, xhr, status, error); - console.debug('API Error: ' + url + ': ' + xhr.responseText); + console.debug('API Error: ' + url + ': ' + text); }); return d.promise(); diff --git a/src/js/control-view.js b/src/js/control-view.js index d5cd60b..fab2277 100644 --- a/src/js/control-view.js +++ b/src/js/control-view.js @@ -234,6 +234,7 @@ module.exports = { if (this.last_file != file) return; if (typeof toolpath.progress == 'undefined') { + toolpath.filename = file; this.toolpath = toolpath; var state = this.$root.state; @@ -294,6 +295,9 @@ module.exports = { this.last_file = undefined; // Force reload this.$broadcast('gcode-reload', file.name); this.update(); + + }.bind(this)).fail(function (error) { + alert('Upload failed: ' + error) }.bind(this)); }, diff --git a/src/js/gcode-viewer.js b/src/js/gcode-viewer.js index bbae51a..16369d1 100644 --- a/src/js/gcode-viewer.js +++ b/src/js/gcode-viewer.js @@ -78,23 +78,26 @@ module.exports = { this.clear(); this.file = file; - api.get('file/' + file) - .done(function (data) { - if (this.file != file) return; + var xhr = new XMLHttpRequest(); + xhr.open('GET', '/api/file/' + file + '?' + Math.random(), true); + xhr.responseType = 'text'; - var lines = data.trimRight().split(/\r?\n/); + xhr.onload = function (e) { + if (this.file != file) return; + var lines = xhr.response.trimRight().split(/\r?\n/); - for (var i = 0; i < lines.length; i++) { - lines[i] = '
  • ' + - '' + (i + 1) + '' + - lines[i] + '
  • '; - } + for (var i = 0; i < lines.length; i++) { + lines[i] = '
  • ' + + '' + (i + 1) + '' + lines[i] + '
  • '; + } + + this.clusterize.update(lines); + this.empty = false; - this.clusterize.update(lines); - this.empty = false; + Vue.nextTick(this.update_line); + }.bind(this) - Vue.nextTick(this.update_line); - }.bind(this)); + xhr.send(); }, @@ -117,7 +120,7 @@ module.exports = { var e = $(this.$el).find('.highlight'); if (e.length) e.removeClass('highlight'); - e = $(this.$el).find('.line-' + this.line); + e = $(this.$el).find('.ln' + this.line); if (e.length) e.addClass('highlight'); }, @@ -158,3 +161,4 @@ module.exports = { } } } +33554400 diff --git a/src/js/orbit.js b/src/js/orbit.js index 5225f9a..45c5768 100644 --- a/src/js/orbit.js +++ b/src/js/orbit.js @@ -175,12 +175,10 @@ var OrbitControls = function (object, domElement) { panOffset.set(0, 0, 0); } - scale = 1; - // update condition is: // min(camera displacement, camera rotation in radians)^2 > EPS // using small-angle approximation cos(x/2) = 1 - x^2 / 8 - if (zoomChanged || + if (zoomChanged || scale != 1 || lastPosition.distanceToSquared(scope.object.position) > EPS || 8 * (1 - lastQuaternion.dot(scope.object.quaternion)) > EPS) { @@ -189,6 +187,7 @@ var OrbitControls = function (object, domElement) { lastPosition.copy(scope.object.position); lastQuaternion.copy(scope.object.quaternion); zoomChanged = false; + scale = 1; return true; } diff --git a/src/js/path-viewer.js b/src/js/path-viewer.js index d3dac07..8e5f29d 100644 --- a/src/js/path-viewer.js +++ b/src/js/path-viewer.js @@ -27,6 +27,7 @@ var orbit = require('./orbit'); var cookie = require('./cookie')('bbctrl-'); +var api = require('./api'); function get(obj, name, defaultValue) { @@ -60,7 +61,6 @@ module.exports = { computed: { - hasPath: function () {return typeof this.toolpath.path != 'undefined'}, target: function () {return $(this.$el).find('.path-viewer-content')[0]} }, @@ -115,25 +115,52 @@ module.exports = { ready: function () { this.graphics(); - if (typeof this.toolpath.path != 'undefined') Vue.nextTick(this.update); + Vue.nextTick(this.update); }, methods: { update: function () { - if (!this.enabled) return; + if (!this.toolpath.filename && !this.loading) { + this.loading = true; + this.scene = new THREE.Scene(); + this.dirty = true; + } + + if (!this.enabled || !this.toolpath.filename) return; + + function get(url) { + var d = $.Deferred(); + var xhr = new XMLHttpRequest(); + + xhr.open('GET', url + '?' + Math.random(), true); + xhr.responseType = 'arraybuffer'; + + xhr.onload = function (e) { + if (xhr.response) d.resolve(new Float32Array(xhr.response)); + else d.reject(); + }; + + xhr.send(); - // Reset message - this.loading = !this.hasPath; + return d.promise(); + } + + var d1 = get('/api/path/' + this.toolpath.filename + '/positions'); + var d2 = get('/api/path/' + this.toolpath.filename + '/speeds'); - // Update scene - this.scene = new THREE.Scene(); - if (this.hasPath) { + $.when(d1, d2).done(function (positions, speeds) { + this.positions = positions + this.speeds = speeds; + this.loading = false; + + // Update scene + this.scene = new THREE.Scene(); this.draw(this.scene); this.snap(this.snapView); - } - this.update_view(); + this.update_view(); + }.bind(this)) }, @@ -284,6 +311,7 @@ module.exports = { keyLight.lookAt(scope.controls.target); fillLight.lookAt(scope.controls.target); backLight.lookAt(scope.controls.target); + scope.dirty = true; } }(this)) @@ -425,47 +453,16 @@ module.exports = { }, - get_color: function (rapid, speed) { - if (rapid) return [1, 0, 0]; + get_color: function (speed) { + if (isNaN(speed)) return [255, 0, 0]; // Rapid var intensity = speed / this.toolpath.maxSpeed; if (typeof speed == 'undefined' || !this.showIntensity) intensity = 1; - return [0, intensity, 0.5 * (1 - intensity)]; + return [0, 255 * intensity, 127 * (1 - intensity)]; }, draw_path: function (scene) { - var s = undefined; - var x = 0; - var y = 0; - var z = 0; - var color = undefined; - - var positions = []; - var colors = []; - - for (var i = 0; i < this.toolpath.path.length; i++) { - var step = this.toolpath.path[i]; - - s = get(step, 's', s); - var newColor = this.get_color(step.rapid, s); - - // Handle color change - if (i && newColor != color) { - positions.push(x, y, z); - colors.push.apply(colors, newColor); - } - color = newColor; - - // Draw to move target - x = get(step, 'x', x); - y = get(step, 'y', y); - z = get(step, 'z', z); - - positions.push(x, y, z); - colors.push.apply(colors, color); - } - var geometry = new THREE.BufferGeometry(); var material = new THREE.LineBasicMaterial({ @@ -473,10 +470,17 @@ module.exports = { linewidth: 1.5 }); - geometry.addAttribute('position', - new THREE.Float32BufferAttribute(positions, 3)); - geometry.addAttribute('color', - new THREE.Float32BufferAttribute(colors, 3)); + var positions = new THREE.Float32BufferAttribute(this.positions, 3); + geometry.addAttribute('position', positions); + + var colors = []; + for (var i = 0; i < this.speeds.length; i++) { + var color = this.get_color(this.speeds[i]); + Array.prototype.push.apply(colors, color); + } + + colors = new THREE.Uint8BufferAttribute(colors, 3, true); + geometry.addAttribute('color', colors); geometry.computeBoundingSphere(); geometry.computeBoundingBox(); diff --git a/src/js/sock.js b/src/js/sock.js index 6d1f7f7..5c274cd 100644 --- a/src/js/sock.js +++ b/src/js/sock.js @@ -32,7 +32,7 @@ var Sock = function (url, retry, timeout) { if (!(this instanceof Sock)) return new Sock(url, retry); if (typeof retry == 'undefined') retry = 2000; - if (typeof timeout == 'undefined') timeout = 8000; + if (typeof timeout == 'undefined') timeout = 16000; this.url = url; this.retry = retry; diff --git a/src/py/bbctrl/APIHandler.py b/src/py/bbctrl/APIHandler.py index a5f02cb..6b58359 100644 --- a/src/py/bbctrl/APIHandler.py +++ b/src/py/bbctrl/APIHandler.py @@ -45,12 +45,11 @@ class APIHandler(RequestHandler): # Override exception logging def log_exception(self, typ, value, tb): - if isinstance(value, HTTPError) and value.status_code == 408: + if isinstance(value, HTTPError) and value.status_code in (404, 408): return log.error(str(value)) trace = ''.join(traceback.format_exception(typ, value, tb)) - log.error(trace) log.debug(trace) diff --git a/src/py/bbctrl/CommandQueue.py b/src/py/bbctrl/CommandQueue.py index 75b68ce..25bc6b7 100644 --- a/src/py/bbctrl/CommandQueue.py +++ b/src/py/bbctrl/CommandQueue.py @@ -78,7 +78,7 @@ class CommandQueue(): def release(self, id): if id and not id_less(self.releaseID, id): - log.warning('id out of order %d <= %d' % (id, self.releaseID)) + log.debug('id out of order %d <= %d' % (id, self.releaseID)) self.releaseID = id self._release() diff --git a/src/py/bbctrl/FileHandler.py b/src/py/bbctrl/FileHandler.py index d122d35..a360c5c 100644 --- a/src/py/bbctrl/FileHandler.py +++ b/src/py/bbctrl/FileHandler.py @@ -80,8 +80,10 @@ class FileHandler(bbctrl.APIHandler): if path: path = path[1:] + self.set_header('Content-Type', 'text/plain') + with open('upload/' + path, 'r') as f: - self.write_json(f.read()) + self.write(f.read()) self.ctrl.mach.select(path) return diff --git a/src/py/bbctrl/Preplanner.py b/src/py/bbctrl/Preplanner.py index 18b0a02..28e36ca 100644 --- a/src/py/bbctrl/Preplanner.py +++ b/src/py/bbctrl/Preplanner.py @@ -50,7 +50,7 @@ def hash_dump(o): def plan_hash(path, config): path = 'upload/' + path h = hashlib.sha256() - h.update('v3'.encode('utf8')) + h.update('v4'.encode('utf8')) h.update(hash_dump(config)) with open(path, 'rb') as f: @@ -89,14 +89,14 @@ class Preplanner(object): def invalidate(self, filename): with self.lock: if filename in self.plans: - self.plans[filename][0].cancel() + self.plans[filename][2].set() # Cancel del self.plans[filename] def invalidate_all(self): with self.lock: for filename, plan in self.plans.items(): - plan[0].cancel() + plan[2].set() # Cancel self.plans = {} @@ -128,7 +128,8 @@ class Preplanner(object): with self.lock: if filename in self.plans: plan = self.plans[filename] else: - plan = [self._plan(filename), 0] + cancel = threading.Event() + plan = [self._plan(filename, cancel), 0, cancel] self.plans[filename] = plan return plan[0] @@ -141,7 +142,7 @@ class Preplanner(object): @gen.coroutine - def _plan(self, filename): + def _plan(self, filename, cancel): # Wait until state is fully initialized yield self.started @@ -151,7 +152,8 @@ class Preplanner(object): del config['default-units'] # Start planner thread - plan = yield self.pool.submit(self._exec_plan, filename, state, config) + plan = yield self.pool.submit( + self._exec_plan, filename, state, config, cancel) return plan @@ -171,19 +173,24 @@ class Preplanner(object): def _progress(self, filename, progress): with self.lock: - if not filename in self.plans: return False - self.plans[filename][1] = progress - return True + if filename in self.plans: + self.plans[filename][1] = progress - def _exec_plan(self, filename, state, config): + def _exec_plan(self, filename, state, config, cancel): try: os.nice(5) hid = plan_hash(filename, config) - plan_path = 'plans/' + filename + '.' + hid + '.gz' + base = 'plans/' + filename + '.' + hid + files = [ + base + '.json', base + '.positions.gz', base + '.speeds.gz'] + + found = True + for path in files: + if not os.path.exists(path): found = False - if not os.path.exists(plan_path): + if not found: self._clean_plans(filename) # Clean up old plans path = os.path.abspath('upload/' + filename) @@ -203,21 +210,28 @@ class Preplanner(object): cwd = tmpdir) as proc: for line in proc.stdout: - if not self._progress(filename, float(line)): + self._progress(filename, float(line)) + if cancel.is_set(): proc.terminate() - return # Cancelled + return out, errs = proc.communicate() - if not self._progress(filename, 1): return # Cancelled + self._progress(filename, 1) + if cancel.is_set(): return if proc.returncode: - log.error('Plan failed: ' + errs) + log.error('Plan failed: ' + errs.decode('utf8')) return # Failed - os.rename(tmpdir + '/plan.json.gz', plan_path) + os.rename(tmpdir + '/meta.json', files[0]) + os.rename(tmpdir + '/positions.gz', files[1]) + os.rename(tmpdir + '/speeds.gz', files[2]) + + with open(files[0], 'r') as f: meta = json.load(f) + with open(files[1], 'rb') as f: positions = f.read() + with open(files[2], 'rb') as f: speeds = f.read() - with open(plan_path, 'rb') as f: - return f.read() + return meta, positions, speeds - except Exception as e: log.error(e) + except Exception as e: log.exception(e) diff --git a/src/py/bbctrl/Web.py b/src/py/bbctrl/Web.py index 92d664b..ee24ea0 100644 --- a/src/py/bbctrl/Web.py +++ b/src/py/bbctrl/Web.py @@ -274,7 +274,7 @@ class UpgradeHandler(bbctrl.APIHandler): class PathHandler(bbctrl.APIHandler): @gen.coroutine - def get(self, filename): + def get(self, filename, dataType, *args): if not os.path.exists('upload/' + filename): raise HTTPError(404, 'File not found') @@ -290,15 +290,27 @@ class PathHandler(bbctrl.APIHandler): return try: - if data is not None: - self.set_header('Content-Encoding', 'gzip') + if data is None: return + meta, positions, speeds = data - # Respond with chunks to avoid long delays - SIZE = 102400 - chunks = [data[i:i + SIZE] for i in range(0, len(data), SIZE)] - for chunk in chunks: - self.write(chunk) - yield self.flush() + if dataType == '/positions': data = positions + elif dataType == '/speeds': data = speeds + else: + self.write_json(meta) + return + + filename = filename + '-' + dataType[1:] + '.gz' + self.set_header('Content-Disposition', 'filename="%s"' % filename) + self.set_header('Content-Type', 'application/octet-stream') + self.set_header('Content-Encoding', 'gzip') + self.set_header('Content-Length', str(len(data))) + + # Respond with chunks to avoid long delays + SIZE = 102400 + chunks = [data[i:i + SIZE] for i in range(0, len(data), SIZE)] + for chunk in chunks: + self.write(chunk) + yield self.flush() except tornado.iostream.StreamClosedError as e: pass @@ -456,7 +468,7 @@ class Web(tornado.web.Application): (r'/api/firmware/update', FirmwareUpdateHandler), (r'/api/upgrade', UpgradeHandler), (r'/api/file(/[^/]+)?', bbctrl.FileHandler), - (r'/api/path/([^/]+)', PathHandler), + (r'/api/path/([^/]+)((/positions)|(/speeds))?', PathHandler), (r'/api/home(/[xyzabcXYZABC]((/set)|(/clear))?)?', HomeHandler), (r'/api/start', StartHandler), (r'/api/estop', EStopHandler), diff --git a/src/py/bbctrl/plan.py b/src/py/bbctrl/plan.py index b4c92e3..4734542 100755 --- a/src/py/bbctrl/plan.py +++ b/src/py/bbctrl/plan.py @@ -35,6 +35,8 @@ import math import os import re import gzip +import struct +import math import camotics.gplan as gplan # pylint: disable=no-name-in-module,import-error @@ -46,61 +48,18 @@ reLogLine = re.compile( 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': + for axis in 'xyz': 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': + for axis in 'xyz': if axis in unit: unit[axis] /= length return unit @@ -109,7 +68,7 @@ def compute_unit(a, b): def compute_move(start, unit, dist): move = dict() - for axis in 'xyzabc': + for axis in 'xyz': if axis in unit and axis in start: move[axis] = start[axis] + unit[axis] * dist @@ -135,7 +94,7 @@ class Plan(object): # Initialized axis states and bounds self.bounds = dict(min = {}, max = {}) - for axis in 'xyzabc': + for axis in 'xyz': self.bounds['min'][axis] = math.inf self.bounds['max'][axis] = -math.inf @@ -153,7 +112,7 @@ class Plan(object): def get_bounds(self): # Remove infinity from bounds - for axis in 'xyzabc': + for axis in 'xyz': if self.bounds['min'][axis] == math.inf: del self.bounds['min'][axis] if self.bounds['max'][axis] == -math.inf: @@ -230,7 +189,7 @@ class Plan(object): line = 0 maxLine = 0 maxLineTime = time.time() - position = {axis: 0 for axis in 'xyzabc'} + position = {axis: 0 for axis in 'xyz'} rapid = False # Execute plan @@ -251,7 +210,7 @@ class Plan(object): move = {} startPos = dict() - for axis in 'xyzabc': + for axis in 'xyz': if axis in target: startPos[axis] = position[axis] position[axis] = target[axis] @@ -305,23 +264,41 @@ class Plan(object): 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)) + lastS = 0 + speed = 0 + first = True + x, y, z = 0, 0, 0 + + with gzip.open('positions.gz', 'wb') as f1: + with gzip.open('speeds.gz', 'wb') as f2: + for move in self._run(): + x = move.get('x', x) + y = move.get('y', y) + z = move.get('z', z) + rapid = move.get('rapid', False) + speed = move.get('s', speed) + s = struct.pack(' b + font-weight normal display inline-block padding 0 0.25em color #e5aa3d -- 2.27.0