ModularOpMode

The base OpMode you subclass to assemble a robot out of reusable parts. Register subsystems and modules once, and ModularOpMode drives their whole lifecycle for you — init, start, loop, and stop — while managing Lynx hub bulk caching so every cycle reads all your sensors in a single transaction.

ModularOpMode extends the FTC SDK's iterative OpMode. Instead of overriding init()/loop() directly and wiring hardware by hand, you override a single hook, preload(), to declare what your robot is made of. From there the base class forwards each SDK lifecycle phase to every registered subsystem and installed module, in the order you registered them.

public abstract class ModularOpMode extends OpMode {
    protected final BindingManager bindingManager = new BindingManager();

    protected abstract void preload();

    protected void registerSubsystem(Subsystem subsystem);
    protected void installModule(Module module);
    protected void setBulkCachingMode(LynxModule.BulkCachingMode mode);
}

Lifecycle

Each phase of the SDK OpMode lifecycle is forwarded to your parts. Subsystems always run before modules within a phase, and both run in registration order.

PhaseWhat ModularOpMode does
init() Runs your preload(); discovers every LynxModule from the hardware map and applies the bulk-caching mode to each; then calls subsystem.init() on every subsystem followed by module.init() on every module.
init_loop() Calls subsystem.init_loop() on each subsystem, then module.init_loop() on each module. Repeats while the driver waits on the init screen.
start() Calls subsystem.start() on each subsystem, then module.start() on each module, then polls taskMaster.update() once.
loop() In MANUAL mode, clears every hub's bulk cache once; then calls subsystem.update() on each subsystem, module.loop() on each module, and finally polls taskMaster.update().
stop() Calls subsystem.stop() on each subsystem, then module.stop() on each module, so they can safe or release hardware.
Note. Subsystems expose update() for the main loop, while modules expose loop(). The TaskMaster is polled at the end of both start() and loop(), so queued tasks advance every cycle. See Subsystems and Modules for the full contract of each.

Registration

You never call the lifecycle methods yourself. Instead you declare your robot in preload(), which runs once at the very top of init() before anything is initialized.

MemberPurpose
protected abstract void preload() The one method you must implement. Build the robot here: register subsystems, install modules, and optionally change the bulk-caching mode. Called once at the start of init().
void registerSubsystem(Subsystem subsystem) Adds a subsystem to be driven every phase, in registration order. Call from preload().
void installModule(Module module) Adds a module to be driven every phase, in installation order. Call from preload().
protected final BindingManager bindingManager The shared BindingManager for this OpMode. Modules such as BindingModule and TeleOpPedroModule register and query gamepad bindings against it; it is exposed to subclasses so you can wire your own bindings too.
@TeleOp(name = "My TeleOp")
public class MyTeleOp extends ModularOpMode {

    private DriveSubsystem drive;

    @Override
    protected void preload() {
        drive = new DriveSubsystem(hardwareMap);
        registerSubsystem(drive);
        registerSubsystem(new ThreadedIMU(hardwareMap, "imu"));

        // Modules typically wire gamepad bindings against bindingManager.
        installModule(new TeleOpPedroModule(/* ... */));
    }
}

Lynx bulk caching

Every Control Hub and Expansion Hub (a "Lynx" module) can batch its sensor and encoder reads. ModularOpMode sets the caching mode on every hub in init(); the mode defaults to MANUAL and can be changed from preload():

protected void setBulkCachingMode(LynxModule.BulkCachingMode mode)

In MANUAL mode the cache is cleared exactly once, at the top of each loop(). The first hardware read after that clear pulls every sensor and encoder off the hub in a single bulk transaction; every subsequent read that loop is served from that in-memory snapshot instead of hitting the bus. Because CDALib's devices read hardware in their update(), this means a full loop's worth of sensor reads costs one round-trip to each hub rather than one per sensor.

Why it matters. Each USB round-trip to a Lynx hub costs roughly 2–3 ms. A robot polling six sensors the naive way spends most of its loop blocked on the bus; folding those into one bulk read per hub can cut loop times dramatically and keep control loops responsive.
The one rule of MANUAL mode. Because the cache is cleared only once per loop, reading the same sensor twice in a loop returns the same cached value. That is exactly what you want here — CDALib devices cache their reads on purpose — but it means you should read a given sensor at most once per cycle through its device's update(). Do not clear the cache yourself mid-loop.

I2C devices (such as the universal IMU or a REV Color Sensor V3) are not covered by Lynx bulk caching. CDALib handles those separately: see ThreadedIMU and the RevColorSensorV3Device, both of which minimize per-loop I2C cost.

PhotonFTC

For teams that want even lower loop times, annotate your concrete OpMode subclass with @Photon. PhotonCore then swaps in its optimized motor and servo controllers under the hood.

@Photon
@TeleOp(name = "My TeleOp")
public class MyTeleOp extends ModularOpMode {
    // ...
}

Photon pairs naturally with the default MANUAL bulk-caching mode used by ModularOpMode — one cache clear per loop(). Leave the caching mode on MANUAL when Photon is enabled; the two optimizations compound rather than conflict.