Convenience Features

Convenience Features

Các command dựng sẵn giúp thực hiện nhanh các hành động phổ biến.

Các command dựng sẵn giúp thực hiện nhanh các hành động phổ biến.

Level

Intermediate

Source

Source

Author

Author

FTC Lib

FTC Lib

Translator

Translator

FTC26749 aDudu

FTC26749 aDudu

Date Published

Date Published

Jan 18, 2026

Jan 18, 2026

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(() -> {
        // phần triển khai run() của bạn tại đây
    }));
GamepadEx toolOp = new GamepadEx(gamepad2);
toolOp.getGamepadButton(GamepadKeys.Button.A)
    .whenPressed(new InstantCommand(() -> {
        // phần triển khai run() của bạn tại đây
    }));

/ 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

/**
 * Đây là một intake subsystem mang tính sư phạm
 * cho một intake phổ dụng gồm một motor
 * điều khiển dây đai nối với một puly
 * dẫn động một ống PVC.
 */
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();
    }
}
/**
 * Đây là một intake subsystem mang tính sư phạm
 * cho một intake phổ dụng gồm một motor
 * điều khiển dây đai nối với một puly
 * dẫn động một ống PVC.
 */
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:

/* trong opmode của bạn */
Motor intakeMotor = new Motor(hardwareMap, "intake");
Intake intake = new Intake(intakeMotor);
toolOp.getGamepadButton(GamepadKeys.Button.RIGHT_BUMPER)
    // tham số thứ hai là varargs của các subsystem
    // cần được yêu cầu (require)
    .whileHeld(new InstantCommand(intake::run, intake))
    .whenReleased(new InstantCommand(intake::stop, intake));
/* trong opmode của bạn */
Motor intakeMotor = new Motor(hardwareMap, "intake");
Intake intake = new Intake(intakeMotor);
toolOp.getGamepadButton(GamepadKeys.Button.RIGHT_BUMPER)
    // tham số thứ hai là varargs của các subsystem
    // cần được yêu cầu (require)
    .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.

/* trong opmode của bạn */
Motor intakeMotor = new Motor(hardwareMap, "intake");
Intake intake = new Intake(intakeMotor);
toolOp.getGamepadButton(GamepadKeys.Button.RIGHT_BUMPER)
    // tham số thứ hai là varargs của các subsystem
    // cần được yêu cầu
    .whileHeld(new RunCommand(intake::run, intake))
    .whenReleased(new InstantCommand(intake::stop, intake));
    
// giả sử ta có một drive subsystem
// với một phương thức drive
schedule(new RunCommand(driveSubsystem::drive, driveSubsystem));
/* trong opmode của bạn */
Motor intakeMotor = new Motor(hardwareMap, "intake");
Intake intake = new Intake(intakeMotor);
toolOp.getGamepadButton(GamepadKeys.Button.RIGHT_BUMPER)
    // tham số thứ hai là varargs của các subsystem
    // cần được yêu cầu
    .whileHeld(new RunCommand(intake::run, intake))
    .whenReleased(new InstantCommand(intake::stop, intake));
    
// giả sử ta có một drive subsystem
// với một phương thức drive
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

/**
 * Đây là một intake subsystem mang tính sư phạm
 * cho một intake phổ dụng gồm một motor
 * điều khiển dây đai nối với một puly
 * dẫn động một ống PVC.
 */
public class Intake extends SubsystemBase {
    private Motor m_intakeMotor;
    private boolean m_active;
    
    public Intake(Motor intakeMotor) {
        m_intakeMotor = intakeMotor;
        m_active = true;
    }
    
    // chuyển trạng thái toggle
    public void toggle() {
        m_active = !m_active;
    }
    
    // trả về trạng thái active
    public boolean active() {
        return m_active;
    }
    
    public void run() {
        m_intakeMotor.set(0.75);
    }
    
    public void stop() {
        m_intakeMotor.stopMotor();
    }
}
/**
 * Đây là một intake subsystem mang tính sư phạm
 * cho một intake phổ dụng gồm một motor
 * điều khiển dây đai nối với một puly
 * dẫn động một ống PVC.
 */
public class Intake extends SubsystemBase {
    private Motor m_intakeMotor;
    private boolean m_active;
    
    public Intake(Motor intakeMotor) {
        m_intakeMotor = intakeMotor;
        m_active = true;
    }
    
    // chuyển trạng thái toggle
    public void toggle() {
        m_active = !m_active;
    }
    
    // trả về trạng thái 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:

/* trong opmode của bạ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();
        }
    ));
/* trong opmode của bạ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();
        }
    ));

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.

// mã giả để khởi tạo command
ConditionalCommand pressBeacon = new ConditionalCommand(
    new InstantCommand(beaconPresser::pushRed, beaconPresser),
    new InstantCommand(beaconPresser::pushBlue, beaconPresser),
    () -> vision.output() == BeaconColor.RED
);
pressBeacon.schedule();    // lên lịch command
/* Điều này cũng hữu ích trong các sequential command group */
// mã giả để khởi tạo command
ConditionalCommand pressBeacon = new ConditionalCommand(
    new InstantCommand(beaconPresser::pushRed, beaconPresser),
    new InstantCommand(beaconPresser::pushBlue, beaconPresser),
    () -> vision.output() == BeaconColor.RED
);
pressBeacon.schedule();    // lên lịch command
/* Điều này cũng hữu ích trong các sequential command group */

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(
            // schedule các command
        ),
        new ScheduleCommand(
            // schedule các command
        ),
        ...    // boolean supplier
    ),
    ...
);
SequentialCommandGroup auto = new SequentialCommandGroup(
    ...,
    new ConditionalCommand(
        new ScheduleCommand(
            // schedule các command
        ),
        new ScheduleCommand(
            // schedule các command
        ),
        ...    // boolean supplier
    ),
    ...
);

Đ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() {
    // một số code để phát hiện chiều cao của starter stack
}
...
SelectCommand wobbleCommand = new SelectCommand(
    // tham số đầu tiên là một map các command
    new HashMap<Object, Command>() {{
        put(Height.ZERO, new PurePursuitCommand(...));
        put(Height.ONE, new PurePursuitCommand(...));
        put(Height.FOUR, new PurePursuitCommand(...));
    }},
    // selector
    this::height
);
public enum Height {
    ZERO, ONE, FOUR
}
public Height height() {
    // một số code để phát hiện chiều cao của starter stack
}
...
SelectCommand wobbleCommand = new SelectCommand(
    // tham số đầu tiên là một map các command
    new HashMap<Object, Command>() {{
        put(Height.ZERO, new PurePursuitCommand(...));
        put(Height.ONE, new PurePursuitCommand(...));
        put(Height.FOUR, new PurePursuitCommand(...));
    }},
    // selector
    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() {
    // một số code để phát hiện chiều cao của starter stack
}
// trả về command cho hành động wobble goal
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(
    // truyền vào một command supplier
    this::wobbleCommand
);
public enum Height {
    ZERO, ONE, FOUR
}
public Height height() {
    // một số code để phát hiện chiều cao của starter stack
}
// trả về command cho hành động wobble goal
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(
    // truyền vào một command supplier
    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.

/* trong opmode của bạn */
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));
// thực ra không cần như trên; bạn thường làm như sau:
intake.setDefaultCommand(new RunCommand(intake::stop, intake));
/* trong opmode của bạn */
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));
// thực ra không cần như trên; bạn thường làm như sau:
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(
    ...,
    // schedule command tách khỏi
    // command group
    new ScheduleCommand(forkedCommand),
    // đợi cho đến khi command đó kết thúc
    new WaitUntilCommand(forkedCommand::isFinished),
    ...    // các command khác
);
SequentialCommandGroup auto = new SequentialCommandGroup(
    ...,
    // schedule command tách khỏi
    // command group
    new ScheduleCommand(forkedCommand),
    // đợi cho đến khi command đó kết thúc
    new WaitUntilCommand(forkedCommand::isFinished),
    ...    // các command khác
);

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(
        // hành động khởi tạo
        driveSubsystem::resetEncoders,
        // hành động execute
        () -> {
            /* danh sách hành động */
            // lưu ý run() trả về void
        },
        // hành động khi kết thúc
        driveSubsystem::stop,
        // supplier xác định khi nào kết thúc
        () -> {
            /* logic trả về boolean */
        },
        driveSubsystem,
        ...    // các subsystem cần require khác
    ),
    new ScheduleCommand(forkedCommand),
    new WaitUntilCommand(forkedCommand::isFinished),
    ...
);
SequentialCommandGroup auto = new SequentialCommandGroup(
    ...,
    new FunctionalCommand(
        // hành động khởi tạo
        driveSubsystem::resetEncoders,
        // hành động execute
        () -> {
            /* danh sách hành động */
            // lưu ý run() trả về void
        },
        // hành động khi kết thúc
        driveSubsystem::stop,
        // supplier xác định khi nào kết thúc
        () -> {
            /* logic trả về boolean */
        },
        driveSubsystem,
        ...    // các subsystem cần require khác
    ),
    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) // kết thúc sau 1000 ms
);
schedule(
    fooCommand.withTimeout(1000) // kết thúc sau 1000 ms
);

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(() -> {
        /* BOOLEAN SUPPLIER */
        return ...;
    })
);
schedule(
    fooCommand.interruptOn(() -> {
        /* BOOLEAN SUPPLIER */
        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(() -> {
        /* RUNNABLE */
    })
);
schedule(
    fooCommand.whenFinished(() -> {
        /* RUNNABLE */
    })
);

beforeStarting

Trả về một SequentialCommandGroup chạy một Runnable trước khi command được khởi tạo.

schedule(
    fooCommand.beforeStarting(() -> {
        /* RUNNABLE */
    })
);
schedule(
    fooCommand.beforeStarting(() -> {
        /* RUNNABLE */
    })
);

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(
    // kết thúc khi fooCommand kết thúc
    fooCommand.deadlineWith(
        barCommand, bazCommand, ...
    )
);
schedule(
    // kết thúc khi fooCommand kết thúc
    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(
    // chạy cho đến khi một command kết thúc
    fooCommand.raceWith(
        barCommand, bazCommand, ...
    )
);
schedule(
    // chạy cho đến khi một command kết thúc
    fooCommand.raceWith(
        barCommand, bazCommand, ...
    )
);

perpetually

“Nuốt” command vào một PerpetualCommand và trả về nó.

// trả về một perpetual command
PerpetualCommand perpetual = fooCommand.perpetually();
// trả về một perpetual command
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.

// trả về một proxy schedule command
ProxyScheduleCommand proxySchedule = fooCommand.asProxy();
// trả về một proxy schedule command
ProxyScheduleCommand proxySchedule = fooCommand.asProxy();

ADUDU

A proud team of passionate Robotics Enthusiasts competing in nation-wide Technology competitions in Vietnam, the FIRST Tech Challenge and the FIRST Robotics Competition.

Copyright ©

, all rights reserved

Made by aDudu's Programming Department

made by aDudu

made by aDudu

Convenience Features