From bef14114b36a244d96eb81eb95ec896253cd77e8 Mon Sep 17 00:00:00 2001 From: Joseph Coffland Date: Wed, 28 Feb 2018 05:58:57 -0800 Subject: [PATCH] - 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. - 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. --- CHANGELOG.md | 22 +++++ package.json | 2 +- src/avr/src/exec.c | 5 ++ src/avr/src/jog.c | 121 ++++++++++++++++----------- src/avr/src/line.c | 33 ++++---- src/avr/src/scurve.c | 28 +++++-- src/avr/src/scurve.h | 3 +- src/avr/src/seek.c | 3 + src/avr/src/seek.h | 1 + src/avr/src/state.c | 2 + src/jade/index.jade | 4 +- src/jade/templates/control-view.jade | 84 ++++++++++--------- src/js/app.js | 1 + src/js/control-view.js | 94 ++++++++++++++++----- src/py/bbctrl/Comm.py | 18 ++-- src/py/bbctrl/CommandQueue.py | 77 +++++++++++++++++ src/py/bbctrl/Ctrl.py | 2 +- src/py/bbctrl/LCD.py | 9 +- src/py/bbctrl/Mach.py | 92 +++++++++++--------- src/py/bbctrl/MainLCDPage.py | 1 + src/py/bbctrl/Planner.py | 83 +++++++++--------- src/py/bbctrl/Pwr.py | 14 +++- src/py/bbctrl/Web.py | 12 ++- src/py/bbctrl/__init__.py | 1 + src/py/inevent/JogHandler.py | 1 + src/stylus/style.styl | 15 +++- 26 files changed, 482 insertions(+), 246 deletions(-) create mode 100644 src/py/bbctrl/CommandQueue.py diff --git a/CHANGELOG.md b/CHANGELOG.md index ebd69e7..5daaefa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,28 @@ 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. diff --git a/package.json b/package.json index b55956c..c018713 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "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+", diff --git a/src/avr/src/exec.c b/src/avr/src/exec.c index 8869734..68614cf 100644 --- a/src/avr/src/exec.c +++ b/src/avr/src/exec.c @@ -36,6 +36,7 @@ #include "switch.h" #include "seek.h" #include "estop.h" +#include "state.h" #include "config.h" @@ -101,6 +102,10 @@ stat_t exec_move_to_target(float time, const float target[]) { 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 diff --git a/src/avr/src/jog.c b/src/avr/src/jog.c index b52452b..801dec8 100644 --- a/src/avr/src/jog.c +++ b/src/avr/src/jog.c @@ -67,29 +67,6 @@ typedef struct { 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) { @@ -116,33 +93,57 @@ 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); @@ -156,6 +157,7 @@ static float _soft_limit(int axis, float V, float Vt, float A) { 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; @@ -168,25 +170,51 @@ static float _soft_limit(int axis, float V, float Vt, float A) { // 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]; @@ -202,15 +230,12 @@ static float _compute_axis_velocity(int 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; } diff --git a/src/avr/src/line.c b/src/avr/src/line.c index c73fae0..de219ed 100644 --- a/src/avr/src/line.c +++ b/src/avr/src/line.c @@ -171,29 +171,20 @@ static stat_t _pause() { 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; } @@ -202,15 +193,18 @@ static stat_t _pause() { _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(); @@ -263,7 +257,6 @@ static stat_t _line_exec() { // Release exec if we are done if (lastSection) { - if (state_get() == STATE_STOPPING) state_holding(); exec_set_velocity(l.line.target_vel); _done(); } @@ -358,6 +351,7 @@ void command_line_exec(void *data) { l.section = -1; if (!_section_next()) return; +#if 0 // Compare start position to actual position float diff[AXES]; bool report = false; @@ -370,6 +364,7 @@ void command_line_exec(void *data) { 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); diff --git a/src/avr/src/scurve.c b/src/avr/src/scurve.c index ff7f247..ddf973b 100644 --- a/src/avr/src/scurve.c +++ b/src/avr/src/scurve.c @@ -28,6 +28,7 @@ #include "scurve.h" #include +#include float scurve_distance(float t, float v, float a, float j) { @@ -45,12 +46,27 @@ float scurve_velocity(float t, 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; + } } diff --git a/src/avr/src/scurve.h b/src/avr/src/scurve.h index bb1c196..0f3c5ab 100644 --- a/src/avr/src/scurve.h +++ b/src/avr/src/scurve.h @@ -31,4 +31,5 @@ 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); diff --git a/src/avr/src/seek.c b/src/avr/src/seek.c index a6e7d6b..ce3d71b 100644 --- a/src/avr/src/seek.c +++ b/src/avr/src/seek.c @@ -80,6 +80,9 @@ void seek_end() { } +void seek_cancel() {seek.active = false;} + + // Command callbacks stat_t command_seek(char *cmd) { int8_t sw = decode_hex_nibble(cmd[1]); diff --git a/src/avr/src/seek.h b/src/avr/src/seek.h index 6c6fa53..baa133c 100644 --- a/src/avr/src/seek.h +++ b/src/avr/src/seek.h @@ -35,3 +35,4 @@ switch_id_t seek_get_switch(); bool seek_switch_found(); void seek_end(); +void seek_cancel(); diff --git a/src/avr/src/state.c b/src/avr/src/state.c index f0296ac..ebdeae5 100644 --- a/src/avr/src/state.c +++ b/src/avr/src/state.c @@ -34,6 +34,7 @@ #include "outputs.h" #include "jog.h" #include "estop.h" +#include "seek.h" #include "report.h" #include @@ -140,6 +141,7 @@ static void _stop() { s.flush_requested = true; spindle_stop(); outputs_stop(); + seek_cancel(); _set_state(STATE_READY); break; diff --git a/src/jade/index.jade b/src/jade/index.jade index 793936c..96d549d 100644 --- a/src/jade/index.jade +++ b/src/jade/index.jade @@ -162,7 +162,9 @@ html(lang="en") 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") diff --git a/src/jade/templates/control-view.jade b/src/jade/templates/control-view.jade index 277a738..504eb60 100644 --- a/src/jade/templates/control-view.jade +++ b/src/jade/templates/control-view.jade @@ -28,12 +28,18 @@ 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}", @@ -44,16 +50,16 @@ script#control-view-template(type="text/x-template") 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 @@ -98,14 +104,15 @@ script#control-view-template(type="text/x-template") 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 @@ -156,29 +163,6 @@ script#control-view-template(type="text/x-template") 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 @@ -200,8 +184,25 @@ script#control-view-template(type="text/x-template") 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", @@ -225,7 +226,7 @@ script#control-view-template(type="text/x-template") 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") @@ -237,11 +238,16 @@ script#control-view-template(type="text/x-template") | {{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. @@ -279,7 +285,7 @@ script#control-view-template(type="text/x-template") .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. diff --git a/src/js/app.js b/src/js/app.js index f0404da..7045209 100644 --- a/src/js/app.js +++ b/src/js/app.js @@ -289,6 +289,7 @@ module.exports = new Vue({ } update_object(this.state, e.data, false); + this.$broadcast('update'); }.bind(this) diff --git a/src/js/control-view.js b/src/js/control-view.js index dbd75cc..2d8c00b 100644 --- a/src/js/control-view.js +++ b/src/js/control-view.js @@ -82,6 +82,39 @@ module.exports = { }, + 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 = {}; @@ -89,7 +122,9 @@ module.exports = { api.put('jog', data); }, - connected: function () {this.update()} + connected: function () {this.update()}, + + update: function () {console.log(this.state.xx, this.state.cycle)} }, @@ -100,22 +135,6 @@ module.exports = { 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)}, @@ -156,8 +175,23 @@ module.exports = { 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']; + } }, @@ -210,6 +244,8 @@ module.exports = { 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; @@ -256,6 +292,16 @@ module.exports = { }, + 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];}, @@ -342,7 +388,15 @@ module.exports = { }, - 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 () { diff --git a/src/py/bbctrl/Comm.py b/src/py/bbctrl/Comm.py index 1c61111..7ec9747 100644 --- a/src/py/bbctrl/Comm.py +++ b/src/py/bbctrl/Comm.py @@ -38,12 +38,9 @@ import bbctrl.Cmd as Cmd 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 @@ -64,6 +61,10 @@ class Comm(): 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 @@ -131,7 +132,7 @@ class Comm(): # 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) @@ -163,6 +164,8 @@ class Comm(): 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: @@ -229,9 +232,6 @@ class Comm(): 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 diff --git a/src/py/bbctrl/CommandQueue.py b/src/py/bbctrl/CommandQueue.py new file mode 100644 index 0000000..dd37dd4 --- /dev/null +++ b/src/py/bbctrl/CommandQueue.py @@ -0,0 +1,77 @@ +################################################################################ +# # +# 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 . # +# # +# 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 # +# . # +# # +# For information regarding this software email: # +# "Joseph Coffland" # +# # +################################################################################ + +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() diff --git a/src/py/bbctrl/Ctrl.py b/src/py/bbctrl/Ctrl.py index f95dab2..c1366cf 100644 --- a/src/py/bbctrl/Ctrl.py +++ b/src/py/bbctrl/Ctrl.py @@ -51,7 +51,7 @@ class Ctrl(object): 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)) diff --git a/src/py/bbctrl/LCD.py b/src/py/bbctrl/LCD.py index 4cbd408..ea10511 100644 --- a/src/py/bbctrl/LCD.py +++ b/src/py/bbctrl/LCD.py @@ -28,7 +28,7 @@ import lcd import atexit import logging -import tornado.ioloop +from tornado.ioloop import PeriodicCallback log = logging.getLogger('LCD') @@ -94,8 +94,7 @@ class 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) @@ -198,7 +197,7 @@ class LCD: 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 @@ -207,4 +206,4 @@ class LCD: 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) diff --git a/src/py/bbctrl/Mach.py b/src/py/bbctrl/Mach.py index 9e9c796..9e98074 100644 --- a/src/py/bbctrl/Mach.py +++ b/src/py/bbctrl/Mach.py @@ -28,8 +28,9 @@ 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') @@ -53,18 +54,30 @@ axis_homing_procedure = ''' ''' +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', '') @@ -73,29 +86,21 @@ class Mach(): 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): @@ -104,7 +109,7 @@ class Mach(): # 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 @@ -113,13 +118,20 @@ class Mach(): 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): @@ -140,23 +152,23 @@ class Mach(): 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): @@ -189,17 +201,17 @@ class Mach(): # 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): @@ -214,23 +226,23 @@ class Mach(): 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): @@ -239,14 +251,14 @@ class Mach(): 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): @@ -256,10 +268,10 @@ class Mach(): 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)) diff --git a/src/py/bbctrl/MainLCDPage.py b/src/py/bbctrl/MainLCDPage.py index 55fb1ca..1b14d53 100644 --- a/src/py/bbctrl/MainLCDPage.py +++ b/src/py/bbctrl/MainLCDPage.py @@ -53,6 +53,7 @@ class MainLCDPage(bbctrl.LCDPage): 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 diff --git a/src/py/bbctrl/Planner.py b/src/py/bbctrl/Planner.py index 368db4c..bfa102d 100644 --- a/src/py/bbctrl/Planner.py +++ b/src/py/bbctrl/Planner.py @@ -32,6 +32,7 @@ import logging 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') @@ -42,15 +43,14 @@ reLogLine = re.compile( 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() @@ -94,16 +94,18 @@ class Planner(): 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 @@ -130,29 +132,10 @@ class Planner(): 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) @@ -161,7 +144,7 @@ class Planner(): return value - def _log(self, line): + def _log_cb(self, line): line = line.strip() m = reLogLine.match(line) if not m: return @@ -181,10 +164,16 @@ class Planner(): 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'], @@ -194,13 +183,17 @@ class Planner(): 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) @@ -229,31 +222,33 @@ class Planner(): 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) diff --git a/src/py/bbctrl/Pwr.py b/src/py/bbctrl/Pwr.py index 3a6e132..bf40db5 100644 --- a/src/py/bbctrl/Pwr.py +++ b/src/py/bbctrl/Pwr.py @@ -26,6 +26,7 @@ ################################################################################ import logging +from tornado.ioloop import PeriodicCallback import bbctrl @@ -62,8 +63,9 @@ class Pwr(): 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] @@ -128,8 +130,12 @@ class Pwr(): 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) @@ -144,4 +150,4 @@ class Pwr(): if len(update): self.ctrl.state.update(update) - self.ctrl.ioloop.call_later(0.25, self._update) + self.failures = 0 diff --git a/src/py/bbctrl/Web.py b/src/py/bbctrl/Web.py index 29796a3..9f7c301 100644 --- a/src/py/bbctrl/Web.py +++ b/src/py/bbctrl/Web.py @@ -79,7 +79,9 @@ def check_password(password): 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): @@ -186,12 +188,14 @@ class FirmwareUpdateHandler(bbctrl.APIHandler): 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']) @@ -269,7 +273,7 @@ class ClientConnection(object): 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 @@ -278,16 +282,17 @@ class ClientConnection(object): 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) @@ -300,7 +305,6 @@ class WSConnection(ClientConnection, tornado.websocket.WebSocketHandler): tornado.websocket.WebSocketHandler.__init__( self, app, request, **kwargs) - def send(self, msg): self.write_message(msg) def open(self): self.on_open() diff --git a/src/py/bbctrl/__init__.py b/src/py/bbctrl/__init__.py index e060631..0298099 100644 --- a/src/py/bbctrl/__init__.py +++ b/src/py/bbctrl/__init__.py @@ -52,6 +52,7 @@ from bbctrl.Planner import Planner 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 diff --git a/src/py/inevent/JogHandler.py b/src/py/inevent/JogHandler.py index 93e6348..0b4ec09 100644 --- a/src/py/inevent/JogHandler.py +++ b/src/py/inevent/JogHandler.py @@ -31,6 +31,7 @@ from inevent.Constants import * log = logging.getLogger('inevent') +log.setLevel(logging.INFO) def axes_to_string(axes): diff --git a/src/stylus/style.styl b/src/stylus/style.styl index efc8456..97f74f1 100644 --- a/src/stylus/style.styl +++ b/src/stylus/style.styl @@ -195,16 +195,17 @@ body 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 @@ -218,6 +219,9 @@ body .absolute, .offset min-width 6em + tr:nth-child(1) th.actions + text-align right + .jog svg text -webkit-user-select none @@ -466,6 +470,9 @@ body line-height 0 min-height 200px + .mjpeg + line-height 10 + .reload float left margin-right -48px -- 2.27.0