Subsystems

A Subsystem is one robot mechanism — a drivetrain, an arm, a lift — with a fixed lifecycle. You register subsystems in your ModularOpMode's preload(), and the OpMode drives each one through init, start, loop, and stop for you.

Subsystem is an abstract class with five lifecycle methods. Extend it, implement the methods for your mechanism, and register it — you never call these methods yourself.

MethodCalledUse it for
void init()Once, on OpMode init. Grab hardware and set starting state.
void init_loop()Repeatedly during init, after init() and before start(). Work that must keep running until play is pressed — reporting sensor readings, waiting on hardware.
void start()Once, when play is pressed. Latch a start time, kick off threads, arm the mechanism.
void update()Every loop. Read inputs and drive the mechanism. This is the hot path.
void stop()Once, when the OpMode ends. Safe or release the mechanism.
Every concrete subsystem must implement init_loop(). It is an abstract method on Subsystem, so the compiler forces you to provide it in every subclass — even when you have nothing to do during init. In that case just leave the body empty. Forgetting this is the most common reason a new subsystem won't compile.
@Override
public void init_loop() {
    // Nothing to do during init for this subsystem — but the method is required.
}

Provided subsystem types

CDALib ships several subsystem base classes and ready-made subsystems. Reach for the one that matches your mechanism.

abstract

Subsystem

The base class every subsystem extends. Defines the five lifecycle methods above. Use it directly when your mechanism doesn't fit one of the more specialized bases.

abstract

DeviceSubsystem

A subsystem backed by a single Device. Its constructor takes the device, and its update() already calls device.update() for you every loop. You implement init()/init_loop()/start()/stop() and expose your own control methods on top of the device.

abstract

CompoundSubsystem

Groups several child subsystems and fans each lifecycle call out to all of them. Add children with registerSubsystem(Subsystem). Great for a mechanism assembled from smaller ones — e.g. an intake made of a roller subsystem and a wrist subsystem.

concrete

PositionMotorSubsystem

Drives a single motor toward a target position using a Controller. Set the target and let it hold there every loop.

concrete

ThreadedIMU

Reads the IMU on a background thread so the main loop never blocks on I2C. Getters return a cached snapshot in constant time.

concrete

NullSubsystem

A subsystem whose lifecycle methods all do nothing. A no-op placeholder or default where a subsystem is required but no behavior is wanted.

DeviceSubsystem

The constructor is protected DeviceSubsystem(Device device). The wrapped device is held in a protected final Device device field and refreshed automatically each loop. This is the base you'll use most: wrap one motor, servo, or sensor and expose intent-level methods.

CompoundSubsystem

Extend it and add children in your constructor. init(), start(), update(), and stop() are already implemented to loop over every child.

public class Intake extends CompoundSubsystem {
    public Intake(HardwareMap hardwareMap) {
        registerSubsystem(new Roller(hardwareMap));
        registerSubsystem(new Wrist(hardwareMap));
    }

    @Override
    public void init_loop() {
        // required; delegate to children yourself if needed
    }
}

PositionMotorSubsystem

Resolves a motor from the hardware map and drives it toward a target position with a Controller. Each update() feeds the motor's current encoder position and the elapsed time to the controller and applies the resulting power.

MemberDescription
PositionMotorSubsystem(HardwareMap, String motorName) Resolves the motor by name; starts with a NullController (no motion) until you set a real one.
PositionMotorSubsystem(HardwareMap, String motorName, Controller controller) Resolves the motor and drives it with the given controller.
void setController(Controller controller) Replaces the controller used to drive the motor toward its target.
void setDirection(DcMotorSimple.Direction direction) Sets the motor's rotation direction.
void setPosition(double position) Sets the target position the controller drives the motor toward (calls controller.setTarget(position)).
PositionMotorSubsystem lift =
        new PositionMotorSubsystem(hardwareMap, "lift", new PIDController(0.01, 0, 0.001));
lift.setDirection(DcMotorSimple.Direction.REVERSE);
registerSubsystem(lift);

// later, from a binding or module:
lift.setPosition(1200);   // drive toward 1200 encoder ticks and hold

ThreadedIMU

The universal IMU is an I2C device, and I2C is not covered by Lynx bulk caching — a normal getRobotYawPitchRollAngles() in the loop costs a full blocking transaction (several milliseconds) every cycle. ThreadedIMU instead spins a daemon reader thread (~200 Hz by default) that continuously polls the IMU into a cached snapshot; the getters return that snapshot in constant time, removing IMU latency from every loop.

The IMU must already be initialized (via imu.initialize(...) with your hub orientation) before you register a ThreadedIMU — like IMUDevice, it does not initialize the IMU itself.
MemberDescription
ThreadedIMU(IMU imu) / ThreadedIMU(IMU imu, long pollIntervalMs) Wraps an already-initialized IMU, at the default (DEFAULT_POLL_INTERVAL_MS = 10 ms) or a custom poll interval.
ThreadedIMU(HardwareMap, String name) / (HardwareMap, String, long) Resolves the IMU from the hardware map by configured name (still must be initialized separately).
double getYaw(AngleUnit), getPitch(AngleUnit), getRoll(AngleUnit) Most recent reading in the requested unit; returns 0 before the first read.
void resetYaw() Resets yaw so the current heading reads zero. Safe to call from the loop thread — it is serialized against the reader thread.
IMU getImu() The wrapped SDK IMU. Prefer the getters and resetYaw(); direct calls race with the reader thread.
IMU imu = hardwareMap.get(IMU.class, "imu");
imu.initialize(new IMU.Parameters(/* your hub orientation */));

ThreadedIMU heading = new ThreadedIMU(imu);
registerSubsystem(heading);   // ModularOpMode starts the reader on start(), stops it on end

// in a module/binding:
double yaw = heading.getYaw(AngleUnit.DEGREES);

NullSubsystem

All five lifecycle methods are empty. Register it where the code expects a subsystem but you want no behavior — a clean default that keeps a slot non-null.

Writing your own subsystem

Extend Subsystem and implement all five lifecycle methods. This lift wraps a MotorDevice — set the power you want in your control methods and let update() flush it once per loop (a bulk-cache-friendly write). Note the required init_loop(), empty here.

package org.firstinspires.ftc.teamcode.subsystems;

import com.cdarobotics.cdalib.subsystems.Subsystem;
import com.cdarobotics.cdalib.devices.actuators.MotorDevice;
import com.qualcomm.robotcore.hardware.DcMotor;
import com.qualcomm.robotcore.hardware.DcMotorSimple;
import com.qualcomm.robotcore.hardware.HardwareMap;

public class Lift extends Subsystem {

    private final MotorDevice motor;
    private double power = 0.0;

    public Lift(HardwareMap hardwareMap) {
        motor = new MotorDevice(hardwareMap, "lift");
    }

    // Control method your modules/bindings call.
    public void setPower(double power) {
        this.power = power;
    }

    @Override
    public void init() {
        motor.setDirection(DcMotorSimple.Direction.REVERSE)
             .setZeroPowerBehavior(DcMotor.ZeroPowerBehavior.BRAKE);
    }

    @Override
    public void init_loop() {
        // Required by Subsystem. Nothing to do during init here.
    }

    @Override
    public void start() {
        power = 0.0;
    }

    @Override
    public void update() {
        // Cache the commanded power; MotorDevice.update() flushes it (write-cached).
        motor.setPower(power).update();
    }

    @Override
    public void stop() {
        motor.setPower(0.0).update();
    }
}

Register it in your OpMode:

@Override
protected void preload() {
    registerSubsystem(new Lift(hardwareMap));
}
Tip. If your subsystem wraps exactly one device, extend DeviceSubsystem instead — its update() already refreshes the device, so you only write the intent-level methods.