Last Updated: 2020-07-03
Navicat is a Java library for use in FTC robot control systems that helps with navigation. Navicat builds on two other libraries we have created: Schrodinger for state machines and Nyan for subsystems. It also uses the awesome motion planning library Road Runner, which was created by Team 8367 Acme Robotics.
Navigation means a robot's ability to know its location (also called localization) and move to a target location. With a good navigation system, you can focus more on giving the robot instructions on where to go and what to do instead of how to get there.
With Road Runner, you can specify complex paths for your robot to travel including start and finish orientation and position. Road Runner figures out how the motors should be powered to follow the path, and computes the position of the robot using odometry in order to make adjustments according to the actual speed and travel of the wheels. It also uses motion profiling to constrain the velocity, acceleration, and jerk of the robot to reduce wheel slippage and wear on robot parts. You can learn a lot more about Road Runner in this tour.
By using Schrodinger, Nyan, & Navicat together, you can create autonomous op modes which have a powerful, easy-to-use, reliable navigation ability which is accessible as a subsystem and is part of a state machine framework which makes it easy for robot components to perform complex operations while navigating.
Currently, Navicat works only for robots with a mecanum drivetrain and no odometry free wheels. For the purpose of this codelab, the drivetrain motors must have encoders which are properly configured, also.
In this codelab, you're going to go through the steps required to setup Road Runner for your robot and create an op mode for an FTC robot that uses the Navicat library for navigation.
If you do not already have Android Studio installed, follow the instructions for Installing Android Studio
Download the Road Runner Quickstart project from https://github.com/acmerobotics/road-runner-quickstart/archive/master.zip. Unzip the file and open the project in Android Studio.
Find and open the DriveConstraints class.
The first two configuration values we'll set are specific to the motor in your drive train and do not require testing. You can find these values in information provided by the motor manufacturer. TICKS_PER_REV refers to the encoder ticks per revolution of the motor and MAX_RPM refers to the maximum RPMs of the motor itself.
public static final double TICKS_PER_REV = 28;
public static final double MAX_RPM = 6000;
The next three values can all be determined by examining the drivetrain: the radius of the drive wheels, the gear ratio which is applied between the motor and the wheels, and the track width between left and right wheels. Please note that the gear ratio should include any gearbox which is mounted onto the motor. For example, a Neverest 40 motor with a mounted 80-tooth gear driving a 40-tooth gear on the wheel axle would have a total gear ratio of 20:1. The example below shows a simple case of a "20:1" gearbox, such as the Rev or AndyMark orbitals. These have an actual ratio of 19.2:1.
public static double WHEEL_RADIUS = 1.475; // in
public static double GEAR_RATIO = 1 / 19.2; // output (wheel) speed / input (motor) speed
public static double TRACK_WIDTH = 14; // in
Next, we'll need to make a few modifications to the SimpleMecanumDrive class. We'll update the motor names and imu name to match the configuration for your robot, and reverse the direction for two of the motors.
If your robot configuration has a different name for the imu, replace the string in line 108. Similarly, update the motor names in lines 117-120 to match your robot configuration.
imu = hardwareMap.get(BNO055IMU.class, "imu");
BNO055IMU.Parameters parameters = new BNO055IMU.Parameters();
parameters.angleUnit = BNO055IMU.AngleUnit.RADIANS;
imu.initialize(parameters);
// TODO: if your hub is mounted vertically, remap the IMU axes so that the z-axis points
// upward (normal to the floor) using a command like the following:
// BNO055IMUUtil.remapAxes(imu, AxesOrder.XYZ, AxesSigns.NPN);
leftFront = hardwareMap.get(DcMotorEx.class, "leftFront");
leftRear = hardwareMap.get(DcMotorEx.class, "leftRear");
rightRear = hardwareMap.get(DcMotorEx.class, "rightRear");
rightFront = hardwareMap.get(DcMotorEx.class, "rightFront");
Insert lines 141-142 as shown below to reverse the direction of the two right motors since these face the opposite direction from the left motors.
// TODO: reverse any motors using DcMotor.setDirection()
rightFront.setDirection(DcMotorSimple.Direction.REVERSE);
rightRear.setDirection(DcMotorSimple.Direction.REVERSE);
With all the necessary DriveConstant values set, we're ready to run the first test, LocalizationTest. While this op mode is running and you drive the robot, Road Runner is computing the robot's position and heading based on the DriveConstants you provided earlier (gear ratio, wheel size, etc.) in combination with the information about the activity of the motors from the motor encoders. Make sure that the values displayed correspond to the movement of the robot as you drive.
In this example, we have driven the robot straight for 12 inches, and we see that reflected on the Driver Station phone.
If you haven't heard of Proportional-Integral-Derivative (PID) Control before, it probably sounds really complex. But, the basic concepts are pretty simple. If we take driving as an example, proportionate control would have you turn the steering wheel more when the car is further out of it's lane. Integral control would have you turn the steering wheel more if it doesn't respond enough when you turn it less. Derivative control would have you turn the steering wheel more if the car is moving further out of its lane.
The next test is about making sure that the motor controllers in the expansion (or control) hub has PID constants which cause the motors to move precisely (as much as possible) at the speeds that Road Runner computes for navigating along the paths that are specified. So, if the motor needs to speed up a lot, the motor controller should set the power much higher. If the amount of power supplied isn't making the motor goes fast enough, it should be increased, etc.
The test has the robot repeatedly travel back and forth over a distance of 72 inches. If you don't have that much room available, change the distance to a smaller number (of inches).
While running the test, you'll use the FTC Dashboard at http://192.168.49.1:8080/dash to see how closely the actual motor velocity (green below) matches the target (red below). Change the kP, kI, & kD parameters under VELO_PID in the dashboard to reduce the distance between the red & green lines (i.e. error). The official Quickstart tuning guide describes the process as the following:
https://acme-robotics.gitbook.io/road-runner/quickstart/tuning#drive-velocity-pid
If you struggle with finding the best values, see how well you can do by just adjusting kP, working your way up from a low value slowly.
When you have determined the values for kP, kI, & kD, put them in the MOTOR_VELO_PID constant in DriveConstants:
public static final PIDCoefficients MOTOR_VELO_PID = new PIDCoefficients(15.0, 0.0, 0.0);
The next test will validate the values set by the preceding steps. StraightTest will use RoadRunner to plan and execute a drive 60 inches straight ahead. The robot should land within a few inches of the target distance.
TrackWidthTuner will calculate the best value to use for your robot's TRACK_WIDTH parameter by turning the robot and monitoring the actual heading as reported by the IMU.
The computed track width is displayed as telemetry on the Driver Station. Use this value to replace the one you entered into DriveConstants.java.
TurnTest will allow you to verify that the calculated TRACK_WIDTH is suitable for the turning characteristics of your robot. Try changing the ANGLE constant in TurnTest.java to several different values and observing how well the robot matches after running the op mode.
Now that you've done all the tuning to configure Road Runner for your robot, SplineTest will show off the ability that Road Runner provides to smoothly execute complex paths.
Up until this point, Road Runner has been keeping track of the estimated position of the robot during tests, but hasn't used this information to affect it's movement. FollowerPIDTuner is your opportunity to turn on and tune the follower capability which uses the robot's position as feedback to make corrections to the robot's movements. Find the TRANSLATIONAL_PID and HEADING_PID constants in the SampleMecanumDrive.java class and try low single-digit values for kP to improve the robot's ability to execute the square pattern accurately.
If you are familiar with git, create a new clone of the FTC project repository on your computer. Otherwise, just follow these instructions for Downloading the Android Studio Project Folder
You'll need to let Android Studio (the gradle build system, really) know from where it can download the schrodinger, nyan, and navicat libraries. Add maven { url 'https://dl.bintray.com/crazycatladies/maven' }
to the repositories section at the bottom of the build.gradle
file in the root folder of your project.
/**
* Top-level build file for ftc_app project.
*
* It is extraordinarily rare that you will ever need to edit this file.
*/
buildscript {
repositories {
google()
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:3.4.1'
}
}
// This is now required because aapt2 has to be downloaded from the
// google() repository beginning with version 3.2 of the Android Gradle Plugin
allprojects {
repositories {
google()
jcenter()
maven { url 'https://dl.bintray.com/crazycatladies/maven' }
}
}
You'll make your project dependent on Schrodinger, Nyan, and Navicat, which will cause Android Studio (gradle, really) to download the library, by adding six new entries (shown below) to the end of the dependencies section in TeamCode/build.release.gradle
dependencies {
implementation project(':FtcRobotController')
implementation (name: 'RobotCore-release', ext: 'aar')
implementation (name: 'Hardware-release', ext: 'aar')
implementation (name: 'FtcCommon-release', ext: 'aar')
implementation (name: 'WirelessP2p-release', ext:'aar')
implementation (name: 'tfod-release', ext:'aar')
implementation (name: 'tensorflow-lite-0.0.0-nightly', ext:'aar')
implementation 'com.acmerobotics.roadrunner:core:0.4.7'
implementation 'org.openftc:rev-extensions-2:1.2'
implementation 'com.acmerobotics.dashboard:dashboard:0.3.7'
implementation 'ftc.crazycatladies:schrodinger:0.1.4'
implementation 'ftc.crazycatladies:nyan:0.1.1'
implementation 'ftc.crazycatladies:navicat:0.1.15'
}
Nyan uses Java 8 features, and you'll probably want to do the same in your own code. You can enable Java 8 by changing the compileOptions
section in build.common.gradle
(in the root folder of the project):
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
After changing and saving these files, you should be able to update gradle by clicking "Sync Now" in the bar which appears at the top of these files:
If everything has gone well, your project will now reference the downloaded schrodinger, nyan, navicat, and roadrunner external libraries, which you can see in the "Project" view of the Project window:
If you've created a new project for trying out Navicat, copy the DriveConstants.java file from the Quick Start project into your new project.
If you've completed the Nyan codelab, you're aware that Nyan provides a framework for creating Autonomous op modes that cuts down on the amount of code that you need to write. Navicat takes this further with base classes for your autonomous op mode and top-level robot class which has navigation features built in. The result is that you can have a navigation-enabled autonomous op mode with only the 27 lines of code shown here:
package org.firstinspires.ftc.teamcode;
import com.acmerobotics.roadrunner.control.PIDCoefficients;
import com.qualcomm.robotcore.eventloop.opmode.Autonomous;
import ftc.crazycatladies.navicat.navigation.*;
import ftc.crazycatladies.schrodinger.state.*;
@Autonomous(name = "AwesomeAuto")
public class AwesomeAuto extends AbstractNavAuto {
PIDCoefficients TRANSLATIONAL_PID = new PIDCoefficients(3, 0, 0);
PIDCoefficients HEADING_PID = new PIDCoefficients(3, 0, 0);
public AwesomeAuto() {
robot = navRobot = new NavRobot(true,
DriveConstants.class, TRANSLATIONAL_PID, HEADING_PID,
new MecanumDrive.DriveMotorConfig("FL", 2, "FR", 2,
"BL", 2, "BR", 2),
"imu", "imu 1");
}
@Override
protected StateMachine stateMachine() {
StateMachine sm = new StateMachine("AwesomeAutoSM");
return sm.init();
}
}
You'll recognize the TRANSLATIONAL_PID & HEADING_PID constants from FollowerPIDTuner. Replace the values with the ones that you have determined for your own robot. You'll also want to use the appropriate drive motor names for you robot in place of "FL", "FR", "BL", and "BR", along with the hub number for each (in place of "2"). Update "imu" & "imu 1" if your have different names, and replace "imu 1" with null if you have only a single control/expansion hub.
This op mode won't do anything interesting, but try it out to make sure that there aren't any errors, and then we'll make it more interesting.
In the Nyan codelab, you'll see a description of how you can orchestrate the actions of your robot in an autonomous op mode using a state machine. That's exactly what we'll do here by adding to the stateMachine() method:
@Override
protected StateMachine stateMachine() {
StateMachine sm = new StateMachine("AwesomeAutoSM");
sm.once((state, context) -> {
navRobot.getNav().setPoseEstimate(0, 0, 0);
go(splineTo(30, 30, 0));
// (optional) start operations in subsystem state machines
});
sm.repeat(waitForNav());
sm.once((state, context) -> {
go(slowTo(30, -30, 0));
});
sm.repeat(waitForNav());
sm.once((state, context) -> {
go(reverseSplineTo(0, 0, 180));
});
sm.repeat(waitForNav());
sm.once((state, context) -> {
go(to(30, 30, 90));
});
sm.repeat(waitForNav());
sm.once((state, context) -> {
go(to(30, -30, 0));
});
sm.repeat(waitForNav());
sm.once((state, context) -> {
go(to(0, 0, -90));
});
sm.repeat(waitForNav());
return sm.init();
}
Note that several different navigation methods are used to create the complex path that the robot follows: splineTo, slowTo, reverseSplineTo, and to. Each of these methods takes a target pose (x, y, & heading). Although Road Runner natively uses radians, Navicat accepts degree values to make things easier.