Fixed ETA line wrapping on Web interface, EStop on motor fault, Added warning about...
authorJoseph Coffland <joseph@cauldrondevelopment.com>
Tue, 2 Apr 2019 22:08:20 +0000 (15:08 -0700)
committerJoseph Coffland <joseph@cauldrondevelopment.com>
Tue, 2 Apr 2019 22:08:20 +0000 (15:08 -0700)
CHANGELOG.md
src/avr/src/drv8711.c
src/avr/src/drv8711.h
src/avr/src/messages.def
src/pug/templates/admin-network-view.pug
src/py/bbctrl/AVREmu.py
src/py/bbctrl/Config.py
src/py/bbctrl/FileHandler.py
src/py/bbctrl/Preplanner.py
src/py/bbctrl/plan.py
src/stylus/style.styl

index 19dfbed312cece2bba92a39dfd7f97946344852b..162e217acd47b21a4a9b4f48cd83918dc8eeb677 100644 (file)
@@ -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.
index 4ff98bc07cc1e0cab48e12c8d155f464e48bb1d4..84a99a9ae0e189206a6765a6d4673eaa2cf47017 100644 (file)
@@ -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
index 3119eb3eb4999e7606abc8171907dff2b6300270..0ea8fc7c983074118c1cb4796c3cf20880230288 100644 (file)
@@ -143,6 +143,7 @@ enum {
   DRV8711_DRIVE_IDRIVEP_200       = 3 << 10,
 };
 
+
 enum {
   DRV8711_STATUS_OTS_bm           = 1 << 0,
   DRV8711_STATUS_AOCP_bm          = 1 << 1,
index 284b5422942fb2aad726bcca6881dfd562369cc1..8f1be68be9115d8f8b338727689c530a08ea56c5 100644 (file)
@@ -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")
index 341a4b8f2e39227cc60f823c9e43cabb9874a412..a1c328ae1226546fa3fb1f8a5aa80af764b7d5cb 100644 (file)
@@ -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")
index f2f562263c58b0addb8a0c5dda241888ff5983d3..de11cbdcf64a4576e59895243363b0dcad233729 100644 (file)
@@ -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
index ea0af9cd06e0eaca8e3ad591a19b9f9c65d73e25..68004fc2ce3fef1bb13ce9d00e45d731ea31cb2f 100644 (file)
@@ -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')
index 0d8a7065fa2f0944c1b478f26edb74f2521318f6..b9ba4e5b63dd5b9a11fb2cb0cdd71ea3f7d24a4d 100644 (file)
@@ -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)
index 005198d614e3e886cb8b61f434bc61bfca72f8af..d7ebb7ac35d6a9ea712177de88c735b6055d5a50 100644 (file)
@@ -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
index 5c5ca36a799d4477dffa1862a9548e4510783746..0961970007e9b654aa432e1da6408dccbb6e19b1 100755 (executable)
@@ -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)
index 4dffc46a0dbd2fa1da15e63c908a471591358a1a..ae6236875219eb04a9047b098ca14b497caeb6ca 100644 (file)
@@ -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