rcbApi.js

/**
 * @file Provides an interface between scripts and the RCbenchmark app
 * @name RCbenchmark script API
 * @author Tyto Robotics Inc. <[email protected]>
 * @copyright 2015-2016 Tyto Robotics Inc.
 */

//receive data from GUI
window.addEventListener('message', function(event) {
	switch(event.data.command) {
	case 'init':
		rcb(event);
	break;
	case 'sensorReadCallback':
		if(rcb.vars.callbacks.sensorRead) rcb.vars.callbacks.sensorRead(event.data.content);
	break;
	case 'GUIerror':
		rcb.console.error(event.data.content.message);
	break;
	case 'tareComplete':
		if(rcb.vars.callbacks.tareComplete) rcb.vars.callbacks.tareComplete();
	break;
	case 'updateSystemLimits':
		rcb.vars.systemLimits = event.data.content;
	break;
	case 'ohmUpdate':
		if(rcb.vars.callbacks.ohmRead) rcb.vars.callbacks.ohmRead(event.data.content);
	break;
	case 'newLogEntryCallback':
		if(rcb.vars.callbacks.newLogEntry) rcb.vars.callbacks.newLogEntry();
	break;
	case 'appendTextCallback':
		if(rcb.vars.callbacks.appendTextFile) rcb.vars.callbacks.appendTextFile(); 
	break;
	case 'stop':
		rcb.console.error('Script stopped by user.');
		rcb.endScript();
	break;
	case 'pollUpdate':
		//called everytime the usb has an update
		if(rcb.vars.callbacks.outputRamp) rcb.vars.callbacks.outputRamp();
    break;
    case 'keyboardPress':
		//called everytime the keyboard is pressed
		if(rcb.vars.callbacks.keyboardPress) rcb.vars.callbacks.keyboardPress(event.data.content);
	break;

	  // case 'somethingElse':
	  //   ...
	}
});
 
/**
 * Api constructor. Called by the GUI to start the script (do not use in scripts).
 * @param {Object} args - GUI arguments.
 * @constructor
 */
var rcb = function (args) {
    vars = {};
    var cont = args.data.content;
    vars.boardId = cont.config.boardId;
    vars.boardVersion = cont.config.boardVersion;
    vars.firmwareVersion = cont.config.firmwareVersion;
    vars.sourcePage = args.source;
    vars.sourceOrigin = args.origin;
    vars.systemLimits = cont.system;
    vars.userLimits = cont.user;
    vars.output = cont.output;
    vars.sensors = cont.sensors;
    vars.printId = 0;
    vars.verbose = true;
    vars.callbacks = {};  
    rcb.vars = vars;
    rcb.files.newLogFile.called = false;
    rcb.files.newTextFile.called = false;

	//run the script
	rcb.console.print('Started script: ' + cont.name + '.js');
	try{
		eval(cont.script); //run user script
	}catch(e){
		rcb.console.error(e.toString()); //error with user script -> show error on user console
	} 
};

/**
 * Sends data back to the GUI
 * @private
 * @param {string} command - The command ID of the message being sent.
 * @param {*} [content] - The content to send.
 */
rcb._sendGUIData = function (_command, _content) {
	rcb.vars.sourcePage.postMessage({
		  command: _command,
		  content: _content
	}, rcb.vars.sourceOrigin);
};

/**
 * Interface function that makes sure callbacks are properly called (with error reporting)
 * @private
 * @param {string} command - The command ID of the message being sent.
 * @param {*} [content] - The content to send.
 */
rcb._callCallback = function (_callback, params) {
	function isFunction(functionToCheck) {
     var getType = {};
     return functionToCheck && getType.toString.call(functionToCheck) === '[object Function]';
    }
    
    if(!_callback || !isFunction(_callback)){
        rcb.console.error("Invalid callback function specified");
    }else{
        try{
            _callback(params);
        }catch(e){
            rcb.console.error(e.toString()); //error with user script -> show error on user console
        }
    }
};

/**
 * Console interface functions
 * @class
 */
rcb.console = {
	/**
	 * Prints a new line to the user console.
	 * @param {string} message - The string to print on the console. Can have html markup.
	 * @return {integer} A unique id, to reference this printed line later.
	 * @example
	 * rcb.console.print('Normal text');
	 * rcb.console.print('<strong>Bold font</strong>');
	 * rcb.console.print('<span style=\"color: green\">Green text</span>');
	 * @example
	 * //Simple examples
	 * rcb.console.print("I will always be there!");
	 * rcb.console.print("I will be gone");
	 * rcb.console.remove();
	 * rcb.console.append(" Still here!");
	 * rcb.console.print("I will be overwritten");
	 * rcb.console.overwrite("Fruits:");
	 * 
	 * //Working with ids
	 * var id1 = rcb.console.print("I WILL BE REMOVED");
	 * var id2 = rcb.console.print("2");
	 * var id3 = rcb.console.print("I WILL BE OVERWRITTEN");
	 * rcb.console.print("3");
	 * rcb.console.append(" apples");
	 * rcb.console.append(" oranges", id2);
	 * rcb.console.remove(id1);
	 * rcb.console.overwrite("Doesn't compare with:", id3);
	 * rcb.endScript();
	 * @example
	 * //Animation example
	 * rcb.console.setVerbose(false);
	 * var lineQty = 12;
	 * var colQty = 25;
	 * var lines = [];
	 * 
	 * //Create the lines
	 * for(var i=0; i<lineQty; i++) lines[i] = rcb.console.print("");
	 * 
	 * //Starting coordinates
	 * var line = 0;
	 * var col = 0;
	 * var dirL = true;
	 * var dirC = true;

	 * //Travel loop until user stops script
	 * move();
	 * function move(){
	 *     //Update coordinates
	 *     if(dirL){
	 *       line++;
	 *       if (line === lineQty-1) dirL = false;
	 *     }else{
	 *       line--;
	 *       if (line === 0) dirL = true; 
	 *     }
	 *     if(dirC){
	 *       col++;
	 *       if (col === colQty-1) dirC = false;
	 *     }else{
	 *       col--;
	 *       if (col === 0) dirC = true; 
	 *     }
	 *     
	 *     //Draw
	 *     for(var i=0; i<lineQty; i++){
	 *         if(i===line){
	 *             var text = "";
	 *             for(var j=0; j<col-1; j++) text+="&nbsp;&nbsp;&nbsp;";
	 *             text+="O";
	 *             rcb.console.overwrite(text,lines[i]);
 	 *        }else
	 *             rcb.console.overwrite("",lines[i]);
	 *     }
	 *     
	 *     //iterate
	 *     rcb.wait(move,0.05); 
	 * }
	 */
	print: function (_message) {
		rcb.vars.printId++;
		rcb._sendGUIData("print", {message:_message, id:rcb.vars.printId});
		return rcb.vars.printId;
	},
	
	/**
	 * Adds to the last printed line (does not create a new line).
	 * @param {string} message - The string to print on the console. Can have html markup.
	 * @param {integer} [id] - If specified, will append to this line instead.
	 * @example
	 * rcb.console.print('Doing...');
	 * rcb.console.append('<strong>done</strong>');
	 */
	append: function (message, id) {
		var params={};
		params.message = message;
		params.id = id;
		rcb._sendGUIData("append", params);
	},
	
	/**
	 * Reprints over the last line of the user console. Useful to display progress updates.
	 * @param {string} message - The string to print on the console. Can have html markup.
	 * @param {integer} [id] - If specified, will overwrite this line instead.
	 */
	overwrite: function (message, id) {
		var params={};
		params.message = message;
		params.id = id;
		rcb._sendGUIData("overwrite", params);
	},
	
	/**
	 * Removes the last line of the user console.
	 * @param {integer} [id] - If specified, will remove this line instead.
	 */
	remove: function (id) {
		rcb._sendGUIData("remove", id);
	},
	
	/**
	 * Prints a message in orange. Does not interrupt script.
	 * @param {string} message - The string to print on the console. Can have html markup.
	 * @return {integer} A unique id, to reference this printed line later.
	 * @example
	 * rcb.console.warning('<strong>Warning:</strong> winter is coming!');
	 */
	warning: function (message) {
		return rcb.console.print('<span style=\"color: orange\">' + message + '</span>');
	},
	
	/**
	 * Prints an error on the user console. The script will be stopped.
	 * @param {string} message - The string to print on the console. Can have html markup.
	 * @example
	 * rcb.console.print("A basic message");
	 * rcb.console.warning("About to throw error");
	 * rcb.console.error("Bazinga!");
	 */
	error: function (message) {
		rcb._sendGUIData("error", message);
		rcb.endScript();
	},
  
  	/**
	 * Clears the script console. Needed for performance reasons if console log has too much text.
	 * @example
	 * rcb.console.clear();
	 */
	clear: function () {
		rcb._sendGUIData("clear");
	},
	
	/**
	 * Prints if verbose mode is active
	 * @param {string} message - The string to print on the console. Can have html markup.
	 * @return {integer} A unique id, to reference this printed line later.
	 * @private
	 */
	_verbosePrint: function (message) {
		if(rcb.vars.verbose){
			return rcb.console.print('<span style=\"color: silver\">' + message + '</span>');
		}else{
			return false;
		}
	},
	
	/**
	 * Reprints if verbose mode is active
	 * @param {string} message - The string to print on the console. Can have html markup.
 	 * @param {integer} [id] - If specified, will overwrite this line instead.
	 * @private
	 */
	_verboseOverwrite: function (message, id) {
		if(rcb.vars.verbose || id) rcb.console.overwrite('<span style=\"color: silver\">' + message + '</span>', id);
	},
	
	/**
	 * Appends if verbose mode is active
	 * @param {string} message - The string to print on the console. Can have html markup.
 	 * @param {integer} [id] - If specified, will append this line instead.
	 * @private
	 */
	_verboseAppend: function (message, id) {
		if(rcb.vars.verbose || id) rcb.console.append('<span style=\"color: silver\">' + message + '</span>', id);
	},
	
	/**
	 * Removes if verbose mode is active
 	 * @param {integer} [id] - If specified, will remove this line instead.
	 * @private
	 */
	_verboseRemove: function (id) {
		if(rcb.vars.verbose || id) rcb.console.remove(id);
	},

	/**
	 * Returns the verbose setting
	 * @return {boolean}
	 * @private
	 */
	_isVerbose: function () {
		return(rcb.vars.verbose===true);
	},
	
	/**
	 * Sets the verbose mode for the rcb API. If activated (default), some API functions will print some text (in grey color). It may be useful to deactivate verbose mode in loops to avoid excessive text in the console.
	 * @param {boolean} value - If true, verbose mode is activated.
	 * @example
	 * //Example showing the effect of changing verbose mode
	 * rcb.console.print("Verbose activated...");
	 * rcb.setDebugMode(true);
	 * rcb.setDebugMode(false);
	 * rcb.console.setVerbose(false);
	 * rcb.console.print("Verbose deactivated...");
	 * rcb.setDebugMode(true);
	 * rcb.setDebugMode(false);
	 * rcb.endScript();
	 */
	setVerbose: function (value) {
		rcb.vars.verbose = value;
	}
};

/**
 * Returns the board's unique ID.
 * @return {string} A string representing the board's unique serial number.
 * @example
 * var boardId = rcb.getBoardId();
 * rcb.console.print(boardId);
 * rcb.endScript();
 */
rcb.getBoardId = function () {
	return rcb.vars.boardId;
};

/**
 * Returns the board's hardware version.
 * @return {string} A string representing the board's hardware version.
 */
rcb.getBoardVersion = function () {
	return rcb.vars.boardVersion;
};

/**
 * Returns the board's firmware version.
 * @return {string} A string representing the board's firmware version.
 */
rcb.getFirmwareVersion = function () {
	return rcb.vars.firmwareVersion;
};

/**
 * Finishes the script execution. If this function is not called, the user will have to press the "Stop" button to stop the script.
 */
rcb.endScript = function () {
	rcb.console.print("Script finished");
	clearTimeout(rcb.vars.callbacks.wait);
	vars.callbacks = {};  
	rcb._sendGUIData("endScript");
};

/**
 * Activates or deactivates the debug mode.
 * @param {boolean} enable - Set to "true" to activate debug mode, "false" otherwise.
 */
rcb.setDebugMode = function (enable) {
	if(enable){
		rcb.console._verbosePrint("Enabling debug mode");
	}else{
		rcb.console._verbosePrint("Disabling debug mode");
	}
	rcb._sendGUIData("debugModeEnable",enable);
};

/**
 * Callback for the rcb.onKeyboardPress function.
 * @callback keyPressed
 * @param {number} keyValue - The ASCII code of the key that was pressed.
 */

/**
 * Allows for interactive scripts by triggering a special callback when a key is pressed. To use the 'enter' key, make sure the focus is not on the 'stop script' button otherwise the script will stop. The callback you specify will be returned with the ASCII value of the key pressed. For example, spacebar is 32.
 * @param {keyPressed} callback - Function to execute when a key is pressed
 * @example
 * // Example illustrating how to use the onKeyboardPress function 
 * 
 * rcb.console.print("Listening for keypress...");
 * 
 * // Setup keypress callback function
 * rcb.onKeyboardPress(function(key){
 *     // Print on screen which key was pressed
 *     var ascii = String.fromCharCode(key);
 *     rcb.console.print("You pressed " + ascii + " (ASCII " + key + ")");
 * });
 */
rcb.onKeyboardPress = function (callback) {
    //function to execute when keyboard is pressed
    rcb.vars.callbacks.keyboardPress = callback;
}

/**
 * Output control interface functions.
 * @class
 */
rcb.output = {
	/**
	 * Directly sets the pwm output.
	 * @private
	 * @param {string} outputId - "esc", "servo1", "servo2", "servo3", "escA", "escB", "servoA", "servoB". escA, servoA, escB, servoB are for the Series 1780 coaxial channels. For the mono version of Series 1780, use the A side. Can also be an array of multiple outputs eg. ['escA','servoA'].
	 * @param {number} pulseWidth - Pulse width between 700 and 2300 microseconds. Must be an array if the first parameter is also an array.
	 * @param {boolean} [activate] - If specified, will change the active status.
	 */
	_control: function (outputId, pulseWidth, activate) {
      if(pulseWidth===undefined) console.log.error("Missing pulseWidth parameter");
      if(Array.isArray(outputId) || Array.isArray(pulseWidth)){
        //both must be arrays and the same lenght
        if(!(Array.isArray(outputId) && Array.isArray(pulseWidth)) || (outputId.length !== pulseWidth.length)){
          rcb.console.error("Both the pulseWidth and outputId must be arrays and be the same lenght. Outputs: " + outputId + " (length " + outputId.length + ") pulseWidth: " + pulseWidth + " (length " + pulseWidth.length + ")");
        }

      }else{
        //transform the inputs as arrays
        outputId = [outputId];
        pulseWidth = [pulseWidth];
      }
      for(var i = 0; i < outputId.length; i++){
        if(pulseWidth[i] >= 700 && pulseWidth[i] <= 2300){
            var act = !(activate===undefined);
            switch (outputId[i].toLowerCase()){
                case "esc":
                    //1580 1520
                    rcb.vars.output.ESC_PWM = pulseWidth[i];
                    if(act) rcb.vars.output.active[0] = activate;

                    //example scripts use esc instead of esca, so they will not work for the Series 1780
                    if(rcb.getBoardVersion().includes("1780")){
                      rcb.console.warning('For the Series 1780, if you want to control the motor you should use "escA" instead of "esc" in the rcb.output functions. To simultaneously control both escA and escB, specify an array instead ["escA","escB"]. If you use an array you must also specify an array for the PWM values. See RCB API for more info.');
                    }
                break;
                case "servo1":
                    rcb.vars.output.Servo_PWM[0] = pulseWidth[i];
                    if(act) rcb.vars.output.active[1] = activate;
                break;
                case "servo2":
                    rcb.vars.output.Servo_PWM[1] = pulseWidth[i];
                    if(act) rcb.vars.output.active[2] = activate;
                break;
                case "servo3":
                    rcb.vars.output.Servo_PWM[2] = pulseWidth[i];
                    if(act) rcb.vars.output.active[3] = activate;
                break;
                case "esca":
                    //1780
                    rcb.vars.output.ESCA = pulseWidth[i];
                    if(act) rcb.vars.output.active[0] = activate;
                break;
                case "servoa":
                    rcb.vars.output.ServoA = pulseWidth[i];
                    if(act) rcb.vars.output.active[1] = activate;
                break;
                case "escb":
                    rcb.vars.output.ESCB = pulseWidth[i];
                    if(act) rcb.vars.output.active[2] = activate;
                break;
                case "servob":
                    rcb.vars.output.ServoB = pulseWidth[i];
                    if(act) rcb.vars.output.active[3] = activate;
                break;
                default:
                    rcb.console.error('"' + outputId[i] + '" is an invalid id (valid: "esc" "servo1" servo2" "servo3", "escA", "escB", "servoA", "servoB").');
                break;
            }
            rcb._sendGUIData("control",rcb.vars.output);
        }else{
            rcb.console.error("Out of range (700-2300) pwm parameter: " + pulseWidth[i]);
        }
      }
	},
	
	/**
	 * Controls the pwm signal outputs, effective immediatly.
	 * @param {string} outputId - "esc", "servo1", "servo2", "servo3", "escA", "escB", "servoA", "servoB". escA, servoA, escB, servoB are for the Series 1780 coaxial channels. For the mono version of Series 1780, use the A side. Can also be an array of multiple outputs eg. ['escA','servoA'].
	 * @param {number} pulseWidth - Pulse width between 700 and 2300 microseconds. Must be an array if the first parameter is also an array.
	 * @example
	 * //Sets the motor at throttle 1300 
	 * rcb.console.print("Allow time to initialize ESC");
	 * rcb.output.pwm("esc",1000);
	 * rcb.wait(callback, 4);
	 * 
	 * function callback(){
	 *     rcb.console.print("Run motor");
	 *     rcb.output.pwm("esc",1300);
	 *     rcb.wait(rcb.endScript, 5);
	 * }
     * @example
	 * //Shows how to use multiple outputs simultaneously 
	 * rcb.console.print("Allow time to initialize ESC");
	 * rcb.output.pwm(["escA","escB"],[1000,1000]);
	 * rcb.wait(callback, 4);
	 * 
	 * function callback(){
	 *     rcb.console.print("Run motor");
	 *     rcb.output.pwm(["escA","escB"],[1300,1400]);
	 *     rcb.wait(rcb.endScript, 5);
	 * }
	 */
	pwm: function (outputId, pulseWidth) {
		if(pulseWidth===undefined) rcb.console.error("Missing pulseWidth parameter");
		rcb.console._verbosePrint('Setting "' + outputId + '" to ' + pulseWidth);
		rcb.output._control(outputId, pulseWidth, true);
	},
		
	/**
	 * Callback for the rcb.output.ramp function.
	 * @callback rampDone
	 */

	/**
	 * Smoothly ramps up or down the pwm signal. For safety reasons, will only work if the output was previously activated using the pwm function. To cancel/update a ramp in progress, simply call this function again with new parameters. For example if you want to stop the ramp with the output at 1000us, call:
     rcb.output.ramp("esc", 1000, 1000, 0, null);
     Note that only one control function can be used simultaneously. You must wait for the ramp/steps function to start another.
     
	 * @param {string} outputId - outputId - "esc", "servo1", "servo2", "servo3", "escA", "escB", "servoA", "servoB". escA, servoA, escB, servoB are for the Series 1780 coaxial channels. For the mono version of Series 1780, use the A side. Can also be an array of multiple outputs eg. ['escA','servoA'].
	 * @param {number} from - Ramp starting value between 700 and 2300 microseconds. Must be an array if the first parameter is also an array.
	 * @param {number} to - Ramp finishing value between 700 and 2300 microseconds. Must be an array if the first parameter is also an array.
	 * @param {number} duration - The duration of the ramp in seconds.
	 * @param {rampDone} callback - Function to execute when the ramp is finished.
	 * @example
	 * //Illustrates the use of the ramp function
	 * rcb.console.print("Initializing ESC...");
	 * rcb.output.pwm("esc",1000);
	 * rcb.wait(callback, 4);
	 * 
	 * function callback(){
	 *     var from = 1000;
	 *     var to = 1400;
	 *     var duration = 15;
	 *     var done = rcb.endScript;
	 *     rcb.output.ramp("esc", from, to, duration, done);
	 * }
     * @example
	 * //Same as example above but with multiple outputs simultaneously.
	 * rcb.console.print("Initializing ESC...");
     * var outputs = ["escA","escB"];
     * var minVal = [1000,1000];
     * var maxVal = [1400,1300];
	 * rcb.output.pwm(outputs,minVal);
	 * rcb.wait(callback, 4);
	 * 
	 * function callback(){
	 *     var duration = 15;
	 *     var done = rcb.endScript;
	 *     rcb.output.ramp(outputs, minVal, maxVal, duration, done);
	 * }
	 */
	ramp: function (outputId, from, to, duration, callback) {
		var startTime = window.performance.now()/1000
		rcb.console._verbosePrint("Ramping " + outputId + " from " + from + " to " + to + " in " + duration + "s.");
		var status = rcb.console._verbosePrint("");
      
        if(!Array.isArray(outputId)){
          outputId = [outputId];
        }
        if(!Array.isArray(from)){
          from = [from];
        }
        if(!Array.isArray(to)){
          to = [to];
        }
        if(to.length !== from.length){
          rcb.console.error("The from and to inputs must be of the same length in the steps function.");
        }
		
		//function to execute when sensors are updated
		rcb.vars.callbacks.outputRamp = function(){
            rcb._callCallback(function (){
                currTime = window.performance.now()/1000;
                var outputVal = [];
                var percentage;
                var rampDone = false;
                if(currTime - startTime >= duration){
                    //done with ramp, finish it
                    rcb.vars.callbacks.outputRamp = undefined;
                    outputVal = to;
                    percentage = 1.0;
                    rampDone = true;
                }else{
                    //ramp in progress
                    percentage = (currTime-startTime)/duration;
                    for(var i=0; i<to.length; i++){
                      outputVal[i] = Math.round(percentage * (to[i]-from[i]) + from[i]);
                    }
                }

                //output value
                rcb.output._control(outputId, outputVal);
                rcb.console._verboseOverwrite("&nbsp;&nbsp;&nbsp;&nbsp;ramping " + (100*percentage).toFixed(1) + "%, val " + outputVal, status);
                if(callback && rampDone) callback();  
            });
		}
		
		rcb.vars.callbacks.outputRamp();
	},
	
	/**
	 * Callback for the rcb.output.steps function.
	 * @callback stepDone
	 * @param {boolean} lastStep - Will be true if this is the last step.
	 * @param {function} nextStep - Function that you should call when ready to go to the next step.
	 */

	/**
	 * Steps up or down the pwm signal allowing you to perform tasks between each step. For safety reasons, will only work if the output was previously activated using the pwm function. Note that only one control function can be used simultaneously. You must wait for the steps/ramp function to finish to start another.
	 * @param {string} outputId - outputId - "esc", "servo1", "servo2", "servo3", "escA", "escB", "servoA", "servoB". escA, servoA, escB, servoB are for the Series 1780 coaxial channels. For the mono version of Series 1780, use the A side. Can also be an array of multiple outputs eg. ['escA','servoA'].
	 * @param {number} from - Steps starting value between 700 and 2300 microseconds. Must be an array if the first parameter is also an array.
	 * @param {number} to - Steps finishing value between 700 and 2300 microseconds. Must be an array if the first parameter is also an array.
	 * @param {integer} steps - Number of steps to perform.
	 * @param {stepDone} [callback] - Function to execute when a step finishes. This function should introduce some sort of delay for the steps function to be effective.
     * @example
     * //Illustrates the use of the steps function
     * rcb.console.print("Initializing ESC...");
     * rcb.output.pwm("esc",1000);
     * rcb.wait(callback, 4);
     * var sensorPrintId;
     * 
     * function callback(){
     *     var from = 1000;
     *     var to = 1400;
     *     var steps = 10;
     *     rcb.output.steps("esc", from, to, steps, stepFct);
     * }
     * 
     * //Function called at every step
     * function stepFct(lastStep, nextStepFct){
     *     if(lastStep){
     *         rcb.endScript();
     *     }else{
     *         rcb.console.setVerbose(false);
     *         rcb.wait(function(){ //2 seconds settling time
     * 
     *             //Do stuff here... (record to log file, calculate something,  etc...)
     *             rcb.sensors.read(readDone);
     *            
     *         }, 2);
     *     }
     *     
     *     //Function called when read complete
     *     function readDone(result){
     *         var speed = result.motorElectricalSpeed.displayValue;
     *         var unit = result.motorElectricalSpeed.displayUnit;
     *         if(sensorPrintId === undefined) sensorPrintId = rcb.console.print("");
     *         rcb.console.overwrite("Motor Speed: " + speed + " " + unit, sensorPrintId);
     *         
     *         //When done working, go to the next step
     *         rcb.console.setVerbose(true);
     *         nextStepFct();
     *     }
     * }   
	 */
	steps: function (outputId, from, to, steps, callback) {
		//increment in steps    
		rcb.console._verbosePrint("Stepping " + outputId + " from " + from + " to " + to + " in " + steps + " steps.");
		var status = rcb.console._verbosePrint("");
		var currentStep = 0;
      
        if(!Array.isArray(outputId)){
          outputId = [outputId];
        }
        if(!Array.isArray(from)){
          from = [from];
        }
        if(!Array.isArray(to)){
          to = [to];
        }
        if(to.length !== from.length){
          rcb.console.error("The from and to inputs must be of the same length in the steps function.");
        }
      
		var step = function(){
            var outputVal = [];  
            for(var i=0; i<to.length; i++){
                outputVal[i] = from[i] + Math.round((to[i]-from[i])*(currentStep/steps));
            }
			rcb.console._verboseOverwrite("&nbsp;&nbsp;&nbsp;&nbsp;step " + currentStep + " of " + steps + " (val: " + outputVal + ").", status);
    		rcb.output._control(outputId, outputVal);
    		if(currentStep<steps){
                rcb._callCallback(function(){
                    callback(false, step);
                });
    		}else{
                rcb._callCallback(function(){
                    callback(true, function(){rcb.console.warning("No more steps!")});  
                });	
    		}
    		currentStep++;		    	
		} 
		step();   
	},
};

/**
 * Sensor interface functions.
 * @class
 */
rcb.sensors = {
	/**
	 * Callback for the readSensors function when readings are ready.
	 * @callback readSensorsReady
	 * @param {Object} results - Averaged reading results
	 * @param {function} results.print - Prints the content of the results structure
	 */
	
	/**
	 * Gets new sensor readings. Automatically averages a few readings for reducing noise. IMPORTANT: use the result.print() function to see the structure of the result variable. This structure will vary depending on the hardware (1520 or 1580), if there are accessories connected, or if debug mode is active. See the example below for using the print() function. Note: each entry has 'working' and 'display' sections. 'working' will always remain the same, while 'display' will follow the user's display unit preferences. Use 'display' if reporting to the user, and use 'working' if performing calculations.
	 * @param {readSensorsReady} callback - The function to execute when readings are ready.
	 * @param {integer} [averageQty=5] - The number of samples to average before returning the result.
	 * @example
	 * //This sample script prints the content of the structure
	 * //returned by the rcb.sensors.read callback
	 * rcb.sensors.read(callback);
	 * 
	 * function callback(result){
	 *     //print structure content
	 *     result.print();
	 *     rcb.endScript(); 
	 * }
	 * @example
	 * //Read 10 samples averaged, and print thrust on screen
	 * rcb.sensors.read(callback,10);
	 *
	 * function callback(result){
	 *    var thrust = result.thrust.displayValue;
	 *    var unit = result.thrust.displayUnit;
	 *    rcb.console.print("Thrust: " + thrust.toPrecision(3) + " " + unit);
	 *    rcb.endScript(); 
	 * }
	 */
	read: function (callback,averageQty) {
		if(averageQty===undefined) averageQty = 5;
		var status = rcb.console._verbosePrint("Reading and averaging next " + averageQty + " readings...");
		rcb.vars.callbacks.sensorRead = function(result){
			rcb.console._verboseAppend("done", status);
			//append the print function to the structure
			result.print = function(){
                //help with struct
                rcb.console.warning("Note: each entry has 'working' and 'display' sections. 'working' will always remain the same, while 'display' will follow the user's display unit preferences. Use 'display' if reporting to the user, and use 'working' if performing calculations.");
                
				//print structure content
			    for (var key in result) {
			        if(key!="print"){
				        rcb.console.print('result.'+key);    
				        for (var subkey in result[key]) {
				            rcb.console.print('&nbsp;&nbsp;&nbsp;&nbsp;.' + subkey+' = '+result[key][subkey]);    
				        }
				        rcb.console.print(''); 
				    }
			    }
			}
			rcb._callCallback(callback, result);
		};
		rcb._sendGUIData("sensorsRead",averageQty);
	},
		
	/**
	 * Callback for the readOhm function when reading is ready.
	 * @callback readOhmReady
	 * @param {number} result - Ohmmeter reading. If NaN means value is out of measurement range.
	 */
	
	/**
	 * Reads the ohmmeter. If verbose mode is active, the reading will be displayed on the console.
	 * @param {readOhmReady} [callback] - The function to execute when the reading is ready.
	 * @example
	 * //Gets the ohmmeter reading
	 * rcb.sensors.readOhm(callback);
	 * 
	 * function callback(reading){
	 *     rcb.console.print("Ohm reading: " + reading.toPrecision(4));
	 *     rcb.endScript();
	 * }
	 */
	readOhm: function (callback) {
		var status = rcb.console._verbosePrint('Reading ohmmeter...');
		rcb.vars.callbacks.ohmRead = function(val){
			rcb.console._verboseAppend(val.toPrecision(4), status);
			if(callback){
                rcb._callCallback(callback, val);   
            }
		};
		rcb._sendGUIData("readOhm","");
	},

	/**
	 * Callback for the tareLoadCells function when tare is complete.
	 * @callback tareComplete
	 */
	
	/**
	 * Performs a tare function on the load cells.
	 * @param {tareComplete} [callback] - The function to execute when the tare is complete.
	 * @example
	 * //Simple script that only tares the load cells and finishes when tare is complete.
	 * rcb.sensors.tareLoadCells(rcb.endScript);
	 */
	tareLoadCells: function (callback) {
		var status = rcb.console._verbosePrint('Tare in progress...');
		rcb.vars.callbacks.tareComplete = function(){
			rcb.console._verboseAppend(' done', status);
			if(callback){
                rcb._callCallback(callback);
            }
		};
		rcb._sendGUIData("tareLoadCells","");
	},
	
	/**
	 * Changes the safety limit for a sensor. Units are internal working units (A, V, RPM, g, and N·m) regardless of the user display units. It is not possible to set limits beyond hardware limits.
	 * @param {string} sensorId - "current", "voltage", "rpm", "thrust", or "torque".
	 * @param {number} min - Minimum sensor value before cutoff activates.
	 * @param {number} max - Maximum sensor value before cutoff activates.
	 * @example
	 * rcb.sensors.setSafetyLimit("current",10,20);
	 * //rcb.endScript -> the safety cutoff will prevent motor from spinning
	 */
	setSafetyLimit: function (sensorId, min, max) {
		setTimeout(function(){ //Timout required to ensure the number of poles propagates the updated system limits...
			var id = sensorId.toLowerCase();
			var capitalized = id.charAt(0).toUpperCase() + id.substring(1);
			var minSystem = rcb.vars.systemLimits[sensorId+'Min'];
			var maxSystem = rcb.vars.systemLimits[sensorId+'Max'];
			
			if(id==="current" || id==="voltage" || id==="rpm" || id==="thrust" || id==="torque"){
				if(min<max){
					if(min >= minSystem && max <= maxSystem){
	                	rcb.vars.userLimits[sensorId+'Min'] = min;
	                	rcb.vars.userLimits[sensorId+'Max'] = max;
	                	rcb.console._verbosePrint(capitalized + ' limits set to ['+min+' - '+max+']');
	                	rcb._sendGUIData("changeSafetyLimits",rcb.vars.userLimits);
	                } else rcb.console.error(capitalized + ': ['+min+' - '+max+'] values exceed system limits ['+minSystem+' - '+maxSystem+']');
				}else rcb.console.error('Invalid safety limits input range ['+min+' - '+max+']');
			}else{
				rcb.console.error('Unrecognized sensorId: ' + sensorId + '. Accepted values are "current", "voltage", "rpm", "thrust", "torque".');
			} 	
		},20);				
	},
	
	/**
	 * Changes the number of motor poles. The correct number of poles is required to obtain a correct rpm reading.
	 * @param {integer} numberOfPoles - The motor number of poles. Must be an multiple of 2.
	 * @example
	 * rcb.sensors.setMotorPoles(6);
	 */
	setMotorPoles: function (numberOfPoles) {
		if(numberOfPoles>0 && numberOfPoles%2===0){
			rcb.console._verbosePrint('Setting motor poles to ' + numberOfPoles);
			rcb._sendGUIData("setPoles",numberOfPoles);
		}else
			rcb.console.error("Invalid number of poles: " + numberOfPoles);
	},
    
    /**
	 * Helper function that averages an array of 'results'. Must be an array of 'results', where 'results' is obtained from the read function.
	 * @param {array} resultsArray - An array holding multiple results
     * @return {object} A single averaged results object structure. See the read function for more details on this object.
	 */
    averageResultsArray: function(results) {
        function isNumber(n) {
            return !isNaN(parseFloat(n)) && isFinite(n);
        }

        var average = {};
        results.forEach(function(res){
            for(var key in res){
                if(key!=='print'){
                    if(!average[key]){
                        average[key] = {
                            displayUnit: res[key].displayUnit,
                            workingUnit: res[key].workingUnit,
                            displayValue: res[key].displayValue,
                            workingValue: res[key].workingValue
                        };
                    }else{
                        if(isNumber(res[key].displayValue)){
                            average[key].displayValue += res[key].displayValue;
                        }
                        if(isNumber(res[key].workingValue)){
                            average[key].workingValue += res[key].workingValue;
                        }
                    }
                }
            }
        });
        for(var key in average){
            if(isNumber( average[key].workingValue )){
                average[key].workingValue = average[key].workingValue / results.length;
            }
            if(isNumber( average[key].displayValue )){
                average[key].displayValue = average[key].displayValue / results.length;
            }
        }
        // restore the print function
        average.print = function(){
            rcb.console.warning("Print function not available with the average results array. Not yet implemented, contact us at [email protected] if you need to use this function.");
        };

        return average;
    },
	
	/**
	 * Allows to completely disable safety limits (dangerous function).
	 * @private
	 * @param {boolean} enable - Set to "false" to disable safety limit check.
	 */
	_setSafetyEnable: function (enable) {
		if(!enable){
			rcb.console.warning("Warning: disabling safety cutoffs!");
		}else{
			rcb.console._verbosePrint('Safety cutoffs enabled.');
		}
		rcb._sendGUIData("safetyCutoffDisable",!enable);
	}
};


/**
 * Callback for the wait function.
 * @callback waitDone
 */

/**
 * Waits a certain number of seconds before executing the callback function. Note that calling this function again will cancel a previous wait. Use the javascript setTimeout function if you need multiple delays in parallel.
 * @param {waitDone} callback - The function to execute after the delay is over.
 * @param {number} delay - Wait delay in seconds (can be floating numbers like 0.1 for 100ms).
 * @example
 * //Illustrates the use of the wait and overwrite functions
 * rcb.console.print("LEGEND...");
 * rcb.console.setVerbose(false);
 * rcb.wait(callback1, 2);
 * 
 * function callback1(){
 *     rcb.console.overwrite("LEGEND... wait for it...");
 *     rcb.wait(callback2, 2);
 * }
 * 
 * function callback2(){
 *     rcb.console.overwrite("LEGEND... wait for it... DARY!");
 *     rcb.wait(callback3, 1.5);
 * }
 * 
 * function callback3(){
 *     rcb.console.overwrite("LEGENDARY!");
 *     rcb.endScript();
 * }
 */
rcb.wait = function (callback,delay) {
	var s = "s";
	if(delay===1) s = "";
    if(delay === undefined || delay === null || delay<0 || delay === NaN || !Number(delay)){
        rcb.console.error("Invalid delay parameter for the rcb.wait function");
        return;
    }
	var status = rcb.console._verbosePrint("Waiting " + delay + " second" + s + "...");
	clearTimeout(rcb.vars.callbacks.wait);
	rcb.vars.callbacks.wait = setTimeout(function(){	
        rcb.console._verboseAppend("done", status);        
		rcb._callCallback(callback);
	}, delay*1000);
};

/**
 * File interface functions
 * @class
 */
rcb.files = {
	/**
	 * Creates a new file for logging (in the user's working directory). Generates an error if user's working directory is not set.
	 * @param {Object} [params] - parameters for file creation.
	 * @param {string} [params.prefix="Auto"] - A prefix to append in front of the file.
	 * @param {Array.<string>} [params.additionalHeaders] - Additional header(s) in addition to the standard headers.
	 * @example
	 * //Example script recording 5 rows in a loop
	 * var numberOflines = 5;
	 * 
	 * //Create a new log
	 * rcb.files.newLogFile({prefix: "Example1"});
	 * 
	 * //Start the sequence
	 * readSensor();
	 * 
	 * function readSensor(){
	 *     if(numberOflines>0){
	 *         rcb.sensors.read(readDone);
	 *         numberOflines--;
	 *     }else
	 *         rcb.endScript();
	 * }
	 * 
	 * function readDone(result){
	 *     rcb.files.newLogEntry(result,readSensor);
	 * } 
	 * @example
	 * //Example recording with extra data
	 * var numberOfLines = 5;
	 * 
	 * //Create a new log
	 * var add = ["Remaining", "Line"];
	 * rcb.files.newLogFile({prefix: "Example2", additionalHeaders: add});
	 * 
	 * //Start the sequence
	 * readSensor();
	 * 
	 * function readSensor(){
	 *     if(numberOfLines>0){
	 *         rcb.sensors.read(readDone);
	 *         numberOfLines--;
	 *     }else
	 *         rcb.endScript();
	 * }
	 * 
	 * function readDone(result){
	 *     var add = [numberOfLines, 5-numberOfLines];
	 *     rcb.files.newLogEntry(result,readSensor,add);
	 * }
	 * @example
	 * //This example continuously records data at full
	 * //speed until the user stops the script
	 * 	  
	 * //Create a new log
	 * rcb.files.newLogFile({prefix: "Continuous"});
	 * 
	 * //Start the sequence
	 * readSensor();
	 * 	  
	 * function readSensor(){
	 *     rcb.sensors.read(saveResult,10);
	 * }
	 * 
	 * function saveResult(result){
	 *     rcb.files.newLogEntry(result, readSensor);
	 * }
	 */
	newLogFile: function (params) {
		if(params===undefined) params={};
		if(params.prefix===undefined) params.prefix = "Auto";
		rcb._sendGUIData("newLogFile",params);
		rcb.files.newLogFile.called = true;
		rcb.files.newTextFile.called = false;
		rcb.console._verbosePrint('Creating new log file with "' + params.prefix + '" prefix in working directory');
	},
	
	/**
	 * Callback for the newLogEntry function when finished writing to file.
	 * @callback newLogSaved
	 */
	
	/**
	 * Records a new entry to the created log file. Generates an error if the function newLogFile was never called.
	 * @param {Object} readings - Sensor readings as returned by the readSensors function.
	 * @param {newLogSaved} [callback] - The function to execute when recording is done.
	 * @param {Array.<number|string>} [additionalValues] - Additional values to append to the entry.
	 */
	newLogEntry: function (readings,callback,additionalValues) {
        var restorePrint = readings.print;
		delete readings.print;
		if(!rcb.files.newLogFile.called) rcb.console.error("newLogFile function must be called before calling the newLogEntry function");
		else{
			var status = rcb.console._verbosePrint("Saving new log entry...");
			rcb.vars.callbacks.newLogEntry = function(){
				rcb.console._verboseAppend("done", status);
				if(callback) {
                    rcb._callCallback(callback);
                }
			}
			params={};
			params.data = readings;
			params.additionalValues = additionalValues;
			rcb._sendGUIData("newLogEntry",params);
		}
        readings.print = restorePrint;
	},

	/**
	 * Creates a new empty file for writing raw text (in the user's working directory). Generates an error if user's working directory is not set.
	 * @param {Object} [params] - parameters for file creation.
	 * @param {string} [params.prefix="Auto"] - A prefix to append in front of the file.
	 * @param {string} [params.extension="txt"] - Custom file extension.
	 * @example
	 * //Example recording to a raw text file
	 * rcb.files.newTextFile({prefix: "RawTextExample"});
	 * rcb.files.appendTextFile("Plain raw text\r\n", function(){
	 *     rcb.files.appendTextFile("A new line...", function(){
	 *         rcb.files.appendTextFile("same line...", function(){
	 *             rcb.files.appendTextFile("more...", rcb.endScript);
	 *         });
	 *     });
	 * });
	 */
	newTextFile: function (params) {
		if(params===undefined) params={};
		if(params.prefix===undefined) params.prefix = "Auto";
		if(params.extension===undefined) params.extension = "txt";
		rcb._sendGUIData("newTextFile",params);
		rcb.files.newTextFile.called = true;
		rcb.files.newLogFile.called = false;
		rcb.console._verbosePrint('Creating new text file with "' + params.prefix + '" prefix in working directory');
	},

	/**
	 * Callback for the appendTextFile function when finished writing to file.
	 * @callback textSaved
	 */
	
	/**
	 * Appends new text to the created text file. Generates an error if the function newTextFile was never called.
	 * @param {String} text - The raw text to append to the file.
	 * @param {textSaved} [callback] - The function to execute when recording is done.
	 */
	appendTextFile: function (text,callback) {
		if(!rcb.files.newTextFile.called) rcb.console.error("newTextFile function must be called before calling the appendTextFile function");
		else{
			var status = rcb.console._verbosePrint("Writing text to file...");
			rcb.vars.callbacks.appendTextFile = function(){
				rcb.console._verboseAppend("done", status);
				if(callback){
                    rcb._callCallback(callback);
                }
			}
			params={};
			params.data = text;
			rcb._sendGUIData("appendTextFile",params);
		}
	},
};