From: Joseph Coffland Date: Sun, 9 Jul 2017 00:47:03 +0000 (-0700) Subject: Fixed work offsets, improved offset/homing user interface, run homing procedure,... X-Git-Url: https://git.buildbotics.com/?a=commitdiff_plain;h=7e0edb61762ca85e25615d252649b625247007e0;p=bbctrl-firmware Fixed work offsets, improved offset/homing user interface, run homing procedure, added manual homing --- diff --git a/avr/src/axis.c b/avr/src/axis.c index 2ad4511..5a48b78 100644 --- a/avr/src/axis.c +++ b/avr/src/axis.c @@ -119,7 +119,7 @@ float axis_get_vector_length(const float a[], const float b[]) { AXIS_GET(velocity_max, float, 0) AXIS_GET(homed, bool, false) AXIS_SET(homed, bool) -AXIS_GET(homing_mode, homing_mode_t, HOMING_DISABLED) +AXIS_GET(homing_mode, homing_mode_t, HOMING_MANUAL) AXIS_SET(homing_mode, homing_mode_t) AXIS_GET(radius, float, 0) AXIS_GET(travel_min, float, 0) @@ -160,7 +160,7 @@ AXIS_VAR_SET(jerk_max, float) float get_homing_dir(int axis) { switch (axes[axis].homing_mode) { - case HOMING_DISABLED: break; + case HOMING_MANUAL: break; case HOMING_STALL_MIN: case HOMING_SWITCH_MIN: return -1; case HOMING_STALL_MAX: case HOMING_SWITCH_MAX: return 1; } @@ -170,7 +170,7 @@ float get_homing_dir(int axis) { float get_home(int axis) { switch (axes[axis].homing_mode) { - case HOMING_DISABLED: break; + case HOMING_MANUAL: break; case HOMING_STALL_MIN: case HOMING_SWITCH_MIN: return get_travel_min(axis); case HOMING_STALL_MAX: case HOMING_SWITCH_MAX: return get_travel_max(axis); } diff --git a/avr/src/axis.h b/avr/src/axis.h index d577398..eede58f 100644 --- a/avr/src/axis.h +++ b/avr/src/axis.h @@ -41,7 +41,7 @@ enum { typedef enum { - HOMING_DISABLED, + HOMING_MANUAL, HOMING_STALL_MIN, HOMING_STALL_MAX, HOMING_SWITCH_MIN, diff --git a/avr/src/command.c b/avr/src/command.c index 89e12e2..3939751 100644 --- a/avr/src/command.c +++ b/avr/src/command.c @@ -52,20 +52,6 @@ static char *_cmd = 0; -static void _reboot() {hw_request_hard_reset();} - - -static unsigned _parse_axis(uint8_t axis) { - switch (axis) { - case 'x': return 0; case 'y': return 1; case 'z': return 2; - case 'a': return 3; case 'b': return 4; case 'c': return 5; - case 'X': return 0; case 'Y': return 1; case 'Z': return 2; - case 'A': return 3; case 'B': return 4; case 'C': return 5; - default: return axis; - } -} - - static void command_i2c_cb(i2c_cmd_t cmd, uint8_t *data, uint8_t length) { switch (cmd) { case I2C_NULL: break; @@ -77,11 +63,7 @@ static void command_i2c_cb(i2c_cmd_t cmd, uint8_t *data, uint8_t length) { case I2C_STEP: mp_request_step(); break; case I2C_FLUSH: mp_request_flush(); break; case I2C_REPORT: report_request_full(); break; - case I2C_REBOOT: _reboot(); break; - case I2C_ZERO: - if (length == 0) mach_zero_all(); - else if (length == 1) mach_zero_axis(_parse_axis(*data)); - break; + case I2C_REBOOT: hw_request_hard_reset(); break; } } @@ -332,7 +314,7 @@ uint8_t command_report(int argc, char *argv[]) { uint8_t command_reboot(int argc, char *argv[]) { - _reboot(); + hw_request_hard_reset(); return 0; } diff --git a/avr/src/i2c.h b/avr/src/i2c.h index d8f4ef0..766396d 100644 --- a/avr/src/i2c.h +++ b/avr/src/i2c.h @@ -44,7 +44,6 @@ typedef enum { I2C_FLUSH, I2C_REPORT, I2C_REBOOT, - I2C_ZERO, } i2c_cmd_t; diff --git a/avr/src/machine.c b/avr/src/machine.c index f15fb35..73fde29 100644 --- a/avr/src/machine.c +++ b/avr/src/machine.c @@ -122,6 +122,7 @@ coord_system_t mach_get_coord_system() {return mach.gm.coord_system;} bool mach_get_absolute_mode() {return mach.gm.absolute_mode;} path_mode_t mach_get_path_mode() {return mach.gm.path_mode;} bool mach_is_exact_stop() {return mach.gm.path_mode == PATH_EXACT_STOP;} +bool mach_in_absolute_mode() {return mach.gm.distance_mode == ABSOLUTE_MODE;} distance_mode_t mach_get_distance_mode() {return mach.gm.distance_mode;} distance_mode_t mach_get_arc_distance_mode() {return mach.gm.arc_distance_mode;} @@ -316,13 +317,13 @@ void mach_set_position_from_runtime() { * Axes that need processing are signaled in @param flags. */ void mach_calc_target(float target[], const float values[], - const bool flags[]) { + const bool flags[], bool absolute) { for (int axis = 0; axis < AXES; axis++) { target[axis] = mach.position[axis]; if (!flags[axis] || !axis_is_enabled(axis)) continue; - target[axis] = mach.gm.distance_mode == ABSOLUTE_MODE ? - mach_get_active_coord_offset(axis) : mach.position[axis]; + target[axis] = absolute ? mach_get_active_coord_offset(axis) : + mach.position[axis]; float radius = axis_get_radius(axis); if (radius) // Handle radius mode if radius is non-zero @@ -435,33 +436,16 @@ void mach_set_coord_system(coord_system_t cs) { } -stat_t mach_zero_all() { - for (unsigned axis = 0; axis < AXES; axis++) { - stat_t status = mach_zero_axis(axis); - if (status != STAT_OK) return status; - } - - return STAT_OK; -} - - -stat_t mach_zero_axis(unsigned axis) { - if (!mp_is_quiescent()) return STAT_MACH_NOT_QUIESCENT; - if (AXES <= axis) return STAT_INVALID_AXIS; - - mach_set_axis_position(axis, 0); - - return STAT_OK; -} - - // G28.3 functions and support static stat_t _exec_home(mp_buffer_t *bf) { - const float *origin = bf->target; + const float *target = bf->target; const float *flags = bf->unit; for (int axis = 0; axis < AXES; axis++) - if (flags[axis]) mp_runtime_set_axis_position(axis, origin[axis]); + if (flags[axis]) { + mp_runtime_set_axis_position(axis, target[axis]); + axis_set_homed(axis, true); + } mp_runtime_set_steps_from_position(); @@ -483,16 +467,17 @@ static stat_t _exec_home(mp_buffer_t *bf) { void mach_set_home(float origin[], bool flags[]) { mp_buffer_t *bf = mp_queue_get_tail(); + // Compute target position + mach_calc_target(bf->target, origin, flags, true); + for (int axis = 0; axis < AXES; axis++) if (flags[axis] && isfinite(origin[axis])) { - // TODO What about work offsets? - mach.position[axis] = TO_MM(origin[axis]); // set model position - mp_set_axis_position(axis, mach.position[axis]); // set mm position - axis_set_homed(axis, true); + bf->target[axis] -= mach_get_active_coord_offset(axis); + mach.position[axis] = bf->target[axis]; + mp_set_axis_position(axis, bf->target[axis]); // set mm position + bf->unit[axis] = true; - bf->target[axis] = origin[axis]; - bf->unit[axis] = flags[axis]; - } + } else bf->unit[axis] = false; // Synchronized update of runtime position mp_queue_push_nonstop(_exec_home, mach_get_line()); @@ -517,6 +502,8 @@ void mach_set_origin_offsets(float offset[], bool flags[]) { if (flags[axis]) mach.origin_offset[axis] = mach.position[axis] - mach.offset[mach.gm.coord_system][axis] - TO_MM(offset[axis]); + + mach_update_work_offsets(); // update resolved offsets } @@ -526,15 +513,23 @@ void mach_reset_origin_offsets() { for (int axis = 0; axis < AXES; axis++) mach.origin_offset[axis] = 0; + + mach_update_work_offsets(); // update resolved offsets } /// G92.2 -void mach_suspend_origin_offsets() {mach.origin_offset_enable = false;} +void mach_suspend_origin_offsets() { + mach.origin_offset_enable = false; + mach_update_work_offsets(); // update resolved offsets +} /// G92.3 -void mach_resume_origin_offsets() {mach.origin_offset_enable = true;} +void mach_resume_origin_offsets() { + mach.origin_offset_enable = true; + mach_update_work_offsets(); // update resolved offsets +} stat_t mach_plan_line(float target[], switch_id_t sw) { @@ -582,7 +577,7 @@ static stat_t _feed(float values[], bool flags[], switch_id_t sw) { // Compute target position float target[AXES]; - mach_calc_target(target, values, flags); + mach_calc_target(target, values, flags, mach_in_absolute_mode()); // test soft limits stat_t status = mach_test_soft_limits(target); @@ -657,7 +652,7 @@ stat_t mach_seek(float target[], bool flags[], motion_mode_t mode) { switch_id_t sw = SW_PROBE; for (int axis = 0; axis < AXES; axis++) - if (flags[axis]) { + if (flags[axis] && isfinite(target[axis])) { // Convert to incremental move if (mach.gm.distance_mode == ABSOLUTE_MODE) target[axis] += mach.position[axis]; diff --git a/avr/src/machine.h b/avr/src/machine.h index 271959f..51e0480 100644 --- a/avr/src/machine.h +++ b/avr/src/machine.h @@ -52,6 +52,7 @@ coord_system_t mach_get_coord_system(); bool mach_get_absolute_mode(); path_mode_t mach_get_path_mode(); bool mach_is_exact_stop(); +bool mach_in_absolute_mode(); distance_mode_t mach_get_distance_mode(); distance_mode_t mach_get_arc_distance_mode(); @@ -71,7 +72,8 @@ void mach_set_axis_position(unsigned axis, float position); void mach_set_position_from_runtime(); // Critical helpers -void mach_calc_target(float target[], const float values[], const bool flags[]); +void mach_calc_target(float target[], const float values[], const bool flags[], + bool absolute); stat_t mach_test_soft_limits(float target[]); // machining functions defined by NIST [organized by NIST Gcode doc] @@ -91,9 +93,6 @@ void mach_set_coord_system(coord_system_t coord_system); void mach_set_home(float origin[], bool flags[]); void mach_clear_home(bool flags[]); -stat_t mach_zero_all(); -stat_t mach_zero_axis(unsigned axis); - void mach_set_origin_offsets(float offset[], bool flags[]); void mach_reset_origin_offsets(); void mach_suspend_origin_offsets(); diff --git a/avr/src/plan/arc.c b/avr/src/plan/arc.c index 25ad833..2cb1fcc 100644 --- a/avr/src/plan/arc.c +++ b/avr/src/plan/arc.c @@ -435,7 +435,7 @@ stat_t mach_arc_feed(float values[], bool values_f[], // arc endpoints // Set model target const float *position = mach_get_position(); - mach_calc_target(arc.target, values, values_f); + mach_calc_target(arc.target, values, values_f, mach_in_absolute_mode()); // in radius mode it's an error for start == end if (radius_f && fp_EQ(position[AXIS_X], arc.target[AXIS_X]) && diff --git a/avr/src/plan/buffer.c b/avr/src/plan/buffer.c index 9943d21..6a1ad0a 100644 --- a/avr/src/plan/buffer.c +++ b/avr/src/plan/buffer.c @@ -216,6 +216,8 @@ void mp_buffer_print(const mp_buffer_t *bf) { void mp_buffer_validate(const mp_buffer_t *bp) { ASSERT(bp); + if (!(bp->flags & BUFFER_LINE)) return; // Only check line buffers + ASSERT(isfinite(bp->value)); ASSERT(isfinite(bp->target[0]) && isfinite(bp->target[1]) && diff --git a/avr/src/plan/buffer.h b/avr/src/plan/buffer.h index 96efd16..de1d4d5 100644 --- a/avr/src/plan/buffer.h +++ b/avr/src/plan/buffer.h @@ -51,6 +51,7 @@ typedef enum { BUFFER_RAPID = 1 << 5, BUFFER_INVERSE_TIME = 1 << 6, BUFFER_EXACT_STOP = 1 << 7, + BUFFER_LINE = 1 << 8, } buffer_flags_t; diff --git a/avr/src/plan/jog.c b/avr/src/plan/jog.c index 346cc92..92ff6fb 100644 --- a/avr/src/plan/jog.c +++ b/avr/src/plan/jog.c @@ -44,101 +44,116 @@ #include +typedef struct { + float delta; + float t; + bool changed; + + int sign; + float velocity; + float next; + float initial; + float target; +} jog_axis_t; + + typedef struct { bool writing; + bool done; + float Vi; float Vt; - float velocity_delta[AXES]; - float velocity_t[AXES]; - int sign[AXES]; - float velocity[AXES]; - float next_velocity[AXES]; - float initial_velocity[AXES]; - float target_velocity[AXES]; + jog_axis_t axes[AXES]; } jog_runtime_t; static jog_runtime_t jr; -static stat_t _exec_jog(mp_buffer_t *bf) { - // Load next velocity - bool changed = false; - bool done = true; - if (!jr.writing) - for (int axis = 0; axis < AXES; axis++) { - if (!axis_is_enabled(axis)) continue; +static bool _next_axis_velocity(int axis) { + jog_axis_t *a = &jr.axes[axis]; - float Vn = jr.next_velocity[axis] * axis_get_velocity_max(axis); - float Vi = jr.velocity[axis]; - float Vt = jr.target_velocity[axis]; + float Vn = a->next * axis_get_velocity_max(axis); + float Vi = a->velocity; + float Vt = a->target; - if (JOG_MIN_VELOCITY < fabs(Vn)) done = false; + if (JOG_MIN_VELOCITY < fabs(Vn)) jr.done = false; - if (!fp_ZERO(Vi) && (Vn < 0) != (Vi < 0)) - Vn = 0; // Plan to zero on sign change + if (!fp_ZERO(Vi) && (Vn < 0) != (Vi < 0)) + Vn = 0; // Plan to zero on sign change - if (fabs(Vn) < JOG_MIN_VELOCITY) Vn = 0; + if (fabs(Vn) < JOG_MIN_VELOCITY) Vn = 0; - if (Vt != Vn) { - jr.target_velocity[axis] = Vn; - if (Vn) jr.sign[axis] = Vn < 0 ? -1 : 1; - changed = true; - } - } + if (Vt == Vn) return false; // No change - float velocity_sqr = 0; + a->target = Vn; + if (Vn) a->sign = Vn < 0 ? -1 : 1; - // Compute per axis velocities - for (int axis = 0; axis < AXES; axis++) { - if (!axis_is_enabled(axis)) continue; + return true; +} - float V = fabs(jr.velocity[axis]); - float Vt = fabs(jr.target_velocity[axis]); - if (changed) { - if (fp_EQ(V, Vt)) { - V = Vt; - jr.velocity_t[axis] = 1; +static float _compute_axis_velocity(int axis) { + jog_axis_t *a = &jr.axes[axis]; - } else { - // Compute axis max jerk - float jerk = axis_get_jerk_max(axis) * JERK_MULTIPLIER; + float V = fabs(a->velocity); + float Vt = fabs(a->target); - // Compute length to velocity given max jerk - float length = mp_get_target_length(V, Vt, jerk * JOG_JERK_MULT); + if (JOG_MIN_VELOCITY < Vt) jr.done = false; - // Compute move time - float move_time = 2 * length / (V + Vt); + if (fp_EQ(V, Vt)) return Vt; - if (move_time < SEGMENT_TIME) { - V = Vt; - jr.velocity_t[axis] = 1; + if (a->changed) { + // Compute axis max jerk + float jerk = axis_get_jerk_max(axis) * JERK_MULTIPLIER; - } else { - jr.initial_velocity[axis] = V; - jr.velocity_t[axis] = jr.velocity_delta[axis] = - SEGMENT_TIME / move_time; - } - } - } + // Compute length to velocity given max jerk + float length = mp_get_target_length(V, Vt, jerk * JOG_JERK_MULT); - if (jr.velocity_t[axis] < 1) { - // Compute quintic Bezier curve - V = velocity_curve(jr.initial_velocity[axis], Vt, jr.velocity_t[axis]); - jr.velocity_t[axis] += jr.velocity_delta[axis]; + // Compute move time + float move_time = 2 * length / (V + Vt); - } else V = Vt; + if (move_time <= SEGMENT_TIME) return Vt; - if (JOG_MIN_VELOCITY < V || JOG_MIN_VELOCITY < Vt) done = false; + a->initial = V; + a->t = a->delta = SEGMENT_TIME / move_time; + } + if (a->t <= 0) return V; + if (1 <= a->t) return Vt; + + // Compute quintic Bezier curve + V = velocity_curve(a->initial, Vt, a->t); + a->t += a->delta; + + return V; +} + + +static stat_t _exec_jog(mp_buffer_t *bf) { + // Load next velocity + jr.done = true; + + if (!jr.writing) + for (int axis = 0; axis < AXES; axis++) { + if (!axis_is_enabled(axis)) continue; + jr.axes[axis].changed = _next_axis_velocity(axis); + } + + float velocity_sqr = 0; + + // Compute per axis velocities + for (int axis = 0; axis < AXES; axis++) { + if (!axis_is_enabled(axis)) continue; + float V = _compute_axis_velocity(axis); velocity_sqr += square(V); - jr.velocity[axis] = V * jr.sign[axis]; + jr.axes[axis].velocity = V * jr.axes[axis].sign; + if (JOG_MIN_VELOCITY < V) jr.done = false; } // Check if we are done - if (done) { + if (jr.done) { // Update machine position mach_set_position_from_runtime(); mp_set_cycle(CYCLE_MACHINING); // Default cycle @@ -151,7 +166,7 @@ static stat_t _exec_jog(mp_buffer_t *bf) { float target[AXES]; for (int axis = 0; axis < AXES; axis++) target[axis] = mp_runtime_get_axis_position(axis) + - jr.velocity[axis] * SEGMENT_TIME; + jr.axes[axis].velocity * SEGMENT_TIME; // Set velocity and target mp_runtime_set_velocity(sqrt(velocity_sqr)); @@ -178,7 +193,7 @@ uint8_t command_jog(int argc, char *argv[]) { jr.writing = true; for (int axis = 0; axis < AXES; axis++) - jr.next_velocity[axis] = velocity[axis]; + jr.axes[axis].next = velocity[axis]; jr.writing = false; if (mp_get_cycle() != CYCLE_JOGGING) { diff --git a/avr/src/plan/line.c b/avr/src/plan/line.c index e83b583..2f080b0 100644 --- a/avr/src/plan/line.c +++ b/avr/src/plan/line.c @@ -35,6 +35,7 @@ #include "buffer.h" #include +#include /* Sonny's algorithm - simple @@ -114,7 +115,7 @@ static float _get_junction_vmax(const float a_unit[], const float b_unit[]) { for (int axis = 0; axis < AXES; axis++) costheta -= a_unit[axis] * b_unit[axis]; - if (costheta < -0.99) return 10000000; // straight line cases + if (costheta < -0.99) return FLT_MAX; // straight line cases if (0.99 < costheta) return 0; // reversal cases // Fuse the junction deviations into a vector sum @@ -244,8 +245,15 @@ static void _calc_max_velocities(mp_buffer_t *bf, float move_time, bf->cruise_vmax = bf->length / move_time; // target velocity requested - float junction_velocity = - _get_junction_vmax(mp_buffer_prev(bf)->unit, bf->unit); + float junction_velocity = FLT_MAX; + + mp_buffer_t *prev = mp_buffer_prev(bf); + while (prev->state != BUFFER_OFF) + if (prev->flags & BUFFER_LINE) { + _get_junction_vmax(prev->unit, bf->unit); + break; + + } else prev = mp_buffer_prev(prev); bf->entry_vmax = min(bf->cruise_vmax, junction_velocity); bf->delta_vmax = mp_get_target_velocity(0, bf->length, bf); @@ -416,6 +424,7 @@ stat_t mp_aline(const float target[], buffer_flags_t flags, switch_id_t sw, // Note, the following lines must remain in order. bf->line = line; // Planner needs this when planning steps + flags |= BUFFER_LINE; bf->flags = flags; // Move flags bf->sw = sw; // Seek switche mp_plan(bf); // Plan block list diff --git a/avr/src/plan/planner.c b/avr/src/plan/planner.c index 55e51f4..6be174e 100644 --- a/avr/src/plan/planner.c +++ b/avr/src/plan/planner.c @@ -605,7 +605,6 @@ void mp_queue_push_nonstop(buffer_cb_t cb, uint32_t line) { mp_buffer_t *bp = mp_queue_get_tail(); bp->entry_vmax = bp->cruise_vmax = bp->exit_vmax = INFINITY; - copy_vector(bp->unit, bp->prev->unit); bp->flags |= BUFFER_REPLANNABLE; mp_queue_push(cb, line); diff --git a/avr/src/varcb.c b/avr/src/varcb.c index 283b156..3650fb5 100644 --- a/avr/src/varcb.c +++ b/avr/src/varcb.c @@ -34,14 +34,17 @@ #include "plan/state.h" // Axis -float get_position(int axis) {return mp_runtime_get_work_position(axis);} +float get_axis_mach_coord(int axis) {return mp_runtime_get_axis_position(axis);} -void set_position(int axis, float position) { +void set_axis_mach_coord(int axis, float position) { mach_set_axis_position(axis, position); } +float get_axis_work_coord(int axis) {return mp_runtime_get_work_position(axis);} + + // GCode getters int32_t get_line() {return mp_runtime_get_line();} PGM_P get_unit() {return gs_get_units_pgmstr(mach_get_units());} diff --git a/avr/src/vars.def b/avr/src/vars.def index dee8446..70fb49a 100644 --- a/avr/src/vars.def +++ b/avr/src/vars.def @@ -68,14 +68,16 @@ VAR(probe_switch, pw, bool, 0, 0, 1, "Probe switch state") // Homing VAR(homing_mode, ho, uint8_t, MOTORS, 1, 1, "Homing type") VAR(homing_dir, hd, float, MOTORS, 0, 1, "Homing direction") -VAR(home, h, float, MOTORS, 0, 1, "Home position") +VAR(home, hp, float, MOTORS, 0, 1, "Home position") +VAR(homed, h, bool, MOTORS, 0, 1, "True if axis is homed") VAR(search_velocity,sv, float, MOTORS, 1, 1, "Homing search velocity") VAR(latch_velocity, lv, float, MOTORS, 1, 1, "Homing latch velocity") VAR(latch_backoff, lb, float, MOTORS, 1, 1, "Homing latch backoff") VAR(zero_backoff, zb, float, MOTORS, 1, 1, "Homing zero backoff") // Axis -VAR(position, p, float, AXES, 1, 1, "Current axis position") +VAR(axis_mach_coord, p, float, AXES, 1, 1, "Axis machine coordinate") +VAR(axis_work_coord, w, float, AXES, 0, 1, "Axis work coordinate") // Spindle VAR(spindle_type, st, uint8_t, 0, 1, 1, "PWM=0 or HUANYANG=1") diff --git a/package.json b/package.json index 284f3a8..bbaf7fe 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "bbctrl", - "version": "0.1.11", + "version": "0.1.12", "homepage": "https://github.com/buildbotics/rpi-firmware", "license": "GPL 3+", diff --git a/scripts/install.sh b/scripts/install.sh index cdc912c..8761ae9 100755 --- a/scripts/install.sh +++ b/scripts/install.sh @@ -24,6 +24,6 @@ fi if $UPDATE_PY; then rm -rf /usr/local/lib/python*/dist-packages/bbctrl-* - ./setup.py install + ./setup.py install --force service bbctrl start fi diff --git a/setup.py b/setup.py index 8a8ca35..97a104e 100755 --- a/setup.py +++ b/setup.py @@ -24,7 +24,11 @@ setup( 'bbctrl = bbctrl:run' ] }, - scripts = ['scripts/update-bbctrl', 'scripts/upgrade-bbctrl'], + scripts = [ + 'scripts/update-bbctrl', + 'scripts/upgrade-bbctrl', + 'scripts/sethostname', + ], install_requires = 'tornado sockjs-tornado pyserial pyudev smbus2'.split(), zip_safe = False, ) diff --git a/src/jade/templates/admin-view.jade b/src/jade/templates/admin-view.jade index 5256b27..fee2ce4 100644 --- a/src/jade/templates/admin-view.jade +++ b/src/jade/templates/admin-view.jade @@ -1,5 +1,31 @@ script#admin-view-template(type="text/x-template") #admin + h2 Hostname + .pure-form.pure-form-aligned + .pure-control-group + label(for="hostname") Hostname + input(name="hostname", v-model="hostname", @keyup.enter="set_hostname") + button.pure-button.pure-button-primary(@click="set_hostname") Set + + h2 Remote SSH User + .pure-form.pure-form-aligned + .pure-control-group + label(for="username") Username + input(name="username", v-model="username", @keyup.enter="set_username") + button.pure-button.pure-button-primary(@click="set_username") Set + + .pure-form.pure-form-aligned + .pure-control-group + label(for="current") Current Password + input(name="current", v-model="current", type="password") + .pure-control-group + label(for="pass1") New Password + input(name="pass1", v-model="password", type="password") + .pure-control-group + label(for="pass2") New Password + input(name="pass2", v-model="password2", type="password") + button.pure-button.pure-button-primary(@click="set_password") Set + h2 Configuration button.pure-button.pure-button-primary(@click="backup") Backup @@ -40,3 +66,17 @@ script#admin-view-template(type="text/x-template") message(:show.sync="firmwareUpgrading") h3(slot="header") Firmware upgrading p(slot="body") Please wait. . . + + message(:show.sync="hostnameSet") + h3(slot="header") Hostname Set + p(slot="body") + | Hostname was successfuly set to {{hostname}}. + | Poweroff and restart the controller for this change to take effect. + + message(:show.sync="passwordSet") + h3(slot="header") Password Set + p(slot="body") + + message(:show.sync="usernameSet") + h3(slot="header") Username Set + p(slot="body") diff --git a/src/jade/templates/control-view.jade b/src/jade/templates/control-view.jade index d814a0a..f7faa88 100644 --- a/src/jade/templates/control-view.jade +++ b/src/jade/templates/control-view.jade @@ -4,27 +4,73 @@ script#control-view-template(type="text/x-template") tr th.name Axis th.position Position + th.absolute Absolute th.offset Offset - th.errors Errors th.actions Actions each axis in 'xyzabc' - tr.axis(class="axis-#{axis}", v-if="enabled('#{axis}')") + tr.axis(:class="{'homed': is_homed('#{axis}'), 'axis-#{axis}': true}", + v-if="enabled('#{axis}')") th.name #{axis} - td.position {{state.#{axis}p || 0 | fixed 3}} - td.offset {{state.#{axis}o || 0 | fixed 3}} - td.errors - .fa.fa-hot(v-if="state.#{axis}t", title="Driver overtemp.") - .fa.fa-ban(v-if="state.#{axis}s", title="Motor stalled.") + td.position {{state.#{axis}w || 0 | fixed 3}} + td.absolute {{state.#{axis}p || 0 | fixed 3}} + td.offset {{(state.#{axis}w - state.#{axis}p) || 0 | fixed 3}} th.actions - button.pure-button(title="Zero {{'#{axis}' | upper}} axis.", - @click="zero('#{axis}')") + button.pure-button( + title="Set {{'#{axis}' | upper}} axis position.", + @click="show_set_position('#{axis}')") + .fa.fa-cog + + button.pure-button( + title="Zero {{'#{axis}' | upper}} axis offset.", + @click="set_position('#{axis}', state['#{axis}p'])") | ∅ + button.pure-button(title="Home {{'#{axis}' | upper}} axis.", @click="home('#{axis}')") .fa.fa-home - table.info + message(:show.sync="position_msg['#{axis}']") + h3(slot="header") Set {{'#{axis}' | upper}} axis position + + div(slot="body") + .pure-form + .pure-control-group + label Position + input(v-model="axis_position", + @keyup.enter="set_position('#{axis}', axis_position)") + p + + div(slot="footer") + button.pure-button(@click="position_msg['#{axis}'] = false") + | Cancel + + button.pure-button.button-success( + @click="set_position('#{axis}', axis_position)") Set + + message(:show.sync="manual_home['#{axis}']") + h3(slot="header") Manually home {{'#{axis}' | upper}} axis + + div(slot="body") + p Set axis absolute position. + + .pure-form + .pure-control-group + label Absolute + input(v-model="axis_position", + @keyup.enter="set_home('#{axis}', axis_position)") + + p + + div(slot="footer") + button.pure-button(@click="manual_home['#{axis}'] = false") + | Cancel + + button.pure-button.button-success( + title="Home {{'#{axis}' | upper}} axis.", + @click="set_home('#{axis}', axis_position)") Set + + table.info tr th State td {{get_state()}} diff --git a/src/jade/templates/message.jade b/src/jade/templates/message.jade index 98b16e3..fdc3d13 100644 --- a/src/jade/templates/message.jade +++ b/src/jade/templates/message.jade @@ -4,8 +4,10 @@ script#message-template(type="text/x-template") .modal-container .modal-header slot(name="header") default header + .modal-body slot(name="body") default body + .modal-footer slot(name="footer") button.pure-button.button-success(@click="show = false") OK diff --git a/src/js/admin-view.js b/src/js/admin-view.js index 9de8c79..b85c8fa 100644 --- a/src/js/admin-view.js +++ b/src/js/admin-view.js @@ -15,7 +15,15 @@ module.exports = { confirmReset: false, configReset: false, firmwareUpgrading: false, + hostnameSet: false, + usernameSet: false, + passwordSet: false, latest: '', + hostname: '', + username: '', + current: '', + password: '', + password2: '' } }, @@ -27,10 +35,57 @@ module.exports = { }, - ready: function () {}, + ready: function () { + api.get('hostname').done(function (hostname) { + this.hostname = hostname; + }.bind(this)); + api.get('remote/username').done(function (username) { + this.username = username; + }.bind(this)); + }, methods: { + set_hostname: function () { + api.put('hostname', {hostname: this.hostname}).done(function () { + this.hostnameSet = true; + }.bind(this)).fail(function (error) { + alert('Set hostname failed: ' + JSON.stringify(error)); + }) + }, + + + set_username: function () { + api.put('remote/username', {username: this.username}).done(function () { + this.usernameSet = true; + }.bind(this)).fail(function (error) { + alert('Set username failed: ' + JSON.stringify(error)); + }) + }, + + + set_password: function () { + if (this.password != this.password2) { + alert('Passwords to not match'); + return; + } + + if (this.password.length < 6) { + alert('Password too short'); + return; + } + + api.put('remote/password', { + current: this.current, + password: this.password + }).done(function () { + this.passwordSet = true; + }.bind(this)).fail(function (error) { + alert('Set password failed: ' + JSON.stringify(error)); + }) + }, + + backup: function () { document.getElementById('download-target').src = '/api/config/download'; }, diff --git a/src/js/control-view.js b/src/js/control-view.js index b182a0b..2ac67ec 100644 --- a/src/js/control-view.js +++ b/src/js/control-view.js @@ -36,7 +36,11 @@ module.exports = { history: [], console: [], speed_override: 1, - feed_override: 1 + feed_override: 1, + manual_home: {x: false, y: false, z: false, a: false, b: false, c: false}, + position_msg: + {x: false, y: false, z: false, a: false, b: false, c: false}, + axis_position: 0 } }, @@ -94,16 +98,33 @@ module.exports = { }, - enabled: function (axis) { + get_axis_motor_id: function (axis) { var axis = axis.toLowerCase(); for (var i = 0; i < this.config.motors.length; i++) { var motor = this.config.motors[i]; - if (motor.axis.toLowerCase() == axis && - (motor.enabled || typeof motor.enabled == 'undefined')) return true; + if (motor.axis.toLowerCase() == axis) return i; } - return false; + return -1; + }, + + + get_axis_motor: function (axis) { + var motor = this.get_axis_motor_id(axis); + if (motor != -1) return this.config.motors[motor]; + }, + + + enabled: function (axis) { + var motor = this.get_axis_motor(axis); + return typeof motor != 'undefined' && motor['power-mode'] != 'disabled'; + }, + + + is_homed: function (axis) { + var motor = this.get_axis_motor_id(axis); + return motor != -1 && this.state[motor + 'h']; }, @@ -199,11 +220,36 @@ module.exports = { }, - home: function () {api.put('home')}, + home: function (axis) { + var motor = this.get_axis_motor(axis); + if (motor['homing-mode'] == 'manual') { + this.axis_position = this.state[axis + 'w']; + this.manual_home[axis] = true; + } else api.put('home' + (typeof axis == 'undefined' ? '' : ('/' + axis))); + }, - zero: function (axis) { - api.put('zero' + (typeof axis == 'undefined' ? '' : '/' + axis)); + + set_home: function (axis, position) { + this.manual_home[axis] = false; + api.put('home/' + axis + '/set', {position: parseFloat(position)}); + }, + + + show_set_position: function (axis) { + this.axis_position = 0; + this.position_msg[axis] = true; + }, + + + get_offset: function (axis) { + return this.state[axis + 'w'] - this.state[axis + 'p']; + }, + + + set_position: function (axis, position) { + this.position_msg[axis] = false; + api.put('position/' + axis, {position: parseFloat(position)}); }, @@ -225,9 +271,7 @@ module.exports = { step: function () {api.put('step/' + this.file).done(this.update)}, - override_feed: function () { - api.put('override/feed/' + this.feed_override) - }, + override_feed: function () {api.put('override/feed/' + this.feed_override)}, override_speed: function () { diff --git a/src/js/sock.js b/src/js/sock.js index 244a046..908d53d 100644 --- a/src/js/sock.js +++ b/src/js/sock.js @@ -5,7 +5,7 @@ var Sock = function (url, retry, timeout) { if (!(this instanceof Sock)) return new Sock(url, retry); if (typeof retry == 'undefined') retry = 2000; - if (typeof timeout == 'undefined') timeout = 5000; + if (typeof timeout == 'undefined') timeout = 8000; this.url = url; this.retry = retry; diff --git a/src/pwr/main.c b/src/pwr/main.c index 1aa7cc2..b6e124a 100644 --- a/src/pwr/main.c +++ b/src/pwr/main.c @@ -254,6 +254,8 @@ int main() { GATE_DDR = (1 << GATE1_PIN) | (1 << 2); GATE_PORT = (1 << GATE1_PIN) | (1 << 2); + while (true) continue; + // Start ADC adc_conversion(); diff --git a/src/py/bbctrl/APIHandler.py b/src/py/bbctrl/APIHandler.py index 4e6bf58..53e1eb1 100644 --- a/src/py/bbctrl/APIHandler.py +++ b/src/py/bbctrl/APIHandler.py @@ -41,7 +41,12 @@ class APIHandler(tornado.web.RequestHandler): def write_error(self, status_code, **kwargs): e = {} - e['message'] = str(kwargs['exc_info'][1]) + + if 'message' in kwargs: e['message'] = kwargs['message'] + elif 'exc_info' in kwargs: + e['message'] = str(kwargs['exc_info'][1]) + else: e['message'] = 'Unknown error' + e['code'] = status_code self.write_json(e) diff --git a/src/py/bbctrl/AVR.py b/src/py/bbctrl/AVR.py index c00b726..8a67423 100644 --- a/src/py/bbctrl/AVR.py +++ b/src/py/bbctrl/AVR.py @@ -21,13 +21,27 @@ I2C_STEP = 6 I2C_FLUSH = 7 I2C_REPORT = 8 I2C_REBOOT = 9 -I2C_ZERO = 10 machine_state_vars = ''' xp yp zp ap bp cp u s f t fm pa cs ao pc dm ad fo so mc fc '''.split() +# Axis homing procedure +# - Set axis unhomed +# - Find switch +# - Backoff switch +# - Find switch more accurately +# - Backoff to machine zero +# - Set axis home position +axis_homing_procedure = ''' + G28.2 %(axis)c0 F[#<%(axis)c.sv>] + G38.6 %(axis)c[#<%(axis)c.hd> * [#<%(axis)c.tm> - #<%(axis)c.tn>]] + G38.8 %(axis)c[#<%(axis)c.hd> * -#<%(axis)c.lb>] F[#<%(axis)c.lv>] + G38.6 %(axis)c[#<%(axis)c.hd> * #<%(axis)c.lb> * 1.5] + G0 %(axis)c[#<%(axis)c.hd> * -#<%(axis)c.zb> + #<%(axis)cp>] + G28.3 %(axis)c[#<%(axis)c.hp>] +''' class AVR(): def __init__(self, ctrl): @@ -267,7 +281,18 @@ class AVR(): self.queue_command('${}{}={}'.format(index, code, value)) - def home(self): log.debug('NYI') + def home(self, axis, position = None): + if self.stream is not None: raise Exception('Busy, cannot home') + + if position is not None: + self.queue_command('G28.3 %c%f' % (axis, position)) + + else: + gcode = axis_homing_procedure % {'axis': axis} + for line in gcode.splitlines(): + self.queue_command(line.strip()) + + def estop(self): self._i2c_command(I2C_ESTOP) def clear(self): self._i2c_command(I2C_CLEAR) @@ -292,7 +317,12 @@ class AVR(): # Resume processing once current queue of GCode commands has flushed self.queue_command('$resume') + def pause(self): self._i2c_command(I2C_PAUSE) def unpause(self): self._i2c_command(I2C_RUN) def optional_pause(self): self._i2c_command(I2C_OPTIONAL_PAUSE) - def zero(self, axis = None): self._i2c_command(I2C_ZERO, byte = axis) + + + def set_position(self, axis, position): + if self.stream is not None: raise Exception('Busy, cannot set position') + self.queue_command('G92 %c%f' % (axis, position)) diff --git a/src/py/bbctrl/Config.py b/src/py/bbctrl/Config.py index 5590300..b4a23b3 100644 --- a/src/py/bbctrl/Config.py +++ b/src/py/bbctrl/Config.py @@ -64,7 +64,11 @@ class Config(object): def encode_cmd(self, index, value, spec): if not 'code' in spec: return - if spec['type'] == 'enum': value = spec['values'].index(value) + if spec['type'] == 'enum': + if value in spec['values']: + value = spec['values'].index(value) + else: value = spec['default'] + elif spec['type'] == 'bool': value = 1 if value else 0 elif spec['type'] == 'percent': value /= 100.0 diff --git a/src/py/bbctrl/Ctrl.py b/src/py/bbctrl/Ctrl.py index 27e6043..5e2749e 100644 --- a/src/py/bbctrl/Ctrl.py +++ b/src/py/bbctrl/Ctrl.py @@ -1,4 +1,5 @@ import logging +import subprocess import bbctrl @@ -6,6 +7,25 @@ import bbctrl log = logging.getLogger('Ctrl') +class IPPage(bbctrl.LCDPage): + def update(self): + p = subprocess.Popen(['hostname', '-I'], stdout = subprocess.PIPE) + ips = p.communicate()[0].decode('utf-8').split() + + p = subprocess.Popen(['hostname'], stdout = subprocess.PIPE) + hostname = p.communicate()[0].decode('utf-8').strip() + + self.clear() + + self.text('Host: %s' % hostname[0:14], 0, 0) + + for i in range(min(3, len(ips))): + self.text('IP: %s' % ips[i], 0, i + 1) + + + def activate(self): self.update() + + class Ctrl(object): def __init__(self, args, ioloop): self.args = args @@ -20,3 +40,5 @@ class Ctrl(object): self.pwr = bbctrl.Pwr(self) self.avr.connect() + + self.lcd.add_new_page(IPPage(self.lcd)) diff --git a/src/py/bbctrl/LCD.py b/src/py/bbctrl/LCD.py index ba072db..5b270fc 100644 --- a/src/py/bbctrl/LCD.py +++ b/src/py/bbctrl/LCD.py @@ -7,7 +7,7 @@ import tornado.ioloop log = logging.getLogger('LCD') -class Page: +class LCDPage: def __init__(self, lcd, text = None): self.lcd = lcd self.data = lcd.new_screen() @@ -16,6 +16,10 @@ class Page: self.text(text, (lcd.width - len(text)) // 2, 1) + def activate(self): pass + def deactivate(self): pass + + def put(self, c, x, y): y += x // self.lcd.width x %= self.lcd.width @@ -31,6 +35,12 @@ class Page: self.put(c, x, y) x += 1 + + def clear(self): + self.data = self.lcd.new_screen() + self.lcd.redraw = True + + def shift_left(self): pass def shift_right(self): pass def shift_up(self): pass @@ -63,7 +73,7 @@ class LCD: def set_message(self, msg): try: - self.load_page(Page(self, msg)) + self.load_page(LCDPage(self, msg)) self._update() except IOError as e: log.error('LCD communication failed: %s' % e) @@ -73,12 +83,12 @@ class LCD: return [[' ' for y in range(self.height)] for x in range(self.width)] - def new_page(self): return Page(self) + def new_page(self): return LCDPage(self) def add_page(self, page): self.pages.append(page) - def add_new_page(self): - page = self.new_page() + def add_new_page(self, page = None): + if page is None: page = self.new_page() page.id = len(self.pages) self.add_page(page) return page @@ -86,6 +96,8 @@ class LCD: def load_page(self, page): if self.page != page: + if self.page is not None: self.page.deactivate() + page.activate() self.page = page self.redraw = True self.update() diff --git a/src/py/bbctrl/Pwr.py b/src/py/bbctrl/Pwr.py index 0e9be6e..397f84e 100644 --- a/src/py/bbctrl/Pwr.py +++ b/src/py/bbctrl/Pwr.py @@ -5,7 +5,7 @@ import bbctrl log = logging.getLogger('PWR') -# Must match regs in pwr firmare +# Must match regs in pwr firmware TEMP_REG = 0 VIN_REG = 1 VOUT_REG = 2 diff --git a/src/py/bbctrl/Web.py b/src/py/bbctrl/Web.py index 72448bb..57b7a39 100644 --- a/src/py/bbctrl/Web.py +++ b/src/py/bbctrl/Web.py @@ -7,6 +7,7 @@ import logging import datetime import shutil import tarfile +import subprocess import bbctrl @@ -14,6 +15,89 @@ import bbctrl log = logging.getLogger('Web') +def call_get_output(cmd): + p = subprocess.Popen(cmd, stdout = subprocess.PIPE) + s = p.communicate()[0].decode('utf-8') + if p.returncode: raise Exception('Command failed') + return s + + +class HostnameHandler(bbctrl.APIHandler): + def get(self): + p = subprocess.Popen(['hostname'], stdout = subprocess.PIPE) + hostname = p.communicate()[0].decode('utf-8').strip() + self.write_json(hostname) + + + def put(self): + if 'hostname' in self.json: + if subprocess.call(['/usr/local/bin/sethostname', + self.json['hostname']]) == 0: + self.write_json('ok') + return + + self.send_error(400, message = 'Failed to set hostname: %s' % self.json) + + +def get_username(): + return call_get_output(['getent', 'passwd', '1001']).split(':')[0] + + +class UsernameHandler(bbctrl.APIHandler): + def get(self): self.write_json(get_username()) + + + def put(self): + if 'username' in self.json: + username = get_username() + + if subprocess.call(['usermod', '-l', self.json['username'], + username]) == 0: + self.write_json('ok') + return + + self.send_error(400, message = 'Failed to set username: %s' % self.json) + + +class PasswordHandler(bbctrl.APIHandler): + def put(self): + if 'current' in self.json and 'password' in self.json: + # Get current user name + username = get_username() + + # Get current password + s = call_get_output(['getent', 'shadow', username]) + password = s.split(':')[1].split('$') + + # Check password type + if password[1] != '1': + self.send_error(400, message = + "Don't know how to update non-MD5 password") + return + + # Check current password + cmd = ['openssl', 'passwd', '-salt', password[2], '-1', + self.json['current']] + s = call_get_output(cmd).strip() + if s.split('$') != password: + print('%s != %s' % (s.split('$'), password)) + self.send_error(401, message = 'Wrong password') + return + + # Set password + s = '%s:%s' % (username, self.json['password']) + s = s.encode('utf-8') + + p = subprocess.Popen(['chpasswd', '-c', 'MD5'], + stdin = subprocess.PIPE) + p.communicate(input = s) + + if p.returncode == 0: + self.write_json('ok') + return + + self.send_error(400, message = 'Failed to set password') + class ConfigLoadHandler(bbctrl.APIHandler): def get(self): self.write_json(self.ctrl.config.load()) @@ -55,20 +139,26 @@ class FirmwareUpdateHandler(bbctrl.APIHandler): with open('firmware/update.tar.bz2', 'wb') as f: f.write(firmware['body']) - import subprocess - ret = subprocess.Popen(['update-bbctrl']) + subprocess.Popen(['/usr/local/bin/update-bbctrl']) self.write_json('ok') class UpgradeHandler(bbctrl.APIHandler): - def put_ok(self): - import subprocess - ret = subprocess.Popen(['upgrade-bbctrl']) + def put_ok(self): subprocess.Popen(['/usr/local/bin/upgrade-bbctrl']) class HomeHandler(bbctrl.APIHandler): - def put_ok(self): self.ctrl.avr.home() + def put_ok(self, axis, set_home): + if axis is not None: axis = ord(axis[1:2].lower()) + + if set_home: + if not 'position' in self.json: + raise Exception('Missing "position"') + + self.ctrl.avr.home(axis, self.json['position']) + + else: self.ctrl.avr.home(axis) class StartHandler(bbctrl.APIHandler): @@ -103,10 +193,9 @@ class StepHandler(bbctrl.APIHandler): def put_ok(self, path): self.ctrl.avr.step(path) -class ZeroHandler(bbctrl.APIHandler): +class PositionHandler(bbctrl.APIHandler): def put_ok(self, axis): - if axis is not None: axis = ord(axis[1:].lower()) - self.ctrl.avr.zero(axis) + self.ctrl.avr.set_position(ord(axis.lower()), self.json['position']) class OverrideFeedHandler(bbctrl.APIHandler): @@ -189,6 +278,9 @@ class Web(tornado.web.Application): handlers = [ (r'/websocket', WSConnection), + (r'/api/hostname', HostnameHandler), + (r'/api/remote/username', UsernameHandler), + (r'/api/remote/password', PasswordHandler), (r'/api/config/load', ConfigLoadHandler), (r'/api/config/download', ConfigDownloadHandler), (r'/api/config/save', ConfigSaveHandler), @@ -196,7 +288,7 @@ class Web(tornado.web.Application): (r'/api/firmware/update', FirmwareUpdateHandler), (r'/api/upgrade', UpgradeHandler), (r'/api/file(/.+)?', bbctrl.FileHandler), - (r'/api/home', HomeHandler), + (r'/api/home(/[xyzabcXYZABC](/set)?)?', HomeHandler), (r'/api/start(/.+)', StartHandler), (r'/api/estop', EStopHandler), (r'/api/clear', ClearHandler), @@ -205,7 +297,7 @@ class Web(tornado.web.Application): (r'/api/unpause', UnpauseHandler), (r'/api/pause/optional', OptionalPauseHandler), (r'/api/step(/.+)', StepHandler), - (r'/api/zero(/[xyzabcXYZABC])?', ZeroHandler), + (r'/api/position/([xyzabcXYZABC])', PositionHandler), (r'/api/override/feed/([\d.]+)', OverrideFeedHandler), (r'/api/override/speed/([\d.]+)', OverrideSpeedHandler), (r'/(.*)', StaticFileHandler, diff --git a/src/py/bbctrl/__init__.py b/src/py/bbctrl/__init__.py index 0b67bef..e271235 100755 --- a/src/py/bbctrl/__init__.py +++ b/src/py/bbctrl/__init__.py @@ -13,7 +13,7 @@ from bbctrl.APIHandler import APIHandler from bbctrl.FileHandler import FileHandler from bbctrl.GCodeStream import GCodeStream from bbctrl.Config import Config -from bbctrl.LCD import LCD +from bbctrl.LCD import LCD, LCDPage from bbctrl.AVR import AVR from bbctrl.Web import Web from bbctrl.Jog import Jog diff --git a/src/resources/config-template.json b/src/resources/config-template.json index 14f3bb1..613e96d 100644 --- a/src/resources/config-template.json +++ b/src/resources/config-template.json @@ -17,7 +17,7 @@ "code": "pm" }, "drive-current": { - "type": "aloat", + "type": "float", "min": 0, "max": 8, "unit": "amps", @@ -28,7 +28,7 @@ "type": "float", "min": 0, "max": 8, - "unit": "Amps", + "unit": "amps", "default": 0, "code": "ic" } @@ -109,9 +109,9 @@ "homing-mode": { "type": "enum", "values": [ - "disabled", "stall-min", "stall-max", "switch-min", "switch-max" + "manual", "stall-min", "stall-max", "switch-min", "switch-max" ], - "default": "disabled", + "default": "manual", "code": "ho" }, "search-velocity": { diff --git a/src/stylus/style.styl b/src/stylus/style.styl index ee556f2..7cc3302 100644 --- a/src/stylus/style.styl +++ b/src/stylus/style.styl @@ -180,6 +180,10 @@ body font-family Courier .axis + &.homed + background-color #ccffcc + color #000 + .name text-transform capitalize @@ -190,12 +194,8 @@ body .position width 99% - .offset - min-width 8em - - .errors + .absolute, .offset min-width 6em - white-space normal .jog svg text @@ -432,12 +432,20 @@ body font-family Helvetica, Arial, sans-serif +.modal-header + text-decoration underline + +.modal-footer + text-align right + .modal-enter, .modal-leave opacity 0 -.modal-enter .modal-container, .modal-leave .modal-container +.modal-enter .modal-container transform scale(1.1) +.modal-leave .modal-container + transform scale(0.9) label.file-upload display inline-block @@ -461,7 +469,7 @@ label.file-upload font-size 24pt line-height 24pt - .offset, .errors + .absolute, .offset display none > *:nth-of-type(n)