{
"name": "bbctrl",
- "version": "0.3.1",
+ "version": "0.3.2",
"homepage": "https://github.com/buildbotics/bbctrl-firmware",
"license": "GPL 3+",
"dependencies": {
}
+void command_set_axis_position(int axis, const float p) {
+ cmd.position[axis] = p;
+}
+
+
void command_set_position(const float position[AXES]) {
memcpy(cmd.position, position, sizeof(cmd.position));
}
void command_flush_queue();
void command_push(char code, void *data);
bool command_callback();
+void command_set_axis_position(int axis, const float p);
void command_set_position(const float position[AXES]);
void command_get_position(float position[AXES]);
void command_reset_position();
stat_t status = decode_axes(&cmd, line.target);
if (status) return status;
- // Zero disabled axes (TODO should moving a disable axis cause an error?)
- for (int i = 0; i < AXES; i++)
- if (!axis_is_enabled(i)) line.target[i] = 0;
-
// Get times
bool has_time = false;
while (*cmd) {
float time;
if (!decode_float(&cmd, &time)) return STAT_BAD_FLOAT;
- if (time < 0 || 0x10000 * SEGMENT_TIME <= time) return STAT_BAD_SEG_TIME;
+ if (time < 0) return STAT_BAD_SEG_TIME;
line.times[seg] = time;
if (time) has_time = true;
}
command_push(*cmd, &buffer);
+ // Special case for synchronizing axis position in command queue
+ if (info.set.set_f32_index == set_axis_position)
+ command_set_axis_position(buffer.index, buffer.value._f32);
+
return STAT_OK;
}
is_homed: function (axis) {
var motor = this.get_axis_motor_id(axis);
- return motor != -1 && this.state[motor + 'h'];
+ return motor != -1 && this.state[motor + 'homed'];
},
# Axis homing procedure:
#
-# Set axis unhomed
+# Mark axis unhomed
# Seek closed (home_dir * (travel_max - travel_min) * 1.5) at search_velocity
# Seek open (home_dir * -latch_backoff) at latch_vel
# Seek closed (home_dir * latch_backoff * 1.5) at latch_vel
-# Rapid to (home_dir * -zero_backoff + seek_position)
-# Set axis homed and home position
+# Rapid to (home_dir * -zero_backoff + position)
+# Mark axis homed and set absolute position
axis_homing_procedure = '''
G28.2 %(axis)s0 F[#<_%(axis)s_sv>]
G38.6 %(axis)s[#<_%(axis)s_hd> * [#<_%(axis)s_tm> - #<_%(axis)s_tn>] * 1.5]
G38.8 %(axis)s[#<_%(axis)s_hd> * -#<_%(axis)s_lb>] F[#<_%(axis)s_lv>]
G38.6 %(axis)s[#<_%(axis)s_hd> * #<_%(axis)s_lb> * 1.5]
- G0 G53 %(axis)s[#<_%(axis)s_hd> * -#<_%(axis)s_zb> + #<_%(axis)s_sp>]
- G28.3 %(axis)s[#<_%(axis)s_hp>]
+ G91 G0 G53 %(axis)s[#<_%(axis)s_hd> * -#<_%(axis)s_zb>]
+ G90 G28.3 %(axis)s[#<_%(axis)s_hp>]
'''
def _is_busy(self): return self.ctrl.planner.is_running()
+
def _i2c_command(self, cmd, byte = None, word = None):
log.info('I2C: ' + cmd)
retry = 5
def _start_sending_gcode(self, path):
- if self._is_busy(): raise Exception('Busy, cannot start new GCode file')
-
- log.info('Running ' + path)
self.ctrl.planner.load(path)
self._set_write(True)
def mdi(self, cmd):
- if self._is_busy(): raise Exception('Busy, cannot queue MDI command')
-
if len(cmd) and cmd[0] == '$':
equal = cmd.find('=')
if equal == -1:
log.info('%s=%s' % (cmd, self.ctrl.state.get(cmd[1:])))
- else: self._queue_command(cmd)
+ 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
+
+ self.ctrl.state.config(name, value)
elif len(cmd) and cmd[0] == '\\': self._queue_command(cmd[1:])
def start(self, path):
- if self._is_busy(): raise Exception('Busy, cannot start file')
if path: self._start_sending_gcode(path)
if self._is_busy(): raise Exception('Busy, cannot set position')
if self.ctrl.state.is_axis_homed('%c' % axis):
- raise Exception('NYI') # TODO
- self._queue_command('G92 %c%f' % (axis, position))
+ self.ctrl.planner.mdi('G92 %c%f' % (axis, position))
else: self._queue_command('$%cp=%f' % (axis, position))
+#!/usr/bin/env python3
+
import struct
import base64
+import json
import logging
log = logging.getLogger('Cmd')
def tool(tool): return '#t=%d' % tool
def speed(speed): return '#s=:' + encode_float(speed)
+def set_position(axis, value): return '#%sp=:%s' % (axis, encode_float(value))
def output(port, value):
if port == 'mist': return '#1oa=' + ('1' if value else '0')
cmd += chr(flags + ord('0'))
return cmd
+
+
+def decode_command(cmd):
+ if not len(cmd): return
+
+ data = {}
+
+ if cmd[0] == SET or cmd[0] == SET_SYNC:
+ data['type'] = 'set'
+ if cmd[0] == SET_SYNC: data['sync'] = True
+
+ equal = cmd.find('=')
+ data['name'] = cmd[1:equal]
+
+ value = cmd[equal + 1:]
+
+ if value.lower() == 'true': value = True
+ elif value.lower() == 'false': value = False
+ elif value.find('.') == -1: data['value'] = int(value)
+ else: data['value'] = float(value)
+
+ elif cmd[0] == SEEK:
+ data['type'] = 'seek'
+
+ data['port'] = int(cmd[2], 16)
+ flags = int(cmd[2], 16)
+
+ data['active'] = bool(flags & SEEK_ACTIVE)
+ data['error'] = bool(flags & SEEK_ERROR)
+
+ elif cmd[0] == LINE:
+ data['type'] = 'line'
+ data['exit-vel'] = decode_float(cmd[1:7])
+ data['max-accel'] = decode_float(cmd[7:13])
+ data['max-jerk'] = decode_float(cmd[13:19])
+
+ data['target'] = {}
+ data['times'] = [0] * 7
+ cmd = cmd[19:]
+
+ while len(cmd):
+ name = cmd[0]
+ value = decode_float(cmd[1:7])
+ cmd = cmd[7:]
+
+ if name in 'xyzabcuvw': data['target'][name] = value
+ else: data['times'][int(name)] = value
+
+ elif cmd[0] == REPORT: data['type'] = 'report'
+ elif cmd[0] == PAUSE: data['type'] = 'pause'
+ elif cmd[0] == UNPAUSE: data['type'] = 'unpause'
+ 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))
+
+
+def decode(cmd):
+ for line in cmd.split('\n'):
+ decode_command(line.strip())
+
+
+if __name__ == "__main__":
+ import sys
+
+ if 1 < len(sys.argv):
+ for arg in sys.argv[1:]:
+ decode(arg)
+
+ else:
+ for line in sys.stdin:
+ decode(line)
def __init__(self, ctrl):
self.ctrl = ctrl
self.lastID = -1
- self.done = False
+ self.mode = 'idle'
ctrl.state.add_listener(lambda x: self.update(x))
def update(self, update):
- if 'id' in update:
- id = update['id']
- if id: self.planner.release(id - 1)
+ if 'id' in update: self.planner.set_active(update['id'])
- if update.get('x', '') == 'HOLDING' and \
- self.ctrl.state.get('pr', '') == 'Switch found':
+ if self.ctrl.state.get('x', '') == 'HOLDING' and \
+ self.ctrl.state.get('pr', '') == 'Switch found' and \
+ self.planner.is_synchronizing():
self.ctrl.avr.unpause()
log.info('Planner restart: %d %s' % (id, json.dumps(position)))
self.planner.restart(id, position)
- self.done = False
def get_var(self, name):
if len(line) < 3: return
if line[0] == 'I': log.info(line[3:])
- if line[0] == 'D': log.debug(line[3:])
- if line[0] == 'W': log.warning(line[3:])
- if line[0] == 'E': log.error(line[3:])
- if line[0] == 'C': log.critical(line[3:])
+ elif line[0] == 'D': log.debug(line[3:])
+ # TODO send these to the LCD and Web
+ elif line[0] == 'W': log.warning(line[3:])
+ elif line[0] == 'E': log.error(line[3:])
+ elif line[0] == 'C': log.critical(line[3:])
+ else: raise Exception('Could not parse planner log line: ' + line)
def mdi(self, cmd):
- self.planner.set_config(self.get_config())
- self.planner.mdi(cmd)
- self.done = False
+ if self.mode == 'gcode':
+ raise Exception('Cannot issue MDI command while GCode running')
+
+ log.info('MDI:' + cmd)
+ self.planner.load_string(cmd)
+ self.mode = 'mdi'
def load(self, path):
- self.planner.set_config(self.get_config())
+ if self.mode != 'idle':
+ raise Exception('Busy, cannot start new GCode program')
+
+ log.info('GCode:' + path)
self.planner.load('upload' + path)
- self.done = False
def reset(self):
self.planner = gplan.Planner(self.get_config())
self.planner.set_resolver(self.get_var)
- self.planner.set_logger(self.log)
+ self.planner.set_logger(self.log, 1, 'LinePlanner:3')
def encode(self, block):
if name == 'line': return Cmd.line_number(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 \
+ name[2:] == '_home':
+ return Cmd.set_position(name[1], value)
if len(name) and name[0] == '_':
self.ctrl.state.set(name[1:], value)
def next(self):
+ if not self.is_running():
+ config = self.get_config()
+ log.info('Planner config:' + json.dumps(config))
+ self.planner.set_config(config)
+
while self.planner.has_more():
cmd = self.planner.next()
self.lastID = cmd['id']
cmd = self.encode(cmd)
if cmd is not None: return cmd
- if not self.done:
- self.done = True
-
- # Cause last cmd to flush when complete
- if 0 <= self.lastID:
- return '#id=%d' % (self.lastID + 1)
+ if not self.is_running(): self.mode = 'idle'
self.set_callback(str(i) + 'hd',
lambda name, i = i: self.motor_home_direction(i))
+ # Add home position callbacks
+ self.set_callback(str(i) + 'hp',
+ lambda name, i = i: self.motor_home_position(i))
+
+ # Set not homed
+ self.set('%dhomed' % i, False)
+
def _notify(self):
if not self.changes: return
def config(self, code, value):
if code in self.machine_var_set: self.ctrl.avr.set(code, value)
- else: self.vars[code] = value
+ else: self.set(code, value)
def add_listener(self, listener):
if homing_mode == 1: return -1 # Switch min
if homing_mode == 2: return 1 # Switch max
return 0 # Disabled
+
+
+ def motor_home_position(self, motor):
+ homing_mode = self.motor_homing_mode(motor)
+ if homing_mode == 1: return self.vars['%dtn' % motor] # Min soft limit
+ if homing_mode == 2: return self.vars['%dtm' % motor] # Max soft limit
+ return 0 # Disabled