From 0a803ee1470ab62fb3b28ae59c79e89f8b315cbf Mon Sep 17 00:00:00 2001 From: Joseph Coffland Date: Tue, 2 Apr 2019 15:08:20 -0700 Subject: [PATCH] Fixed ETA line wrapping on Web interface, EStop on motor fault, Added warning about reliability in a noisy environment on WiFi config page, Sync GCode and planner files to disk after write, CHANGELOG.md spelling errors --- CHANGELOG.md | 24 ++- src/avr/src/drv8711.c | 21 +- src/avr/src/drv8711.h | 1 + src/avr/src/messages.def | 1 + src/pug/templates/admin-network-view.pug | 7 + src/py/bbctrl/AVREmu.py | 6 +- src/py/bbctrl/Config.py | 2 +- src/py/bbctrl/FileHandler.py | 3 +- src/py/bbctrl/Preplanner.py | 255 ++++++++++++----------- src/py/bbctrl/plan.py | 9 +- src/stylus/style.styl | 2 + 11 files changed, 186 insertions(+), 145 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 19dfbed..162e217 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,10 @@ Buildbotics CNC Controller Firmware Changelog - Handle corrupt GCode simulation data correctly. - Fixes for exception logging. - Always limit motor max-velocity. #209 + - Sync GCode and planner files to disk after write. + - Added warning about reliability in a noisy environment on WiFi config page. + - EStop on motor fault. + - Fixed ETA line wrapping on Web interface. ## v0.4.6 - Fixed a rare ``Negative s-curve time`` error. @@ -80,7 +84,7 @@ Buildbotics CNC Controller Firmware Changelog - Preplan GCode and check for errors. - Display 3D view of program tool paths in browser. - Display accurate time remaining, ETA and progress during run. - - Automatically collapase moves in planner which are too short in time. + - Automatically collapse 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. @@ -140,7 +144,7 @@ Buildbotics CNC Controller Firmware Changelog - Faster switching of large GCode files in Web. - Fixed reported gcode line off by one. - Disable MDI input while running. - - Stablized direction pin output during slow moves. + - Stabilized direction pin output during slow moves. ## v0.3.20 - Eliminated drift caused by miscounting half microsteps. @@ -158,7 +162,7 @@ Buildbotics CNC Controller Firmware Changelog - Improved jogging with soft limits. - Added site favicon. - Fixed problems with offsets and imperial units. - - Fixed ``All zero s-curve times`` caused by extreemly short, non-zero moves. + - Fixed ``All zero s-curve times`` caused by extremely short, non-zero moves. - Fixed position drift. ## v0.3.18 @@ -167,12 +171,12 @@ Buildbotics CNC Controller Firmware Changelog ## v0.3.17 - Fixed pausing fail near end of run bug. - Show "Upgrading firmware" when upgrading. - - Log excessive pwr communcation failures as errors. + - Log excessive pwr communication failures as errors. - Ensure we can still get out of non-idle cycles when there are errors. - Less frequent pwr variable updates. - Stop cancels seek and subsequent estop. - Fixed bug in AVR/Planner command synchronization. - - Consistently display HOMMING state during homing operation. + - Consistently display HOMING state during homing operation. - Homing zeros axis global offset. - Added zero all button. #126 - Separate "Auto" and "MDI" play/pause & stop buttons. #126 @@ -224,10 +228,10 @@ Buildbotics CNC Controller Firmware Changelog - Support programmed pauses. i.e. M0 ## v0.3.11 - - Supressed ``firmware rebooted`` warning. + - Suppressed ``firmware rebooted`` warning. - Error on unexpected AVR reboot. - Fixed pin fault output. - - No longer using interupts for switch inputs. Debouncing on clock tick. + - No longer using interrupts for switch inputs. Debouncing on clock tick. ## v0.3.10 - Fixed "Flood" display, changed to "Load 1" and "Load 2". #108 @@ -261,12 +265,12 @@ Buildbotics CNC Controller Firmware Changelog - Show actual error message on planner errors - Reset planner on serious error - Fixed console clear - - Added helful info to Video tab + - Added helpful info to Video tab - Changed "Console" tab to "Messages" - Removed spin up/down velocity options, they don't do anything - Allow RS485 to work when wires are swapped - Allow setting VFD ID - - Only show relavant spindle config items + - Only show relevant spindle config items - More robust video camera reset - Added help page - Allow upgrade with out Internet @@ -301,7 +305,7 @@ Buildbotics CNC Controller Firmware Changelog - Accel units mm/min² -> m/min² - Search and latch velocity mm/min -> m/min - Fixed password update (broken in last version) - - Start Web server eariler in case of Python coding errors + - Start Web server earlier in case of Python coding errors Changelog not maintained in previous versions. See git commit log. diff --git a/src/avr/src/drv8711.c b/src/avr/src/drv8711.c index 4ff98bc..84a99a9 100644 --- a/src/avr/src/drv8711.c +++ b/src/avr/src/drv8711.c @@ -105,6 +105,8 @@ static void _current_set(current_t *c, float current) { c->current = current; float torque_over_gain = current * CURRENT_SENSE_RESISTOR / CURRENT_SENSE_REF; + +#if 0 float gain = 0; if (torque_over_gain <= 1.0 / 40) { @@ -126,6 +128,14 @@ static void _current_set(current_t *c, float current) { gain = 5; } +#else + // Max configurable current is 11A + if (1.0 / 5 < torque_over_gain) torque_over_gain = 1.0 / 5; + + float gain = 5; + c->isgain = DRV8711_CTRL_ISGAIN_5; +#endif + c->torque = round(torque_over_gain * gain * 255); } @@ -169,7 +179,7 @@ static uint8_t _driver_get_torque(int driver) { if (estop_triggered()) return 0; switch (drv->state) { - case DRV8711_IDLE: return drv->idle.torque; + case DRV8711_IDLE: return drv->idle.torque; case DRV8711_ACTIVE: return drv->drive.torque; default: return 0; // Off } @@ -186,7 +196,8 @@ static uint8_t _spi_next_command(uint8_t cmd) { switch (DRV8711_CMD_ADDR(command)) { case DRV8711_STATUS_REG: drv->status = spi.responses[driver]; - drv->flags |= drv->status; + drv->flags = (drv->flags & 0xff00) | drv->status; + if (_driver_fault(driver)) estop_trigger(STAT_MOTOR_FAULT); break; case DRV8711_OFF_REG: @@ -348,9 +359,9 @@ void drv8711_init() { // Setup pins // Must set the SS pin either in/high or any/output for master mode to work // Note, this pin is also used by the USART as the CTS line - DIRSET_PIN(SPI_SS_PIN); // Output - OUTSET_PIN(SPI_CLK_PIN); // High - DIRSET_PIN(SPI_CLK_PIN); // Output + DIRSET_PIN(SPI_SS_PIN); // Output + OUTSET_PIN(SPI_CLK_PIN); // High + DIRSET_PIN(SPI_CLK_PIN); // Output DIRCLR_PIN(SPI_MISO_PIN); // Input OUTSET_PIN(SPI_MOSI_PIN); // High DIRSET_PIN(SPI_MOSI_PIN); // Output diff --git a/src/avr/src/drv8711.h b/src/avr/src/drv8711.h index 3119eb3..0ea8fc7 100644 --- a/src/avr/src/drv8711.h +++ b/src/avr/src/drv8711.h @@ -143,6 +143,7 @@ enum { DRV8711_DRIVE_IDRIVEP_200 = 3 << 10, }; + enum { DRV8711_STATUS_OTS_bm = 1 << 0, DRV8711_STATUS_AOCP_bm = 1 << 1, diff --git a/src/avr/src/messages.def b/src/avr/src/messages.def index 284b542..8f1be68 100644 --- a/src/avr/src/messages.def +++ b/src/avr/src/messages.def @@ -50,6 +50,7 @@ STAT_MSG(SEEK_NOT_FOUND, "Switch not found") STAT_MSG(MOTOR_ID_INVALID, "Invalid motor ID") STAT_MSG(MOTOR_NOT_PREPPED, "Motor move not prepped") STAT_MSG(MOTOR_NOT_READY, "Motor not ready for move") +STAT_MSG(MOTOR_FAULT, "Motor fault") STAT_MSG(STEPPER_NULL_MOVE, "Null move in stepper driver") STAT_MSG(STEPPER_NOT_READY, "Stepper driver not ready for move") STAT_MSG(LONG_SEG_TIME, "Long segment time") diff --git a/src/pug/templates/admin-network-view.pug b/src/pug/templates/admin-network-view.pug index 341a4b8..a1c328a 100644 --- a/src/pug/templates/admin-network-view.pug +++ b/src/pug/templates/admin-network-view.pug @@ -80,19 +80,26 @@ script#admin-network-view-template(type="text/x-template") option(value="ap") Access Point button.pure-button.pure-button-primary(@click="wifiConfirm = true", v-if="wifi_mode == 'disabled'") Set + .pure-control-group(v-if="wifi_mode == 'ap'") label(for="wifi_ch") Channel select(name="wifi_ch", v-model="wifi_ch") each ch in [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11] option(value=ch)= ch + .pure-control-group(v-if="wifi_mode != 'disabled'") label(for="ssid") Network (SSID) input(name="ssid", v-model="wifi_ssid") + .pure-control-group(v-if="wifi_mode != 'disabled'") label(for="wifi_pass") Password input(name="wifi_pass", v-model="wifi_pass", type="password") button.pure-button.pure-button-primary(@click="wifiConfirm = true") Set + p(v-if="wifi_mode != 'disabled'"). + WARNING: WiFi may be unreliable in an electrically noisy environment + such as a machine shop. + message.wifi-confirm(:show.sync="wifiConfirm") h3(slot="header") Configure Wifi and reboot? div(slot="body") diff --git a/src/py/bbctrl/AVREmu.py b/src/py/bbctrl/AVREmu.py index f2f5622..de11cbd 100644 --- a/src/py/bbctrl/AVREmu.py +++ b/src/py/bbctrl/AVREmu.py @@ -95,6 +95,7 @@ class AVREmu(object): os.set_blocking(stdinFDs[1], False) os.set_blocking(stdoutFDs[0], False) + os.set_blocking(i2cFDs[1], False) self.avrOut = stdinFDs[1] self.avrIn = stdoutFDs[0] @@ -178,12 +179,13 @@ class AVREmu(object): def i2c_command(self, cmd, byte = None, word = None, block = None): - if byte is not None: data = byte + if byte is not None: data = chr(byte) elif word is not None: data = word elif block is not None: data = block else: data = '' try: - os.write(self.i2cOut, bytes(cmd + data + '\n', 'utf-8')) + if self.i2cOut is not None: + os.write(self.i2cOut, bytes(cmd + data + '\n', 'utf-8')) except BrokenPipeError: pass diff --git a/src/py/bbctrl/Config.py b/src/py/bbctrl/Config.py index ea0af9c..68004fc 100644 --- a/src/py/bbctrl/Config.py +++ b/src/py/bbctrl/Config.py @@ -175,7 +175,7 @@ class Config(object): with open(self.ctrl.get_path('config.json'), 'w') as f: json.dump(config, f) - subprocess.check_call(['sync']) + os.sync() self.ctrl.preplanner.invalidate_all() self.log.info('Saved') diff --git a/src/py/bbctrl/FileHandler.py b/src/py/bbctrl/FileHandler.py index 0d8a706..b9ba4e5 100644 --- a/src/py/bbctrl/FileHandler.py +++ b/src/py/bbctrl/FileHandler.py @@ -64,8 +64,9 @@ class FileHandler(bbctrl.APIHandler): if not os.path.exists(self.get_upload()): os.mkdir(self.get_upload()) - with open(self.get_upload(filename), 'wb') as f: + with open(self.get_upload(filename).encode('utf8'), 'wb') as f: f.write(gcode['body']) + os.sync() self.get_ctrl().preplanner.invalidate(filename) self.get_ctrl().state.add_file(filename) diff --git a/src/py/bbctrl/Preplanner.py b/src/py/bbctrl/Preplanner.py index 005198d..d7ebb7a 100644 --- a/src/py/bbctrl/Preplanner.py +++ b/src/py/bbctrl/Preplanner.py @@ -58,6 +58,124 @@ def plan_hash(path, config): return h.hexdigest() +def safe_remove(path): + try: + os.unlink(path) + except: pass + + +class Plan(object): + def __init__(self, preplanner, root, filename): + self.preplanner = preplanner + self.progress = 0 + self.cancel = threading.Event() + self.gcode = '%s/upload/%s' % (root, filename) + self.base = '%s/plans/%s' % (root, filename) + + + def delete(self): + files = glob.glob(self.base + '.*') + for path in files: safe_remove(path) + + + def clean(self, max = 2): + plans = glob.glob(self.base + '.*.json') + if len(plans) <= max: return + + # Delete oldest plans + plans = [(os.path.getmtime(path), path) for path in plans] + plans.sort() + + for mtime, path in plans[:len(plans) - max]: + safe_remove(path) + safe_remove(path[:-4] + 'positions.gz') + safe_remove(path[:-4] + 'speeds.gz') + + + def _update_progress(self, progress): + with self.preplanner.lock: + self.progress = progress + + + def _exec(self, files, state, config): + self.clean() # Clean up old plans + + with tempfile.TemporaryDirectory() as tmpdir: + cmd = ( + '/usr/bin/env', 'python3', + bbctrl.get_resource('plan.py'), + os.path.abspath(self.gcode), json.dumps(state), + json.dumps(config), + '--max-time=%s' % self.preplanner.max_plan_time, + '--max-loop=%s' % self.preplanner.max_loop_time + ) + + self.preplanner.log.info('Running: %s', cmd) + + with subprocess.Popen(cmd, stdout = subprocess.PIPE, + stderr = subprocess.PIPE, + cwd = tmpdir) as proc: + + for line in proc.stdout: + self._update_progress(float(line)) + if self.cancel.is_set(): + proc.terminate() + return + + out, errs = proc.communicate() + + self._update_progress(1) + if self.cancel.is_set(): return + + if proc.returncode: + raise Exception('Plan failed: ' + errs.decode('utf8')) + + os.rename(tmpdir + '/meta.json', files[0]) + os.rename(tmpdir + '/positions.gz', files[1]) + os.rename(tmpdir + '/speeds.gz', files[2]) + os.sync() + + + def load(self, state, config): + try: + os.nice(5) + + hid = plan_hash(self.gcode, config) + base = '%s.%s.' % (self.base, hid) + files = [base + 'json', base + 'positions.gz', base + 'speeds.gz'] + + def exists(): + for path in files: + if not os.path.exists(path): return False + return True + + def read(): + if self.cancel.is_set(): return + + try: + 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() + + return meta, positions, speeds + + except: + self.preplanner.log.exception() + + for path in files: + if os.path.exists(path): + os.remove(path) + + if exists(): + data = read() + if data is not None: return data + + if not exists(): self._exec(files, state, config) + return read() + + except: + self.preplanner.log.exception() + class Preplanner(object): def __init__(self, ctrl, threads = 4, max_plan_time = 60 * 60 * 24, @@ -87,37 +205,28 @@ class Preplanner(object): def invalidate(self, filename): with self.lock: if filename in self.plans: - self.plans[filename][2].set() # Cancel + self.plans[filename].cancel.set() del self.plans[filename] def invalidate_all(self): with self.lock: for filename, plan in self.plans.items(): - plan[2].set() # Cancel + plan.cancel.set() self.plans = {} def delete_all_plans(self): files = glob.glob(self.ctrl.get_plan('*')) - - for path in files: - try: - os.unlink(path) - except OSError: pass - + for path in files: safe_remove(path) self.invalidate_all() def delete_plans(self, filename): - files = glob.glob(self.ctrl.get_plan(filename + '.*')) - - for path in files: - try: - os.unlink(path) - except OSError: pass - - self.invalidate(filename) + with self.lock: + if filename in self.plans: + self.plans[filename].delete() + self.invalidate(filename) def get_plan(self, filename): @@ -126,21 +235,22 @@ class Preplanner(object): with self.lock: if filename in self.plans: plan = self.plans[filename] else: - cancel = threading.Event() - plan = [self._plan(filename, cancel), 0, cancel] + plan = Plan(self, self.ctrl.get_path(), filename) + plan.future = self._plan(plan) self.plans[filename] = plan - return plan[0] + return plan.future def get_plan_progress(self, filename): with self.lock: - if filename in self.plans: return self.plans[filename][1] + if filename in self.plans: + return self.plans[filename].progress return 0 @gen.coroutine - def _plan(self, filename, cancel): + def _plan(self, plan): # Wait until state is fully initialized yield self.started @@ -150,105 +260,6 @@ class Preplanner(object): del config['default-units'] # Start planner thread - plan = yield self.pool.submit( - self._load_plan, filename, state, config, cancel) - return plan - - - def _clean_plans(self, filename, max = 2): - plans = glob.glob(self.ctrl.get_plan(filename + '.*')) - if len(plans) <= max: return - - # Delete oldest plans - plans = [(os.path.getmtime(path), path) for path in plans] - plans.sort() - - for mtime, path in plans[:len(plans) - max]: - try: - os.unlink(path) - except OSError: pass - + future = yield self.pool.submit(plan.load, state, config) - def _progress(self, filename, progress): - with self.lock: - if filename in self.plans: - self.plans[filename][1] = progress - - - def _read_files(self, files): - 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() - - return meta, positions, speeds - - - def _exec_plan(self, filename, files, state, config, cancel): - self._clean_plans(filename) # Clean up old plans - - path = os.path.abspath(self.ctrl.get_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 - ) - - self.log.info('Running: %s', cmd) - - with subprocess.Popen(cmd, stdout = subprocess.PIPE, - stderr = subprocess.PIPE, - cwd = tmpdir) as proc: - - for line in proc.stdout: - self._progress(filename, float(line)) - if cancel.is_set(): - proc.terminate() - return - - out, errs = proc.communicate() - - self._progress(filename, 1) - if cancel.is_set(): return - - if proc.returncode: - raise Exception('Plan failed: ' + errs.decode('utf8')) - - os.rename(tmpdir + '/meta.json', files[0]) - os.rename(tmpdir + '/positions.gz', files[1]) - os.rename(tmpdir + '/speeds.gz', files[2]) - - - def _files_exist(self, files): - for path in files: - if not os.path.exists(path): return False - - return True - - - def _load_plan(self, filename, state, config, cancel): - try: - os.nice(5) - - hid = plan_hash(self.ctrl.get_upload(filename), config) - base = self.ctrl.get_plan(filename + '.' + hid) - files = [ - base + '.json', base + '.positions.gz', base + '.speeds.gz'] - - try: - if not self._files_exist(files): - self._exec_plan(filename, files, state, config, cancel) - - if not cancel.is_set(): return self._read_files(files) - - except: - self.log.exception() - - for path in files: - if os.path.exists(path): - os.remove(path) - - except: - self.log.exception() + return future diff --git a/src/py/bbctrl/plan.py b/src/py/bbctrl/plan.py index 5c5ca36..0961970 100755 --- a/src/py/bbctrl/plan.py +++ b/src/py/bbctrl/plan.py @@ -59,8 +59,9 @@ def compute_unit(a, b): length = math.sqrt(length) - for axis in 'xyz': - if axis in unit: unit[axis] /= length + if length: + for axis in 'xyz': + if axis in unit: unit[axis] /= length return unit @@ -81,7 +82,7 @@ class Plan(object): self.state = state self.config = config - self.lines = sum(1 for line in open(path)) + self.lines = sum(1 for line in open(path, 'rb')) self.planner = gplan.Planner() self.planner.set_resolver(self.get_var_cb) @@ -257,7 +258,7 @@ class Plan(object): raise Exception('Max loop time (%d sec) exceeded.' % args.max_loop) - self.progress(maxLine / self.lines) + if self.lines: self.progress(maxLine / self.lines) except Exception as e: self.log_cb('error', str(e), os.path.basename(self.path), line, 0) diff --git a/src/stylus/style.styl b/src/stylus/style.styl index 4dffc46..ae62368 100644 --- a/src/stylus/style.styl +++ b/src/stylus/style.styl @@ -374,6 +374,7 @@ span.unit text-align right overflow hidden text-overflow ellipsis + white-space nowrap th min-width 5.25em @@ -442,6 +443,7 @@ span.unit select max-width 11em + min-width inherit !important .progress display inline-block -- 2.27.0