- Fixed pausing fail near end of run bug.
authorJoseph Coffland <joseph@cauldrondevelopment.com>
Wed, 28 Feb 2018 13:58:57 +0000 (05:58 -0800)
committerJoseph Coffland <joseph@cauldrondevelopment.com>
Wed, 28 Feb 2018 13:58:57 +0000 (05:58 -0800)
 - 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.

26 files changed:
CHANGELOG.md
package.json
src/avr/src/exec.c
src/avr/src/jog.c
src/avr/src/line.c
src/avr/src/scurve.c
src/avr/src/scurve.h
src/avr/src/seek.c
src/avr/src/seek.h
src/avr/src/state.c
src/jade/index.jade
src/jade/templates/control-view.jade
src/js/app.js
src/js/control-view.js
src/py/bbctrl/Comm.py
src/py/bbctrl/CommandQueue.py [new file with mode: 0644]
src/py/bbctrl/Ctrl.py
src/py/bbctrl/LCD.py
src/py/bbctrl/Mach.py
src/py/bbctrl/MainLCDPage.py
src/py/bbctrl/Planner.py
src/py/bbctrl/Pwr.py
src/py/bbctrl/Web.py
src/py/bbctrl/__init__.py
src/py/inevent/JogHandler.py
src/stylus/style.styl

index ebd69e762591d6b0a1b5e5d25001286cbac83465..5daaefa4a27ae26dd3ccb92c3e40145cfe823787 100644 (file)
@@ -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.
 
index b55956cbd213296b6b27af80ef4f136317fdc4ef..c0187130812c546cdaa6da2965b31d674c80a40e 100644 (file)
@@ -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+",
index 886973452259dea0acec88279e48efc7ad63d760..68614cf4bb16be10dc9d6a7b71a262ef83365c8d 100644 (file)
@@ -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
index b52452b1bf2556f409f9ae3e3f6f4a107c585994..801dec8f21a7032169976508fbd61c6ddaf15f7d 100644 (file)
@@ -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;
 }
index c73fae08f355042204cd1dba477b47b9c63e595c..de219ed56c06f49f888965a139c43e2a46979db8 100644 (file)
@@ -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);
index ff7f2479cf80a6225b31adb9309b1395a69c339d..ddf973b185f45acded8f322bc555eb402a0222f5 100644 (file)
@@ -28,6 +28,7 @@
 #include "scurve.h"
 
 #include <math.h>
+#include <stdbool.h>
 
 
 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;
+  }
 }
index bb1c1964e93cc882fef0571c5c94dc2db725896d..0f3c5ab37ee4a4560937d7e6142daec1537981f0 100644 (file)
@@ -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);
index a6e7d6b2e7ffa3674b756f284f80ff5d3cd5daec..ce3d71bbfe0bbf9868bff9001e9157da1e757e90 100644 (file)
@@ -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]);
index 6c6fa53f52d378960f6e0f08e6c1f3faac5015d0..baa133c780e05e02f036d92e4ff77db35b94e3a3 100644 (file)
@@ -35,3 +35,4 @@
 switch_id_t seek_get_switch();
 bool seek_switch_found();
 void seek_end();
+void seek_cancel();
index f0296acbc21e80c779947eafc1f9a9d7b0961df0..ebdeae54d2d24f74b9d9c0ebaae737469d3c0143 100644 (file)
@@ -34,6 +34,7 @@
 #include "outputs.h"
 #include "jog.h"
 #include "estop.h"
+#include "seek.h"
 #include "report.h"
 
 #include <stdio.h>
@@ -140,6 +141,7 @@ static void _stop() {
     s.flush_requested = true;
     spindle_stop();
     outputs_stop();
+    seek_cancel();
     _set_state(STATE_READY);
     break;
 
index 793936c53bbabd3d7018d7e733707aaa3e2efdf5..96d549dc0a25448673dfaf627594b8e764ce3961 100644 (file)
@@ -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")
index 277a738a918ea21a0d98352a3a70968c7a90efd3..504eb60dd75955e5f0cfcd841894c654fab7ebfd 100644 (file)
 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()") &empty;
+
+          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}')") &empty;
+              @click="zero('#{axis}')") &empty;
 
-            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.
index f0404da2f0b0cfc6da65127292ccec7f2acd6032..704520907a36ce8d0b29ec74cb738274ccfe35a8 100644 (file)
@@ -289,6 +289,7 @@ module.exports = new Vue({
         }
 
         update_object(this.state, e.data, false);
+        this.$broadcast('update');
 
       }.bind(this)
 
index dbd75cce4a4847de67af14ec1dd673dfcbdd96f3..2d8c00bd5a9d042125eef2b084db0cf983f05d6d 100644 (file)
@@ -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 () {
index 1c611113863b037ea13564fa57ba3f1bca445807..7ec97470251da4554926bafdb04bc65282c03b4f 100644 (file)
@@ -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 (file)
index 0000000..dd37dd4
--- /dev/null
@@ -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 <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()
index f95dab29a1fa3576f8660ea0ae8f81ed2853e7d4..c1366cf4edcdf1eb9d8e891e71f96e176b02be12 100644 (file)
@@ -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))
index 4cbd408651fbce13710d140b3c5a991f0194e0a8..ea10511a6ad7410613cc48a5c9e7344c8ad381ef 100644 (file)
@@ -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)
index 9e9c7965decfccda939c490798395a4a244029a8..9e9807445253e1338e60a1dbb337dc14934cdd57 100644 (file)
@@ -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))
index 55fb1ca004e6d1a8d8d505a90b3660c6de9177f3..1b14d5345742c52369744f83f13f6b38fa8b99f4 100644 (file)
@@ -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
 
index 368db4c49e23dfdc708973e4a85ed3228725b409..bfa102d88242d2798bbb1d0d1d5572610d2ae47d 100644 (file)
@@ -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)
index 3a6e1320c27be0b69e46f5f9fef5fef97f080aef..bf40db5fd1f8e9c77c09a108b50985cc63fba6a6 100644 (file)
@@ -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
index 29796a3f38b827abe6253e89d161e7f3f9f2f8b1..9f7c301197c698a90689e596ea275269b9474902 100644 (file)
@@ -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()
 
index e060631b36f930dbc05f6b575621a1dff63cee0e..0298099e8a2a16e3b6eb9069868f480927a4f153 100644 (file)
@@ -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
index 93e63483e865f60cb7a3b8c06b064f72d01086b9..0b4ec095241203ae600b320fcde3e160ca9ebaf7 100644 (file)
@@ -31,6 +31,7 @@ from inevent.Constants import *
 
 
 log = logging.getLogger('inevent')
+log.setLevel(logging.INFO)
 
 
 def axes_to_string(axes):
index efc8456a6b47a826cccdc4834412fb0bcc05bc70..97f74f1ad1ccb86376c7dcacebe13be37bbf2cb5 100644 (file)
@@ -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