UNPKG

rollo

Version:

The Rollo programming language for the Orbotix Sphero robot using Node.js and Cylon.js

826 lines (710 loc) 16.2 kB
/** * Created with IntelliJ IDEA. * User: mfo * Date: 5/11/15 * Time: 8:48 PM */ var async = require('async'); var colors = require('./colors'); var events = require('./events'); var config = require('./config'); var _ = require('lodash'); var constants = require('./constants'); var subroutines = {}; var variables = {}; var operands = { '+': add, '-': subtract, '*': multiply, '/': divide, '=': assign, '==': compareEquals, '!=': compareNotEquals, '<': compareLessThan, '>': compareGreaterThan, '<=': compareLessThanEquals, '>=': compareGreaterThanEquals }; var state = { sphero: null, speed: 0, defaultSpeed: 50, heading: 0, cmdCount: 0, unknownCmdCount: 0, stopped: false }; var functions = { '^getspeed': function () { return state.speed; }, '^getdefaultspeed': function () { return state.defaultSpeed; }, '^getheading': function () { return state.heading; }, '^getcommandcount': function () { return state.cmdCount; }, '^getunknowncommandcount': function () { return state.unknownCmdCount; } }; var commands = { log: log, say: log, wait: wait, delay: wait, turnright: turnRight, right: turnRight, turnleft: turnLeft, left: turnLeft, turn: turn, speed: speed, flash: flash, color: color, pulse: pulse, go: go, stop: stop, waitfortap: waitForTap, waitforhit: waitForTap, turnaround: turnAround, reverse: turnAround, pointme: pointMe, loop: repeat, repeat: repeat, gosub: gosub, call: gosub, 'let': _let, 'if': _if, 'while': _while, dowhile: doWhile, dountil: doUntil, '#': comment }; // *********** // EXPRESSIONS // *********** /* * LET */ function _let(params, cb) { var total; var varName = params[0]; var exp = params[1]; value = evaluate(exp); setVar(varName, value); return cb(); } // ************ // FLOW CONTROL // ************ /* * IF */ function _if(params, cb) { var condition = params[0]; // condition = [op, exp, exp] var op = condition[0]; var a = condition[1]; var b = condition[2]; var lines = params[1]; var elseLines = params[2]; if (operands[op](a, b)) { logIt("if: " + op); executeLines(lines, function () { logIt("if: end " + op); return cb(); }); } else { if (elseLines) { logIt("if-else: " + op); executeLines(elseLines, function () { logIt("if-else: end " + op); return cb(); }); } else { return cb(); } } } /* * DO ... UNTIL */ function doUntil(params, cb) { var condition = params[0]; // condition = [op, exp, exp] var op = condition[0]; var a = condition[1]; var b = condition[2]; var lines = params[1]; logIt("do until:"); async.doUntil( function () { return operands[op](a, b); }, function (next) { if (state.stopped) { return false; // returning false is basically returning an error to stop the loop } logIt(' do until: loop'); executeLines(lines, function (err) { logIt(' do until: end-loop'); return next(); }); }, function (err, res) { logIt("do until: end"); return cb(); }); } /* * DO ... WHILE */ function doWhile(params, cb) { var condition = params[0]; // condition = [op, exp, exp] var op = condition[0]; var a = condition[1]; var b = condition[2]; var lines = params[1]; logIt("do while:"); async.doWhilst( function () { return operands[op](a, b); }, function (next) { if (state.stopped) { return false; // returning false is basically returning an error to stop the loop } logIt(' do while: loop'); executeLines(lines, function (err) { logIt(' do while: end-loop'); return next(); }); }, function (err, res) { logIt("do while: end"); return cb(); } ); } /* * WHILE */ function _while(params, cb) { var condition = params[0]; // condition = [op, exp, exp] var op = condition[0]; var a = condition[1]; var b = condition[2]; var lines = params[1]; logIt("while:"); async.whilst( function () { return operands[op](a, b); }, function (next) { if (state.stopped) { return false; // returning false is basically returning an error to stop the loop } logIt(' while: loop'); executeLines(lines, function (err) { logIt(' while: end-loop'); return next(); }); }, function (err, res) { logIt("while: end"); return cb(); }); } /* * WAIT */ function wait(params, cb) { var count = params[0]; logIt("wait: " + count + " seconds"); setTimeout(function () { logIt("wait: done"); return cb(); }, count * 1000); } /* * WAIT FOR TAP */ function waitForTap(params, cb) { logIt("waitForTap:"); var timer; var topic = events.subscribe(constants.TOPIC_COLLISION, function () { logIt("TAP!"); topic.remove(); // no longer listen once we catch one collision if (sphero()) { sphero().finishCalibration(); } if (timer) { clearTimeout(timer); // abort the timeOut } setTimeout(function () { return cb(); }, 300); // gives 300ms for ball to settle after tapping }); if (params[0]) { timer = setTimeout(function () { topic.remove(); logIt("waitForTap: timed out waiting for tap"); if (sphero()) { sphero().finishCalibration(); } return cb(); }, (params[0] * 1000)); } } /* * REPEAT */ function repeat(params, cb) { var count = params[0]; var lines = params[1]; async.timesSeries(count, function (n, next) { if (state.stopped) { return false; // returning false is basically returning an error to stop the loop } logIt("repeat: " + (n + 1) + " of " + count); executeLines(lines, function (err) { logIt("repeat: end"); return next(); }); }, function (err, res) { return cb(); }); } function gosub(params, cb) { var label = params[0]; if (subroutines.hasOwnProperty(label)) { return executeLines(subroutines[label], cb); } else { return cb(); } } // **** // MOVE // **** /* * GO */ function go(params, cb) { logIt("go: speed=" + getDefaultSpeed() + " heading=" + state.heading + (params[0] ? " duration=" + params[0] : '')); if (sphero()) { sphero().roll(getDefaultSpeed(), state.heading) } setSpeed(getDefaultSpeed()); if (params[0]) { var count = params[0]; setTimeout(function () { //logIt("go: done"); setSpeed(0); sphero().roll(0, state.heading); return cb(); }, count * 1000); } else { return cb(); } } /* * STOP */ function stop(params, cb) { logIt("stop:"); setSpeed(0); if (sphero()) { sphero().roll(0, state.heading) } return cb(); } /* * POINT ME */ function pointMe(params, cb) { logIt("pointMe:"); sphero().startCalibration(); return cb(); } // ********** // NAVIGATION // ********** /* * SPEED */ function speed(params, cb) { var speed = params[0]; setDefaultSpeed(Math.floor(255 * speed / 100)); // speed is a percentage of max, 255 being max logIt("speed: " + speed); return cb(); } /* * TURN */ function turn(params, cb) { var degrees = params[0]; adjustHeading(degrees); logIt("turn: " + degrees); return cb(); } /* * TURN RIGHT */ function turnRight(params, cb) { var degrees = 90; if (params.length > 0) { degrees = params[0]; } adjustHeading(degrees); logIt("turnRight: " + degrees); return cb(); } /* * TURN LEFT */ function turnLeft(params, cb) { var degrees = 90; if (params.length > 0) { degrees = params[0]; } adjustHeading(-degrees); logIt("turnLeft: " + degrees); return cb(); } /* * TURN AROUND */ function turnAround(params, cb) { adjustHeading(180, cb); logIt("turnAround:"); } // ****** // COLORS // ****** /* * COLOR */ function color(params, cb) { var color = colors.parseColor(params[0]); setColor(color); logIt("color: " + color); return cb(); } /* * FLASH */ function flash(params, cb) { var color = colors.parseColor(params[0]); var oldColor = getColor(); setColor(color); setTimeout(function () { setColor(oldColor); }, 500); logIt("flash: " + color); return cb(); } /* * PULSE */ function pulse(params, cb) { var brightness = [10, 18, 25, 50, 75, 82, 90, 95, 100, 95, 90, 82, 75, 50, 25, 18, 10, 0]; var color = colors.parseColor(params[0]); var oldColor = getColor(); logIt("pulse: " + color); async.eachSeries(brightness, function (b, next) { if (state.stopped) { return false; // returning false is basically returning an error to stop the loop } if (b !== 0) { setColor(darkenColor(color, b)); setTimeout(function () { return next(); }, 18); } else { setColor(oldColor); return next(); } }); return cb(); } // ****** // OUTPUT // ****** /* * LOG / SAY */ function log(params, cb) { params.forEach(function (param, index, array) { logIt("say: " + param); events.publish(constants.SAY_LINE, param); }); return cb(); } /* * COMMENT */ function comment(params, cb) { return cb(); } // -------- Commands to adjust Sphero features and state function setSpeed(speed) { if (state.speed == null) { state.speed = 0; } state.speed = speed; } function getSpeed() { return state.speed; } function setDefaultSpeed(speed) { if (state.defaultSpeed == null) { state.defaultSpeed = 0; } state.defaultSpeed = speed; } function getDefaultSpeed() { return state.defaultSpeed; } function setHeading(heading, cb) { if (sphero()) { // maybe this will actually turn us if we're not moving if (getSpeed() === 0) { sphero().roll(0, state.heading); if (cb) { setTimeout(function () { return cb(); }, 600); // gives 600ms for ball to fully turn around if still } } else { sphero().roll(state.speed, state.heading); if (cb) { return cb(); } } } } function adjustHeading(heading, cb) { if (state.heading == null) { state.heading = 0; } state.heading += heading; if (state.heading > 359) { state.heading -= 360; } if (state.heading < 0) { state.heading += 360; } return setHeading(heading, cb); } function setColor(color) { if (state.color == null) { state.color = 0; } state.color = color; if (sphero()) { sphero().setColor(color); } } function getColor() { return state.color || 0x000000; } function sphero() { return state.sphero; } function collisionHandler(data) { events.publish(constants.TOPIC_COLLISION, convertCollisionData(data.DATA)); } function convertCollisionData(data) { var obj = {}; obj.xImpact = convertToSignedInt(data[7], data[8]); obj.yImpact = convertToSignedInt(data[9], data[10]); obj.speed = data[11]; return obj; } function convertToSignedInt(msb, lsb) { var negative = msb > 128; if (negative) { msb -= 128; } var value = msb * 256 + lsb; if (negative) { value = 0 - value; } return value; } function darkenColor(color, percentage) { var r = (color >> 16) & 0xFF; var g = (color >> 8) & 0xFF; var b = color & 0xFF; r = Math.floor(r * (percentage / 100)) << 16; g = Math.floor(g * (percentage / 100)) << 8; b = Math.floor(b * (percentage / 100)); return r + g + b; } // -------- expression processing function getVar(varName) { if (!variables.hasOwnProperty(varName)) { variables[varName] = 0; } return variables[varName]; } function setVar(varName, value) { variables[varName] = value; return value; } function evaluate(data) { var op; var exp; if (typeof data === 'number') { return data; } else if (typeof data[0] === 'number') { return data[0]; } else if (typeof data === 'string') { if (data.charAt(0) === '$') { return getVar(data); } if (data.charAt(0) === '^') { if (functions.hasOwnProperty(data)) { return functions[data](); } } } else { op = data[0]; exp = data[1]; } var v1 = exp[0]; if (typeof v1 === 'object') { v1 = evaluate(v1); } else if (typeof v1 === 'string') { // assume variable v1 = getVar(v1); } var v2 = exp[1]; if (typeof v2 === 'object') { v2 = evaluate(v2); } else if (typeof v2 === 'string') { // assume variable v2 = getVar(v2); } return operands[op](v1, v2); } function assign(a) { return a; } function add(a, b) { if (typeof a === 'string' || typeof b === 'string') { return "" + a + b; } return a + b; } function subtract(a, b) { if (typeof a === 'string' || typeof b === 'string') { throw {err: "Subtract type error", msg: "Cannot subtract a string from a number, a string from a string, or a number from a string. Check your variables."} } return a - b; } function multiply(a, b) { if (typeof a === 'string' || typeof b === 'string') { throw {err: "Multiply type error", msg: "Cannot multiple a string with a number, a string with a string, or a number with a string. Check your variables."} } return a * b; } function divide(a, b) { if (typeof a === 'string' || typeof b === 'string') { throw {err: "Divide type error", msg: "Cannot divide a string by a number, a string by a string, or a number by a string. Check your variables."} } if (b === 0) { throw "Cannot divide a number by 0."; } return a / b; } function compareEquals(a, b) { return evaluate(a) == evaluate(b); } function compareNotEquals(a, b) { return evaluate(a) != evaluate(b); } function compareLessThan(a, b) { return evaluate(a) < evaluate(b); } function compareGreaterThan(a, b) { return evaluate(a) > evaluate(b); } function compareLessThanEquals(a, b) { return evaluate(a) <= evaluate(b); } function compareGreaterThanEquals(a, b) { return evaluate(a) >= evaluate(b); } // -------- event handlers function logIt(string) { if (config.debug) { console.log(string); events.publish(constants.LOG_LINE, string); } } // -------- parse and execute lines of Rollo code function hoistSubroutines(lines) { return _.filter(lines, function (lineObject) { var line = lineObject.line; var cmd = line[0]; var params = line.slice(1); if (cmd === 'sub') { subroutines[params[0]] = params[1]; return false; } else { return true; } }); } function executeLine(lineObject, callback) { if (state.stopped) { return callback({err: "received a stop command"}); // aka an error } else { var line = lineObject.line; var cmd = line[0]; var params = line.slice(1); if (lineObject.hasOwnProperty('line') && commands.hasOwnProperty(cmd)) { state.cmdCount++; if (cmd !== '#') { events.publish(constants.LINE_RUNNING, lineObject); // don't publish comment line events } return commands[cmd].call(this, params, callback); } else { state.unknownCmdCount++; events.publish(constants.UNKNOWN_LINE_RUNNING, lineObject); logIt("Rollo: Unsupported command -> " + cmd); return callback(); } } } function executeLines(lines, done) { async.eachSeries(lines, executeLine, function (err) { done(); }); } function execute(mySphero, lines, done) { state.sphero = mySphero; state.stopped = false; var catchStop = events.subscribe(constants.STOP, function () { state.stopped = true; logIt("Rollo: Received STOP event"); }); if (sphero()) { setHeading(0); sphero().configureCollisionDetection(0x01, 0x40, 0x40, 0x40, 0x40, 0x50); // defaults: 0x01, 0x40, 0x40, 0x50, 0x50, 0x50 sphero().on("collision", collisionHandler) } return executeLines(hoistSubroutines(lines), function () { catchStop.remove(); // stop catching stop requests done(); }); } module.exports.execute = execute; module.exports.state = state; module.exports.commands = commands; module.exports.subroutines = subroutines; module.exports.variables = variables;