Sử dụng các Command do FTCLib cung cấp để nâng cao chương trình của bạn
FTCLib cung cấp các tính năng tiện lợi nhằm làm cho chương trình theo mô hình paradigm của bạn trở nên gọn gàng hơn. Ý tưởng cốt lõi là cải thiện cấu trúc tổng thể của chương trình. Một số command tiện lợi này đúng như tên gọi của chúng: chỉ nhằm mục đích tiện lợi. Điều đó không có nghĩa là chúng đặc biệt xuất sắc. Nhiều command được đơn giản hóa để phục vụ cho việc thi đấu ở mức tối thiểu, ví dụ như PurePursuitCommand.
Framework Commands (Các Command trong Framework)
Framework commands tồn tại nhằm giảm lượng code cần thiết cho các tác vụ đơn giản, chẳng hạn như cập nhật một con số. Thay vì buộc người dùng phải tạo cả một command chỉ cho một nhiệm vụ rất nhỏ (điều mà khi lặp lại cho nhiều tác vụ vụn vặt sẽ khiến cấu trúc chương trình trở nên rối rắm), người dùng có thể tận dụng các framework commands.
InstantCommand
InstantCommand là một framework command linh hoạt, khi được khởi tạo sẽ chạy một tác vụ và sau đó kết thúc ngay trong cùng một vòng lặp của phương thức run() trong CommandScheduler. Điều này đặc biệt hữu ích cho các sự kiện được kích hoạt bằng nút bấm.
GamepadEx toolOp = new GamepadEx(gamepad2);
toolOp.getGamepadButton(GamepadKeys.Button.A)
.whenPressed(new InstantCommand(() -> {
}));GamepadEx toolOp = new GamepadEx(gamepad2);
toolOp.getGamepadButton(GamepadKeys.Button.A)
.whenPressed(new InstantCommand(() -> {
}));/ MỘT VÍ DỤ /
Motor intakeMotor = new Motor(hardwareMap, "intake");
toolOp.getGamepadButton(GamepadKeys.Button.RIGHT_BUMPER)
.whileHeld(new InstantCommand(() -> {
intakeMotor.set(0.75);
}))
.whenReleased(new InstantCommand(intakeMotor::stopMotor));Motor intakeMotor = new Motor(hardwareMap, "intake");
toolOp.getGamepadButton(GamepadKeys.Button.RIGHT_BUMPER)
.whileHeld(new InstantCommand(() -> {
intakeMotor.set(0.75);
}))
.whenReleased(new InstantCommand(intakeMotor::stopMotor));Trên thực tế, bạn nên sử dụng subsystem thay vì thao tác trực tiếp với đối tượng motor. Bằng cách đó, ta có thể thêm các yêu cầu (requirements) của subsystem vào InstantCommand. Dưới đây là ví dụ đúng cách.
Intake.java
public class Intake extends SubsystemBase {
private Motor m_intakeMotor;
public Intake(Motor intakeMotor) {
m_intakeMotor = intakeMotor;
}
public void run() {
m_intakeMotor.set(0.75);
}
public void stop() {
m_intakeMotor.stopMotor();
}
}
public class Intake extends SubsystemBase {
private Motor m_intakeMotor;
public Intake(Motor intakeMotor) {
m_intakeMotor = intakeMotor;
}
public void run() {
m_intakeMotor.set(0.75);
}
public void stop() {
m_intakeMotor.stopMotor();
}
}Sau đó, ta có thể thiết lập các ràng buộc command như sau:
Motor intakeMotor = new Motor(hardwareMap, "intake");
Intake intake = new Intake(intakeMotor);
toolOp.getGamepadButton(GamepadKeys.Button.RIGHT_BUMPER)
.whileHeld(new InstantCommand(intake::run, intake))
.whenReleased(new InstantCommand(intake::stop, intake));
Motor intakeMotor = new Motor(hardwareMap, "intake");
Intake intake = new Intake(intakeMotor);
toolOp.getGamepadButton(GamepadKeys.Button.RIGHT_BUMPER)
.whileHeld(new InstantCommand(intake::run, intake))
.whenReleased(new InstantCommand(intake::stop, intake));
Cách này loại bỏ rất nhiều command không cần thiết, bởi vì trong một cách triển khai tùy chỉnh, người dùng sẽ phải định nghĩa một command để chạy intake và một command khác để dừng intake. Với InstantCommand, lượng code phía người dùng được giảm đi đáng kể.
RunCommand
Trái ngược với InstantCommand, RunCommand sẽ chạy một phương thức trong pha execute. Điều này rất hữu ích cho các PerpetualCommand, default command, và các command đơn giản như điều khiển robot di chuyển.
Motor intakeMotor = new Motor(hardwareMap, "intake");
Intake intake = new Intake(intakeMotor);
toolOp.getGamepadButton(GamepadKeys.Button.RIGHT_BUMPER)
.whileHeld(new RunCommand(intake::run, intake))
.whenReleased(new InstantCommand(intake::stop, intake));
schedule(new RunCommand(driveSubsystem::drive, driveSubsystem));
Motor intakeMotor = new Motor(hardwareMap, "intake");
Intake intake = new Intake(intakeMotor);
toolOp.getGamepadButton(GamepadKeys.Button.RIGHT_BUMPER)
.whileHeld(new RunCommand(intake::run, intake))
.whenReleased(new InstantCommand(intake::stop, intake));
schedule(new RunCommand(driveSubsystem::drive, driveSubsystem));
ConditionalCommand
ConditionalCommand có rất nhiều cách sử dụng. Nó nhận vào hai command và sẽ chạy một command khi giá trị cung cấp là true, và command còn lại khi giá trị là false.
Một cách sử dụng là tạo một cơ chế toggle giữa các command mà không cần dùng đến trạng thái active/inactive của toggleWhenPressed(). Hãy cập nhật intake để có thêm hai phương thức phục vụ cho tính năng này:
Intake.java
public class Intake extends SubsystemBase {
private Motor m_intakeMotor;
private boolean m_active;
public Intake(Motor intakeMotor) {
m_intakeMotor = intakeMotor;
m_active = true;
}
public void toggle() {
m_active = !m_active;
}
public boolean active() {
return m_active;
}
public void run() {
m_intakeMotor.set(0.75);
}
public void stop() {
m_intakeMotor.stopMotor();
}
}
public class Intake extends SubsystemBase {
private Motor m_intakeMotor;
private boolean m_active;
public Intake(Motor intakeMotor) {
m_intakeMotor = intakeMotor;
m_active = true;
}
public void toggle() {
m_active = !m_active;
}
public boolean active() {
return m_active;
}
public void run() {
m_intakeMotor.set(0.75);
}
public void stop() {
m_intakeMotor.stopMotor();
}
}Sau đó, ta có thể dùng ConditionalCommand trong một trigger binding để tạo hiệu ứng toggle khi nhấn:
Motor intakeMotor = new Motor(hardwareMap, "intake");
Intake intake = new Intake(intakeMotor);
toolOp.getGamepadButton(GamepadKeys.Button.RIGHT_BUMPER)
.whenPressed(new ConditionalCommand(
new InstantCommand(intake::run, intake),
new InstantCommand(intake::stop, intake),
() -> {
intake.toggle();
return intake.active();
}
));
Motor intakeMotor = new Motor(hardwareMap, "intake");
Intake intake = new Intake(intakeMotor);
toolOp.getGamepadButton(GamepadKeys.Button.RIGHT_BUMPER)
.whenPressed(new ConditionalCommand(
new InstantCommand(intake::run, intake),
new InstantCommand(intake::stop, intake),
() -> {
intake.toggle();
return intake.active();
}
));Một ví dụ sử dụng khác có thể là trong mùa giải Velocity Vortex, nơi các beacon có thể là màu đỏ hoặc xanh. Dùng cảm biến màu, ta có thể phát hiện màu sắc và thực hiện hành động tương ứng.
ConditionalCommand pressBeacon = new ConditionalCommand(
new InstantCommand(beaconPresser::pushRed, beaconPresser),
new InstantCommand(beaconPresser::pushBlue, beaconPresser),
() -> vision.output() == BeaconColor.RED
);
pressBeacon.schedule();
ConditionalCommand pressBeacon = new ConditionalCommand(
new InstantCommand(beaconPresser::pushRed, beaconPresser),
new InstantCommand(beaconPresser::pushBlue, beaconPresser),
() -> vision.output() == BeaconColor.RED
);
pressBeacon.schedule();
Như bạn thấy, ConditionalCommand rất hữu ích cho việc chuyển đổi giữa các trạng thái dựa trên một điều kiện nhất định. Sau này, ta sẽ thấy rằng khi làm việc với nhiều trạng thái, ta nên dùng SelectCommand thay vì một command đơn giản chỉ chuyển giữa hai trạng thái.
ScheduleCommand
ScheduleCommand đúng như tên gọi của nó: dùng để lên lịch các command. Bạn có thể truyền vào một số lượng biến đổi các command để schedule, và command này sẽ schedule chúng ngay khi được khởi tạo. Sau đó, nó sẽ kết thúc.
Điều này rất hữu ích cho việc tách nhánh (fork) khỏi một command group.
SequentialCommandGroup auto = new SequentialCommandGroup(
...,
new ConditionalCommand(
new ScheduleCommand(
),
new ScheduleCommand(
),
...
),
...
);
SequentialCommandGroup auto = new SequentialCommandGroup(
...,
new ConditionalCommand(
new ScheduleCommand(
),
new ScheduleCommand(
),
...
),
...
);
Điều quan trọng cần lưu ý là ScheduleCommand sẽ kết thúc ngay lập tức, nghĩa là các command phía sau trong SequentialCommandGroup sẽ được chạy tiếp. Mục đích của ScheduleCommand là để tách một command ra khỏi command group để nó được scheduler chạy độc lập.
SelectCommand
SelectCommand tương tự như ConditionalCommand nhưng dành cho nhiều command. Điều này đặc biệt hữu ích cho state machine. Hãy xét ví dụ về ring stack trong mùa giải Ultimate Goal 2020–2021.
public enum Height {
ZERO, ONE, FOUR
}
public Height height() {
}
...
SelectCommand wobbleCommand = new SelectCommand(
new HashMap<Object, Command>() {{
put(Height.ZERO, new PurePursuitCommand(...));
put(Height.ONE, new PurePursuitCommand(...));
put(Height.FOUR, new PurePursuitCommand(...));
}},
this::height
);public enum Height {
ZERO, ONE, FOUR
}
public Height height() {
}
...
SelectCommand wobbleCommand = new SelectCommand(
new HashMap<Object, Command>() {{
put(Height.ZERO, new PurePursuitCommand(...));
put(Height.ONE, new PurePursuitCommand(...));
put(Height.FOUR, new PurePursuitCommand(...));
}},
this::height
);Một SelectCommand sẽ kết thúc khi command được chọn cũng kết thúc. Một cách sử dụng khác là chạy command dựa trên một supplier thay vì một map.
public enum Height {
ZERO, ONE, FOUR
}
public Height height() {
}
public Command wobbleCommand() {
Height rings = this.height();
switch (rings) {
case Height.ZERO:
return ...;
case Height.ONE:
return ...;
case Height.FOUR:
return ...;
}
}
...
SelectCommand wobbleCommand = new SelectCommand(
this::wobbleCommand
);public enum Height {
ZERO, ONE, FOUR
}
public Height height() {
}
public Command wobbleCommand() {
Height rings = this.height();
switch (rings) {
case Height.ZERO:
return ...;
case Height.ONE:
return ...;
case Height.FOUR:
return ...;
}
}
...
SelectCommand wobbleCommand = new SelectCommand(
this::wobbleCommand
);PerpetualCommand
Khác với InstantCommand, một PerpetualCommand sẽ “nuốt” một command và chạy nó vĩnh viễn, tức là tiếp tục thực thi command được truyền vào và bỏ qua điều kiện isFinished của command đó. Nó chỉ có thể kết thúc khi bị ngắt.
Điều này khiến nó rất hữu ích cho default command, vốn sẽ bị ngắt khi có command khác yêu cầu cùng subsystem và chỉ được lên lịch lại khi command kia kết thúc.
Motor intakeMotor = new Motor(hardwareMap, "intake");
Intake intake = new Intake(intakeMotor);
toolOp.getGamepadButton(GamepadKeys.Button.RIGHT_BUMPER)
.whileHeld(new InstantCommand(intake::run, intake));
intake.setDefaultCommand(new PerpetualCommand(stopIntakeCommand));
intake.setDefaultCommand(new RunCommand(intake::stop, intake));
Motor intakeMotor = new Motor(hardwareMap, "intake");
Intake intake = new Intake(intakeMotor);
toolOp.getGamepadButton(GamepadKeys.Button.RIGHT_BUMPER)
.whileHeld(new InstantCommand(intake::run, intake));
intake.setDefaultCommand(new PerpetualCommand(stopIntakeCommand));
intake.setDefaultCommand(new RunCommand(intake::stop, intake));
Lưu ý rằng một PerpetualCommand sẽ thêm tất cả các requirements của command bị “nuốt”.
WaitUntilCommand
WaitUntilCommand sẽ chạy cho đến khi boolean được cung cấp trả về true. Điều này hữu ích khi bạn đã tách nhánh khỏi một command group.
SequentialCommandGroup auto = new SequentialCommandGroup(
...,
new ScheduleCommand(forkedCommand),
new WaitUntilCommand(forkedCommand::isFinished),
...
);
SequentialCommandGroup auto = new SequentialCommandGroup(
...,
new ScheduleCommand(forkedCommand),
new WaitUntilCommand(forkedCommand::isFinished),
...
);
Các command phía sau trong auto command group chỉ được chạy sau khi command tách nhánh đã kết thúc nhờ WaitUntilCommand.
StartEndCommand
StartEndCommand về bản chất là một InstantCommand nhưng có thêm hàm kết thúc tùy chỉnh. Nó nhận vào hai tham số kiểu Runnable và một danh sách tùy chọn các subsystem cần require. Tham số đầu tiên được chạy khi khởi tạo, tham số thứ hai được chạy khi kết thúc.
FunctionalCommand
Framework command cuối cùng ta bàn tới là FunctionalCommand. Nó hữu ích khi bạn muốn định nghĩa inline một command phức tạp thay vì tạo một class command mới. Tuy nhiên, với các logic vượt quá một mức độ phức tạp nhất định, việc viết class riêng vẫn tốt hơn.
FunctionalCommand nhận vào bốn đầu vào và một số lượng biến đổi các subsystem để thêm requirements. Bốn đầu vào này xác định hành vi khởi tạo, thực thi, kết thúc và điều kiện hoàn thành.
SequentialCommandGroup auto = new SequentialCommandGroup(
...,
new FunctionalCommand(
driveSubsystem::resetEncoders,
() -> {
},
driveSubsystem::stop,
() -> {
},
driveSubsystem,
...
),
new ScheduleCommand(forkedCommand),
new WaitUntilCommand(forkedCommand::isFinished),
...
);SequentialCommandGroup auto = new SequentialCommandGroup(
...,
new FunctionalCommand(
driveSubsystem::resetEncoders,
() -> {
},
driveSubsystem::stop,
() -> {
},
driveSubsystem,
...
),
new ScheduleCommand(forkedCommand),
new WaitUntilCommand(forkedCommand::isFinished),
...
);Command Decorators
Decorators là các phương thức cho phép bạn tạo ràng buộc command và logic mà không cần tạo command mới.
withTimeout
Trả về một ParallelRaceGroup với thời gian timeout (tính bằng mili giây).
schedule(
fooCommand.withTimeout(1000)
);
schedule(
fooCommand.withTimeout(1000)
);
interruptOn
Trả về một ParallelRaceGroup kết thúc khi một điều kiện xác định được thỏa mãn.
schedule(
fooCommand.interruptOn(() -> {
return ...;
})
);schedule(
fooCommand.interruptOn(() -> {
return ...;
})
);whenFinished
Trả về một SequentialCommandGroup chạy một Runnable sau khi command gọi nó kết thúc.
schedule(
fooCommand.whenFinished(() -> {
})
);schedule(
fooCommand.whenFinished(() -> {
})
);beforeStarting
Trả về một SequentialCommandGroup chạy một Runnable trước khi command được khởi tạo.
schedule(
fooCommand.beforeStarting(() -> {
})
);schedule(
fooCommand.beforeStarting(() -> {
})
);andThen
Trả về một SequentialCommandGroup chạy các command được truyền vào theo thứ tự sau khi command gọi nó kết thúc.
schedule(
fooCommand.andThen(
barCommand, bazCommand, ...
)
);
schedule(
fooCommand.andThen(
barCommand, bazCommand, ...
)
);
deadlineWith
Trả về một ParallelDeadlineGroup với command gọi nó là deadline, chạy song song với các command được truyền vào.
schedule(
fooCommand.deadlineWith(
barCommand, bazCommand, ...
)
);
schedule(
fooCommand.deadlineWith(
barCommand, bazCommand, ...
)
);
alongWith
Trả về một ParallelCommandGroup chạy song song các command được truyền vào với command gọi nó.
schedule(
fooCommand.alongWith(
barCommand, bazCommand, ...
)
);
schedule(
fooCommand.alongWith(
barCommand, bazCommand, ...
)
);
raceWith
Trả về một ParallelRaceGroup chạy song song các command cho đến khi một trong số chúng kết thúc.
schedule(
fooCommand.raceWith(
barCommand, bazCommand, ...
)
);
schedule(
fooCommand.raceWith(
barCommand, bazCommand, ...
)
);
perpetually
“Nuốt” command vào một PerpetualCommand và trả về nó.
PerpetualCommand perpetual = fooCommand.perpetually();
PerpetualCommand perpetual = fooCommand.perpetually();
asProxy
“Nuốt” command vào một ProxyScheduleCommand và trả về nó. Command này tương tự ScheduleCommand nhưng chỉ kết thúc khi tất cả các command mà nó schedule đã kết thúc, thay vì kết thúc ngay lập tức.
ProxyScheduleCommand proxySchedule = fooCommand.asProxy();
ProxyScheduleCommand proxySchedule = fooCommand.asProxy();