- Disable spindle and loads on stop.
authorJoseph Coffland <joseph@cauldrondevelopment.com>
Sun, 25 Feb 2018 02:29:25 +0000 (18:29 -0800)
committerJoseph Coffland <joseph@cauldrondevelopment.com>
Sun, 25 Feb 2018 02:29:25 +0000 (18:29 -0800)
 - Fixed several state transition (stop, pause, estop, etc.) problems.

17 files changed:
CHANGELOG.md
package.json
src/avr/src/command.def
src/avr/src/exec.c
src/avr/src/jog.c
src/avr/src/jog.h
src/avr/src/outputs.c
src/avr/src/outputs.h
src/avr/src/state.c
src/avr/src/state.h
src/jade/templates/control-view.jade
src/js/control-view.js
src/py/bbctrl/Cmd.py
src/py/bbctrl/Comm.py
src/py/bbctrl/Mach.py
src/py/bbctrl/Planner.py
src/py/bbctrl/State.py

index 0e1b7725dc7bbcd5cc0adbba74035d9e47cfe3da..832ed78347f49c2fced5728fb184c459ae1e3354 100644 (file)
@@ -1,6 +1,10 @@
 Buildbotics CNC Controller Firmware Change Log
 ==============================================
 
+## v0.3.13
+ - Disable spindle and loads on stop.
+ - Fixed several state transition (stop, pause, estop, etc.) problems.
+
 ## v0.3.12
  - Updated DB25 M2 breakout diagram.
  - Enabled AVR watchdog.
index 30d65d6fd5e76373a8256341692b772fba72e9dc..33e9411e536f5fedf12509409d2515d37d8af021 100644 (file)
@@ -1,6 +1,6 @@
 {
   "name": "bbctrl",
-  "version": "0.3.12",
+  "version": "0.3.13",
   "homepage": "http://buildbotics.com/",
   "repository": "https://github.com/buildbotics/bbctrl-firmware",
   "license": "GPL-3.0+",
index 53eca13aa6d68a51c2bff36195eac22812b1d9ca..0cea17b5295a0f7dc9d7afd4dfc03543b502d154 100644 (file)
@@ -32,6 +32,7 @@ CMD('a', set_axis,  1, "[axis][position] Set axis position")
 CMD('l', line,      1, "[targetVel][maxJerk][axes][times]")
 CMD('d', dwell,     1, "[seconds]")
 CMD('P', pause,     1, "[type] Pause control")
+CMD('S', stop,      0, "Stop move, spindle and load outputs")
 CMD('U', unpause,   0, "Unpause")
 CMD('j', jog,       0, "[axes]")
 CMD('r', report,    0, "<0|1>[var] Enable or disable var reporting")
@@ -39,7 +40,6 @@ CMD('R', reboot,    0, "Reboot the controller")
 CMD('c', resume,    0, "Continue processing after a flush")
 CMD('E', estop,     0, "Emergency stop")
 CMD('C', clear,     0, "Clear estop")
-CMD('S', step,      0, "Advance one step")
 CMD('F', flush,     0, "Flush command queue")
 CMD('D', dump,      0, "Report all variables")
 CMD('h', help,      0, "Print this help screen")
index 71be146cc74a1fb4a8b1d1f5e16feece38a6eb1b..97edf582f00196aa7c7de1a52ac2c240748e81aa 100644 (file)
@@ -53,9 +53,7 @@ void exec_init() {
   memset(&ex, 0, sizeof(ex));
   ex.feed_override = 1;
   ex.spindle_override = 1;
-  // TODO implement move stepping
   // TODO implement overrides
-  // TODO implement optional pause
 }
 
 
index 947fda2aba6f7664573d1c137934ccc0bf84adb1..ed1cd4cb12b14c5373195eb1d41c69f2ebf4658e 100644 (file)
@@ -247,9 +247,16 @@ stat_t jog_exec() {
 }
 
 
+void jog_stop() {
+  jr.writing = true;
+  for (int axis = 0; axis < AXES; axis++)
+    jr.axes[axis].next = 0;
+  jr.writing = false;
+}
+
 
 stat_t command_jog(char *cmd) {
-  // Ignore jog commands when not already idle
+  // Ignore jog commands when not READY or JOGGING
   if (state_get() != STATE_READY && state_get() != STATE_JOGGING)
     return STAT_NOP;
 
index 14281d347331439824d5ca8bf4c3ac4438cd55af..ea093ab5fcaeb48741cb0749c5a44ed958046654 100644 (file)
@@ -27,8 +27,8 @@
 
 #pragma once
 
-
 #include "status.h"
 
 
 stat_t jog_exec();
+void jog_stop();
index 90b8818dd5ec157d6e678a6bbbbe53d4d167fed3..80c7fdc8553b74bb7b1a7833ccae0522edbbf097 100644 (file)
@@ -113,6 +113,12 @@ output_state_t outputs_get_state(uint8_t pin) {
 }
 
 
+void outputs_stop() {
+  outputs_set_active(SWITCH_1_PIN, false);
+  outputs_set_active(SWITCH_2_PIN, false);
+}
+
+
 // Var callbacks
 uint8_t get_output_state(uint8_t id) {
   return OUTS <= id ? OUT_TRI : outputs[id].state;
index b775303de998ca8358c6825ec31228a195f8f595..0e5a6f8f961b34a5d4be7b30650c74e6b5f79418 100644 (file)
@@ -55,3 +55,4 @@ bool outputs_is_active(uint8_t pin);
 void outputs_set_active(uint8_t pin, bool active);
 void outputs_set_mode(uint8_t pin, output_mode_t mode);
 output_state_t outputs_get_state(uint8_t pin);
+void outputs_stop();
index 37dda1760337f153e23babbc9b3544873373e170..73b55a172f9b438176df0557665e3a7af0bd2443 100644 (file)
@@ -31,6 +31,8 @@
 #include "command.h"
 #include "stepper.h"
 #include "spindle.h"
+#include "outputs.h"
+#include "jog.h"
 #include "estop.h"
 #include "report.h"
 
@@ -41,11 +43,12 @@ static struct {
   state_t state;
   hold_reason_t hold_reason;
 
+  bool stop_requested;
   bool pause_requested;
+  bool optional_pause_requested;
+  bool unpause_requested;
   bool flush_requested;
-  bool start_requested;
   bool resume_requested;
-  bool optional_pause_requested;
 
 } s = {
   .flush_requested = true, // Start out flushing
@@ -69,9 +72,9 @@ PGM_P state_get_pgmstr(state_t state) {
 PGM_P state_get_hold_reason_pgmstr(hold_reason_t reason) {
   switch (reason) {
   case HOLD_REASON_USER_PAUSE:    return PSTR("User paused");
+  case HOLD_REASON_USER_STOP:     return PSTR("User stop");
   case HOLD_REASON_PROGRAM_PAUSE: return PSTR("Program paused");
-  case HOLD_REASON_STEPPING:      return PSTR("Stepping");
-  case HOLD_REASON_SEEK:          return PSTR("Switch found");
+  case HOLD_REASON_SWITCH_FOUND:  return PSTR("Switch found");
   }
 
   return PSTR("INVALID");
@@ -108,13 +111,54 @@ bool state_is_quiescent() {
 
 void state_seek_hold() {
   if (state_get() == STATE_RUNNING) {
-    _set_hold_reason(HOLD_REASON_SEEK);
+    _set_hold_reason(HOLD_REASON_SWITCH_FOUND);
+    _set_state(STATE_STOPPING);
+  }
+}
+
+
+static void _stop() {
+  switch (state_get()) {
+  case STATE_STOPPING:
+  case STATE_RUNNING:
+    _set_hold_reason(HOLD_REASON_USER_STOP);
     _set_state(STATE_STOPPING);
+    break;
+
+  case STATE_JOGGING:
+    jog_stop();
+    // Fall through
+
+  case STATE_READY:
+  case STATE_HOLDING:
+    s.flush_requested = true;
+    spindle_stop();
+    outputs_stop();
+    _set_state(STATE_READY);
+    break;
+
+  case STATE_ESTOPPED:
+    break; // Ignore
   }
 }
 
 
-void state_holding() {_set_state(STATE_HOLDING);}
+void state_holding() {
+  _set_state(STATE_HOLDING);
+
+  switch (s.hold_reason) {
+  case HOLD_REASON_PROGRAM_PAUSE: break;
+
+  case HOLD_REASON_USER_PAUSE:
+  case HOLD_REASON_SWITCH_FOUND:
+    s.flush_requested = true;
+    break;
+
+  case HOLD_REASON_USER_STOP:
+    _stop();
+    break;
+  }
+}
 
 
 void state_pause(bool optional) {
@@ -168,21 +212,26 @@ void state_estop() {_set_state(STATE_ESTOPPED);}
 void state_callback() {
   if (estop_triggered()) return;
 
+  // Pause
   if (s.pause_requested || s.flush_requested) {
-    if (s.pause_requested) _set_hold_reason(HOLD_REASON_USER_PAUSE);
+    if (state_get() == STATE_RUNNING) {
+      if (s.pause_requested) _set_hold_reason(HOLD_REASON_USER_PAUSE);
+      _set_state(STATE_STOPPING);
+    }
+
     s.pause_requested = false;
+  }
 
-    if (state_get() == STATE_RUNNING) _set_state(STATE_STOPPING);
+  // Stop
+  if (s.stop_requested) {
+    _stop();
+    s.stop_requested = false;
   }
 
   // Only flush queue when idle or holding
   if (s.flush_requested && state_is_quiescent()) {
     command_flush_queue();
 
-    // Stop spindle
-    // TODO Spindle should not be stopped when pausing
-    spindle_stop();
-
     // Resume
     if (s.resume_requested) {
       s.flush_requested = s.resume_requested = false;
@@ -191,9 +240,9 @@ void state_callback() {
   }
 
   // Don't start while flushing or stopping
-  if (s.start_requested && !s.flush_requested &&
+  if (s.unpause_requested && !s.flush_requested &&
       state_get() != STATE_STOPPING) {
-    s.start_requested = false;
+    s.unpause_requested = false;
     s.optional_pause_requested = false;
 
     if (state_get() == STATE_HOLDING) {
@@ -236,20 +285,20 @@ void command_pause_exec(void *data) {
 }
 
 
-stat_t command_unpause(char *cmd) {
-  s.start_requested = true;
+stat_t command_stop(char *cmd) {
+  s.stop_requested = true;
   return STAT_OK;
 }
 
 
-stat_t command_resume(char *cmd) {
-  if (s.flush_requested) s.resume_requested = true;
+stat_t command_unpause(char *cmd) {
+  s.unpause_requested = true;
   return STAT_OK;
 }
 
 
-stat_t command_step(char *cmd) {
-  // TODO
+stat_t command_resume(char *cmd) {
+  if (s.flush_requested) s.resume_requested = true;
   return STAT_OK;
 }
 
index 3d4059e8e4413ddc55174d87a6cf1f926e28a2c4..fab452f52b512cf17d6e9757b0bced9eaabb7be3 100644 (file)
@@ -44,9 +44,9 @@ typedef enum {
 
 typedef enum {
   HOLD_REASON_USER_PAUSE,
+  HOLD_REASON_USER_STOP,
   HOLD_REASON_PROGRAM_PAUSE,
-  HOLD_REASON_STEPPING,
-  HOLD_REASON_SEEK,
+  HOLD_REASON_SWITCH_FOUND,
 } hold_reason_t;
 
 
index ac9ff60088bd335ddd2490f12ffeb91fd1e3aefc..277a738a918ea21a0d98352a3a70968c7a90efd3 100644 (file)
@@ -135,13 +135,13 @@ script#control-view-template(type="text/x-template")
         td
       tr
         th Load 1
-        td(:class="state.load1state ? 'load-on' : ''")
-          | {{state.load1state ? 'On' : 'Off'}}
+        td(:class="state['1oa'] ? 'load-on' : ''")
+          | {{state['1oa'] ? 'On' : 'Off'}}
         td
       tr
         th Load 2
-        td(:class="state.load2state ? 'load-on' : ''")
-          | {{state.load2state ? 'On' : 'Off'}}
+        td(:class="state['2oa'] ? 'load-on' : ''")
+          | {{state['2oa'] ? 'On' : 'Off'}}
         td
 
     .override(title="Feed rate override.")
@@ -167,8 +167,7 @@ script#control-view-template(type="text/x-template")
         @click="start_pause", :disabled="!state.selected")
         .fa(:class="state.xx == 'RUNNING' ? 'fa-pause' : 'fa-play'")
 
-      button.pure-button(title="Stop program.", @click="stop",
-        :disabled="state.xx == 'READY'")
+      button.pure-button(title="Stop program.", @click="stop")
         .fa.fa-stop
 
       button.pure-button(title="Pause program at next optional stop (M1).",
index 5edd3186ed03ad7ea6e61e36ad05e20d661008fb..dbd75cce4a4847de67af14ec1dd673dfcbdd96f3 100644 (file)
@@ -275,7 +275,6 @@ module.exports = {
 
       api.upload('file', fd)
         .done(function () {
-          file.name;
           if (file.name == this.last_file) this.last_file = '';
           this.update();
         }.bind(this));
index 6a4e1b4393a97145c001f8e2f514841a47c55eb6..7bcf41d642d2c833cc041870cd81c6f2c31db013 100644 (file)
@@ -41,8 +41,8 @@ SEEK      = 's'
 SET_AXIS  = 'a'
 LINE      = 'l'
 DWELL     = 'd'
-OPT_PAUSE = 'p'
 PAUSE     = 'P'
+STOP      = 'S'
 UNPAUSE   = 'U'
 JOG       = 'j'
 REPORT    = 'r'
@@ -50,7 +50,6 @@ REBOOT    = 'R'
 RESUME    = 'c'
 ESTOP     = 'E'
 CLEAR     = 'C'
-STEP      = 'S'
 FLUSH     = 'F'
 DUMP      = 'D'
 HELP      = 'h'
@@ -197,7 +196,6 @@ def decode_command(cmd):
     elif cmd[0] == ESTOP:   data['type'] = 'estop'
     elif cmd[0] == CLEAR:   data['type'] = 'clear'
     elif cmd[0] == FLUSH:   data['type'] = 'flush'
-    elif cmd[0] == STEP:    data['type'] = 'step'
     elif cmd[0] == RESUME:  data['type'] = 'resume'
 
     print(json.dumps(data))
index 2646a45fcdb7d93400fcbc3670465bd2084e95d2..0281342a405c4553c1b26ac379c41b3a3392dca8 100644 (file)
@@ -47,7 +47,7 @@ class Comm():
         self.queue = deque()
         self.in_buf = ''
         self.command = None
-        self.reboot_expected = False
+        self.reboot_expected = True
 
         try:
             self.sp = serial.Serial(ctrl.args.serial, ctrl.args.baud,
@@ -92,7 +92,7 @@ class Comm():
                     raise
 
 
-    def set_write(self, enable):
+    def _set_write(self, enable):
         if self.sp is None: return
 
         flags = self.ctrl.ioloop.READ
@@ -105,9 +105,12 @@ class Comm():
         self.command = bytes(cmd.strip() + '\n', 'utf-8')
 
 
+    def resume(self): self.queue_command(Cmd.RESUME)
+
+
     def queue_command(self, cmd):
         self.queue.append(cmd)
-        self.set_write(True)
+        self._set_write(True)
 
 
     def _serial_write(self):
@@ -117,7 +120,7 @@ class Comm():
                 count = self.sp.write(self.command)
 
             except Exception as e:
-                self.set_write(False)
+                self.command = None
                 raise e
 
             self.command = self.command[count:]
@@ -131,7 +134,7 @@ class Comm():
         else:
             cmd = self.next_cb()
 
-            if cmd is None: self.set_write(False) # Stop writing
+            if cmd is None: self._set_write(False) # Stop writing
             else: self._load_next_command(cmd)
 
 
@@ -189,14 +192,13 @@ class Comm():
 
                 if 'variables' in msg:
                     self._update_vars(msg)
+                    self.reboot_expected = False
 
-                elif 'msg' in msg:
-                    self._log_msg(msg)
+                elif 'msg' in msg: self._log_msg(msg)
 
                 elif 'firmware' in msg:
                     if self.reboot_expected: log.info('AVR firmware rebooted')
                     else: log.error('Unexpected AVR firmware reboot')
-                    self.reboot_expected = False
                     self.connect()
 
                 else: self.ctrl.state.update(msg)
index 2695d9aaf1188a0cc156aa5c0efb237b177e1eeb..9e9c7965decfccda939c490798395a4a244029a8 100644 (file)
@@ -59,7 +59,7 @@ class Mach():
         self.ctrl = ctrl
         self.planner = bbctrl.Planner(ctrl)
         self.comm = bbctrl.Comm(ctrl, self._comm_next, self._comm_connect)
-        self.stopping = False
+        self.update_timer = None
 
         ctrl.state.set('cycle', 'idle')
         ctrl.state.add_listener(self._update)
@@ -67,6 +67,7 @@ class Mach():
         self.comm.reboot()
 
 
+    def _get_state(self): return self.ctrl.state.get('xx', '')
     def _get_cycle(self): return self.ctrl.state.get('cycle')
 
 
@@ -82,36 +83,33 @@ class Mach():
                             (cycle, current))
 
 
+    def _update_cycle(self):
+        # Cancel timer if set
+        if self.update_timer is not None:
+            self.ctrl.ioloop.remove_timeout(self.update_timer)
+            self.update_timer = None
+
+        # Check for idle state
+        if self._get_cycle() != 'idle' and self._get_state() == 'READY':
+            # Check again later if busy
+            if self.planner.is_busy() or self.comm.is_active():
+                self.ctrl.ioloop.call_later(0.5, self._update_cycle)
+
+            else: self.ctrl.state.set('cycle', 'idle')
+
+
     def _update(self, update):
-        state = self.ctrl.state.get('xx', '')
+        state = self._get_state()
 
         # Handle EStop
-        if 'xx' in update and state == 'ESTOPPED':
-            self._stop_sending_gcode()
-            self.stopping = False
-
-        # Handle stop
-        if self.stopping:
-            if state == 'READY' and not self.planner.is_running():
-                self.stopping = False
-
-            if state == 'HOLDING':
-                self._stop_sending_gcode()
-                # Resume once current queue of GCode commands has flushed
-                self.comm.i2c_command(Cmd.FLUSH)
-                self.comm.queue_command(Cmd.RESUME)
-                self.ctrl.state.set('line', 0)
-                self.stopping = False
+        if 'xx' in update and state == 'ESTOPPED': self.planner.reset()
 
         # Update cycle
-        if (self._get_cycle() != 'idle' and not self.planner.is_busy() and
-            not self.comm.is_active() and state == 'READY'):
-            self.ctrl.state.set('cycle', 'idle')
+        self._update_cycle()
 
         # Continue after seek hold
-        if (state == 'HOLDING' and
-            self.ctrl.state.get('pr', '') == 'Switch found' and
-            self.planner.is_synchronizing()):
+        if (state == 'HOLDING' and self.planner.is_synchronizing() and
+            self.ctrl.state.get('pr', '') == 'Switch found'):
             self.unpause()
 
 
@@ -119,15 +117,9 @@ class Mach():
         if self.planner.is_running(): return self.planner.next()
 
 
-    def _comm_connect(self): self._stop_sending_gcode()
-
-
-    def _start_sending_gcode(self, path):
-        self.planner.load('upload/' + path)
-        self.comm.set_write(True)
-
-
-    def _stop_sending_gcode(self): self.planner.reset()
+    def _comm_connect(self):
+        self.ctrl.state.reset()
+        self.planner.reset()
 
 
     def _query_var(self, cmd):
@@ -155,7 +147,7 @@ class Mach():
         else:
             self._begin_cycle('mdi')
             self.planner.mdi(cmd)
-            self.comm.set_write(True)
+            self.comm.resume()
 
 
     def set(self, code, value):
@@ -198,11 +190,16 @@ class Mach():
                 # Home axis
                 log.info('Homing %s axis' % axis)
                 self.planner.mdi(axis_homing_procedure % {'axis': axis})
-                self.comm.set_write(True)
+                self.comm.resume()
 
 
     def estop(self): self.comm.estop()
-    def clear(self): self.comm.clear()
+
+
+    def clear(self):
+        if self._get_state() == 'ESTOPPED':
+            self.ctrl.state.reset()
+            self.comm.clear()
 
 
     def select(self, path):
@@ -216,37 +213,39 @@ class Mach():
 
     def start(self):
         self._begin_cycle('running')
-        self._start_sending_gcode(self.ctrl.state.get('selected'))
+        self.planner.load('upload/' + self.ctrl.state.get('selected'))
+        self.comm.resume()
 
 
     def step(self):
         raise Exception('NYI') # TODO
-        self.comm.i2c_command(Cmd.STEP)
         if self._get_cycle() != 'running': self.start()
+        else: self.comm.i2c_command(Cmd.UNPAUSE)
 
 
     def stop(self):
-        self.pause()
-        self.stopping = True
+        if self._get_cycle() == 'idle': self._begin_cycle('running')
+        self.comm.i2c_command(Cmd.STOP)
+        self.planner.stop()
+        self.ctrl.state.set('line', 0)
 
 
     def pause(self): self.comm.pause()
 
 
     def unpause(self):
-        if self.ctrl.state.get('xx', '') != 'HOLDING': return
+        if self._get_state() != 'HOLDING': return
 
         pause_reason = self.ctrl.state.get('pr', '')
         if pause_reason in ['User paused', 'Switch found']:
-            self.comm.i2c_command(Cmd.FLUSH)
-            self.comm.queue_command(Cmd.RESUME)
             self.planner.restart()
-            self.comm.set_write(True)
+            self.comm.resume()
 
         self.comm.i2c_command(Cmd.UNPAUSE)
 
 
     def optional_pause(self):
+        # TODO this could work better as a variable, i.e. $op=1
         if self._get_cycle() == 'running': self.comm.pause(True)
 
 
index bb08e56c98eb2458e4690b5e07b65ce1c93a9ff3..f91d11d9e75f32e5ebce18c5f8bdc0d387e31857 100644 (file)
@@ -169,12 +169,6 @@ class Planner():
 
             if name == 'speed': return Cmd.speed(value)
 
-            if name == '_mist':
-                self._queue_set_cmd(block['id'], 'load1state', value)
-
-            if name == '_flood':
-                self._queue_set_cmd(block['id'], 'load2state', value)
-
             if (name[0:1] == '_' and name[1:2] in 'xyzabc' and
                 name[2:] == '_home'): return Cmd.set_axis(name[1], value)
 
@@ -206,6 +200,12 @@ class Planner():
         self.setq.clear()
 
 
+    def stop(self):
+        self.planner.stop()
+        self.lastID = -1
+        self.setq.clear()
+
+
     def restart(self):
         state = self.ctrl.state
         id = state.get('id')
index ef794966b61ba42c42a4dce9b572a3bd920bea7f..516d6d42decd75e0afc1b8173b8412cb341c7061 100644 (file)
@@ -62,11 +62,17 @@ class State(object):
             self.set_callback(str(i) + 'hp',
                      lambda name, i = i: self.motor_home_position(i))
 
-            # Set not homed
-            self.set('%dhomed' % i, False)
+        self.reset()
 
-        # Zero offsets
-        for axis in 'xyzabc': self.vars['offset_' + axis] = 0
+
+    def reset(self):
+        # Unhome all motors
+        for i in range(4): self.set('%dhomed' % i, False)
+
+        # Zero offsets and positions
+        for axis in 'xyzabc':
+            self.set(axis + 'p', 0)
+            self.set('offset_' + axis, 0)
 
 
     def _notify(self):