First OpMode
This page gets a single motor spinning from the left stick. It assumes you have already installed Synapse. The whole OpMode is one file — copy it into TeamCode/src/main/java/org/firstinspires/ftc/teamcode/ and change the device name if yours is different.
import com.qualcomm.robotcore.eventloop.opmode.TeleOp;
import com.qualcomm.robotcore.hardware.DcMotorEx;
import com.aaravlabs.synapse.Node;
import com.aaravlabs.synapse.Orchestrator;
import com.aaravlabs.synapse.annotation.RunPeriodically;
import com.aaravlabs.synapse.ftc.GamepadAdaptor;
import com.aaravlabs.synapse.ftc.SafeDevice;
import com.aaravlabs.synapse.ftc.SafeOpMode;
@TeleOp(name = "SynapseFirst", group = "Test")
public class SynapseFirst extends SafeOpMode {
@Override
protected void onSafeInit() {
SafeDevice<DcMotorEx> motor = safeMap.device(DcMotorEx.class, "motor");
orchestrator.registerNode("drive", new DriveNode(orchestrator, motor));
GamepadAdaptor.attach(orchestrator, gamepad1, "g1");
}
public static class DriveNode extends Node {
DriveNode(Orchestrator orchestrator, SafeDevice<DcMotorEx> motor) {
super(orchestrator);
this.motor = motor;
}
private final SafeDevice<DcMotorEx> motor;
@RunPeriodically(hz = 50, hardware = true)
public void drive() {
// GamepadAdaptor publishes axes as Float; map to double for setPower.
double y = -orchestrator.getLatestValue("g1/left_stick_y", Float.class)
.map(Float::doubleValue).orElse(0.0);
motor.run(m -> m.setPower(y));
}
}
}
Push the left stick forward and the motor spins at 50 Hz.
What each line does
safeMap.device(DcMotorEx.class, "motor")looks the device up inhardwareMapand wraps it in aSafeDevice<DcMotorEx>. Every call through the wrapper runs on the dedicated hardware thread — never on whatever thread happens to call it.orchestrator.registerNode("drive", ...)binds the node: every annotated method inDriveNodeis wired up at this moment. This is why the OpMode callsregisterNode— nodes never register themselves.GamepadAdaptor.attach(orchestrator, gamepad1, "g1")publishes every button and axis ofgamepad1onto topics namedg1/...at 60 Hz. Left stick Y now lives atg1/left_stick_yas aFloat.@RunPeriodically(hz = 50, hardware = true)runsdrive()every 20 ms on the hardware thread. Thehardware = trueflag is what makes touching motors safe — leave it off and the loop would run on the scheduler pool, where hardware calls are forbidden.- Inside the loop,
orchestrator.getLatestValue(...)reads the most recent axis value without subscribing.orElse(0.0)covers the moments before the first gamepad tick arrives. motor.run(...)submits thesetPowercall to the hardware thread. Inside ahardware = trueloop you are already on that thread, so the call is queued to itself and executes immediately after.
Axes are
Float, notDouble.GamepadAdaptorpublishes the SDK’sfloatfields, sogetLatestValue(name, Float.class)is the correct fetch, and.map(Float::doubleValue)converts for math. Fetching withDouble.classsilently returns an emptyOptionalbecause the types do not match.
Forward is negative. FTC sticks rest at −1 when pushed fully forward, hence the leading minus. Without it the motor would drive backwards.
Nothing moves after pressing Init? Hardware only starts moving after Start —
onSafeInitruns at init, but the 50 Hz loop ticks only inloop(). Watch the Driver Station state, not just the log.
Verify the direction
Motor direction depends on wiring. Before driving with real hardware, run a direction test that spins each motor individually from a button — the pattern used on a real robot before the first drive.
Next steps
- Mecanum drive — the full four-motor pattern, robot-validated.
- Architecture — what
hardware = trueactually does, and what runs where. - SafeOpMode — all five lifecycle hooks.