Devices
A Device is a thin wrapper over a single FTC SDK hardware object — a
motor, a servo, a sensor — with one job: an update() that is bulk-cache friendly.
Devices are the building blocks that subsystems drive.
Device is an abstract class with a single method:
public abstract class Device {
public abstract void update();
}
Everything a device does flows through update(), called once per loop:
- Actuator devices (motor, servo, CR servo) cache the state you command and
flush it to hardware in
update()— and only when the value has actually changed. Setting the same power every loop then costs no bus write. This is write caching. - Sensor devices read hardware once in
update()and cache the value, so inspecting several channels later in the loop costs a single hardware read. This is read caching.
The common pattern is: set desired state, then let update() apply it.
Setters that configure or command return this, so the fluent
setPower(x).update() style works:
motor.setPower(0.75).update(); // command, then flush
servo.setPosition(0.5).update();
double yaw = imu.getYaw(AngleUnit.DEGREES); // reads the value cached by update()
update() yourself. A
DeviceSubsystem refreshes its device every loop automatically, and
ModularOpMode pairs this with Lynx MANUAL bulk
caching so all your reads land in one bulk transaction per hub. See
ModularOpMode › Lynx bulk caching.Every device offers two constructors: one taking an already-resolved SDK object, and one taking
(HardwareMap, String name) to resolve it by configured name. Each also exposes a
getter (getMotor(), getServo(), getSensor(), …) for SDK
operations not surfaced here.
Actuators
Actuator devices write-cache their commands and only flush a changed value in
update(), collapsing redundant bus writes.
MotorDevice
Wraps a DcMotorEx. Power commands are cached and written in update()
only when they've changed meaningfully — joystick jitter and steady holds cost no bus transaction.
The first command and an exact stop (0.0) are always flushed. Configuration and read
setters return this for chaining.
| Method | Description |
|---|---|
MotorDevice setPower(double power) | Power to command on the next
update(). Range [-1.0, 1.0]. |
MotorDevice setWriteThreshold(double t) | Minimum power change (vs.
the last written value) that triggers an actual write. Larger collapses more redundant
writes; 0 flushes every change. Default 0.005. |
MotorDevice setDirection(DcMotorSimple.Direction) | Rotation direction. |
MotorDevice setZeroPowerBehavior(DcMotor.ZeroPowerBehavior) | What
the motor does at zero power — BRAKE or FLOAT. |
MotorDevice setMode(DcMotor.RunMode) | Run mode (e.g.
RUN_USING_ENCODER, RUN_TO_POSITION). |
double getPower() | Last commanded power (not necessarily flushed yet). |
int getCurrentPosition() | Encoder position, in ticks. |
double getVelocity() | Velocity, in ticks/second. |
double getCurrent(CurrentUnit) | Current draw in the given unit. |
DcMotorEx getMotor() | The wrapped SDK motor. |
PositionMotorDevice
A MotorDevice that closes a position loop on the robot (rather than on the Lynx
firmware's RUN_TO_POSITION). You give it a Controller
and a target encoder position; each update() measures the current encoder ticks,
runs the controller to produce a power, and flushes that power through the inherited
write‑caching. Because it extends MotorDevice, all the configuration and read
methods above (setDirection, setZeroPowerBehavior,
getCurrentPosition, getMotor, …) are available too.
| Method | Description |
|---|---|
PositionMotorDevice(DcMotorEx, Controller) | Wrap a resolved motor and drive it to the controller's target. |
PositionMotorDevice(HardwareMap, String, Controller) | Resolve the motor by configured name, with the given controller. |
PositionMotorDevice(DcMotorEx) /PositionMotorDevice(HardwareMap,
String) | No controller supplied — defaults to a NullController(0)
(holds zero) until you call setController. |
void setPosition(int position) | Set the target encoder position, in
ticks. Applied via the controller on the next update(). |
void setController(Controller controller) | Swap the control law; the target is re-read from the new controller. |
update() every loop. The controller is a discrete loop —
each update() computes the elapsed dt and steps the controller once, so a
steady cadence matters. Held inside a DeviceSubsystem, this happens automatically.VelocityMotorDevice
The velocity analogue of PositionMotorDevice: a MotorDevice that closes
a velocity loop with a Controller. Each
update() measures the current velocity (ticks/second), runs the controller to produce a
power, and flushes it. Useful for flywheels and intakes where you want a held RPM rather than a
fixed open-loop power. It too inherits every MotorDevice method.
| Method | Description |
|---|---|
VelocityMotorDevice(DcMotorEx, Controller) | Wrap a resolved motor and drive it to the controller's target velocity. |
VelocityMotorDevice(HardwareMap, String, Controller) | Resolve the motor by configured name, with the given controller. |
VelocityMotorDevice(DcMotorEx) /VelocityMotorDevice(HardwareMap,
String) | No controller supplied — defaults to a NullController(0)
until you call setController. |
void setVelocity(double velocity) | Set the target velocity, in
ticks/second. Applied via the controller on the next update(). |
void setController(Controller controller) | Swap the control law; the target is re-read from the new controller. |
FFController (or a CompoundController of PID + feedforward) rather than
pure PID. See Controllers for the control laws you can drop in.ServoDevice
Wraps a Servo. Position commands are cached and written in update()
only when the target has changed.
| Method | Description |
|---|---|
ServoDevice setPosition(double position) | Position to command on
the next update(). Range [0.0, 1.0]. |
double getPosition() | Last commanded position. |
ServoDevice setDirection(Servo.Direction) | Servo direction. |
ServoDevice scaleRange(double min, double max) | Constrains the
usable range (see Servo.scaleRange). |
Servo getServo() | The wrapped SDK servo. |
CRServoDevice
Wraps a CRServo (continuous-rotation servo). Power commands are cached and written
in update() only when the target has changed.
| Method | Description |
|---|---|
CRServoDevice setPower(double power) | Power to command on the next
update(). Range [-1.0, 1.0]. |
double getPower() | Last commanded power. |
CRServoDevice setDirection(DcMotorSimple.Direction) | CR servo direction. |
CRServo getServo() | The wrapped SDK CR servo. |
Sensors
Sensor devices read hardware once per update() and cache the result, so the getters
return the value from the most recent update in constant time.
| Device | Wraps | Typical reads |
|---|---|---|
IMUDevice | IMU (universal) |
getYaw/getPitch/getRoll(AngleUnit); resetYaw(). Cached per
update(). IMU must be initialized separately. |
ColorSensorDevice | ColorSensor |
red(), green(), blue(), alpha(),
argb(); enableLed(boolean). |
RevColorSensorV3Device | RevColorSensorV3 |
Normalized red/green/blue(), raw redRaw/greenRaw/blueRaw(),
getHue/getSaturation/getValue(), getDistance(DistanceUnit),
setGain(float). One 14-byte I2C read per update. |
DistanceSensorDevice | DistanceSensor |
getDistance(DistanceUnit), getDistanceCm(). |
TouchSensorDevice | TouchSensor |
isPressed(), getValue(). |
AnalogInputDevice | AnalogInput |
getVoltage(), getMaxVoltage(). |
DigitalInputDevice | DigitalChannel (input mode) |
getState(). Put in INPUT mode at construction. |
RevColorSensorV3Device works around this by
pulling proximity plus all three colour channels off the sensor in a single 14-byte I2C
read per update() and decoding the rest in software. For the IMU in a hot loop, prefer
the threaded reader — see ThreadedIMU.NullDevice
NullDevice is a no-op device whose update() does nothing. Use it as a
placeholder for optional or missing hardware so a subsystem can hold a non-null device reference
and run unchanged when the real device isn't present.
Example: driving a motor
Construct a MotorDevice from the hardware map, configure it once, then command and
flush it each loop.
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;
// Resolve by configured name (matches your robot configuration).
MotorDevice motor = new MotorDevice(hardwareMap, "arm");
// One-time configuration — fluent, chainable.
motor.setDirection(DcMotorSimple.Direction.FORWARD)
.setZeroPowerBehavior(DcMotor.ZeroPowerBehavior.BRAKE);
// Every loop: command the power you want, then flush it.
// The write only hits the bus if the power changed meaningfully.
motor.setPower(0.6).update();
// Read cached telemetry (encoder read happens on the SDK object directly).
int ticks = motor.getCurrentPosition();
In practice you'd hold this device inside a subsystem — most often
a DeviceSubsystem, whose update() already refreshes the device for you —
and expose intent-level methods like raise() or setPower(double).
Example: a closed-loop position motor
Give a PositionMotorDevice a PID controller and a
target in encoder ticks. Each update() runs the controller against the live encoder
reading and flushes the resulting power — no RUN_TO_POSITION needed.
import com.cdarobotics.cdalib.devices.actuators.PositionMotorDevice;
import com.cdarobotics.cdalib.controllers.PIDController;
// PIDController(target, kP, kI, kD) — start the target at the resting position.
PositionMotorDevice arm =
new PositionMotorDevice(hardwareMap, "arm", new PIDController(0, 0.004, 0, 0.0002));
// Command a new setpoint whenever the goal changes…
arm.setPosition(1200); // ticks
// …and step the loop every cycle. (A DeviceSubsystem would do this for you.)
arm.update();
A VelocityMotorDevice is identical but closes on velocity — swap in an
FFController (or a CompoundController of PID + feedforward) and call
setVelocity(ticksPerSecond) to hold a flywheel speed.