Skip to content

2. 创建第一个子系统

在本教程中,我们将学习如何创建一个完整的机器人子系统。我们将参考Chassis子系统的结构,创建一个新的Intake子系统。Intake子系统通常用于收集游戏物品,如球体或立方体。代码下载链接

WPILib 的子系统设计遵循多项关键原则,共同保障代码的健壮性、可维护性与可测试性:

  • 依赖注入模式:通过工厂方法在不同环境(如真实机器人、模拟器或测试平台)中创建对应实例,实现环境切换的无缝衔接。
  • 接口隔离原则:借助硬件抽象层将业务逻辑与具体硬件解耦,子系统仅依赖抽象的硬件接口,而非具体电机或传感器类,提升代码灵活性。
  • 配置集中管理:所有硬件参数、PID 常数及机械特性统一在配置类中管理,便于调整并确保系统配置的一致性。
  • 状态监测机制:深度集成实时硬件监控,跟踪连接状态与运行数据,并通过警报系统及时反馈异常,保障机器人稳定运行。

这些原则共同构建了一套强大且易于维护的机器人软件架构。

示例程序

让我们以底盘子系统为例,分析组成一个子系统的关键结构:

java
public class Chassis extends SubsystemBase {
  // a. 硬件接口
  // b. 核心方法
  // c. 静态初始化块
  // d. 周期性方法
  // e. 构造方法和工厂方法
}
java
public class ChassisConfig {
  // f. 配置参数
}

a. 硬件接口

java
public class Chassis extends SubsystemBase {
  private final GenericWheelIO leftIO;
  private final GenericWheelIO rightIO;
  private final GenericWheelIOInputsAutoLogged leftInputs = new GenericWheelIOInputsAutoLogged();
  private final GenericWheelIOInputsAutoLogged rightInputs = new GenericWheelIOInputsAutoLogged();
  private final Alert leftOfflineAlert = new Alert("Chassis Left Offline", Alert.AlertType.WARNING);
  private final Alert rightOfflineAlert = new Alert("Chassis Right Offline", Alert.AlertType.WARNING);
}

这里使用GenericWheelIO接口而不是具体的电机类,这样可以在真实机器人、模拟器和测试环境中无缝切换。*InputsAutoLogged用于自动记录数据到AdvantageKit。

b. 核心方法

java
public class Chassis extends SubsystemBase {
  public void setWheelsVelocities(double leftVelocity, double rightVelocity) {
    leftIO.setVelocity(leftVelocity / ChassisConfig.WHEEL_RADIUS_METER, 0.0);
    rightIO.setVelocity(rightVelocity / ChassisConfig.WHEEL_RADIUS_METER, 0.0);
  }
}

公共方法应该以有意义的单位(如米/秒)接受参数,在内部转换为电机需要的单位(如弧度/秒)。

c. 静态初始化块

java
public class Chassis extends SubsystemBase {
  static {
    final var driveGains = ChassisConfig.getDriveGains();
    ChassisConfig.driveKp.initDefault(driveGains.kp());
    ChassisConfig.driveKd.initDefault(driveGains.kd());
    ChassisConfig.driveKs.initDefault(driveGains.ks());
  }
}

静态块在类加载时执行,用于根据运行模式设置不同的默认PID参数。

d. 周期性方法

java
public class Chassis extends SubsystemBase {
  @Override
  public void periodic() {
    // 1. 更新硬件输入
    leftIO.updateInputs(leftInputs);
    rightIO.updateInputs(rightInputs);

    // 2. 记录数据
    Logger.processInputs("Chassis Left", leftInputs);
    Logger.processInputs("Chassis Right", rightInputs);

    // 3. 状态监测和警报
    leftOfflineAlert.set(!leftInputs.connected);
    rightOfflineAlert.set(!rightInputs.connected);

    // 4. 动态参数更新
    LoggedTunableNumber.ifChanged(
        hashCode(),
        () -> {
          leftIO.setPdf(
              ChassisConfig.driveKp.get(),
              ChassisConfig.driveKd.get(),
              ChassisConfig.driveKs.get());
          rightIO.setPdf(
              ChassisConfig.driveKp.get(),
              ChassisConfig.driveKd.get(),
              ChassisConfig.driveKs.get());
        },
        ChassisConfig.driveKp,
        ChassisConfig.driveKd,
        ChassisConfig.driveKs);

    // 5. 里程计更新
    // Simplify for tank drive odometry
    RobotContainer.getOdometry()
        .addWheeledObservation(
            new WheeledObservation(
                Timer.getFPGATimestamp(),
                new DifferentialDriveWheelPositions(
                    leftInputs.positionRad * ChassisConfig.WHEEL_RADIUS_METER,
                    rightInputs.positionRad * ChassisConfig.WHEEL_RADIUS_METER),
                null));
  }
}

周期性方法按固定频率执行,负责读取硬件状态、记录关键数据、监测故障、动态更新可调参数和处理其他子系统的特殊逻辑。

e. 构造方法和工厂方法

java
public class Chassis extends SubsystemBase {
  private Chassis(GenericWheelIO leftIO, GenericWheelIO rightIO) {
    this.leftIO = leftIO;
    this.rightIO = rightIO;
  }

  // 真实实例 - 使用Kraken电机
  public static Chassis createReal() {
    return new Chassis(
        new GenericWheelIOKraken(
                "Left Drive",
                Constants.Ports.Can.LEFT_DRIVE_MASTER,
                ChassisConfig.getDriveConfig())
            .withFollower(Constants.Ports.Can.LEFT_DRIVE_SLAVE, true),
        new GenericWheelIOKraken(
                "Right Drive",
                Constants.Ports.Can.RIGHT_DRIVE_MASTER,
                ChassisConfig.getDriveConfig())
            .withFollower(Constants.Ports.Can.RIGHT_DRIVE_SLAVE, true));
  }

  // 模拟实例 - 使用模拟电机
  public static Chassis createSim() {
    return new Chassis(
        new GenericWheelIOSim(
            2,
            0.025,
            ChassisConfig.DRIVE_REDUCTION,
            ChassisConfig.driveKp.get(),
            ChassisConfig.driveKd.get()),
        new GenericWheelIOSim(
            2,
            0.025,
            ChassisConfig.DRIVE_REDUCTION,
            ChassisConfig.driveKp.get(),
            ChassisConfig.driveKd.get()));
  }

  // IO测试实例 - 空实现
  public static Chassis createIO() {
    return new Chassis(new GenericWheelIO() {}, new GenericWheelIO() {});
  }
}

工厂模式让创建逻辑与使用逻辑分离,使得在不同环境间切换变得简单。

f. 配置参数

java
public class ChassisConfig {
  // 可调参数
  static final LoggedTunableNumber driveKp = new LoggedTunableNumber(DebugGroup.CHASSIS, "DriveKp");
  static final LoggedTunableNumber driveKd = new LoggedTunableNumber(DebugGroup.CHASSIS, "DriveKd");
  static final LoggedTunableNumber driveKs = new LoggedTunableNumber(DebugGroup.CHASSIS, "DriveKs");

  // 根据运行模式返回不同的增益参数
  static Gains getDriveGains() {
    return switch (Constants.MODE) {
      case REAL -> new Gains(10.0, 0.1, 0.2);
      case SIM, REPLAY -> new Gains(0.25, 0.0, 0.0);
    };
  }

  // 机械常数
  static final double DRIVE_REDUCTION = 10.71; // 10.71:1
  public static final double TRACK_WIDTH = 0.69; // meters
  static final double WHEEL_RADIUS_METER = 0.0479;

  // 电机配置
  static TalonFXConfiguration getDriveConfig() {
    var config = new TalonFXConfiguration();
    // ...
    return config;
  }

  record Gains(double kp, double kd, double ks) {}
}

配置类集中管理所有参数,包括可在Dashbord中实时调整的可调参数、机械常数以及不同环境的特定配置。

你的回合

现在你需要参考Chassis的结构,完成Intake子系统的设计。必须实现的核心方法和配置参数已经给出,你可以根据它来完成你的代码设计。

java
public class Intake extends SubsystemBase {
  public double getPivotDegree() {
    // TODO: implement
    return 0.0;
  }

  public void setPivotDegree(double degree) {
    // TODO: implement
  }

  public double getRollerVelocityRPM() {
    // TODO: implement
    return 0.0;
  }

  public void setRollerVoltage(double voltage) {
    // TODO: implement
  }

  static {
    // TODO: implement
  }

  private final GenericArmIO pivotIO;
  private final GenericRollerIO rollerIO;
  // TODO: implement

  @Override
  public void periodic() {
    // TODO: implement
  }

  private Intake(GenericArmIO pivotIO, GenericRollerIO rollerIO) {
    this.pivotIO = pivotIO;
    this.rollerIO = rollerIO;
  }

  public static Intake createReal() {
    return new Intake(/**/);
  }

  public static Intake createSim() {
    return new Intake(/**/);
  }

  public static Intake createIO() {
    return new Intake(new GenericArmIO() {}, new GenericRollerIO() {});
  }
}

效果演示

通过左右摇杆分别控制机器人的左、右轮前进。左上扳机键用于控制吸取装置:按下时吐出水管,松开时则尝试吸入。右上扳机键则用于调整吸取装置的角度。