- Fixed several state transition (stop, pause, estop, etc.) problems.
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.
{
"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+",
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")
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")
memset(&ex, 0, sizeof(ex));
ex.feed_override = 1;
ex.spindle_override = 1;
- // TODO implement move stepping
// TODO implement overrides
- // TODO implement optional pause
}
}
+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;
#pragma once
-
#include "status.h"
stat_t jog_exec();
+void jog_stop();
}
+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;
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();
#include "command.h"
#include "stepper.h"
#include "spindle.h"
+#include "outputs.h"
+#include "jog.h"
#include "estop.h"
#include "report.h"
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
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");
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) {
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;
}
// 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) {
}
-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;
}
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;
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.")
@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).",
api.upload('file', fd)
.done(function () {
- file.name;
if (file.name == this.last_file) this.last_file = '';
this.update();
}.bind(this));
SET_AXIS = 'a'
LINE = 'l'
DWELL = 'd'
-OPT_PAUSE = 'p'
PAUSE = 'P'
+STOP = 'S'
UNPAUSE = 'U'
JOG = 'j'
REPORT = 'r'
RESUME = 'c'
ESTOP = 'E'
CLEAR = 'C'
-STEP = 'S'
FLUSH = 'F'
DUMP = 'D'
HELP = 'h'
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))
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,
raise
- def set_write(self, enable):
+ def _set_write(self, enable):
if self.sp is None: return
flags = self.ctrl.ioloop.READ
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):
count = self.sp.write(self.command)
except Exception as e:
- self.set_write(False)
+ self.command = None
raise e
self.command = self.command[count:]
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)
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)
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)
self.comm.reboot()
+ def _get_state(self): return self.ctrl.state.get('xx', '')
def _get_cycle(self): return self.ctrl.state.get('cycle')
(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()
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):
else:
self._begin_cycle('mdi')
self.planner.mdi(cmd)
- self.comm.set_write(True)
+ self.comm.resume()
def set(self, code, value):
# 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):
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)
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)
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')
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):