Skip to content

Intro to CBP_Subsystems

Olivier Frechette edited this page Nov 5, 2022 · 12 revisions

Disclaimer: The majority of text content on this page is pulled almost verbatim from WPILib documentation, with light edits, and the examples have been reworked to correspond to their Taproot equivalents.

Subsystems

Subsystems are the basic unit of robot organization in the command-based paradigm. A subsystem is an abstraction for a collection of robot hardware that operates together as a unit. Subsystems encapsulate this hardware, “hiding” it from the rest of the robot code (e.g. commands) and restricting access to it except through the subsystem’s public methods. Restricting the access in this way provides a single convenient place for code that might otherwise be duplicated in multiple places (such as scaling motor outputs or checking limit switches) if the subsystem internals were exposed. It also allows changes to the specific details of how the subsystem works (the “implementation”) to be isolated from the rest of robot code, making it far easier to make substantial changes if/when the design constraints change.

Subsystems also serve as the backbone of the CommandScheduler’s resource management system. Commands may declare resource requirements by specifying which subsystems they interact with; the scheduler will never concurrently schedule more than one command that requires a given subsystem. An attempt to schedule a command that requires a subsystem that is already-in-use will either interrupt the currently-running command, or else be ignored.

Subsystems can be associated with “default commands” that will be automatically scheduled when no other command is currently using the subsystem. This is useful for continuous “background” actions such as controlling the robot drive, or keeping an arm held at a setpoint. Similar functionality can be achieved in the subsystem’s refresh() method, which is run once per run of the scheduler; teams should try to be consistent within their codebase about which functionality is achieved through either of these methods. Subsystems are represented in the command-based library by the Subsystem interface.

Creating a Subsystem

Subsystems are created as a subclass of the Subsystem interface.

#include "tap/control/subsystem.hpp"

class MySubsystem : public tap::control::Subsystem
{
public:

    MySubsystem()

    ~MySubsystem() = default;

    /**
     * Called once when the subsystem is added to the scheduler.
     */
    void initialize() override;

    /**
     * Will be called periodically whenever the CommandScheduler runs.
     */
    void refresh() override;

private:
  // Components (e.g. motor controllers and sensors) should generally be
  // declared private and exposed only through public methods.
};

Feeder subsystem example

What might a functional subsystem look like in practice? Below is a simplified version of the code for the feeder subsystem, which controls the robot's feeder motor:

#include "tap/control/subsystem.hpp"
#include "modm/math/filter/pid.hpp"
#include "tap/motor/dji_motor.hpp"
#include "feeder_constants.hpp"

namespace control
{
namespace feeder
{
/**
 * A bare bones Subsystem for interacting with a feeder.
 */
class FeederSubsystem : public tap::control::Subsystem
{
public:

    /**
     * Constructs a new FeederSubsystem with default parameters specified in
     * the private section of this class.
     */
    FeederSubsystem(tap::Drivers *drivers)
        : tap::control::Subsystem(drivers),
          feederMotor(drivers, FEEDER_MOTOR_ID, CAN_BUS_MOTORS, false, "feeder motor"),
          feederPid(FEEDER_PID_KP,FEEDER_PID_KI,FEEDER_PID_KD,FEEDER_PID_MAX_ERROR_SUM,FEEDER_PID_MAX_OUTPUT)

    {
    }

    ~FeederSubsystem() = default;

    /** Initialize communication with the feeder motor. */
    void initialize() override 
    {
      feederMotor.initialize();
    }

    /** Run on every iteration of the command scheduler. Uses a PID to set motor output based on desired and current rpm. */
    void refresh() override
    {
      int16_t shaftRPM = feederMotor->getShaftRPM(); // Get current RPM from motor
      pid->update(desiredRPM - shaftRPM);            // Update the PID with the latest error value
      float pidValue = pid->getValue();
      feederMotor->setDesiredOutput(pidValue);       // Set motor output to value determined by PID
    }

    /** Public function used to set the desired RPM of the feeder motor. Used by commands. */
    void setDesiredOutput(float rpm)
    {
      feederDesiredRpm = rpm;
    }

private:
    ///< Hardware constants, not specific to any particular feeder.
    static constexpr tap::motor::MotorId FEEDER_MOTOR_ID = tap::motor::MOTOR8;      // Feeder motor ID
    static constexpr tap::can::CanBus CAN_BUS_MOTORS = tap::can::CanBus::CAN_BUS1;  // CAN bus to which the motor is connected

    ///< Motors.  Use these to interact with any dji style motors.
    tap::motor::DjiMotor feederMotor;

    ///< PID controller for rpm feedback from motor
    modm::Pid<float> feederPid;

    ///< Desired rpm for the motor. Acts as a setpoint for the subsystem.
    float feederDesiredRpm;

};

Notice that the subsystem hides the presence of the feederMotor from outside code (it is declared private), and instead publicly exposes a higher-level, descriptive robot method: setDesiredOutput. Note also that in this implementation, the subsystem uses a PID controller, and commands only set a desired RPM for the motor, rather than passing on signals directly to the motor. It is extremely important that “implementation details” such as the DJI motor and the PID controller to be “hidden” in this manner; this ensures that code outside the subsystem will never cause the motor or controller to be in an unexpected state. It also allows the user to change the implementation (for instance, a different type of controller could be implemented) without any of the code outside of the subsystem having to change with it.

Registering a subsystem

Subsystems must be registered with the command scheduler before they can be utilized. For instance, if we were to create a turret subsystem (the constructor parameter drivers() is an example of dependency injection):

TurretSubsystem theTurret(drivers());

We then register the subsystem (note once again the use of the drivers interface):

drivers->commandScheduler.registerSubsystem(&theTurret);

Setting default commands

“Default commands” are commands that run automatically whenever a subsystem is not being used by another command.

Setting a default command for a subsystem is very easy; one simply calls the setDefaultCommand() method of the Subsystem interface:

theChassis.setDefaultCommand(&chassisDrive);

Next Page : Commands

Clone this wiki locally