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()
You usually don't call 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.

MethodDescription
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.

MethodDescription
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.
Call 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.

MethodDescription
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.
Pairs with a feedforward controller. Velocity loops usually want an 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.

MethodDescription
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.

MethodDescription
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.

DeviceWrapsTypical reads
IMUDeviceIMU (universal) getYaw/getPitch/getRoll(AngleUnit); resetYaw(). Cached per update(). IMU must be initialized separately.
ColorSensorDeviceColorSensor red(), green(), blue(), alpha(), argb(); enableLed(boolean).
RevColorSensorV3DeviceRevColorSensorV3 Normalized red/green/blue(), raw redRaw/greenRaw/blueRaw(), getHue/getSaturation/getValue(), getDistance(DistanceUnit), setGain(float). One 14-byte I2C read per update.
DistanceSensorDeviceDistanceSensor getDistance(DistanceUnit), getDistanceCm().
TouchSensorDeviceTouchSensor isPressed(), getValue().
AnalogInputDeviceAnalogInput getVoltage(), getMaxVoltage().
DigitalInputDeviceDigitalChannel (input mode) getState(). Put in INPUT mode at construction.
I2C is not bulk-cached. The IMU and REV Color Sensor V3 are I2C devices, which Lynx bulk caching does not cover. 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.