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() {});
}
}
效果演示
通过左右摇杆分别控制机器人的左、右轮前进。左上扳机键用于控制吸取装置:按下时吐出水管,松开时则尝试吸入。右上扳机键则用于调整吸取装置的角度。