- Show "Upgrading firmware" when upgrading.
- Log excessive pwr communcation failures as errors.
- Ensure we can still get out of non-idle cycles when there are errors.
- Less frequent pwr variable updates.
- Stop cancels seek and subsequent estop.
- Fixed bug in AVR/Planner command synchronization.
- Consistently display HOMMING state during homing operation.
- Homing zeros axis global offset.
- Added zero all button. #126
- Separate "Auto" and "MDI" play/pause & stop buttons. #126
- Moved home all button. #126
- Display "Video camera not found." instead of broken image icon.
- Show offset positions not absolute on LCD.
- Dont change gcode lines while homing.
- Dont change button states while homing.
- Adding warning about power cyclying during an upgrade.
- Reset planner on AVR errors.
- Fixed pausing with short moves.
- Corrected s-curve accel increasing jogging velocities.
Buildbotics CNC Controller Firmware Change Log
==============================================
+## v0.3.17
+ - Fixed pausing fail near end of run bug.
+ - Show "Upgrading firmware" when upgrading.
+ - Log excessive pwr communcation failures as errors.
+ - Ensure we can still get out of non-idle cycles when there are errors.
+ - Less frequent pwr variable updates.
+ - Stop cancels seek and subsequent estop.
+ - Fixed bug in AVR/Planner command synchronization.
+ - Consistently display HOMMING state during homing operation.
+ - Homing zeros axis global offset.
+ - Added zero all button. #126
+ - Separate "Auto" and "MDI" play/pause & stop buttons. #126
+ - Moved home all button. #126
+ - Display "Video camera not found." instead of broken image icon.
+ - Show offset positions not absolute on LCD.
+ - Don't change gcode lines while homing.
+ - Don't change button states while homing.
+ - Adding warning about power cyclying during an upgrade.
+ - Reset planner on AVR errors.
+ - Fixed pausing with short moves.
+ - Corrected s-curve accel increasing jogging velocities.
+
## v0.3.16
- Fixed switch debounce bug.
{
"name": "bbctrl",
- "version": "0.3.16",
+ "version": "0.3.17",
"homepage": "http://buildbotics.com/",
"repository": "https://github.com/buildbotics/bbctrl-firmware",
"license": "GPL-3.0+",
#include "switch.h"
#include "seek.h"
#include "estop.h"
+#include "state.h"
#include "config.h"
stat_t exec_next() {
+ // Hold if we've reached zero velocity between commands an stopping
+ if (!ex.cb && !exec_get_velocity() && state_get() == STATE_STOPPING)
+ state_holding();
+
if (!ex.cb && !command_exec()) return STAT_NOP; // Queue empty
if (!ex.cb) return STAT_AGAIN; // Non-exec command
return ex.cb(); // Exec
static jog_runtime_t jr;
-static bool _axis_velocity_target(int axis) {
- jog_axis_t *a = &jr.axes[axis];
-
- float Vn = a->next * axis_get_velocity_max(axis);
- float Vi = a->velocity;
- float Vt = a->target;
-
- if (MIN_VELOCITY < fabs(Vn)) jr.done = false; // Still jogging
-
- if (!fp_ZERO(Vi) && (Vn < 0) != (Vi < 0))
- Vn = 0; // Plan to zero on sign change
-
- if (fabs(Vn) < MIN_VELOCITY) Vn = 0;
-
- if (Vt == Vn) return false; // No change
-
- a->target = Vn;
- if (Vn) a->sign = Vn < 0 ? -1 : 1;
-
- return true; // Velocity changed
-}
-
-
#if 0
// Numeric version
static float _compute_deccel_dist(float vel, float accel, float jerk) {
// Analytical version
-static float _compute_deccel_dist(float vel, float accel, float jerk) {
+static float _compute_deccel_dist(float vel, float accel, float maxA,
+ float jerk) {
// TODO Fix this function
float dist = 0;
- float t = accel / jerk;
// Compute distance to decrease accel to zero
if (0 < accel) {
- // s(t) = v * t + 2/3 * a * t^2
- dist += vel * t + 2.0 / 3.0 * accel * t * t;
+ float t = accel / jerk;
+
+ // s(t) = v * t + 1/3 * a * t^2
+ dist += vel * t + 1.0 / 3.0 * accel * t * t;
// v(t) = a * t / 2 + v
vel += accel * t / 2;
accel = 0;
+ t = 0;
}
- // Compute max deccel given accel, vel and jerk
- float maxDeccel = -sqrt(0.5 * square(accel) + vel * jerk);
+ // At this point accel <= 0, aka a deccelleration
- // Compute distance to max deccel
+ // Compute max deccel by applying the quadratic formula.
+ // (1 / j) * Am^2 + ((1 - a) / j) * Am + (a^2 - 0.5 * a) / j + v = 0
+ float t = accel / jerk;
+ float a = 1 / jerk;
+ float b = a - t;
+ float c = t * (accel - 0.5) + vel;
+ float maxDeccel = (-b - sqrt(b * b - 4 * a * c)) / a * 0.5;
+
+ // Limit decceleration
+ if (maxDeccel < -maxA) maxDeccel = -maxA;
+
+ // Compute distance and velocity change to max deccel
if (maxDeccel < accel) {
- float t = (maxDeccel - accel) / -jerk;
+ float t = (accel - maxDeccel) / jerk;
dist += scurve_distance(t, vel, accel, -jerk);
vel += scurve_velocity(t, accel, -jerk);
accel = maxDeccel;
}
+ // Compute max deltaV for remaining deccel
+ t = -accel / jerk; // Time to shed remaining accel
+ float deltaV = -scurve_velocity(t, accel, jerk);
+
+ // Compute constant deccel period
+ if (deltaV < vel) {
+ float t = -(vel - deltaV) / accel;
+ dist += scurve_distance(t, vel, accel, 0);
+ vel += scurve_velocity(t, accel, 0);
+ }
+
// Compute distance to zero vel
dist += scurve_distance(t, vel, accel, jerk);
float dir = jr.axes[axis].velocity;
if (!dir) dir = jr.axes[axis].target;
if (!dir) return 0;
+ bool positive = 0 < dir;
// Check if axis is homed
if (!axis_get_homed(axis)) return Vt;
// Move allowed if at or past limit but headed out
// Move not allowed if at or past limit and heading further in
float position = exec_get_axis_position(axis);
- if (position <= min) return 0 < dir ? Vt : 0;
- if (max <= position) return dir < 0 ? Vt : 0;
+ if (position <= min) return positive ? Vt : 0;
+ if (max <= position) return !positive ? Vt : 0;
- // Compute dist to decel
- float jerk = axis_get_jerk_max(axis);
- float deccelDist = _compute_deccel_dist(V, A, jerk);
+ // Min velocity near limits
+ if (positive && max < position + 5) return MIN_VELOCITY;
+ if (!positive && position - 5 < min) return MIN_VELOCITY;
+
+ return Vt; // TODO compute deccel dist
- // Check if decell distance will lead to exceeding a limit
- if (0 < dir && position <= min + deccelDist) return 0;
- if (dir < 0 && max - deccelDist <= position) return 0;
+ // Compute dist to deccel
+ float jerk = axis_get_jerk_max(axis);
+ float maxA = axis_get_accel_max(axis);
+ float deccelDist = _compute_deccel_dist(V, A, maxA, jerk);
- // Check if decell distance will lead near limit
- if (0 < dir && position <= min + deccelDist + 5) return MIN_VELOCITY;
- if (dir < 0 && max - deccelDist - 5 <= position) return MIN_VELOCITY;
+ // Check if deccel distance will lead to exceeding a limit
+ if (positive && max <= position + deccelDist) return 0;
+ if (!positive && position - deccelDist <= min) return 0;
return Vt;
}
+static bool _axis_velocity_target(int axis) {
+ jog_axis_t *a = &jr.axes[axis];
+
+ float Vn = a->next * axis_get_velocity_max(axis);
+ float Vi = a->velocity;
+ float Vt = a->target;
+
+ if (MIN_VELOCITY < fabs(Vn)) jr.done = false; // Still jogging
+
+ if (!fp_ZERO(Vi) && (Vn < 0) != (Vi < 0))
+ Vn = 0; // Plan to zero on sign change
+
+ if (fabs(Vn) < MIN_VELOCITY) Vn = 0;
+
+ if (Vt == Vn) return false; // No change
+
+ a->target = Vn;
+ if (Vn) a->sign = Vn < 0 ? -1 : 1;
+
+ return true; // Velocity changed
+}
+
+
static float _compute_axis_velocity(int axis) {
jog_axis_t *a = &jr.axes[axis];
return Vt;
}
- // Compute axis max jerk
+ // Compute axis max accel and jerk
float jerk = axis_get_jerk_max(axis);
+ float maxA = axis_get_accel_max(axis);
// Compute next accel
- a->accel = scurve_next_accel(SEGMENT_TIME, V, Vt, a->accel, jerk);
-
- // Limit acceleration
- if (axis_get_accel_max(axis) < fabs(a->accel))
- a->accel = (a->accel < 0 ? -1 : 1) * axis_get_accel_max(axis);
+ a->accel = scurve_next_accel(SEGMENT_TIME, V, Vt, a->accel, maxA, jerk);
return V + a->accel * SEGMENT_TIME;
}
return STAT_NOP;
}
- // Compute new velocity and acceleration
- a = scurve_next_accel(t, v, 0, a, j);
- if (l.line.max_accel < fabs(a)) {
- a = (a < 0 ? -1 : 1) * l.line.max_accel;
- j = 0;
- } else if (0 < a) j = -j;
+ // Compute new velocity, acceleration and travel distance
+ a = scurve_next_accel(t, v, 0, a, l.line.max_accel, j);
v += a * t;
-
- // Compute distance that will be traveled
l.dist += v * t;
+ // Target end of line exactly if we are close
+ if (l.dist - 0.001 < l.line.length && l.line.length < l.dist + 0.001)
+ l.dist = l.line.length;
+
if (l.line.length < l.dist) {
// Compute time left in current section
l.current_time = t - (l.dist - l.line.length) / v;
-
- exec_set_acceleration(0);
- exec_set_jerk(0);
_done();
- // TODO it's possible to exit here and have no more moves
- // Apparently this pause method can take longer to pause than the
- // actual move. FIX ME!!!
-
return STAT_AGAIN;
}
_segment_target(target, l.dist);
l.current_time = 0;
- exec_set_jerk(j);
+
+ // Compute jerk
+ float oldAccel = exec_get_acceleration();
+ exec_set_jerk(oldAccel == a ? 0 : (oldAccel < a ? j : -j));
return _move(SEGMENT_TIME, target, v, a);
}
static stat_t _line_exec() {
- // Pause if requested. If we are already stopping, just continue.
- if (state_get() == STATE_STOPPING && (l.section < 4 || l.line.target_vel)) {
+ // Pause if requested.
+ if (state_get() == STATE_STOPPING) {
if (SEGMENT_TIME < l.current_time) l.current_time = 0;
exec_set_cb(_pause);
return _pause();
// Release exec if we are done
if (lastSection) {
- if (state_get() == STATE_STOPPING) state_holding();
exec_set_velocity(l.line.target_vel);
_done();
}
l.section = -1;
if (!_section_next()) return;
+#if 0
// Compare start position to actual position
float diff[AXES];
bool report = false;
if (report)
STATUS_DEBUG("diff: %.4f %.4f %.4f %.4f",
diff[0], diff[1], diff[2], diff[3]);
+#endif
// Set callback
exec_set_cb(_line_exec);
#include "scurve.h"
#include <math.h>
+#include <stdbool.h>
float scurve_distance(float t, float v, float a, float j) {
float scurve_acceleration(float t, float j) {return j * t;}
-float scurve_next_accel(float dT, float iV, float tV, float iA, float jerk) {
- float tA = sqrt(jerk * fabs(tV - iV)) * (tV < iV ? -1 : 1); // Target accel
- float dA = jerk * dT; // Delta accel
+float scurve_next_accel(float time, float Vi, float Vt, float accel, float aMax,
+ float jerk) {
+ bool increasing = Vi < Vt;
+ float deltaA = time * jerk;
- if (iA < tA) return (iA < tA + dA) ? tA : (iA + dA);
- if (tA < iA) return (iA - dA < tA) ? tA : (iA - dA);
+ if (increasing && accel < -deltaA)
+ return accel + deltaA; // negative accel, increasing speed
- return iA;
+ if (!increasing && deltaA < accel)
+ return accel - deltaA; // positive accel, decreasing speed
+
+ float deltaV = fabs(Vt - Vi);
+ float targetA = sqrt(2 * deltaV * jerk);
+ if (aMax < targetA) targetA = aMax;
+
+ if (increasing) {
+ if (targetA < accel + deltaA) return targetA;
+ return accel + deltaA;
+
+ } else {
+ if (accel - deltaA < -targetA) return -targetA;
+ return accel - deltaA;
+ }
}
float scurve_distance(float time, float vel, float accel, float jerk);
float scurve_velocity(float time, float accel, float jerk);
float scurve_acceleration(float time, float jerk);
-float scurve_next_accel(float dT, float iV, float tV, float iA, float jerk);
+float scurve_next_accel(float time, float Vi, float Vt, float accel, float aMax,
+ float jerk);
}
+void seek_cancel() {seek.active = false;}
+
+
// Command callbacks
stat_t command_seek(char *cmd) {
int8_t sw = decode_hex_nibble(cmd[1]);
switch_id_t seek_get_switch();
bool seek_switch_found();
void seek_end();
+void seek_cancel();
#include "outputs.h"
#include "jog.h"
#include "estop.h"
+#include "seek.h"
#include "report.h"
#include <stdio.h>
s.flush_requested = true;
spindle_stop();
outputs_stop();
+ seek_cancel();
_set_state(STATE_READY);
break;
message(:show.sync="firmwareUpgrading")
h3(slot="header") Firmware upgrading
- p(slot="body") Please wait...
+ div(slot="body")
+ h3 Please wait...
+ p Loss of power during an upgrade may damage the controller.
div(slot="footer")
message(:show.sync="showMessages")
script#control-view-template(type="text/x-template")
#control
table.axes
- tr
+ tr(:class="{'homed': is_homed()}")
th.name Axis
th.position Position
th.absolute Absolute
th.offset Offset
- th.actions Actions
+ th.actions
+ button.pure-button(:disabled="!is_ready",
+ title="Zero all axis offsets.", @click="zero()") ∅
+
+ button.pure-button(title="Home all axes.", @click="home()",
+ :disabled="!is_ready")
+ .fa.fa-home
each axis in 'xyzabc'
tr.axis(:class="{'homed': is_homed('#{axis}'), 'axis-#{axis}': true}",
td.absolute {{state.#{axis}p || 0 | fixed 3}}
td.offset {{get_offset('#{axis}') | fixed 3}}
th.actions
- button.pure-button(:disabled="state.xx != 'READY'",
+ button.pure-button(:disabled="!is_ready",
title="Set {{'#{axis}' | upper}} axis position.",
@click="show_set_position('#{axis}')")
.fa.fa-cog
- button.pure-button(:disabled="state.xx != 'READY'",
+ button.pure-button(:disabled="!is_ready",
title="Zero {{'#{axis}' | upper}} axis offset.",
- @click="zero_axis('#{axis}')") ∅
+ @click="zero('#{axis}')") ∅
- button.pure-button(:disabled="state.xx != 'READY'",
+ button.pure-button(:disabled="!is_ready",
title="Home {{'#{axis}' | upper}} axis.",
@click="home('#{axis}')")
.fa.fa-home
title="Home {{'#{axis}' | upper}} axis.",
@click="set_home('#{axis}', axis_position)") Set
+
table.info
tr
th State
- td(:class="{attention: highlight_reason()}") {{get_state()}}
+ td(:class="{attention: highlight_reason}") {{mach_state}}
td
tr
th Message
- td.reason(:class="{attention: highlight_reason()}") {{get_reason()}}
+ td.reason(:class="{attention: highlight_reason}") {{reason}}
td
tr
th Units
v-model="speed_override", @change="override_speed")
span.percent {{speed_override | percent 0}}
-
- .toolbar
- button.pure-button(title="Home the machine.", @click="home()",
- :disabled="state.xx != 'READY'")
- .fa.fa-home
-
- button.pure-button(
- title="{{state.xx == 'RUNNING' ? 'Pause' : 'Start'}} program.",
- @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
-
- button.pure-button(title="Pause program at next optional stop (M1).",
- @click="optional_pause", v-if="false")
- .fa.fa-stop-circle-o
-
- button.pure-button(title="Execute one program step.", @click="step",
- :disabled="(state.xx != 'READY' && state.xx != 'HOLDING') || " +
- "!state.selected", v-if="false")
- .fa.fa-step-forward
-
.tabs
input#tab1(type="radio", name="tabs" checked)
label(for="tab1", title="Run GCode programs") Auto
section#content1.tab-content
.toolbar
+ button.pure-button(
+ title="{{is_running ? 'Pause' : 'Start'}} program.",
+ @click="start_pause", :disabled="!state.selected")
+ .fa(:class="is_running ? 'fa-pause' : 'fa-play'")
+
+ button.pure-button(title="Stop program.", @click="stop")
+ .fa.fa-stop
+
+ button.pure-button(title="Pause program at next optional stop (M1).",
+ @click="optional_pause", v-if="false")
+ .fa.fa-stop-circle-o
+
+ button.pure-button(title="Execute one program step.", @click="step",
+ :disabled="(!is_ready && !is_holding) || !state.selected",
+ v-if="false")
+ .fa.fa-step-forward
+
button.pure-button(title="Upload a new GCode program.", @click="open",
- :disabled="state.xx == 'RUNNING' || state.xx == 'STOPPING'")
+ :disabled="is_running || is_stopping")
.fa.fa-folder-open
input.gcode-file-input(type="file", @change="upload",
select(title="Select previously uploaded GCode programs.",
v-model="state.selected", @change="load",
- :disabled="state.xx == 'RUNNING' || state.xx == 'STOPPING'")
+ :disabled="is_running || is_stopping")
option(v-for="file in files", :value="file") {{file}}
.gcode(:class="{placeholder: !gcode}", @scroll="gcode_scroll")
| {{item}}
section#content2.tab-content
- .mdi.pure-form
- fieldset
- button.pure-button.pure-button-primary(
- title="Manually execute instructions.", @click="submit_mdi") MDI
- input(v-model="mdi", @keyup.enter="submit_mdi")
+ .mdi.pure-form(title="Manual GCode entry.")
+ button.pure-button(
+ title="{{is_running ? 'Pause' : 'Start'}} command.",
+ @click="mdi_start_pause")
+ .fa(:class="is_running ? 'fa-pause' : 'fa-play'")
+
+ button.pure-button(title="Stop command.", @click="stop")
+ .fa.fa-stop
+
+ input(v-model="mdi", @keyup.enter="submit_mdi")
.history(:class="{placeholder: !history}")
span(v-if="!history.length") MDI history displays here.
.video
img.reload(src="/images/reload.png", @click="reload_video",
title="Reload video")
- img.mjpeg(:src="video_url")
+ img.mjpeg(:src="video_url", alt="Video camera not found.")
p(style="padding:0 1em")
| Plug in a USB video camera to monitor your machine remotely.
}
update_object(this.state, e.data, false);
+ this.$broadcast('update');
}.bind(this)
},
+ computed: {
+ mach_state: function () {
+ var cycle = this.state.cycle;
+ var state = this.state.xx;
+
+ if (typeof cycle != 'undefined' && state != 'ESTOPPED' &&
+ (cycle == 'jogging' || cycle == 'homing'))
+ return cycle.toUpperCase();
+ return state || ''
+ },
+
+
+ is_running: function () {
+ return this.mach_state == 'RUNNING' || this.mach_state == 'HOMING';
+ },
+
+
+ is_stopping: function() {return this.mach_state == 'STOPPING'},
+ is_holding: function() {return this.mach_state == 'HOLDING'},
+ is_ready: function() {return this.mach_state == 'READY'},
+
+
+ reason: function () {
+ if (this.mach_state == 'ESTOPPED') return this.state.er;
+ if (this.mach_state == 'HOLDING') return this.state.pr;
+ return '';
+ },
+
+
+ highlight_reason: function () {return this.reason != ''}
+ },
+
+
events: {
jog: function (axis, power) {
var data = {};
api.put('jog', data);
},
- connected: function () {this.update()}
+ connected: function () {this.update()},
+
+ update: function () {console.log(this.state.xx, this.state.cycle)}
},
methods: {
- 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 () {
- if (this.state.xx == 'ESTOPPED') return this.state.er;
- if (this.state.xx == 'HOLDING') return this.state.pr;
- return '';
- },
-
-
- highlight_reason: function () {return this.get_reason() != ''},
send: function (msg) {this.$dispatch('send', msg)},
is_homed: function (axis) {
- var motor = this.get_axis_motor_id(axis);
- return motor != -1 && this.state[motor + 'homed'];
+ if (typeof axis == 'undefined') {
+ var enabled = false;
+ var axes = 'xyzabc';
+
+ for (var i in axes) {
+ if (this.enabled(axes.charAt(i))) {
+ if (!this.is_homed(axes.charAt(i))) return false;
+ else enabled = true;
+ }
+ }
+
+ return enabled;
+
+ } else {
+ var motor = this.get_axis_motor_id(axis);
+ return motor != -1 && this.state[motor + 'homed'];
+ }
},
update_gcode_line: function () {
+ if (this.mach_state == 'HOMING') return;
+
if (typeof this.last_line != 'undefined') {
$('#gcode-line-' + this.last_line).removeClass('highlight');
this.last_line = undefined;
},
+ mdi_start_pause: function () {
+ if (this.state.xx == 'RUNNING') this.pause();
+
+ else if (this.state.xx == 'STOPPING' || this.state.xx == 'HOLDING')
+ this.unpause();
+
+ else this.submit_mdi();
+ },
+
+
load_history: function (index) {this.mdi = this.history[index];},
},
- zero_axis: function (axis) {this.set_position(axis, 0)},
+ zero: function (axis) {
+ if (typeof axis == 'undefined') {
+ var axes = 'xyzabc';
+ for (var i in axes)
+ if (this.enabled(axes.charAt(i)))
+ this.zero(axes.charAt(i));
+
+ } else this.set_position(axis, 0);
+ },
start_pause: function () {
log = logging.getLogger('Comm')
-class Comm():
- def __init__(self, ctrl, next_cb, connect_cb):
+class Comm(object):
+ def __init__(self, ctrl):
self.ctrl = ctrl
- self.next_cb = next_cb
- self.connect_cb = connect_cb
-
self.queue = deque()
self.in_buf = ''
self.command = None
self.i2c_addr = ctrl.args.avr_addr
+ def comm_next(self): raise Exception('Not implemented')
+ def comm_error(self): raise Exception('Not implemented')
+
+
def is_active(self):
return len(self.queue) or self.command is not None
# Load next command from callback
else:
- cmd = self.next_cb()
+ cmd = self.comm_next()
if cmd is None: self._set_write(False) # Stop writing
else: self._load_next_command(cmd)
elif level == 'warning': log.warning(msg, extra = extra)
elif level == 'error': log.error(msg, extra = extra)
+ if level == 'error': self.comm_error()
+
def _serial_read(self):
try:
def connect(self):
try:
- # Call connect callback
- self.connect_cb()
-
# Resume once current queue of GCode commands has flushed
self.queue_command(Cmd.RESUME)
self.queue_command(Cmd.HELP) # Load AVR commands and variables
--- /dev/null
+################################################################################
+# #
+# This file is part of the Buildbotics firmware. #
+# #
+# Copyright (c) 2015 - 2018, Buildbotics LLC #
+# All rights reserved. #
+# #
+# This file ("the software") is free software: you can redistribute it #
+# and/or modify it under the terms of the GNU General Public License, #
+# version 2 as published by the Free Software Foundation. You should #
+# have received a copy of the GNU General Public License, version 2 #
+# along with the software. If not, see <http://www.gnu.org/licenses/>. #
+# #
+# The software is distributed in the hope that it will be useful, but #
+# WITHOUT ANY WARRANTY; without even the implied warranty of #
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU #
+# Lesser General Public License for more details. #
+# #
+# You should have received a copy of the GNU Lesser General Public #
+# License along with the software. If not, see #
+# <http://www.gnu.org/licenses/>. #
+# #
+# For information regarding this software email: #
+# "Joseph Coffland" <joseph@buildbotics.com> #
+# #
+################################################################################
+
+import logging
+from collections import deque
+
+log = logging.getLogger('CmdQ')
+log.setLevel(logging.WARNING)
+
+
+class CommandQueue():
+ def __init__(self):
+ self.releaseID = 0
+ self.q = deque()
+
+
+ def is_active(self): return len(self.q)
+
+
+ def clear(self):
+ self.releaseID = 0
+ self.q.clear()
+
+
+ def enqueue(self, id, immediate, cb, *args, **kwargs):
+ log.info('add(#%d, %s) releaseID=%d', id, immediate, self.releaseID)
+ self.q.append((id, immediate, cb, args, kwargs))
+ self._release()
+
+
+ def _release(self):
+ while len(self.q):
+ id, immediate, cb, args, kwargs = self.q[0]
+
+ # Execute commands <= releaseID and consecutive immediate commands
+ if not immediate and self.releaseID < id: return
+
+ log.info('releasing id=%d' % id)
+ self.q.popleft()
+
+ try:
+ if cb is not None: cb(*args, **kwargs)
+ except Exception as e:
+ log.exception('During command queue callback')
+
+
+
+ def release(self, id):
+ if id and id <= self.releaseID:
+ log.warning('id out of order %d <= %d' % (id, self.releaseID))
+ self.releaseID = id
+
+ self._release()
self.jog = bbctrl.Jog(self)
self.pwr = bbctrl.Pwr(self)
- self.mach.comm.connect()
+ self.mach.connect()
self.lcd.add_new_page(bbctrl.MainLCDPage(self))
self.lcd.add_new_page(bbctrl.IPLCDPage(self.lcd))
import lcd
import atexit
import logging
-import tornado.ioloop
+from tornado.ioloop import PeriodicCallback
log = logging.getLogger('LCD')
self.set_message('Loading...')
# Redraw screen every 5 seconds
- self.redraw_timer = tornado.ioloop.PeriodicCallback(self._redraw, 5000,
- self.ctrl.ioloop)
+ self.redraw_timer = PeriodicCallback(self._redraw, 5000, ctrl.ioloop)
self.redraw_timer.start()
atexit.register(self.goodbye)
self.timeout = self.ctrl.ioloop.call_later(1, self._update)
- def goodbye(self):
+ def goodbye(self, message = ''):
if self.timeout:
self.ctrl.ioloop.remove_timeout(self.timeout)
self.timeout = None
self.redraw_timer.stop()
self.redraw_timer = None
- if self.lcd is not None: self.set_message('')
+ if self.lcd is not None: self.set_message(message)
import logging
import bbctrl
+from bbctrl.Comm import Comm
import bbctrl.Cmd as Cmd
-import bbctrl.Comm
+from tornado.ioloop import PeriodicCallback
log = logging.getLogger('Mach')
'''
+def overrides(interface_class):
+ def overrider(method):
+ if not method.__name__ in dir(interface_class):
+ log.warning('%s does not override %s' % (
+ method.__name__, interface_class.__name__))
-class Mach():
+ return method
+
+ return overrider
+
+
+class Mach(Comm):
def __init__(self, ctrl):
+ super().__init__(ctrl)
+
self.ctrl = ctrl
self.planner = bbctrl.Planner(ctrl)
- self.comm = bbctrl.Comm(ctrl, self._comm_next, self._comm_connect)
- self.update_timer = None
ctrl.state.set('cycle', 'idle')
+ PeriodicCallback(self._update_cycle, 1000, ctrl.ioloop).start()
+
ctrl.state.add_listener(self._update)
- self.comm.reboot()
+ super().reboot()
def _get_state(self): return self.ctrl.state.get('xx', '')
def _begin_cycle(self, cycle):
current = self._get_cycle()
+ if current == cycle: return # No change
if current == 'idle':
self.planner.update_position()
self.ctrl.state.set('cycle', cycle)
- elif current != cycle:
+ else:
raise Exception('Cannot enter %s cycle during %s' %
(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')
+ if (self._get_cycle() != 'idle' and self._get_state() == 'READY' and
+ not self.planner.is_busy() and not super().is_active()):
+ self.ctrl.state.set('cycle', 'idle')
def _update(self, update):
# Handle EStop
if 'xx' in update and state == 'ESTOPPED': self.planner.reset()
- # Update cycle
+ # Update cycle now, if it has changed
self._update_cycle()
# Continue after seek hold
self.unpause()
- def _comm_next(self):
+ @overrides(Comm)
+ def comm_next(self):
if self.planner.is_running(): return self.planner.next()
- def _comm_connect(self):
+ @overrides(Comm)
+ def comm_error(self): self.planner.reset()
+
+
+ @overrides(Comm)
+ def connect(self):
self.ctrl.state.reset()
self.planner.reset()
+ super().connect()
def _query_var(self, cmd):
self.ctrl.state.config(name, value)
- def mdi(self, cmd):
+ def mdi(self, cmd, with_limits = True):
if not len(cmd): return
if cmd[0] == '$': self._query_var(cmd)
- elif cmd[0] == '\\': self.comm.queue_command(cmd[1:])
+ elif cmd[0] == '\\': super().queue_command(cmd[1:])
else:
self._begin_cycle('mdi')
- self.planner.mdi(cmd)
- self.comm.resume()
+ self.planner.mdi(cmd, with_limits)
+ super().resume()
def set(self, code, value):
- self.comm.queue_command('${}={}'.format(code, value))
+ super().queue_command('${}={}'.format(code, value))
def jog(self, axes):
self._begin_cycle('jogging')
- self.comm.queue_command(Cmd.jog(axes))
+ super().queue_command(Cmd.jog(axes))
def home(self, axis, position = None):
# Home axis
log.info('Homing %s axis' % axis)
- self.planner.mdi(axis_homing_procedure % {'axis': axis})
- self.comm.resume()
+ self.planner.mdi(axis_homing_procedure % {'axis': axis}, False)
+ super().resume()
- def estop(self): self.comm.estop()
+ def estop(self): super().estop()
def clear(self):
if self._get_state() == 'ESTOPPED':
self.ctrl.state.reset()
- self.comm.clear()
+ super().clear()
def select(self, path):
def start(self):
self._begin_cycle('running')
self.planner.load('upload/' + self.ctrl.state.get('selected'))
- self.comm.resume()
+ super().resume()
def step(self):
raise Exception('NYI') # TODO
if self._get_cycle() != 'running': self.start()
- else: self.comm.i2c_command(Cmd.UNPAUSE)
+ else: super().i2c_command(Cmd.UNPAUSE)
def stop(self):
if self._get_cycle() == 'idle': self._begin_cycle('running')
- self.comm.i2c_command(Cmd.STOP)
+ super().i2c_command(Cmd.STOP)
self.planner.stop()
self.ctrl.state.set('line', 0)
- def pause(self): self.comm.pause()
+ def pause(self): super().pause()
def unpause(self):
pause_reason = self.ctrl.state.get('pr', '')
if pause_reason in ['User paused', 'Switch found']:
self.planner.restart()
- self.comm.resume()
+ super().resume()
- self.comm.i2c_command(Cmd.UNPAUSE)
+ super().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 self._get_cycle() == 'running': super().pause(True)
def set_position(self, axis, position):
self.mdi('G92 %s%f' % (axis, position))
else:
- if self._get_cycle() != 'idle':
+ if self._get_cycle() not in ['idle', 'mdi']:
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))
+ super().queue_command(Cmd.set_axis(axis, position))
for axis in 'xyzabc':
if state.is_axis_enabled(axis):
position = state.get(axis + 'p', 0)
+ position += state.get('offset_' + axis, 0)
self.text('% 10.3f%s' % (position, axis.upper()), 9, row)
row += 1
from collections import deque
import camotics.gplan as gplan # pylint: disable=no-name-in-module,import-error
import bbctrl.Cmd as Cmd
+from bbctrl.CommandQueue import CommandQueue
log = logging.getLogger('Planner')
class Planner():
def __init__(self, ctrl):
self.ctrl = ctrl
- self.lastID = 0
- self.setq = deque()
+ self.cmdq = CommandQueue()
ctrl.state.add_listener(self._update)
self.reset()
- def is_busy(self): return self.is_running() or len(self.setq)
+ def is_busy(self): return self.is_running() or self.cmdq.is_active()
def is_running(self): return self.planner.is_running()
def is_synchronizing(self): return self.planner.is_synchronizing()
return limit
- def _get_config(self, mdi):
+ def _get_config(self, mdi, with_limits):
config = {
- "min-soft-limit": self._get_soft_limit('tn', -math.inf),
- "max-soft-limit": self._get_soft_limit('tm', math.inf),
- "max-vel": self._get_config_vector('vm', 1000),
- "max-accel": self._get_config_vector('am', 1000000),
- "max-jerk": self._get_config_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
}
+ if with_limits:
+ config['min-soft-limit'] = self._get_soft_limit('tn', -math.inf)
+ config['max-soft-limit'] = self._get_soft_limit('tm', math.inf)
+
if not mdi:
program_start = self.ctrl.config.get('program-start')
if program_start: config['program-start'] = program_start
self.planner.set_active(id)
# Synchronize planner variables with execution id
- self._release_set_cmds(id)
-
-
- def _release_set_cmds(self, id):
- self.lastID = id
-
- # Apply all set commands <= to ID and those that follow consecutively
- while len(self.setq) and self.setq[0][0] - 1 <= self.lastID:
- id, name, value = self.setq.popleft()
-
- if name == 'message': self.ctrl.msgs.broadcast({'message': value})
- else: self.ctrl.state.set(name, value)
-
- if id == self.lastID + 1: self.lastID = id
-
+ self.cmdq.release(id)
- 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)
-
- def _get_var(self, name):
+ def _get_var_cb(self, name):
value = 0
if len(name) and name[0] == '_':
value = self.ctrl.state.get(name[1:], 0)
return value
- def _log(self, line):
+ def _log_cb(self, line):
line = line.strip()
m = reLogLine.match(line)
if not m: return
else: log.error('Could not parse planner log line: ' + line)
+
+ def _enqueue_set_cmd(self, id, name, value):
+ log.info('set(#%d, %s, %s)', id, name, value)
+ self.cmdq.enqueue(id, True, self.ctrl.state.set, name, value)
+
+
def __encode(self, block):
log.info('Cmd:' + json.dumps(block))
- type = block['type']
+ type, id = block['type'], block['id']
if type == 'line':
return Cmd.line(block['target'], block['exit-vel'],
if type == 'set':
name, value = block['name'], block['value']
- if name in ['message', 'line', 'tool']:
- self._queue_set_cmd(block['id'], name, value)
+ if name == 'message':
+ self.cmdq.enqueue(
+ id, True, self.ctrl.msgs.broadcast, {'message': value})
+
+ if name in ['line', 'tool']:
+ self._enqueue_set_cmd(id, name, value)
if name == 'speed': return Cmd.speed(value)
if len(name) and name[0] == '_':
- self._queue_set_cmd(block['id'], name[1:], value)
+ self._enqueue_set_cmd(id, name[1:], value)
if name[0:1] == '_' and name[1:2] in 'xyzabc':
if name[2:] == '_home': return Cmd.set_axis(name[1], value)
def _encode(self, block):
cmd = self.__encode(block)
- if cmd is not None: return Cmd.set('id', block['id']) + '\n' + cmd
+
+ if cmd is not None:
+ self.cmdq.enqueue(block['id'], False, 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()
+ self.planner.set_resolver(self._get_var_cb)
+ self.planner.set_logger(self._log_cb, 1, 'LinePlanner:3')
+ self.cmdq.clear()
- def mdi(self, cmd):
+ def mdi(self, cmd, with_limits = True):
log.info('MDI:' + cmd)
- self.planner.load_string(cmd, self._get_config(True))
+ self.planner.load_string(cmd, self._get_config(True, with_limits))
def load(self, path):
log.info('GCode:' + path)
- self.planner.load(path, self._get_config(False))
+ self.planner.load(path, self._get_config(False, True))
def stop(self):
try:
self.planner.stop()
- self.lastID = 0
- self.setq.clear()
+ self.cmdq.clear()
except Exception as e:
log.exception(e)
################################################################################
import logging
+from tornado.ioloop import PeriodicCallback
import bbctrl
self.i2c_addr = ctrl.args.pwr_addr
self.regs = [-1] * 8
self.lcd_page = ctrl.lcd.add_new_page()
+ self.failures = 0
- self._update()
+ PeriodicCallback(self._update, 1000, ctrl.ioloop).start()
def get_reg(self, i): return self.regs[i]
if i == FLAGS_REG: self.check_faults()
except Exception as e:
- log.info('Pwr communication failed: %s' % e)
- self.ctrl.ioloop.call_later(1, self._update)
+ self.failures += 1
+ msg = 'Pwr communication failed: %s' % e
+ if self.failures != 5: log.info(msg)
+ else:
+ log.warning(msg)
+ self.failures = 0
return
self.lcd_page.text('%3dC Tmp' % self.regs[TEMP_REG], 0, 0)
if len(update): self.ctrl.state.update(update)
- self.ctrl.ioloop.call_later(0.25, self._update)
+ self.failures = 0
class RebootHandler(bbctrl.APIHandler):
- def put_ok(self): subprocess.Popen('reboot')
+ def put_ok(self):
+ self.ctrl.lcd.goodbye('Rebooting...')
+ subprocess.Popen('reboot')
class LogHandler(tornado.web.RequestHandler):
with open('firmware/update.tar.bz2', 'wb') as f:
f.write(firmware['body'])
+ self.ctrl.lcd.goodbye('Upgrading firmware')
subprocess.Popen(['/usr/local/bin/update-bbctrl'])
class UpgradeHandler(bbctrl.APIHandler):
def put_ok(self):
check_password(self.json['password'])
+ self.ctrl.lcd.goodbye('Upgrading firmware')
subprocess.Popen(['/usr/local/bin/upgrade-bbctrl'])
def heartbeat(self):
- self.ctrl.ioloop.call_later(3, self.heartbeat)
+ self.timer = self.ctrl.ioloop.call_later(3, self.heartbeat)
self.send({'heartbeat': self.count})
self.count += 1
def on_open(self, *args, **kwargs):
- self.timer = self.ctrl.ioloop.call_later(3, self.heartbeat)
self.ctrl.state.add_listener(self.send)
self.ctrl.msgs.add_listener(self.send)
self.is_open = True
+ self.heartbeat()
def on_close(self):
self.ctrl.ioloop.remove_timeout(self.timer)
self.ctrl.state.remove_listener(self.send)
self.ctrl.msgs.remove_listener(self.send)
+ self.is_open = False
def on_message(self, data): self.ctrl.mach.mdi(data)
tornado.websocket.WebSocketHandler.__init__(
self, app, request, **kwargs)
-
def send(self, msg): self.write_message(msg)
def open(self): self.on_open()
from bbctrl.State import State
from bbctrl.Messages import Messages
from bbctrl.Comm import Comm
+from bbctrl.CommandQueue import CommandQueue
from bbctrl.MainLCDPage import MainLCDPage
from bbctrl.IPLCDPage import IPLCDPage
import bbctrl.Cmd as Cmd
log = logging.getLogger('inevent')
+log.setLevel(logging.INFO)
def axes_to_string(axes):
th
text-align center
+ vertical-align bottom
td
text-align right
font-family Courier
- .axis
- &.homed
- background-color #ccffcc
- color #000
+ .homed
+ background-color #ccffcc
+ color #000
+ .axis
.name
text-transform capitalize
.absolute, .offset
min-width 6em
+ tr:nth-child(1) th.actions
+ text-align right
+
.jog svg
text
-webkit-user-select none
line-height 0
min-height 200px
+ .mjpeg
+ line-height 10
+
.reload
float left
margin-right -48px