One Codebase, Multiple Robots

Our team uses GitHub and C++. Right now, our practice robot and comp robot are 2 separate GitHub repositories. What is the best strategy for having one codebase where each robot has different constants, different mechanisms but overall a lot of shared code?

We are thinking about using git submodule or subtree to share json auto paths between the robots. Wondering if teams have any experience with this.

Thank you.

Commenting to follow, we are running a practice bot for the first time since bag&tag and are curious what strategies folks are using.

We started playing with a switch to load an entirely different RobotContainer file for each chassis plus sysid, but it seems overly verbose to have three entire robots in the deployed code (and inadvertently switching to the sysid code before a match seems scary)…

Hey I deal with this all the time!

Your two basic flavors are run time variation or build time variation.

Run time is basically having the right set of if() statements in your code to make sure the right set of lines runs for whatever robot you’re on. Driving these if() statements requires looking for something that’s unique to the robot - a jumper wire, the RIO serial number, etc.

Build time is switching which files get built and/or deployed to the robot. It involves some set of common libraries, creative gradle work, etc. splitting code in multiple repositories…

For FRC scale things, my recommendation will be Run Time variation. It makes your codebase more “bloated” than Build time variation, but reduces the complexity outside of the code files you look at on a daily basis. Furthermore, keeping the variation “front and center” while developing code makes it less likely you’ll forget to consider the “other bot” while making a particular change.

I really won’t recommend multiple git repos. Yes, there’s plenty of ways to manage that, and none of them are worth the effort at an FRC team scale.

What I would recommend - for run-time variation - create some sort of “variant” class that makes it obvious a certain value will change depending on what robot you’re on, and manages selecting the right value at runtime. For everything in your codebase that is different depending on the robot, run it through this variant class, rather than having ad-hoc if statements around.

1 Like

My team uses one code base and a unique comment on each roboRIO dashboard. Flip between sets of constants like this:

// Set the name of the robot
private static final String comment = RobotController.getComments();
private static String robotName4237 = "";

if (comment.contains("2024 Robot"))
{
    robotName4237 = "2024 Robot";
}
else if(comment.contains("2023 Robot"))

etc.

if(robotName4237.equals("2024 Robot"))
{
    drivetrainWheelbaseMeters = 23.5 * INCHES_TO_METERS; // Front to back
    drivetrainTrackwidthMeters = 23.5 * INCHES_TO_METERS; // Side to side
    driveMotorEncoderResolution = 1; // Neo1650
}
else if(robotName4237.equals("2023 Robot"))
{
    drivetrainWheelbaseMeters = 27.44 * INCHES_TO_METERS; // Front to back
    drivetrainTrackwidthMeters = 19.50 * INCHES_TO_METERS; // Side to side
    driveMotorEncoderResolution = 2048;  // Falcon500
}
else if(robotName4237.equals("2022 Robot"))
{
    drivetrainWheelbaseMeters = 23.5 * INCHES_TO_METERS; // Front to back
    drivetrainTrackwidthMeters = 23.5 * INCHES_TO_METERS; // Side to side
    driveMotorEncoderResolution = 1;  //42 // Neo1650
}
else
{
    System.out.println("Unknown Robot " + robotName4237);
}

Our team just uses the robo rio serial number. RobotController.getSerialNumber().

1 Like

Our team uses the serial number too! We save it as a constant then do switch statements for all of the constants. One codebase, very clear sections to put constant values in, and no manual selections!

Just be careful here that if you have to quickly swap roborios mid-competition your code doesn’t assume it’s now running on the practice bot

Imo, having IOs and an enum to decide the robot type, advantage kit style (that’s if both robots have the same mechanisms and not completely different ones)

That’s a really useful feature! I may have to switch to that. We figured out how to use the MAC address of the computer we’re running on as well as a Preference on Network Tables. The MAC address identifies which hardware we’re on (RIO, laptop-simulation) and the preference determines which robot to simulate if on a laptop. There’s a robotFactory function which does a switch statement to decide which robot main class to load and these are children of an abstract class that has functions for the main points of entry needed by RobotContainer.

private String whereAmI() {
    try {
      NetworkInterface myNI = NetworkInterface.networkInterfaces().filter(it -> {
        try {
          byte[] MA = it.getHardwareAddress();
          return null != MA;
        } catch (Exception e) {
          e.printStackTrace();
        }
        return false;
      }).findFirst().orElse(NetworkInterface.networkInterfaces().findFirst().get());
      byte[] MAC_ADDRESS = myNI.getHardwareAddress();
      final List<Byte> macList = new ArrayList<>();
      if (null != MAC_ADDRESS) {
        for (byte b : MAC_ADDRESS) {
          macList.add(b);
        }
      }
      String whichRobot = macList.stream().map(it -> String.format("%02X", it))
          .collect(Collectors.joining(":"));
      values.set(MAC_Address, whichRobot.toString());
      return whichRobot.toString();
    } catch (SocketException e) {
      e.printStackTrace();
    }
    return "unknown";
  }
  public static class Robots {
    public static final String CC_BOT_2023 = "00:80:2F:33:17:DD";
    public static final String COMP_BOT_2023 = "00:80:2F:33:04:33";
}
  private void robotFactory() {
    String whichRobot = whereAmI();
    switch (whichRobot) {
      case Robots.CC_BOT_2023: {
        robot = new CubeCruzer(mechVisual, shuffleTab);
        break;
      }
      case Robots.COMP_BOT_2023: {
        robot = new CompBot_2023_T1G3R(mechVisual, shuffleTab);
        break;
      }
      case Robots.CURTS_LAPTOP_SIM: {
        switch (whoAmI.get()) {
          case "T1G3R": {
            robot = new CompBot_2023_T1G3R(mechVisual, shuffleTab);
            break;
          }
          case "Simulator": {
            robot = new CurtsLaptopSimulator(mechVisual, shuffleTab);
            break;
          }
        }
        break;
      }
      default: {
        robot = new DefaultRobot(mechVisual, shuffleTab);
        break;
      }
    }
    log(">>>>>>>>>> Running " + robot.getClass().getSimpleName() + " <<<<<<<<<<");
  }

All of these methods that rely on properties of the roboRIO are subject to failure to be the right robot as @AriMB points out. I think the DIO jumpers to indicate which robot has the edge in mistake-proofing. Otherwise administrative controls are still needed - somehow make it obvious what configuration is being used.

Right, but if I know I might switch out a particular backup Rio, I might code & configure that in ahead of time and have it read the Preference or use the Comment like you mentioned. If I’m borrowing someone’s Rio, that would require a code change, but the values are displayed for easy recalibration. It might not be as quick as a jumper, but it’s not slow. I think it comes down to what you’re comfortable with.

Edit - Also, I can’t set a jumper on my laptop when I’m running the robot code in simulation.

I recommend not using the serial #, or structuring the code such that the practice robot has a specific serial, and all others are the competition robot. You don’t want to think about that if you need to emergency swap a rio at comp. The latter also assumes you don’t use the practice robot rio as your comp spare, so maybe just go with a jumper on the practice robot.

Just make separate projects inside your github repo root directory. Example:

RobotRepo2024/
| mainRobotProject/
| testRobotProject/

In the two sub-directories, you can have individual robot projects that you can individually deploy to your robot.

reposted from this thread…

In 3061-lib, we take the approach of having a RobotConfig abstract class that is extended by a config-specific file for each robot. We create the appropriate config object based on the robot specified in the Constants class. This occurs at the very start of the RobotContainer constructor. We then invoke the static getInstance method on the RobotConfig class to get a reference to the current robot’s config object on which to query properties (example).

In the RobotContainer constructor, we also switch on this constant and construct subsystems differently.