A method of running the same WPILib Project on 2 chassis

The Problem

On 78 we ran into an interesting, and I suspect pretty common, issue. We have two swerve chassis, similar but with slightly different dimensions, that we want to run simultaneously throughout the season. One will be the competition chassis, the other a test chassis used for April tag alignment and first-iteration testing of autonomous routines.

Swerve drive kinematics and odometer are very particular about robot measurements, and even the closed-loop parameters will change between bots. We also want different controls on one bot vs the other.

Ideal Solution

The simplest solutions would be to have two code projects. I didn’t like this because any work that you do on the test robot would need to be copied over to the competition bot project, and vice versa. This would get really messy as the code bases mature, and keeping everything in sync would be a nightmare and we’d probably end up giving up on the test bot altogether.

The overall solution is to have 2 RobotContainer objects, one for the test chassis, and one for the competition chassis. Each one would define (either directly in the RobotContainer class or an adjacent Constants class) the necessary robot characteristics to be used in shared SwerveDrive, April Tag Detection, and any other shared functionality classes / packages. It should be impossible to mix up the constants in the code, and should be hard to accidentally run the test robot code on the competition bot, and vice versa. Finally, the method of determining which robot was being run should not require a redeploy under any circumstance.

I’ve seen some solutions to this that utilize the MAC address of the wifi radio. MAC addresses are unique to hardware devices, so this is certainly a solution that could work, and teams make it work for them. I didn’t like this solution because the MAC address for the test robot would need to be defined in the code. While we could default to the competition code if an unknown radio was detected, if the radio on the test-chassis was swapped out, we’d have to redeploy code to detect the new radio.

Instead, I decided that Environment Variables were the solution. It should have been obvious from the start, since it’s right there in the name. We want to adjust code based on the environment the code is running in (in this case, which roborio).

The Setup

Our project now has two packages: frc.robot.test for the test robot and frc.robot.competition for the competition bot. Inside each of these packages are 3 files:

Robot.java
RobotContainer.java
RobotConstants.java

Both sets of files act similarly to how they would in a single bot setup. Robot.java handles the init / periodic methods, RobotContainer sets up subsystems, commands, and trigger bindings, and RobotConstants contains static constants that the subsystems use.

Both RobotContainer and RobotConstants are package-private, so it’s impossible to access them directly in any subsystem or command class. Instead, the commands / subsystems should accept config parameters in their constructors, and each RobotContainer is responsible for configuring the subsystems as is necessary for each robot.

The magic happens in the main method.

public static void main(String... args) {
    if ("TEST".equalsIgnoreCase(System.getenv("FRC_BOT"))) {
      RobotBase.startRobot(frc.robot.test.Robot::new);
    } else {
      RobotBase.startRobot(frc.robot.competition.Robot::new);
    }
  }

When the program starts, we check the environment variable FRC_BOT. If it is set to TEST, that launches the test Robot, and if not, we launch the competition bot.

Setting an environment variable on the roboRIO is easy. You can ssh in using powershell or terminal (ssh [email protected]) and add export FRC_BOT=TEST to the end of .profile in lvuser’s home folder.

This solution means that we do not have to worry about having specific hardware connected to the test chassis to identify it as a test bot, and on the off chance the test roborio was put on the competition robot, no redeploy of code is required. Just ssh in and remove the environment variable from being set.

1 Like

One of my favorite ways to do this is a HW jumper attached to a spare DIO (most teams nowadays have plenty). And then you just read that DIO on startup. Easier then handling an environment variable usually.

The RobotController class provides some functions you can use for this too:

  • getComments() can be used to read the “comments” field settable via the Rio’s web interface
  • getSerialNumber() can be used to get the unique serial number of the Rio

You could also just copy a file to one and not the other and check for its existence.

1 Like

Can you read that even before calling RobotBase.startRobot? Or would you need a shared Robot to be created on startup?

My team did this a few years ago by saving a Properties file on both Roborios and checking that file in robotInit.

Oh. No it can’t be read that early. We generally do not recommend modifying main at all, because depending on what you add it can cause robot code to not boot. You have to be careful to make sure no static constructors are run either.

I had some suggestions for this. Comments seemed similar to envvars, though not as structured it seems. And some teams disable the web interface altogether.

The rio SN would have the same problem as the MAC address, where the code would have to change if the hardware changed. But I think it’s a better solution that the MAC address because of its much simpler API use

We implemented @Peter_Johnson ‘s suggestion…


        // gets the serial number off of the robot
        // Midas serial number is 031E3241
        // GraveStone serial number is 03178474
        if (RobotController.getSerialNumber().equals("03178474")) {
            this.createGraveStoneDrivetrain();
            DataLogManager.log("%%%%%%%%%%%%%GraveStone Drive%%%%%%%%%%%%%%%%");
        } else {
            this.createMidasDrivetrain();
            DataLogManager.log("%%%%%%%%%%%%%%Midas Drive%%%%%%%%%%%%%");
        }

Also, intentionally defaults to the current bot in case we have to swap Rio.

1 Like

Another method I didn’t see yet… put a jumper across a DIO, and read if the jumper is in place.

Useful if you’re worried about the potential for a RIO getting replaced, but someone forgetting to update software.

:wink:

hey I never claimed to be literate