Two-controller teleop
Synapse needs nothing special for two gamepads — attach a second GamepadAdaptor with a different prefix. What makes the pattern worth copying is how the 23684 Canopy-Biobuzz robot splits authority: each subsystem node decides for itself which gamepad(s) it listens to, and the OpMode only wires things up.
Attach both gamepads
@Override
protected void onSafeInit() {
GamepadAdaptor.attach(orchestrator, gamepad1, "g1");
GamepadAdaptor.attach(orchestrator, gamepad2, "g2");
}
Driver controls now live at g1/... and operator controls at g2/... — same topics as one gamepad, different roots.
Give a subsystem two inputs
The intake takes its power from both gamepads: the operator’s left stick and the driver’s left trigger, whichever is larger. The node encapsulates that policy:
package org.firstinspires.ftc.teamcode.Subsystems;
import com.aaravlabs.synapse.Node;
import com.aaravlabs.synapse.Orchestrator;
import com.aaravlabs.synapse.annotation.RunPeriodically;
import com.aaravlabs.synapse.ftc.SafeDevice;
import com.qualcomm.robotcore.hardware.DcMotorEx;
public class Intake extends Node {
private final SafeDevice<DcMotorEx> intake;
public Intake(Orchestrator orch, SafeDevice<DcMotorEx> intake) {
super(orch);
this.intake = intake;
}
@RunPeriodically(hz = 50, hardware = true)
public void runIntake() {
double operator = -orchestrator.getLatestValue("g2/left_stick_y", Float.class).map(Float::doubleValue).orElse(0.0);
double trigger = orchestrator.getLatestValue("g1/left_trigger", Float.class).map(Float::doubleValue).orElse(0.0);
double power = Math.max(operator, trigger);
intake.run(m -> m.setPower(power));
orchestrator.publish("intake/power", power);
}
}
Register it alongside the drive node in onSafeInit():
orchestrator.registerNode("drive", new Drive(orchestrator, frontLeft, frontRight, backLeft, backRight));
orchestrator.registerNode("intake", new Intake(orchestrator, intake));
Why this shape works on a real robot:
- The priority rule lives in one place. “Driver trigger always wins” is a property of the intake, not of the OpMode — if the operator is running the intake and the driver opens the trigger, the intake obeys the driver, with no ordering logic anywhere else.
- Triggers are
Floataxes, not buttons — read them withgetLatestValue(..., Float.class)exactly like sticks. - The subsystem publishes its state (
intake/power) so telemetry renders it without touching hardware.
Buttons across gamepads
Subscriptions and actions work per-prefix too. An operator-pressed claw toggle looks like:
@SubscribedTo(topic = "g2/right_bumper/rising")
@OnHardwareThread
public void toggleClaw(Boolean v) {
claw.run(s -> s.setPosition(clawOpen ? CLOSED : OPEN));
}
Prefix discipline is the whole pattern.
g1is the driver,g2is the operator — never subscribe a subsystem to both roots “whenever convenient”. If a control needs both gamepads, encode the policy inside the subsystem like the intake above, so the priority is written down exactly once.
Next steps
- Mecanum drive — the drive node this recipe registers alongside.
- GamepadAdaptor — the full topic surface each gamepad publishes.