- JOGGING, HOMMING and MDI states.
authorJoseph Coffland <joseph@cauldrondevelopment.com>
Thu, 22 Feb 2018 11:06:45 +0000 (03:06 -0800)
committerJoseph Coffland <joseph@cauldrondevelopment.com>
Thu, 22 Feb 2018 11:06:45 +0000 (03:06 -0800)
 - Fixed position problem with rapid MDI entry.

12 files changed:
CHANGELOG.md
src/avr/src/command.c
src/avr/src/jog.c
src/avr/src/line.c
src/avr/src/state.c
src/avr/src/state.h
src/js/control-view.js
src/py/bbctrl/Comm.py
src/py/bbctrl/Ctrl.py
src/py/bbctrl/Mach.py
src/py/bbctrl/Planner.py
src/py/bbctrl/State.py

index b8c031d9de9b8677c507ec63ec67fdf14b2d2370..8e22363f27bdb711d6948af2a925b4ede2941fa3 100644 (file)
@@ -11,6 +11,8 @@ Buildbotics CNC Controller Firmware Change Log
  - Log debug messages to console in developer mode.
  - Fixed AVR log message source.
  - Fixed step correction.
+ - JOGGING, HOMMING and MDI states.
+ - Fixed position problem with rapid MDI entry.
 
 ## v0.3.8
  - Fixed pwr flags display
index 2b5ca0d99404dea71a076fd3eaae15277890e3c9..bf37fa8e2d709995a812d334c9c33591140a3c82 100644 (file)
@@ -202,10 +202,7 @@ bool command_callback() {
   }
 
   // Dispatch non-empty commands
-  if (*block && status == STAT_OK) {
-    if (sync_q_empty()) command_reset_position();
-    status = _dispatch(block);
-  }
+  if (*block && status == STAT_OK) status = _dispatch(block);
 
   block = 0; // Command consumed
 
index 81887ad796cbb28a71b0f29a14f6681e4fd94fc6..947fda2aba6f7664573d1c137934ccc0bf84adb1 100644 (file)
@@ -32,6 +32,7 @@
 #include "exec.h"
 #include "state.h"
 #include "scurve.h"
+#include "command.h"
 #include "config.h"
 
 #include <stdbool.h>
@@ -56,7 +57,6 @@ typedef struct {
 
 
 typedef struct {
-  bool active;
   bool writing;
   bool done;
 
@@ -224,9 +224,10 @@ stat_t jog_exec() {
 
   // Check if we are done
   if (jr.done) {
+    command_reset_position();
     exec_set_velocity(0);
     exec_set_cb(0);
-    jr.active = false;
+    state_idle();
 
     return STAT_NOP; // Done, no move executed
   }
@@ -249,7 +250,8 @@ stat_t jog_exec() {
 
 stat_t command_jog(char *cmd) {
   // Ignore jog commands when not already idle
-  if (!jr.active && state_get() != STATE_READY) return STAT_NOP;
+  if (state_get() != STATE_READY && state_get() != STATE_JOGGING)
+    return STAT_NOP;
 
   // Skip command code
   cmd++;
@@ -263,15 +265,15 @@ stat_t command_jog(char *cmd) {
   if (*cmd) return STAT_INVALID_ARGUMENTS;
 
   // Reset
-  if (!jr.active) memset(&jr, 0, sizeof(jr));
+  if (state_get() != STATE_JOGGING) memset(&jr, 0, sizeof(jr));
 
   jr.writing = true;
   for (int axis = 0; axis < AXES; axis++)
     jr.axes[axis].next = velocity[axis];
   jr.writing = false;
 
-  if (!jr.active) {
-    jr.active = true;
+  if (state_get() != STATE_JOGGING) {
+    state_jogging();
     exec_set_cb(jog_exec);
   }
 
index d7c131ef2cf432c2e81ee045e4c71be8ed2fe419..5f656a49b14d6681bb7fbfa229d58ab4dc7f824d 100644 (file)
@@ -161,6 +161,7 @@ static stat_t _pause() {
   float j = l.line.max_jerk;
 
   if (v < MIN_VELOCITY) {
+    command_reset_position();
     exec_set_velocity(0);
     exec_set_acceleration(0);
     exec_set_jerk(0);
index ddbf2a8a54214b102512773114a80e2fd3985320..7a0b545fe864828a4a3627bf4eb762506430d0f2 100644 (file)
@@ -57,6 +57,7 @@ PGM_P state_get_pgmstr(state_t state) {
   case STATE_READY:     return PSTR("READY");
   case STATE_ESTOPPED:  return PSTR("ESTOPPED");
   case STATE_RUNNING:   return PSTR("RUNNING");
+  case STATE_JOGGING:   return PSTR("JOGGING");
   case STATE_STOPPING:  return PSTR("STOPPING");
   case STATE_HOLDING:   return PSTR("HOLDING");
   }
@@ -132,7 +133,17 @@ void state_running() {
 }
 
 
-void state_idle() {if (state_get() == STATE_RUNNING) _set_state(STATE_READY);}
+void state_jogging() {
+  if (state_get() == STATE_READY) _set_state(STATE_JOGGING);
+}
+
+
+void state_idle() {
+  if (state_get() == STATE_RUNNING || state_get() == STATE_JOGGING)
+    _set_state(STATE_READY);
+}
+
+
 void state_estop() {_set_state(STATE_ESTOPPED);}
 
 
index fcae437b1b88daa21ef7db04a5642e2b3aa360a2..3a254ff529bb10d41c8704757f5937d53fe91b51 100644 (file)
@@ -36,6 +36,7 @@ typedef enum {
   STATE_READY,
   STATE_ESTOPPED,
   STATE_RUNNING,
+  STATE_JOGGING,
   STATE_STOPPING,
   STATE_HOLDING,
 } state_t;
@@ -65,6 +66,7 @@ void state_seek_hold();
 void state_holding();
 void state_optional_pause();
 void state_running();
+void state_jogging();
 void state_idle();
 void state_estop();
 
index 52b375618204132b7d87728c6d31a8784fc87700..5fadc4510a0c06b67120f89b3f7b04c8699cbc23 100644 (file)
@@ -95,7 +95,12 @@ module.exports = {
 
 
   methods: {
-    get_state: function () {return this.state.xx || ''},
+    get_state: function () {
+      if (typeof this.state.cycle != 'undefined' &&
+          this.state.cycle != 'idle' && this.state.xx == 'RUNNING')
+        return this.state.cycle.toUpperCase();
+      return this.state.xx || ''
+    },
 
 
     get_reason: function () {
index 3be60bd72549a01f31e41cac43406e1127fb0de5..eaf77fdc639c7922d6515b702ff0ba4bef203f52 100644 (file)
@@ -39,15 +39,15 @@ log = logging.getLogger('Comm')
 
 
 class Comm():
-    def __init__(self, ctrl):
+    def __init__(self, ctrl, next_cb, connect_cb):
         self.ctrl = ctrl
+        self.next_cb = next_cb
+        self.connect_cb = connect_cb
 
         self.queue = deque()
         self.in_buf = ''
         self.command = None
 
-        ctrl.state.add_listener(self._update)
-
         try:
             self.sp = serial.Serial(ctrl.args.serial, ctrl.args.baud,
                                     rtscts = 1, timeout = 0, write_timeout = 0)
@@ -64,12 +64,8 @@ class Comm():
         self.i2c_addr = ctrl.args.avr_addr
 
 
-    def start_sending_gcode(self, path):
-        self.ctrl.planner.load(path)
-        self.set_write(True)
-
-
-    def stop_sending_gcode(self): self.ctrl.planner.reset()
+    def is_active(self):
+        return len(self.queue) or self.command is not None
 
 
     def i2c_command(self, cmd, byte = None, word = None):
@@ -103,11 +99,6 @@ class Comm():
         self.ctrl.ioloop.update_handler(self.sp, flags)
 
 
-    def _update(self, update):
-        if 'xx' in update and update['xx'] == 'ESTOPPED':
-            self.stop_sending_gcode()
-
-
     def _load_next_command(self, cmd):
         log.info('< ' + json.dumps(cmd).strip('"'))
         self.command = bytes(cmd.strip() + '\n', 'utf-8')
@@ -135,16 +126,13 @@ class Comm():
         # Load next command from queue
         if len(self.queue): self._load_next_command(self.queue.popleft())
 
-        # Load next GCode command, if running or paused
-        elif self.ctrl.planner.is_running():
-            cmd = self.ctrl.planner.next()
+        # Load next command from callback
+        else:
+            cmd = self.next_cb()
 
-            if cmd is None: self.set_write(False)
+            if cmd is None: self.set_write(False) # Stop writing
             else: self._load_next_command(cmd)
 
-        # Else stop writing
-        else: self.set_write(False)
-
 
     def _update_vars(self, msg):
         try:
@@ -216,8 +204,8 @@ class Comm():
 
     def connect(self):
         try:
-            # Reset AVR communication
-            self.stop_sending_gcode()
+            # Call connect callback
+            self.connect_cb()
 
             # Resume once current queue of GCode commands has flushed
             self.queue_command(Cmd.RESUME)
index 99a31db81362037d389ca8f20918641f03cd1280..f95dab29a1fa3576f8660ea0ae8f81ed2853e7d4 100644 (file)
@@ -45,7 +45,6 @@ class Ctrl(object):
         self.web = bbctrl.Web(self)
 
         try:
-            self.planner = bbctrl.Planner(self)
             self.i2c = bbctrl.I2C(args.i2c_port)
             self.lcd = bbctrl.LCD(self)
             self.mach = bbctrl.Mach(self)
index 9670b7690ee313d2e1c15c8953eed2bedce9d453..418b3754e5c941429d7c3e21d5502e1e4db2e59f 100644 (file)
@@ -57,68 +57,113 @@ axis_homing_procedure = '''
 class Mach():
     def __init__(self, ctrl):
         self.ctrl = ctrl
-        self.comm = bbctrl.Comm(ctrl)
+        self.planner = bbctrl.Planner(ctrl)
+        self.comm = bbctrl.Comm(ctrl, self._comm_next, self._comm_connect)
         self.stopping = False
 
+        ctrl.state.set('cycle', 'idle')
         ctrl.state.add_listener(self._update)
 
         self.comm.queue_command(Cmd.REBOOT)
 
 
-    def _is_busy(self): return self.ctrl.planner.is_running()
+    def _get_cycle(self): return self.ctrl.state.get('cycle')
+
+
+    def _begin_cycle(self, cycle):
+        current = self._get_cycle()
+
+        if current == 'idle':
+            self.planner.update_position()
+            self.ctrl.state.set('cycle', cycle)
+
+        elif current == 'homing' and cycle == 'mdi': pass
+        elif current != cycle:
+            raise Exception('Cannot enter %s cycle during %s' %
+                            (cycle, current))
 
 
     def _update(self, update):
-        if self.stopping and 'xx' in update and update['xx'] == 'HOLDING':
-            self.comm.stop_sending_gcode()
+        state = self.ctrl.state.get('xx', '')
+
+        # Handle EStop
+        if 'xx' in update and state == 'ESTOPPED':
+            self._stop_sending_gcode()
+
+        # Handle stop
+        if self.stopping and 'xx' in update and 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.stopping = False
 
+        # 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')
+
         # Automatically unpause on seek hold
-        if self.ctrl.state.get('xx', '') == 'HOLDING' and \
-                self.ctrl.state.get('pr', '') == 'Switch found' and \
-                self.ctrl.planner.is_synchronizing():
+        if (state == 'HOLDING' and
+            self.ctrl.state.get('pr', '') == 'Switch found' and
+            self.planner.is_synchronizing()):
             self.ctrl.mach.unpause()
 
 
-    def set(self, code, value):
-        self.comm.queue_command('${}={}'.format(code, value))
+    def _comm_next(self):
+        if self.planner.is_running(): return self.planner.next()
 
 
-    def mdi(self, cmd):
-        if len(cmd) and cmd[0] == '$':
-            equal = cmd.find('=')
-            if equal == -1:
-                log.info('%s=%s' % (cmd, self.ctrl.state.get(cmd[1:])))
+    def _comm_connect(self): self._stop_sending_gcode()
+
+
+    def _start_sending_gcode(self, path):
+        self.planner.load(path)
+        self.comm.set_write(True)
 
-            else:
-                name, value = cmd[1:equal], cmd[equal + 1:]
 
-                if value.lower() == 'true': value = True
-                elif value.lower() == 'false': value = False
-                else:
-                    try:
-                        value = float(value)
-                    except: pass
+    def _stop_sending_gcode(self): self.planner.reset()
 
-                self.ctrl.state.config(name, value)
 
-        elif len(cmd) and cmd[0] == '\\': self.comm.queue_command(cmd[1:])
+    def _query_var(self, cmd):
+        equal = cmd.find('=')
+        if equal == -1:
+            log.info('%s=%s' % (cmd, self.ctrl.state.get(cmd[1:])))
 
         else:
-            self.ctrl.planner.mdi(cmd)
+            name, value = cmd[1:equal], cmd[equal + 1:]
+
+            if value.lower() == 'true': value = True
+            elif value.lower() == 'false': value = False
+            else:
+                try:
+                    value = float(value)
+                except: pass
+
+            self.ctrl.state.config(name, value)
+
+
+    def mdi(self, cmd):
+        if not len(cmd): return
+        if   cmd[0] == '$':  self._query_var(cmd)
+        elif cmd[0] == '\\': self.comm.queue_command(cmd[1:])
+        else:
+            self._begin_cycle('mdi')
+            self.planner.mdi(cmd)
             self.comm.set_write(True)
 
 
+    def set(self, code, value):
+        self.comm.queue_command('${}={}'.format(code, value))
+
+
     def jog(self, axes):
-        if self._is_busy(): raise Exception('Busy, cannot jog')
+        self._begin_cycle('jogging')
         self.comm.queue_command(Cmd.jog(axes))
 
 
     def home(self, axis, position = None):
-        if self._is_busy(): raise Exception('Busy, cannot home')
+        self._begin_cycle('homing')
 
         if position is not None:
             self.mdi('G28.3 %c%f' % (axis, position))
@@ -138,14 +183,17 @@ class Mach():
 
     def estop(self): self.comm.i2c_command(Cmd.ESTOP)
     def clear(self): self.comm.i2c_command(Cmd.CLEAR)
-    def start(self, path): self.comm.start_sending_gcode(path)
+
+
+    def start(self, path):
+        self._begin_cycle('running')
+        self._start_sending_gcode(path)
 
 
     def step(self, path):
+        raise Exception('NYI') # TODO
         self.comm.i2c_command(Cmd.STEP)
-        if not self._is_busy() and path and \
-                self.ctrl.state.get('xx', '') == 'READY':
-            self.comm.start_sending_gcode(path)
+        if self._get_cycle() != 'running': self.start(path)
 
 
     def stop(self):
@@ -161,17 +209,17 @@ class Mach():
 
         self.comm.i2c_command(Cmd.FLUSH)
         self.comm.queue_command(Cmd.RESUME)
-        self.ctrl.planner.restart()
+        self.planner.restart()
         self.comm.set_write(True)
         self.comm.i2c_command(Cmd.UNPAUSE)
 
 
-    def optional_pause(self): self.comm.i2c_command(Cmd.PAUSE, byte = 1)
+    def optional_pause(self):
+        if self._get_cycle() == 'running':
+            self.comm.i2c_command(Cmd.PAUSE, byte = 1)
 
 
     def set_position(self, axis, position):
-        if self._is_busy(): raise Exception('Busy, cannot set position')
-
         if self.ctrl.state.is_axis_homed('%c' % axis):
             self.mdi('G92 %c%f' % (axis, position))
         else: self.comm.queue_command('$%cp=%f' % (axis, position))
index c731e5329ecceabb34a4508960753c10e7e8e2d9..95c5c464bd95706f41eb743e1ec420402b3254d9 100644 (file)
@@ -42,50 +42,48 @@ class Planner():
     def __init__(self, ctrl):
         self.ctrl = ctrl
         self.lastID = -1
-        self.mode = 'idle'
         self.setq = deque()
 
-        ctrl.state.add_listener(self.update)
+        ctrl.state.add_listener(self._update)
 
         self.reset()
 
 
+    def is_busy(self): return self.is_running() or len(self.setq)
     def is_running(self): return self.planner.is_running()
     def is_synchronizing(self): return self.planner.is_synchronizing()
 
 
-    def get_config(self):
+    def update_position(self):
+        position = {}
+
+        for axis in 'xyzabc':
+            if not self.ctrl.state.is_axis_enabled(axis): continue
+            value = self.ctrl.state.get(axis + 'p', None)
+            if value is not None: position[axis] = value
+
+        self.planner.set_position(position)
+
+
+    def _get_config_vector(self, name, scale):
         state = self.ctrl.state
+        v = {}
 
-        # Axis mapping for enabled motors
-        axis2motor = {}
-        for i in range(4):
-            if state.get('%dpm' % i, False):
-                axis = 'xyzabc'[int(state.get('%dan' % i))]
-                axis2motor[axis] = i
-
-        def get_vector(name, scale = 1):
-            v = {}
-            for axis in 'xyzabc':
-                if axis in axis2motor:
-                    motor = axis2motor[axis]
-                    value = state.get(str(motor) + name, None)
-                    if value is not None:
-                        v[axis] = value * scale
-            return v
-
-        # Starting position
-        start = {}
         for axis in 'xyzabc':
-            if not axis in axis2motor: continue
-            value = state.get(axis + 'p', None)
-            if value is not None: start[axis] = value
+            motor = state.find_motor(axis)
+
+            if motor is not None and state.motor_enabled(motor):
+                value = state.get(str(motor) + name, None)
+                if value is not None: v[axis] = value * scale
+
+        return v
+
 
+    def _get_config(self):
         config = {
-            "start":     start,
-            "max-vel":   get_vector('vm', 1000),
-            "max-accel": get_vector('am', 1000000),
-            "max-jerk":  get_vector('jm', 1000000),
+            "max-vel":   self._get_config_vector('vm', 1000),
+            "max-accel": self._get_config_vector('am', 1000000),
+            "max-jerk":  self._get_config_vector('jm', 1000000),
             # TODO junction deviation & accel
             }
 
@@ -94,16 +92,16 @@ class Planner():
         return config
 
 
-    def update(self, update):
+    def _update(self, update):
         if 'id' in update:
             id = update['id']
             self.planner.set_active(id)
 
             # Syncronize planner variables with execution id
-            self.release_set_cmds(id)
+            self._release_set_cmds(id)
 
 
-    def release_set_cmds(self, id):
+    def _release_set_cmds(self, id):
         self.lastID = id
 
         # Apply all set commands <= to ID and those that follow consecutively
@@ -113,26 +111,13 @@ class Planner():
             if id == self.lastID + 1: self.lastID = id
 
 
-    def queue_set_cmd(self, id, name, value):
+    def _queue_set_cmd(self, id, name, value):
         log.info('Planner set(#%d, %s, %s)', id, name, value)
         self.setq.append((id, name, value))
-        self.release_set_cmds(self.lastID)
+        self._release_set_cmds(self.lastID)
 
 
-    def restart(self):
-        state = self.ctrl.state
-        id = state.get('id')
-
-        position = {}
-        for axis in 'xyzabc':
-            if state.has(axis + 'p'):
-                position[axis] = state.get(axis + 'p')
-
-        log.info('Planner restart: %d %s' % (id, json.dumps(position)))
-        self.planner.restart(id, position)
-
-
-    def get_var(self, name):
+    def _get_var(self, name):
         value = 0
         if len(name) and name[0] == '_':
             value = self.ctrl.state.get(name[1:], 0)
@@ -141,7 +126,7 @@ class Planner():
         return value
 
 
-    def log(self, line):
+    def _log(self, line):
         line = line.strip()
         m = reLogLine.match(line)
         if not m: return
@@ -161,32 +146,7 @@ class Planner():
         else: log.error('Could not parse planner log line: ' + line)
 
 
-    def mdi(self, cmd):
-        if self.mode == 'gcode':
-            raise Exception('Cannot issue MDI command while GCode running')
-
-        log.info('MDI:' + cmd)
-        self.planner.load_string(cmd, self.get_config())
-        self.mode = 'mdi'
-
-
-    def load(self, path):
-        if self.mode != 'idle':
-            raise Exception('Busy, cannot start new GCode program')
-
-        log.info('GCode:' + path)
-        self.planner.load('upload' + path, self.get_config())
-        self.mode = 'gcode'
-
-
-    def reset(self):
-        self.planner = gplan.Planner()
-        self.planner.set_resolver(self.get_var)
-        self.planner.set_logger(self.log, 1, 'LinePlanner:3')
-        self.setq.clear()
-
-
-    def _encode(self, block):
+    def __encode(self, block):
         type = block['type']
 
         if type == 'line':
@@ -197,7 +157,7 @@ class Planner():
         if type == 'set':
             name, value = block['name'], block['value']
 
-            if name == 'line': self.queue_set_cmd(block['id'], name, value)
+            if name == 'line': self._queue_set_cmd(block['id'], name, value)
             if name == 'tool': return Cmd.tool(value)
             if name == 'speed': return Cmd.speed(value)
             if name[0:1] == '_' and name[1:2] in 'xyzabc' and \
@@ -205,7 +165,7 @@ class Planner():
                 return Cmd.set_position(name[1], value)
 
             if len(name) and name[0] == '_':
-                self.queue_set_cmd(block['id'], name[1:], value)
+                self._queue_set_cmd(block['id'], name[1:], value)
 
             return
 
@@ -220,11 +180,41 @@ class Planner():
         raise Exception('Unknown planner type "%s"' % type)
 
 
-    def encode(self, block):
-        cmd = self._encode(block)
+    def _encode(self, block):
+        cmd = self.__encode(block)
         if cmd is not None: return Cmd.set('id', block['id']) + '\n' + cmd
 
 
+    def reset(self):
+        self.planner = gplan.Planner()
+        self.planner.set_resolver(self._get_var)
+        self.planner.set_logger(self._log, 1, 'LinePlanner:3')
+        self.setq.clear()
+
+
+    def restart(self):
+        state = self.ctrl.state
+        id = state.get('id')
+
+        position = {}
+        for axis in 'xyzabc':
+            if state.has(axis + 'p'):
+                position[axis] = state.get(axis + 'p')
+
+        log.info('Planner restart: %d %s' % (id, json.dumps(position)))
+        self.planner.restart(id, position)
+
+
+    def mdi(self, cmd):
+        log.info('MDI:' + cmd)
+        self.planner.load_string(cmd, self._get_config())
+
+
+    def load(self, path):
+        log.info('GCode:' + path)
+        self.planner.load('upload' + path, self._get_config())
+
+
     def has_move(self): return self.planner.has_more()
 
 
@@ -232,11 +222,9 @@ class Planner():
         try:
             while self.planner.has_more():
                 cmd = self.planner.next()
-                cmd = self.encode(cmd)
+                cmd = self._encode(cmd)
                 if cmd is not None: return cmd
 
-            if not self.is_running(): self.mode = 'idle'
-
         except Exception as e:
             self.reset()
             log.exception(e)
index 47355e4f19a1bc6c68a0dcdb04e61c7b1d8307a7..9622284faac2a3bba348be09a722d99f94d738c7 100644 (file)
@@ -177,6 +177,11 @@ class State(object):
         return self.get('%s_homed' % axis, False)
 
 
+    def is_axis_enabled(self, axis):
+        motor = self.find_motor(axis)
+        return False if motor is None else self.motor_enabled(motor)
+
+
     def axis_can_home(self, axis):
         motor = self.find_motor(axis)
         if motor is None: return False
@@ -188,7 +193,10 @@ class State(object):
         return False
 
 
-    def motor_enabled(self, motor): return bool(int(self.vars['%dpm' % motor]))
+    def motor_enabled(self, motor):
+        return bool(int(self.vars.get('%dpm' % motor, 0)))
+
+
     def motor_homing_mode(self, motor): return int(self.vars['%dho' % motor])