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