Drive to position

Now that we have a Navigator class which we can consult to give us the robot’s position and orientation, we want to create a way to drive to a specific point on the field.  We will use this in two ways.  First it will enable us to create a command which will move the robot into position to make a shot from anywhere on the field. Secondly, we will be able to use the Robot Mapper program on the driver station to control the robot.

We are going to accomplish this by creating two new commands.  The first command will rotate the robot to a specific location on the field.  The second command will drive to that location. Let’s call the first command RotateToPointCommand.java.  Go ahead and create the framework for this new command and connect it to our test button 12 on the joystick.

In the constructor, we will need to pass in the destination point as follows:

    public RotateToPointCommand(double targetX, double targetY) 
    {
    	Logger.Log("RotateToPointCommand", 3, "RotateToPointCommand()");
    	
    	m_targetX	= targetX;
    	m_targetY	= targetY;
    	
        // Use requires() here to declare subsystem dependencies
        requires(Robot.m_driveSubsystem);
    }

Note that this command will also require the DriveSubsystem so we ask for that as well.  The member variables m_targetX and m_targetY will be used to store the target location and must be delcared:

	private double		m_targetX;
	private double		m_targetY;

Note that since your constructor requires an X and Y value specified, when you create the instance of this class for your OI class, you will need to pick a location.  Let’s use (0, 0) which should point the robot to the base of the tower.  This line in your OI.java file should look like:

    	m_test.whenPressed(new RotateToPointCommand(0, 0));

When the initialize() function is called, we will need to compute the angle to which we must turn to point at the target location.  Using trigonometry and the robot’s current position, we can calculate this angle as follows.

    protected void initialize() 
    {
    	RobotPosition	position	= Robot.m_navigator.GetPosition();
    	double			dx			= m_targetX - position.m_x;
    	double			dy			= m_targetY - position.m_y;
    	
    	m_targetAngle	= Math.atan2(dx, dy);

    	Logger.Log("RotateToPointCommand", 2, String.format("initialize: targetAngle = %.1f", m_targetAngle));
    	
    	m_turnLeft	= GetDeltaAngle(position) > 0;
    }

Declaring the member variables m_targetAngle and m_turnLeft as follows:

	private double 		m_targetAngle;
	private boolean		m_turnLeft;

Note that we also determine whether the robot will be turning left or right.  We do this by computing the difference between the current yaw and the targetAngle.  If this value is greater than zero, then we will need to turn left, otherwise we return right.  Since we will need to compute this deltaAngle in multiple places, we will create a function, GetDeltaAngle which we will use.  This will allow us to keep this calculation in one place in case we need to modify it later.

    private double GetDeltaAngle(RobotPosition position)
    {
    	return(Navigator.NormalizeAngle(position.m_yaw - m_targetAngle));
    }

Here we compute the difference between the current robot angle (yaw) and the target angle.  To make things easier, we want to force this angle to be between -180 and 180 degrees and we have created a function, NormalizeAngle, which does just that.  Rather than implementing this function in this command we are going to add it as a utility function to our Navigation class thus enabling us to use it for other command that we may create.

    public static double NormalizeAngle(double angle)
    {
    	if (angle > 180)
    	{
    		angle	-= 360;
    	}
    	else if (angle <= -180)
    	{
    		angle	+= 360;
    	}
    	
    	return(angle);
    }

We make this function static so we can access it even if we don’t have an instance of the Navigation class handy.

Now back in our RotateToPointCommand class we need to change the execute() function to set the power to the motors to make the robot turn.  We will want to adjust the power depending on how var from the target angle the robot is:

    protected void execute() 
    {
    	Logger.Log("RotateToPointCommand", -1, "execute()");
    	
    	double	deltaAngle	= GetDeltaAngle();
    	double	speed		= Math.abs(deltaAngle) * k_speedScale;
    	
    	if (speed < k_minSpeed)
    	{
    		speed	= k_minSpeed;
    	}
    	else if (speed > k_maxSpeed)
    	{
    		speed	= k_maxSpeed;
    	}
    	
    	if (m_turnLeft)
    	{
    		Robot.m_driveSubsystem.SetSpeed(-speed, speed);
    	}
    	else
    	{
    		Robot.m_driveSubsystem.SetSpeed(speed, -speed);
    	}
    }

We set the speed based on the current deltaAngle, subject to a minimum and maximum speed and then set the motors according to which direction we are turning.  The associated constants are defined as follows:

	private static final double		k_minSpeed		= 0.10;
	private static final double		k_maxSpeed		= 0.50;
	private static final double		k_speedScale	= 0.01;

To get the deltaAngle we are once again calling GetDeltaAngle, but because we don’t have the robot’s position handy here we are creating a second version which will first obtain the position:

    private double GetDeltaAngle()
    {
    	return(GetDeltaAngle(Robot.m_navigator.GetPosition()));
    }

Next we need to change isFinised() to return true when the robot has reached the correct angle:

    protected boolean isFinished() 
    {
    	Logger.Log("RotateToPointCommand", -1, "isFinished()");
    	
    	double			deltaAngle	= GetDeltaAngle();
    
    	if (m_turnLeft)
    	{
    		return(deltaAngle <= 0);
    	}
    	else
    	{
    		return(deltaAngle >= 0);
    	}
    }

Note that the test differs depending on whether we are turning left or right.

Finally, once the turn is complete, we need to turn the motors off in the end() and interrupted() functions:

    protected void end() 
    {
    	Logger.Log("RotateToPointCommand", 2, "end()");
    	
    	Robot.m_driveSubsystem.SetSpeed(0, 0);
    }

    // Called when another command which requires one or more of the same
    // subsystems is scheduled to run
    protected void interrupted() 
    {
    	Logger.Log("RotateToPointCommand", 2, "interrupted()");
    	
    	Robot.m_driveSubsystem.SetSpeed(0, 0);
    }

Your RotateToPointCommand.java file should now look like:

package commands;

import robot.Navigator;
import robot.Navigator.RobotPosition;
import robot.Robot;
import robotCore.Logger;
import wpilib.Command;

/**
 *
 */
public class RotateToPointCommand extends Command 
{
	private static final double		k_minSpeed		= 0.10;
	private static final double		k_maxSpeed		= 0.50;
	private static final double		k_speedScale	= 0.01;
	
	private double		m_targetX;
	private double		m_targetY;
	private double 		m_targetAngle;
	private boolean		m_turnLeft;
	
    public RotateToPointCommand(double targetX, double targetY) 
    {
    	Logger.Log("RotateToPointCommand", 3, "RotateToPointCommand()");
    	
    	m_targetX	= targetX;
    	m_targetY	= targetY;
    	
        // Use requires() here to declare subsystem dependencies
        requires(Robot.m_driveSubsystem);
    }
    
    private double GetDeltaAngle(RobotPosition position)
    {
    	return(Navigator.NormalizeAngle(position.m_yaw - m_targetAngle));
    }
    
    private double GetDeltaAngle()
    {
    	return(GetDeltaAngle(Robot.m_navigator.GetPosition()));
    }

    // Called just before this Command runs the first time
    protected void initialize() 
    {
    	RobotPosition	position	= Robot.m_navigator.GetPosition();
    	double			dx			= m_targetX - position.m_x;
    	double			dy			= m_targetY - position.m_y;
    	
    	m_targetAngle	= Math.atan2(dx, dy);

    	Logger.Log("RotateToPointCommand", 2, String.format("initialize: targetAngle = %.1f", m_targetAngle));
    	
    	
    	m_turnLeft	= GetDeltaAngle(position) > 0;
    }

    // Called repeatedly when this Command is scheduled to run
    protected void execute() 
    {
    	Logger.Log("RotateToPointCommand", -1, "execute()");
    	
    	double	deltaAngle	= GetDeltaAngle();
    	double	speed		= Math.abs(deltaAngle) * k_speedScale;
    	
    	if (speed < k_minSpeed)
    	{
    		speed	= k_minSpeed;
    	}
    	else if (speed > k_maxSpeed)
    	{
    		speed	= k_maxSpeed;
    	}
    	
    	if (m_turnLeft)
    	{
    		Robot.m_driveSubsystem.SetSpeed(-speed, speed);
    	}
    	else
    	{
    		Robot.m_driveSubsystem.SetSpeed(speed, -speed);
    	}
    }

    // Make this return true when this Command no longer needs to run execute()
    protected boolean isFinished() 
    {
    	Logger.Log("RotateToPointCommand", -1, "isFinished()");
    	
    	double			deltaAngle	= GetDeltaAngle();
    
    	if (m_turnLeft)
    	{
    		return(deltaAngle <= 0);
    	}
    	else
    	{
    		return(deltaAngle >= 0);
    	}
    }

    // Called once after isFinished returns true
    protected void end() 
    {
    	Logger.Log("RotateToPointCommand", 2, "end()");
    	
    	Robot.m_driveSubsystem.SetSpeed(0, 0);
    }

    // Called when another command which requires one or more of the same
    // subsystems is scheduled to run
    protected void interrupted() 
    {
    	Logger.Log("RotateToPointCommand", 2, "interrupted()");
    	
    	Robot.m_driveSubsystem.SetSpeed(0, 0);
    }
}

Your OI.java file should look like:

package robot;

import commands.AutoShootCommand;
import commands.CalibrateAngleCommand;
import commands.CalibrateDistanceCommand;
import commands.ExampleCommand;
import commands.IntakeCommand;
import commands.RotateToPointCommand;
import commands.ShootCommand;
import commands.SpinupCommand;
import robotCore.Joystick;
import wpilib.Button;
import wpilib.JoystickButton;

/**
 * This class is the glue that binds the controls on the physical operator
 * interface to the commands and command groups that allow control of the robot.
 */
public class OI {
    //// CREATING BUTTONS
    // One type of button is a joystick button which is any button on a joystick.
    // You create one by telling it which joystick it's on and which button
    // number it is.
     Joystick m_stick = new Joystick(0);
     Button m_trigger = new JoystickButton(m_stick, 1);
     Button m_spinup	= new JoystickButton(m_stick, 2);
     Button m_intake	= new JoystickButton(m_stick, 8);
     Button m_test		= new JoystickButton(m_stick, 12);
     Button m_autoShoot	= new JoystickButton(m_stick, 3);
    
    // There are a few additional built in buttons you can use. Additionally,
    // by subclassing Button you can create custom triggers and bind those to
    // commands the same as any other Button.
    
    //// TRIGGERING COMMANDS WITH BUTTONS
    // Once you have a button, it's trivial to bind it to a button in one of
    // three ways:
    
    // Start the command when the button is pressed and let it run the command
    // until it is finished as determined by it's isFinished method.
    public OI()
    {
//    	m_trigger.whenPressed(new ExampleCommand());
    	m_spinup.whileHeld(new SpinupCommand());
    	m_trigger.whileHeld(new ShootCommand());
    	m_intake.whileHeld(new IntakeCommand());
//    	m_test.whileHeld(new SpinnerSpeedTestCommand());
//    	m_test.whileHeld(new DriveSpeedTestCommand());
//    	m_test.whenPressed(new CalibrateDistanceCommand());
    	m_test.whenPressed(new RotateToPointCommand(0, 0));
//    	m_test.whenPressed(new ExampleCommand());
//    	m_test.whenPressed(new CalibrateAngleCommand());
    	m_autoShoot.whenPressed(new AutoShootCommand());
    }
}

and your Navigator.java file should now look like:

package robot;

import robot.PiCamera.PiCameraData;
import robotCore.Encoder;
import robotCore.Gyro;
import robotCore.Logger;
import robotCore.Network;
import robotCore.Network.NetworkReceiver;
import robotCore.RobotBase;

public class Navigator implements NetworkReceiver
{
	private static final int	k_networkPort	= 5803;
	private static final double k_ticksPerInch	= 8000.0/38.5;
	
	private Gyro			m_gyro		= new Gyro();
	private RobotPosition	m_position	= null;
	private Encoder			m_leftEncoder		= null;
	private Encoder			m_rightEncoder		= null;
	private int				m_lastLeftEncoder	= 0;
	private	int				m_lastRightEncoder	= 0;
	private Network			m_network			= null;
	
	public class RobotPosition
	{
		public double	m_x;
		public double	m_y;
		public double	m_yaw;
		
		RobotPosition(double x, double y, double yaw)
		{
			m_x		= x;
			m_y		= y;
			m_yaw	= yaw;
		}
	}
	
	public RobotPosition GetPosition()
	{
		synchronized(this)
		{
			return(m_position);
		}
	}
	
	private boolean IsTargetVisible(PiCameraData data)
	{
		return(	(data.m_y1 > 2) &&
				(data.m_y2 > 2) &&
				(data.m_x > 2) &&
				((data.m_x + data.m_width) < 638));
	}
	
	private void Update()
	{
		PiCameraData	data 			= Robot.m_piCamera.GetCameraData();
		int				yaw 			= m_gyro.GetYaw();
		int				leftEncoder		= m_leftEncoder.get();
		int 			rightEncoder	= m_rightEncoder.get();
		
		if ((data != null) && IsTargetVisible(data))
		{
			double	top				= (data.m_y1 + data.m_y2) / 2;
			double	center			= (data.m_x + (data.m_width / 2));
			double	angleToCenter	= 0.1014*center - 34.061;
			double	distance 		= (0.000271*top*top + 0.0146*top + 16.64) * (268.46 / (268.46 - 0.0577*angleToCenter*angleToCenter));
			double	angle 			= yaw + angleToCenter;
			double	x 				= -distance * Math.sin(angle * Math.PI/180);
			double	y 				= distance * Math.cos(angle * Math.PI/180);	
		
			synchronized(this)
			{
				m_position	= new RobotPosition(x, y, yaw);
			}

			m_network.SendMessage(String.format("c %.1f %.2f %.2f", m_position.m_yaw, m_position.m_x, m_position.m_y));
		}
		else if (m_position != null)
		{
			double	deltaLeft	= (leftEncoder - m_lastLeftEncoder) / k_ticksPerInch;
			double	deltaRight	= (rightEncoder - m_lastRightEncoder) / k_ticksPerInch;
			double	distance	= (deltaLeft + deltaRight) / 2;
			
			if (distance != 0)
			{
				double	radians	= (m_position.m_yaw * Math.PI) / 180;
				double	deltaX	= distance * Math.sin(radians);
				double	deltaY	= distance * Math.cos(radians);
				
				synchronized(this)
				{
					m_position	= new RobotPosition(m_position.m_x + deltaX, m_position.m_y - deltaY, yaw);
				}
				
				m_network.SendMessage(String.format("p %.1f %.2f %.2f", m_position.m_yaw, m_position.m_x, m_position.m_y));
			}
			
		}
		
		m_lastLeftEncoder	= leftEncoder;
		m_lastRightEncoder	= rightEncoder;
		
		Robot.Sleep(50);
	}
	
	public void Init()
	{
		m_gyro.Reset();
		
		m_leftEncoder	= Robot.m_driveSubsystem.GetLeftEncoder().Clone();
		m_rightEncoder	= Robot.m_driveSubsystem.GetRightEncoder().Clone();
		
		RobotBase.Schedule(new Runnable() 
			{
				@Override
				public void run() 
				{
					Update();
				}
			
			}, 50);
		
		m_network	= new Network();
		m_network.Listen(this, k_networkPort);
	}
	
	@Override
	public void ProcessData(String command) 
	{
	}
	
    public static double NormalizeAngle(double angle)
    {
    	if (angle > 180)
    	{
    		angle	-= 360;
    	}
    	else if (angle <= -180)
    	{
    		angle	+= 360;
    	}
    	
    	return(angle);
    }
	
}

Now go ahead and test your program.  Remember that your robot must start facing the tower to get the zero angle properly set.  Then rotate the robot away from the tower and press button 12.  The robot should turn to face the tower.

Next: Drive to position part 2