- Highlight loads when on.
- Fixed axis zeroing.
- Fixed bug in home position set after successful home. #109
- Fixed ugly Web error dumps.
- Allow access to log file from Web.
- Rotate log so it does not grow too big.
- Keep same GCode file through browser reload. #20
## v0.3.10
- Fixed "Flood" display, changed to "Load 1" and "Load 2". #108
+ - Highlight loads when on.
+ - Fixed axis zeroing.
+ - Fixed bug in home position set after successful home. #109
+ - Fixed ugly Web error dumps.
+ - Allow access to log file from Web.
+ - Rotate log so it does not grow too big.
+ - Keep same GCode file through browser reload. #20
## v0.3.9
- Fixed bug in move exec that was causing bumping between moves.
// Special processing for synchronous commands
if (_is_synchronous(*block)) {
if (estop_triggered()) status = STAT_MACHINE_ALARMED;
- else if (state_is_flushing()) status = STAT_NOP; // Flush
+ else if (state_is_flushing()) status = STAT_NOP; // Flush command
else if (state_is_resuming() || _space() < _size(*block))
return false; // Wait
}
// Reporting
report_request();
- if (status && status != STAT_NOP) status_error(status);
+ if (status && status != STAT_NOP) STATUS_ERROR(status, "");
return true;
}
CMD('$', var, 0, "Set or get variable")
CMD('#', sync_var, 1, "Set variable synchronous")
CMD('s', seek, 1, "[switch][flags:active|error]")
+CMD('a', set_axis, 1, "[axis][position] Set axis position")
CMD('l', line, 1, "[targetVel][maxJerk][axes][times]")
CMD('d', dwell, 1, "[seconds]")
CMD('o', out, 1, "Output")
\******************************************************************************/
#include "exec.h"
-#include "jog.h"
+
#include "stepper.h"
+#include "motor.h"
#include "axis.h"
-#include "spindle.h"
#include "util.h"
#include "command.h"
+#include "report.h"
#include "config.h"
-#include <string.h>
-#include <float.h>
-
static struct {
exec_cb_t cb;
void set_tool(uint8_t tool) {ex.tool = tool;}
void set_feed_override(float value) {ex.feed_override = value;}
void set_speed_override(float value) {ex.spindle_override = value;}
-void set_axis_position(int axis, float p) {ex.position[axis] = p;}
// Command callbacks
+typedef struct {
+ uint8_t axis;
+ float position;
+} set_axis_t;
+
+
+stat_t command_set_axis(char *cmd) {
+ cmd++; // Skip command name
+
+ // Decode axis
+ int axis = axis_get_id(*cmd++);
+ if (axis < 0) return STAT_INVALID_ARGUMENTS;
+
+ // Decode position
+ float position;
+ if (!decode_float(&cmd, &position)) return STAT_BAD_FLOAT;
+
+ // Check for end of command
+ if (*cmd) return STAT_INVALID_ARGUMENTS;
+
+ // Update command
+ command_set_axis_position(axis, position);
+
+ // Queue
+ set_axis_t set_axis = {axis, position};
+ command_push(COMMAND_set_axis, &set_axis);
+
+ return STAT_OK;
+}
+
+
+unsigned command_set_axis_size() {return sizeof(set_axis_t);}
+
+
+void command_set_axis_exec(void *data) {
+ set_axis_t *cmd = (set_axis_t *)data;
+
+ // Update exec
+ ex.position[cmd->axis] = cmd->position;
+
+ // Update motors
+ int motor = axis_get_motor(cmd->axis);
+ if (0 <= motor) motor_set_position(motor, cmd->position);
+
+ // Report
+ report_request();
+}
+
+
stat_t command_opt_pause(char *cmd) {command_push(*cmd, 0); return STAT_OK;}
unsigned command_opt_pause_size() {return 0;}
void command_opt_pause_exec(void *data) {} // TODO pause if requested
}
-stat_t status_error(stat_t code) {
- return status_message_P(0, STAT_LEVEL_ERROR, code, 0);
-}
-
-
stat_t status_message_P(const char *location, status_level_t level,
stat_t code, const char *msg, ...) {
va_list args;
status_level_pgmstr(level));
// Message
- if (msg) {
+ if (msg && pgm_read_byte(msg)) {
va_start(args, msg);
vfprintf_P(stdout, msg, args);
va_end(args);
const char *status_to_pgmstr(stat_t code);
const char *status_level_pgmstr(status_level_t level);
-stat_t status_error(stat_t code);
stat_t status_message_P(const char *location, status_level_t level,
stat_t code, const char *msg, ...);
}
-void st_set_position(const float position[]) {
- for (int motor = 0; motor < MOTORS; motor++)
- motor_set_position(motor, position[motor_get_axis(motor)]);
-}
-
-
void st_shutdown() {
OUTCLR_PIN(MOTOR_ENABLE_PIN);
st.dwell = 0;
void stepper_init();
-void st_set_position(const float position[]);
void st_shutdown();
void st_enable();
bool st_is_busy();
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;
}
VAR(probe_switch, pw, u8, 0, 0, 1, "Probe switch state")
// Axis
-VAR(axis_position, p, f32, AXES, 1, 1, "Axis position")
+VAR(axis_position, p, f32, AXES, 0, 1, "Axis position")
// Outputs
VAR(output_active, oa, bool, OUTS, 1, 1, "Output pin active")
input(name="pass2", v-model="password2", type="password")
button.pure-button.pure-button-primary(@click="set_password") Set
+ h2 Debugging
+ a(href="/api/log", target="_blank")
+ button.pure-button.pure-button-primary View Log
+
h2 Configuration
button.pure-button.pure-button-primary(@click="backup") Backup
td {{msg.source || ''}}
td {{msg.where || ''}}
td {{msg.repeat}}
- td {{msg.msg}}
+ td.message {{msg.msg}}
button.pure-button(
title="{{state.xx == 'RUNNING' ? 'Pause' : 'Start'}} program.",
- @click="start_pause", :disabled="!file")
+ @click="start_pause", :disabled="!state.selected")
.fa(:class="state.xx == 'RUNNING' ? 'fa-pause' : 'fa-play'")
button.pure-button(title="Stop program.", @click="stop",
.fa.fa-stop-circle-o
button.pure-button(title="Execute one program step.", @click="step",
- :disabled="(state.xx != 'READY' && state.xx != 'HOLDING') || !file",
- v-if="false")
+ :disabled="(state.xx != 'READY' && state.xx != 'HOLDING') || " +
+ "!state.selected", v-if="false")
.fa.fa-step-forward
.tabs
style="display:none", accept=".nc,.gcode,.gc,.ngc")
button.pure-button(title="Delete current GCode program.",
- @click="deleteGCode = true", :disabled="!file")
+ @click="deleteGCode = true", :disabled="!state.selected")
.fa.fa-trash
message(:show.sync="deleteGCode")
| selected
select(title="Select previously uploaded GCode programs.",
- v-model="file", @change="load",
+ v-model="state.selected", @change="load",
:disabled="state.xx == 'RUNNING' || state.xx == 'STOPPING'")
option(v-for="file in files", :value="file") {{file}}
data: function () {
return {
mdi: '',
- file: '',
last_file: '',
files: [],
axes: 'xyzabc',
watch: {
- 'state.line': function () {this.update_gcode_line();}
+ 'state.line': function () {this.update_gcode_line()},
+ 'state.selected': function () {this.load()}
},
var data = {};
data[axis] = power;
api.put('jog', data);
- }
+ },
+
+ connected: function () {this.update()}
},
- ready: function () {this.update()},
+ ready: function () {
+ this.update();
+ this.load();
+ },
methods: {
update: function () {
// Update file list
- api.get('file')
- .done(function (files) {
- var index = files.indexOf(this.file);
- if (index == -1 && files.length) this.file = files[0];
-
- this.files = files;
-
- this.load()
- }.bind(this))
+ api.get('file').done(function (files) {this.files = files}.bind(this))
},
api.upload('file', fd)
.done(function () {
- this.file = file.name;
+ file.name;
if (file.name == this.last_file) this.last_file = '';
this.update();
}.bind(this));
load: function () {
- var file = this.file;
-
- if (!file || this.files.indexOf(file) == -1) {
- this.file = '';
- this.all_gcode = [];
- this.gcode = [];
- return;
- }
-
+ var file = this.state.selected;
if (file == this.last_file) return;
api.get('file/' + file)
deleteCurrent: function () {
- if (this.file) api.delete('file/' + this.file).done(this.update);
+ if (this.state.selected)
+ api.delete('file/' + this.state.selected).done(this.update);
this.deleteGCode = false;
},
},
- start: function () {api.put('start/' + this.file).done(this.update)},
+ start: function () {api.put('start')},
pause: function () {api.put('pause')},
unpause: function () {api.put('unpause')},
optional_pause: function () {api.put('pause/optional')},
stop: function () {api.put('stop')},
- step: function () {api.put('step/' + this.file).done(this.update)},
+ step: function () {api.put('step')},
override_feed: function () {api.put('override/feed/' + this.feed_override)},
################################################################################
import json
+import traceback
+import logging
+
from tornado.web import RequestHandler, HTTPError
import tornado.httpclient
+log = logging.getLogger('API')
+
+
class APIHandler(RequestHandler):
def __init__(self, app, request, **kwargs):
super(APIHandler, self).__init__(app, request, **kwargs)
self.ctrl = app.ctrl
+ # Override exception logging
+ def log_exception(self, typ, value, tb):
+ log.error(str(value))
+ trace = ''.join(traceback.format_exception(typ, value, tb))
+ log.debug(trace)
+
+
def delete(self, *args, **kwargs):
self.delete_ok(*args, **kwargs)
self.write_json('ok')
SET = '$'
SET_SYNC = '#'
SEEK = 's'
+SET_AXIS = 'a'
LINE = 'l'
DWELL = 'd'
OUTPUT = 'o'
def set(name, value): return '#%s=%s' % (name, value)
+def set_axis(axis, position): return SET_AXIS + axis + encode_float(position)
def line(target, exitVel, maxAccel, maxJerk, times):
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):
def get(self, path):
if path:
+ path = path[1:]
+ self.ctrl.mach.select(path)
+
with open('upload/' + path, 'r') as f:
self.write_json(f.read())
return
if os.path.isfile('upload/' + path):
files.append(path)
+ selected = self.ctrl.state.get('selected', '')
+ if not selected in files:
+ if len(files): self.ctrl.state.set('selected', files[0])
+
self.write_json(files)
axes = {}
for i in range(len(self.v)): axes["xyzabc"[i]] = self.v[i]
self.ctrl.mach.jog(axes)
- except Exception as e: log.warning('Jog: %s', e)
+
+ except Exception as e:
+ log.warning('Jog: %s', e)
self.ctrl.ioloop.call_later(0.25, self.callback)
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))
# 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
# Update cycle
if (state == 'HOLDING' and
self.ctrl.state.get('pr', '') == 'Switch found' and
self.planner.is_synchronizing()):
- self.ctrl.mach.unpause()
+ self.unpause()
def _comm_next(self):
def _start_sending_gcode(self, path):
- self.planner.load(path)
+ self.planner.load('upload/' + path)
self.comm.set_write(True)
def home(self, axis, position = None):
- self._begin_cycle('homing')
-
if position is not None:
self.mdi('G28.3 %c%f' % (axis, position))
else:
+ self._begin_cycle('homing')
+
if axis is None: axes = 'zxyabc' # TODO This should be configurable
else: axes = '%c' % axis
for axis in axes:
if not self.ctrl.state.axis_can_home(axis):
- log.info('Cannot home ' + axis)
+ log.warning('Cannot home ' + axis)
continue
log.info('Homing %s axis' % axis)
- self.mdi(axis_homing_procedure % {'axis': axis})
+ self.planner.mdi(axis_homing_procedure % {'axis': axis})
+ self.comm.set_write(True)
def estop(self): self.comm.i2c_command(Cmd.ESTOP)
def clear(self): self.comm.i2c_command(Cmd.CLEAR)
- def start(self, path):
+ def select(self, path):
+ if self.ctrl.state.get('selected', '') == path: return
+
+ if self._get_cycle() != 'idle':
+ raise Exception('Cannot select file during ' + self._get_cycle())
+
+ self.ctrl.state.set('selected', path)
+
+
+ def start(self):
self._begin_cycle('running')
- self._start_sending_gcode(path)
+ self._start_sending_gcode(self.ctrl.state.get('selected'))
- def step(self, path):
+ def step(self):
raise Exception('NYI') # TODO
self.comm.i2c_command(Cmd.STEP)
- if self._get_cycle() != 'running': self.start(path)
+ if self._get_cycle() != 'running': self.start()
def stop(self):
def set_position(self, axis, 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))
+ axis = axis.lower()
+
+ if self.ctrl.state.is_axis_homed(axis):
+ self.mdi('G92 %s%f' % (axis, position))
+
+ else:
+ if self._get_cycle() != 'idle':
+ raise Exception('Cannot zero position during ' +
+ self._get_cycle())
+
+ self._begin_cycle('mdi')
+ self.planner.set_position({axis: position})
+ self.comm.queue_command(Cmd.set_axis(axis, position))
def is_synchronizing(self): return self.planner.is_synchronizing()
+ def set_position(self, position):
+ self.planner.set_position(position)
+
+
def update_position(self):
position = {}
value = self.ctrl.state.get(axis + 'p', None)
if value is not None: position[axis] = value
- self.planner.set_position(position)
+ self.set_position(position)
def _get_config_vector(self, name, scale):
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_position(name[1], value)
+ return Cmd.set_axis(name[1], value)
if len(name) and name[0] == '_':
self._queue_set_cmd(block['id'], name[1:], value)
def load(self, path):
log.info('GCode:' + path)
- self.planner.load('upload' + path, self._get_config())
+ self.planner.load(path, self._get_config())
def has_move(self): return self.planner.has_more()
def put_ok(self): subprocess.Popen('reboot')
+class LogHandler(tornado.web.RequestHandler):
+ def __init__(self, app, request, **kwargs):
+ super(LogHandler, self).__init__(app, request, **kwargs)
+ self.filename = app.ctrl.args.log
+
+
+ def get(self):
+ with open(self.filename, 'r') as f:
+ self.write(f.read())
+
+
+ def set_default_headers(self):
+ fmt = socket.gethostname() + '-%Y%m%d.log'
+ filename = datetime.date.today().strftime(fmt)
+ self.set_header('Content-Disposition', 'filename="%s"' % filename)
+ self.set_header('Content-Type', 'text/plain')
+
+
class HostnameHandler(bbctrl.APIHandler):
def get(self): self.write_json(socket.gethostname())
class StartHandler(bbctrl.APIHandler):
- def put_ok(self, path): self.ctrl.mach.start(path)
+ def put_ok(self): self.ctrl.mach.start()
class EStopHandler(bbctrl.APIHandler):
class StepHandler(bbctrl.APIHandler):
- def put_ok(self, path): self.ctrl.mach.step(path)
+ def put_ok(self): self.ctrl.mach.step()
class PositionHandler(bbctrl.APIHandler):
def put_ok(self, axis):
- self.ctrl.mach.set_position(ord(axis.lower()), self.json['position'])
+ self.ctrl.mach.set_position(axis, float(self.json['position']))
class OverrideFeedHandler(bbctrl.APIHandler):
handlers = [
(r'/websocket', WSConnection),
+ (r'/api/log', LogHandler),
(r'/api/reboot', RebootHandler),
(r'/api/hostname', HostnameHandler),
(r'/api/remote/username', UsernameHandler),
(r'/api/upgrade', UpgradeHandler),
(r'/api/file(/.+)?', bbctrl.FileHandler),
(r'/api/home(/[xyzabcXYZABC](/set)?)?', HomeHandler),
- (r'/api/start(/.+)', StartHandler),
+ (r'/api/start', StartHandler),
(r'/api/estop', EStopHandler),
(r'/api/clear', ClearHandler),
(r'/api/stop', StopHandler),
(r'/api/pause', PauseHandler),
(r'/api/unpause', UnpauseHandler),
(r'/api/pause/optional', OptionalPauseHandler),
- (r'/api/step(/.+)', StepHandler),
+ (r'/api/step', StepHandler),
(r'/api/position/([xyzabcXYZABC])', PositionHandler),
(r'/api/override/feed/([\d.]+)', OverrideFeedHandler),
(r'/api/override/speed/([\d.]+)', OverrideSpeedHandler),
sys.exit(1)
log.info('Listening on http://%s:%d/', ctrl.args.addr, ctrl.args.port)
+
+
+ # Override default logger
+ def log_request(self, handler):
+ log.info("%d %s", handler.get_status(), handler._request_summary())
import tornado
import argparse
import logging
+import datetime
from pkg_resources import Requirement, resource_filename
root.addHandler(h)
if args.log:
- h = logging.FileHandler(args.log)
+ h = logging.handlers.RotatingFileHandler(args.log, maxBytes = 1000000,
+ backupCount = 5)
h.setLevel(level)
h.setFormatter(f)
root.addHandler(h)
+ # Log header
+ now = datetime.datetime.now().strftime('%Y/%m/%d %H:%M:%S')
+ root.info('Log started ' + now)
+
# Set signal handler
signal.signal(signal.SIGTERM, on_exit)
max-height 400px
overflow-y auto
+ .message
+ white-space pre
+
table
width 100%
margin 0.5em 0