Lynxmotion Biped BRAT

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.foot === 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

docs/images/brat.png

 

License

Copyright (c) 2012, 2013, 2014 Rick Waldron waldron.rick@gmail.com Licensed under the MIT license. Copyright (c) 2016 The Johnny-Five Contributors Licensed under the MIT license.