Buildbotics CNC Controller Firmware Changelog
=============================================
+## v0.4.12
+ - Segments straddle arc in linearization.
+ - Control max-arc-error with GCode var.
+ - Implemented path modes G61, G61.1 & G64 with naive CAM but not blending, yet.
+ - Log GCode messages to "Messages" tab.
+ - Acknowledging a message on one browser clears it for all.
+ - Automatically reload Web view when file changes.
+ - Changed "Message" field to "Reason" in Web interface.
+
## v0.4.11
- Don't reset global offsets on M2.
- Test shunt and show error on failure.
{
"name": "bbctrl",
- "version": "0.4.11",
+ "version": "0.4.12",
"homepage": "http://buildbotics.com/",
"repository": "https://github.com/buildbotics/bbctrl-firmware",
"license": "GPL-3.0+",
motors: [{}, {}, {}, {}],
version: '<loading>'
},
- state: {},
- messages: [],
+ state: {messages: []},
video_size: cookie.get('video-size', 'small'),
crosshair: cookie.get('crosshair', false),
errorTimeout: 30,
checkedUpgrade: false,
firmwareName: '',
latestVersion: '',
- password: '',
- showMessages: false
+ password: ''
}
},
delete e.data.log;
}
- if ('message' in e.data) {
- this.add_message(e.data.message);
- delete e.data.message;
- }
-
// Check for session ID change on controller
if ('sid' in e.data) {
if (typeof this.sid == 'undefined') this.sid = e.data.sid;
},
- add_message: function (msg) {
- this.messages.unshift(msg);
- this.showMessages = true;
- },
-
-
close_messages: function (action) {
- this.showMessages = false;
- this.messages.splice(0, this.messages.length);
-
if (action == 'stop') api.put('stop');
if (action == 'continue') api.put('unpause');
+
+ // Acknowledge messages
+ if (this.state.messages.length) {
+ var id = this.state.messages.slice(-1)[0].id
+ api.put('message/' + id + '/ack');
+ }
}
}
})
mach_units: 'METRIC',
mdi: '',
last_file: undefined,
+ last_file_time: undefined,
toolpath: {},
toolpath_progress: 0,
axes: 'xyzabc',
},
- 'state.selected': function () {this.load()}
+ 'state.selected_time': function () {this.load()}
},
},
- highlight_reason: function () {return this.reason != ''},
+ highlight_reason: function () {
+ return this.mach_state == 'ESTOPPED' || this.mach_state == 'HOLDING';
+ },
+
+
plan_time: function () {return this.state.plan_time},
load: function () {
+ var file_time = this.state.selected_time;
var file = this.state.selected;
- if (this.last_file == file) return;
+ if (this.last_file == file && this.last_file_time == file_time) return;
this.last_file = file;
+ this.last_file_time = file_time;
this.$broadcast('gcode-load', file);
this.$broadcast('gcode-line', this.state.line);
this.toolpath_progress = 0;
- this.load_toolpath(file);
+ this.load_toolpath(file, file_time);
},
- load_toolpath: function (file) {
+ load_toolpath: function (file, file_time) {
this.toolpath = {};
if (!file) return;
api.get('path/' + file).done(function (toolpath) {
- if (this.last_file != file) return;
+ if (this.last_file_time != file_time) return;
if (typeof toolpath.progress == 'undefined') {
toolpath.filename = file;
} else {
this.toolpath_progress = toolpath.progress;
- this.load_toolpath(file); // Try again
+ this.load_toolpath(file, file_time); // Try again
}
}.bind(this));
},
api.upload('file', fd)
.done(function () {
- this.last_file = undefined; // Force reload
+ this.last_file_time = undefined; // Force reload
this.$broadcast('gcode-reload', file.name);
}.bind(this)).fail(function (error) {
p Loss of power during an upgrade may damage the controller.
div(slot="footer")
- message(:show.sync="showMessages")
+ message(v-if="state.messages.length", :show="true")
h3(slot="header") GCode message
div(slot="body")
ul
- li(v-for="msg in messages", track-by="$index") {{msg}}
+ li(v-for="msg in state.messages", track-by="$index") {{msg.text}}
div(slot="footer")
button.pure-button.button-success(v-if="state.xx != 'HOLDING'",
th State
td(:class="{attention: highlight_reason}") {{mach_state}}
tr
- th Message
+ th Reason
td.reason(:class="{attention: highlight_reason}") {{reason}}
tr(title="Active machine units")
th Units
DEBUG = 0
INFO = 1
-WARNING = 2
-ERROR = 3
+MESSAGE = 2
+WARNING = 3
+ERROR = 4
-def get_level_name(level): return 'debug info warning error'.split()[level]
+level_names = 'debug info message warning error'.split()
+
+def get_level_name(level): return level_names[level]
# Get this file's name
def debug (self, *args, **kwargs): self._log(DEBUG, *args, **kwargs)
+ def message(self, *args, **kwargs): self._log(MESSAGE, *args, **kwargs)
def info (self, *args, **kwargs): self._log(INFO, *args, **kwargs)
def warning(self, *args, **kwargs): self._log(WARNING, *args, **kwargs)
def error (self, *args, **kwargs): self._log(ERROR, *args, **kwargs)
def _log(self, msg, level = INFO, prefix = '', where = None):
if not msg: return
- hdr = '%s:%s:' % ('DIWE'[level], prefix)
+ hdr = '%s:%s:' % ('DIMWE'[level], prefix)
s = hdr + ('\n' + hdr).join(msg.split('\n'))
if self.f is not None:
self.cmdq = CommandQueue(ctrl)
self.planner = None
self._position_dirty = False
+ self.where = ''
ctrl.state.add_listener(self._update)
else: self.log.error('Could not parse planner log line: ' + line)
+ def _add_message(self, text):
+ self.ctrl.state.add_message(text)
+
+ line = self.ctrl.state.get('line', 0)
+ if 0 <= line: where = '%s:%d' % (self.where, line)
+ else: where = self.where
+
+ self.log.message(text, where = where)
+
+
def _enqueue_set_cmd(self, id, name, value):
self.log.info('set(#%d, %s, %s)', id, name, value)
self.cmdq.enqueue(id, self.ctrl.state.set, name, value)
name, value = block['name'], block['value']
if name == 'message':
- msg = dict(message = value)
- self.cmdq.enqueue(id, self.ctrl.log.broadcast, msg)
+ self.cmdq.enqueue(id, self._add_message, value)
if name in ['line', 'tool']: self._enqueue_set_cmd(id, name, value)
def mdi(self, cmd, with_limits = True):
+ self.where = '<mdi>'
self.log.info('MDI:' + cmd)
self._sync_position()
self.planner.load_string(cmd, self.get_config(True, with_limits))
def load(self, path):
+ self.where = path
path = self.ctrl.get_path('upload', path)
self.log.info('GCode:' + path)
self._sync_position()
self.listeners = []
self.timeout = None
self.machine_var_set = set()
+ self.message_id = 0
# Defaults
self.vars = {
'line': -1,
+ 'messages': [],
'tool': 0,
'feed': 0,
'speed': 0,
else: self.select_file('')
- def select_file(self, filename): self.set('selected', filename)
+ def select_file(self, filename):
+ self.set('selected', filename)
+ time = os.path.getmtime(self.ctrl.get_upload(filename))
+ self.set('selected_time', time)
+
+
+ def ack_message(self, id):
+ self.log.info('Message %d acknowledged' % id)
+ msgs = self.vars['messages']
+ msgs = list(filter(lambda m: id < m['id'], msgs))
+ self.set('messages', msgs)
+
+
+ def add_message(self, text):
+ msg = dict(text = text, id = self.message_id)
+ self.message_id += 1
+ msgs = self.vars['messages']
+ msgs = msgs + [msg] # It's important we make a new list here
+ self.set('messages', msgs)
def _notify(self):
self.set_header('Content-Type', 'text/plain')
+class MessageAckHandler(bbctrl.APIHandler):
+ def put_ok(self, id):
+ self.get_ctrl().state.ack_message(int(id))
+
+
class BugReportHandler(bbctrl.RequestHandler):
def get(self):
import tarfile, io
handlers = [
(r'/websocket', WSConnection),
(r'/api/log', LogHandler),
+ (r'/api/message/(\d+)/ack', MessageAckHandler),
(r'/api/bugreport', BugReportHandler),
(r'/api/reboot', RebootHandler),
(r'/api/hostname', HostnameHandler),