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.
| Method | Called | Use 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. |
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.
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.
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.
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.
PositionMotorSubsystem
Drives a single motor toward a target position using a Controller. Set the target and let it hold there every loop.
ThreadedIMU
Reads the IMU on a background thread so the main loop never blocks on I2C. Getters return a cached snapshot in constant time.
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.
| Member | Description |
|---|---|
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.
imu.initialize(...)
with your hub orientation) before you register a ThreadedIMU — like
IMUDevice, it does not initialize the IMU itself.| Member | Description |
|---|---|
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));
}
update() already refreshes
the device, so you only write the intent-level methods.