Run this example from the command line with:

node eg/brat.js
var five = require("johnny-five"),
  compulsive = require("compulsive");

var ED,
  priv = new WeakMap();


/**
 * ED
 *
 * Enforcement Droid Series
 * (Lynxmotion Biped BRAT)
 *
 * http://www.lynxmotion.com/images/jpg/bratjr00.jpg
 * http://www.lynxmotion.com/images/html/build112.htm
 * Hardware:
  - 1 x Alum. Channel - 3" Single Pack (ASB-503)
  - 2 x Multi-Purpose Servo Bracket Two Pack (ASB-04)
  - 1 x "L" Connector Bracket Two Pack (ASB-06)
  - 1 x "C" Servo Bracket w/ Ball Bearings Two Pack (ASB-09)
  - 1 x Robot Feet Pair (ARF-01)
  - 1 x SES Electronics Carrier (EC-02)
  - 1 x SSC-32 Servo Controller (SSC-32)
  - 4 x HS-422 (57oz.in.) Standard Servo (S422)
 *
 * @param {Object} opts Optional properties object
 */

function ED(opts) {

  opts = opts || {};

  // Standard servos center at 90°
  this.center = opts.center || 90;

  // Initiale movement is forward
  this.direction = "fwd";

  // Accessor for reading the current servo position will
  // be defined and assigned to this.degrees object.
  this.degrees = {};

  // holds a reference to the current repeating/looping sequence
  this.sequence = null;

  // Table of times (avg) to complete tasks
  this.times = {
    step: 0,
    attn: 0
  };

  // Minor normalization of incoming properties
  opts.right = opts.right || {};
  opts.left = opts.left || {};

  // Initialize the right and left cooperative servos
  // TODO: Support pre-initialized servo instances
  this.servos = {
    right: {
      hip: opts.right.hip && new five.Servo(opts.right.hip),
      foot: opts.right.foot && new five.Servo(opts.right.foot)
    },
    left: {
      hip: opts.left.hip && new five.Servo(opts.left.hip),
      foot: opts.left.foot && new five.Servo(opts.left.foot)
    }
  };

  // Create shortcut properties
  this.right = this.servos.right;
  this.left = this.servos.left;

  // Create accessor descriptors:
  //
  //  .left { .foot, .hip }
  //  .right { .foot, .hip }
  //
  ["right", "left"].forEach(function(key) {

    var descriptor = {};

    ["foot", "hip"].forEach(function(part) {
      descriptor[part] = {
        get: function() {
          var history = this.servos[key][part].history,
            last = history[history.length - 1];

          return last && last.degrees || 90;
        }.bind(this)
      };
    }, this);

    this.degrees[key] = {};

    // And finally, create properties with the generated descriptor
    Object.defineProperties(this.degrees[key], descriptor);
  }, this);


  Object.defineProperty(this, "isCentered", {
    get: function() {
      var right, left;

      right = this.degrees.right;
      left = this.degrees.left;

      if ((right.foot === 90 && right.hip === 90) &&
        (left.foot === 90 && left.hip === 90)) {
        return true;
      }
      return false;
    }
  });

  // Store a recallable history of movement
  // TODO: Include in savable history
  this.history = [{
    timestamp: Date.now(),
    side: "right",
    right: {
      hip: 0,
      foot: 0
    },
    left: {
      hip: 0,
      foot: 0
    }
  }];

  // Create an entry in the private data store.
  priv.set(this, {
    // `isWalking` is used in:
    //    ED.prototype.(attn|stop)
    //    ED.prototype.(forward|fwd;reverse|rev)
    isWalking: false,

    // Allowed to hit the dance floor.
    canDance: true
  });
}

/**
 * attn Stop and stand still
 * @return {Object} this
 */
//ED.prototype.attn = ED.prototype.stop = function() {
ED.prototype.attn = function(options) {
  options = options || {};

  if (!options.isWalking) {

    if (this.sequence) {
      this.sequence.stop();
      this.sequence = null;
    }

    priv.set(this, {
      isWalking: false
    });
  }

  this.move({
    type: "attn",
    right: {
      hip: 90,
      foot: 90
    },
    left: {
      hip: 90,
      foot: 90
    }
  });
};

/**
 * step Take a step
 *
 * @param {String} instruct Give the step function a specific instruction,
 *                          one of: (fwd, rev, left, right)
 *
 */
ED.prototype.step = function(direct) {
  var isLeft, isFwd, opposing, direction, state;

  state = priv.get(this);

  if (/fwd|rev/.test(direct)) {
    direction = direct;
    direct = undefined;
  } else {
    direction = "fwd";
  }

  // Derive which side to step on; based on last step or explicit step
  this.side = direct || (this.side !== "right" ? "right" : "left");

  // Update the value of the current direction
  this.direction = direction;

  // Determine if the bot is moving fwd
  // Used in phase 3 to conditionally control the servo degrees
  isFwd = this.direction === "fwd";

  // Determine if this is the left foot
  // Used in phase 3 to conditionally control the servo degrees
  isLeft = this.side === "left";

  // Opposing leg side, used in prestep and phase 2;
  // opposing = isLeft ? "right" : "left";

  // Begin stepping movements.
  //
  this.queue([

    // Phase 1
    {
      wait: 500,
      task: function() {
        var stepping, opposing, instruct;

        stepping = isLeft ? "left" : "right";
        opposing = isLeft ? "right" : "left";

        instruct = {};

        // Lift the currently stepping foot, while
        // leaning on the currently opposing foot.
        instruct[stepping] = {
          foot: isLeft ? 40 : 140
        };
        instruct[opposing] = {
          foot: isLeft ? 70 : 110
        };

        // Swing currently stepping hips
        this.move(instruct);
      }.bind(this)
    },

    // Phase 2
    {
      wait: 500,
      task: function() {
        var degrees = isLeft ?
          (isFwd ? 120 : 60) :
          (isFwd ? 60 : 120);

        // Swing currently stepping hips
        this.move({
          type: "swing",
          right: {
            hip: degrees
          },
          left: {
            hip: degrees
          }
        });

      }.bind(this)
    },

    // Phase 3
    {
      wait: 500,
      task: function() {

        // Flatten feet to surface
        this.servos.right.foot.center();
        this.servos.left.foot.center();



      }.bind(this)
    }
  ]);
};

[
  /**
   * forward, fwd
   *
   * Move the bot forward
   */
  {
    name: "forward",
    abbr: "fwd"
  },

  /**
   * reverse, rev
   *
   * Move the bot in reverse
   */
  {
    name: "reverse",
    abbr: "rev"
  }

].forEach(function(dir) {

  ED.prototype[dir.name] = ED.prototype[dir.abbr] = function() {
    var startAt, stepper, state;

    startAt = 10;
    state = priv.get(this);

    // If ED is already walking in this direction, return immediately;
    // This prevents multiple movement loops from being scheduled.
    if (this.direction === dir.abbr && state.isWalking) {
      return;
    }

    // If a sequence reference exists, kill it. This will
    // clear all pending queue repeaters.
    if (this.sequence) {
      this.sequence.stop();
      this.sequence = null;
    }


    this.direction = dir.abbr;

    // Update the private state to indicate
    // that the bot is currently walking.
    //
    // This is used by the behaviour loop to
    // conditionally continue walking or to terminate.
    //
    // Walk termination occurs in the ED.prototype.attn method
    //
    priv.set(this, {
      isWalking: true
    });

    stepper = function(loop) {
      // Capture of sequence queue reference
      if (this.sequence === null) {
        this.sequence = loop;
      }

      this.step(dir.abbr);

      if (!priv.get(this).isWalking) {
        loop.stop();
      }
    }.bind(this);

    // If the bot is not centered, ie. all servos at 90degrees,
    // bring the bot to attention before proceeding.
    if (!this.isCentered) {
      this.attn({
        isWalking: true
      });
      // Offset the amount ms required for attn() to complete
      startAt = 750;
    }

    this.queue([{
      wait: startAt,
      task: function() {
        this.step(dir.abbr);
      }.bind(this)
    }, {
      loop: 1500,
      task: stepper
    }]);
  };
});

ED.prototype.dance = function() {
  var isLeft, restore, state;

  // Derive which side to step on; based on last step or explicit step
  this.side = this.side !== "right" ? "right" : "left";

  // Determine if this is the left foot
  // Used in phase 3 to conditionally control the servo degrees
  isLeft = this.side === "left";

  this.attn();

  if (typeof this.moves === "undefined") {
    this.moves = 0;
  }

  this.queue([
    // Phase 1
    {
      wait: 500,
      task: function() {
        var degrees = isLeft ? 120 : 60;

        if (this.moves % 2 === 0) {
          this.move({
            type: "attn",
            right: {
              hip: 90,
              foot: 60
            },
            left: {
              hip: 90,
              foot: 120
            }
          });
        } else {

          this.move({
            type: "attn",
            right: {
              hip: 90,
              foot: 120
            },
            left: {
              hip: 90,
              foot: 60
            }
          });
        }

        // Swing currently stepping hips
        this.move({
          type: "swing",
          right: {
            hip: degrees
          },
          left: {
            hip: degrees
          }
        });

        // restore = this.servos[ this.side ].foot.last.degrees;
        // this.servos[ this.side ].foot.move( restore === 140 ? 120 : 60 );

      }.bind(this)
    },

    // Phase 2
    {
      wait: 500,
      task: function() {
        var degrees = isLeft ? 60 : 120;

        // Swing currently stepping hips
        this.move({
          type: "swing",
          right: {
            hip: degrees
          },
          left: {
            hip: degrees
          }
        });

        // this.servos[ this.side ].foot.move( restore );

      }.bind(this)
    },

    // Phase 3
    {
      wait: 500,
      task: function() {

        this.move({
          type: "attn",
          right: {
            hip: 90,
            foot: 90
          },
          left: {
            hip: 90,
            foot: 90
          }
        });

        this.dance();

      }.bind(this)
    }
  ]);

  this.moves++;
};


/**
 * move Move the bot in an arbitrary direction
 * @param  {Object} positions left/right hip/foot positions
 *
 */
ED.prototype.move = function(positions) {
  var start, type;

  if (this.history.length) {
    start = this.history[this.history.length - 1];
  }

  type = positions.type || "step";

  ["foot", "hip"].forEach(function(section) {
    ["right", "left"].forEach(function(side) {
      var interval, endAt, startAt, servo, step, s;

      if (typeof positions[side] === "undefined") {
        return;
      }

      endAt = positions[side][section];
      servo = this.servos[side][section];
      startAt = this.degrees[side][section];

      // Degrees per step
      step = 2;

      s = Date.now();

      if (!endAt || endAt === startAt) {
        return;
      }

      if (start) {
        // Determine degree step direction
        if (endAt < startAt) {
          step *= -1;
        }

        // Repeat each step for required number of steps to move
        // servo into new position. Each step is ~20ms duration
        this.repeat(Math.abs(endAt - startAt) / 2, 10, function() {
          // console.log( startAt );
          servo.move(startAt += step);

          if (startAt === endAt) {
            this.times[type] = (this.times[type] + (Date.now() - s)) / 2;
          }
        }.bind(this));

      } else {
        // TODO: Stop doing this
        servo.move(endAt);
        five.Fn.sleep(500);
      }
    }, this);
  }, this);

  // Push a record object into the stepping history
  this.history.push({
    timestamp: Date.now(),
    side: this.side,
    right: five.Fn.extend({
      hip: 0,
      foot: 0
    }, this.degrees.right, positions.right),
    left: five.Fn.extend({
      hip: 0,
      foot: 0
    }, this.degrees.left, positions.left)
  });
};

// Borrow API from Compulsive
["wait", "loop", "queue", "repeat"].forEach(function(api) {
  ED.prototype[api] = compulsive[api];
});

// Begin program when the board, serial and
// firmata are connected and ready
(new five.Board()).on("ready", function() {
  var biped;

  // Create new Enforcement Droid
  // assign servos
  biped = new ED({
    right: {
      hip: 9,
      foot: 11
    },
    left: {
      hip: 10,
      foot: 12
    }
  });

  // Inject into REPL for manual controls
  this.repl.inject({
    s: new five.Servos(),
    b: biped
  });

  biped.attn();

  biped.wait(1000, function() {
    biped.fwd();
  });

  // Controlled via REPL:
  // b.fwd(), b.rev(), b.attn()
});



// http://www.lynxmotion.com/images/html/build112.htm

Illustrations / Photos

Lynxmotion Biped BRAT

Example control of biped robot

brat.png

 

Component Classes in this example:

Hi! The Johnny-Five community is building new projects every day. We made this newsletter to tell you about what's new, what's good, and what's next for Open Source robotics. Join us in exploring what we can make together.

Fork me on GitHub